dsh-plugin-ekko-skin-manager 0.1.3 → 0.2.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/lib/client.js CHANGED
@@ -27,16 +27,370 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
27
27
  ));
28
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
29
 
30
- // work/dsh-plugin-ekko-skin-manager/src/client.jsx
31
- var client_exports = {};
32
- __export(client_exports, {
33
- apply: () => apply,
30
+ // src/client-entry.mjs
31
+ var client_entry_exports = {};
32
+ __export(client_entry_exports, {
33
+ acquireClient: () => acquireClient,
34
+ apply: () => apply2,
34
35
  inject: () => inject
35
36
  });
36
- module.exports = __toCommonJS(client_exports);
37
+ module.exports = __toCommonJS(client_entry_exports);
38
+
39
+ // src/client.jsx
40
+ var import_react2 = __toESM(require("react"), 1);
41
+
42
+ // src/video-settings.jsx
37
43
  var import_react = __toESM(require("react"), 1);
38
44
 
39
- // work/dsh-plugin-ekko-skin-manager/src/controller.mjs
45
+ // src/boot-player.mjs
46
+ var BOOT_CSS = `
47
+ .ekko-boot{position:fixed;inset:0;z-index:2147483000;display:grid;place-items:center;background:#10171e;color:#fff;opacity:1;isolation:isolate;font:14px/1.5 system-ui,sans-serif}
48
+ .ekko-boot[data-skin="rose"]{background:#241a20;--boot-accent:#f3b3c6}.ekko-boot[data-skin="miku"]{--boot-accent:#81eff0}
49
+ .ekko-boot video{display:block;width:100%;height:100%;position:absolute;inset:0;object-fit:cover}
50
+ .ekko-boot:after{content:"";position:absolute;inset:0;pointer-events:none;box-shadow:inset 0 0 0 2px var(--boot-accent),inset 0 -100px 100px -75px #000b}
51
+ .ekko-boot button{position:relative;z-index:2;cursor:pointer;border:1px solid #fff8;background:#101820b8;color:white;border-radius:999px;min-height:44px;padding:9px 16px;font:inherit;backdrop-filter:blur(8px)}
52
+ .ekko-boot button:focus-visible{outline:3px solid var(--boot-accent);outline-offset:3px}
53
+ .ekko-boot-skip{position:absolute!important;right:max(18px,env(safe-area-inset-right));top:max(18px,env(safe-area-inset-top))}
54
+ .ekko-boot-footer{position:absolute;bottom:max(20px,env(safe-area-inset-bottom));left:max(20px,env(safe-area-inset-left));right:max(20px,env(safe-area-inset-right));display:grid;gap:10px;z-index:2;text-shadow:0 1px 8px #000}
55
+ .ekko-boot-toolbar{display:flex;gap:12px;align-items:center;flex-wrap:wrap}.ekko-boot-toolbar span{margin-left:auto}.ekko-boot progress{width:100%;height:4px;accent-color:var(--boot-accent)}
56
+ @media(prefers-reduced-motion:reduce){.ekko-boot{scroll-behavior:auto}}
57
+ `;
58
+ var text = () => /^zh/i.test(document.documentElement.lang || navigator.language) ? {
59
+ skip: "\u8DF3\u8FC7\u89C6\u9891",
60
+ sound: "\u5F00\u542F\u58F0\u97F3",
61
+ mute: "\u9759\u97F3",
62
+ play: "\u64AD\u653E\u89C6\u9891",
63
+ loading: "\u6B63\u5728\u52A0\u8F7D\u542F\u52A8\u89C6\u9891\u2026",
64
+ hint: "\u70B9\u51FB\u753B\u9762\u6216\u6309 Esc \u8DF3\u8FC7",
65
+ title: "\u4E3B\u9898\u542F\u52A8\u89C6\u9891"
66
+ } : { skip: "Skip video", sound: "Enable sound", mute: "Mute", play: "Play video", loading: "Loading startup video\u2026", hint: "Click the picture or press Esc to skip", title: "Theme startup video" };
67
+ var MOBILE_SCREEN = "(max-width: 768px), (hover: none) and (pointer: coarse) and (max-height: 768px)";
68
+ function selectBootVideo(entry, mobile = false) {
69
+ if (!entry) return entry;
70
+ return mobile && entry.mobile ? { ...entry, ...entry.mobile, device: "mobile" } : { ...entry, device: mobile ? "mobile" : "desktop" };
71
+ }
72
+ function videoSources(options, canPlayWebM) {
73
+ const original = options?.url, alternate = options?.fallbackUrl;
74
+ return [...new Set((canPlayWebM ? [alternate, original] : [original, alternate]).filter(Boolean))];
75
+ }
76
+ function playBoot(skin, options, { preview = false } = {}) {
77
+ if (!options?.url || options.available === false || !preview && !options.enabled) return Promise.resolve();
78
+ if (!preview && window.matchMedia?.("(prefers-reduced-motion: reduce)").matches) return Promise.resolve();
79
+ if (document.querySelector(".ekko-boot")) return Promise.resolve();
80
+ let c = text();
81
+ const overlay = document.createElement("section"), video = document.createElement("video");
82
+ overlay.className = "ekko-boot";
83
+ overlay.dataset.skin = skin;
84
+ overlay.dataset.device = options.device || "desktop";
85
+ overlay.setAttribute("role", "dialog");
86
+ overlay.setAttribute("aria-modal", "true");
87
+ overlay.setAttribute("aria-label", c.title);
88
+ const style = document.createElement("style");
89
+ style.dataset.ekkoBoot = "";
90
+ style.textContent = BOOT_CSS;
91
+ document.head.append(style);
92
+ video.playsInline = true;
93
+ video.preload = "auto";
94
+ video.muted = !options.sound;
95
+ video.volume = options.volume ?? 0.65;
96
+ video.style.objectFit = options.fit || "cover";
97
+ const sources = videoSources(options, Boolean(video.canPlayType('video/webm; codecs="vp8, opus"')));
98
+ let sourceIndex = 0;
99
+ video.src = sources[sourceIndex];
100
+ const button = (label, cl) => {
101
+ const b = document.createElement("button");
102
+ b.type = "button";
103
+ b.textContent = label;
104
+ b.className = cl || "";
105
+ return b;
106
+ };
107
+ const skip = button(c.skip, "ekko-boot-skip"), sound = button(video.muted ? c.sound : c.mute), play = button(c.play);
108
+ play.hidden = true;
109
+ const footer = document.createElement("div");
110
+ footer.className = "ekko-boot-footer";
111
+ const toolbar = document.createElement("div");
112
+ toolbar.className = "ekko-boot-toolbar";
113
+ const hint = document.createElement("span");
114
+ hint.textContent = c.loading;
115
+ const progress = document.createElement("progress");
116
+ progress.max = 1;
117
+ progress.value = 0;
118
+ progress.setAttribute("aria-label", c.title);
119
+ toolbar.append(sound, play, hint);
120
+ footer.append(toolbar, progress);
121
+ overlay.append(video, skip, footer);
122
+ document.body.append(overlay);
123
+ const previousFocus = document.activeElement, root = document.getElementById("root"), previousInert = root?.inert;
124
+ if (root) root.inert = true;
125
+ skip.focus();
126
+ const localeObserver = new MutationObserver(() => {
127
+ c = text();
128
+ skip.textContent = c.skip;
129
+ sound.textContent = video.muted ? c.sound : c.mute;
130
+ play.textContent = c.play;
131
+ hint.textContent = video.currentTime > 0 ? c.hint : c.loading;
132
+ overlay.setAttribute("aria-label", c.title);
133
+ progress.setAttribute("aria-label", c.title);
134
+ });
135
+ localeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ["lang"] });
136
+ let leaving = false, finished = false, frame = 0, playTimer, loadTimer, resolve;
137
+ const done = new Promise((r) => resolve = r);
138
+ function cleanup() {
139
+ if (finished) return;
140
+ finished = true;
141
+ clearTimeout(loadTimer);
142
+ clearTimeout(playTimer);
143
+ cancelAnimationFrame(frame);
144
+ localeObserver.disconnect();
145
+ document.removeEventListener("keydown", key, true);
146
+ video.pause();
147
+ video.removeAttribute("src");
148
+ video.load();
149
+ overlay.remove();
150
+ style.remove();
151
+ if (root) root.inert = previousInert;
152
+ if (previousFocus?.isConnected && previousFocus !== document.body) previousFocus.focus({ preventScroll: true });
153
+ resolve();
154
+ }
155
+ function leave(ms = 800) {
156
+ if (leaving) return;
157
+ leaving = true;
158
+ clearTimeout(loadTimer);
159
+ clearTimeout(playTimer);
160
+ cancelAnimationFrame(frame);
161
+ const start = performance.now(), volume = video.volume;
162
+ const fade = () => {
163
+ if (finished) return;
164
+ const p = Math.min(1, (performance.now() - start) / ms);
165
+ overlay.style.opacity = String(1 - p);
166
+ video.volume = volume * (1 - p);
167
+ if (p === 1) cleanup();
168
+ else frame = requestAnimationFrame(fade);
169
+ };
170
+ frame = requestAnimationFrame(fade);
171
+ playTimer = setTimeout(cleanup, ms + 150);
172
+ }
173
+ function key(e) {
174
+ if (e.key === "Escape") {
175
+ e.preventDefault();
176
+ e.stopPropagation();
177
+ leave(250);
178
+ }
179
+ if (e.key === "Tab") {
180
+ const buttons = [skip, sound, ...play.hidden ? [] : [play]], index = buttons.indexOf(document.activeElement);
181
+ e.preventDefault();
182
+ buttons[(index + (e.shiftKey ? -1 : 1) + buttons.length) % buttons.length].focus();
183
+ }
184
+ }
185
+ document.addEventListener("keydown", key, true);
186
+ skip.onclick = (e) => {
187
+ e.stopPropagation();
188
+ leave(250);
189
+ };
190
+ overlay.onclick = (e) => {
191
+ if (!e.target.closest("button")) leave(250);
192
+ };
193
+ sound.onclick = async (e) => {
194
+ e.stopPropagation();
195
+ video.muted = !video.muted;
196
+ try {
197
+ await video.play();
198
+ } catch {
199
+ play.hidden = false;
200
+ }
201
+ sound.textContent = video.muted ? c.sound : c.mute;
202
+ };
203
+ play.onclick = async (e) => {
204
+ e.stopPropagation();
205
+ try {
206
+ await video.play();
207
+ play.hidden = true;
208
+ } catch {
209
+ video.muted = true;
210
+ sound.textContent = c.sound;
211
+ try {
212
+ await video.play();
213
+ play.hidden = true;
214
+ } catch {
215
+ leave(200);
216
+ }
217
+ }
218
+ };
219
+ video.addEventListener("ended", () => leave());
220
+ video.addEventListener("error", () => {
221
+ if (finished || leaving) return;
222
+ if (sourceIndex + 1 < sources.length) {
223
+ clearTimeout(loadTimer);
224
+ loadTimer = setTimeout(() => leave(200), 12e3);
225
+ video.src = sources[++sourceIndex];
226
+ video.load();
227
+ void startPlayback();
228
+ } else leave(200);
229
+ });
230
+ video.addEventListener("playing", () => {
231
+ clearTimeout(loadTimer);
232
+ play.hidden = true;
233
+ if (!playTimer) playTimer = setTimeout(() => leave(), Math.max(5, Math.min(60, options.maxSeconds || 30)) * 1e3);
234
+ hint.textContent = c.hint;
235
+ });
236
+ video.addEventListener("timeupdate", () => {
237
+ if (Number.isFinite(video.duration) && video.duration > 0) progress.value = Math.min(1, video.currentTime / video.duration);
238
+ });
239
+ loadTimer = setTimeout(() => leave(200), 12e3);
240
+ async function startPlayback() {
241
+ try {
242
+ await video.play();
243
+ } catch {
244
+ if (finished || leaving) return;
245
+ video.muted = true;
246
+ sound.textContent = c.sound;
247
+ try {
248
+ await video.play();
249
+ } catch {
250
+ if (!finished && !leaving) play.hidden = false;
251
+ }
252
+ }
253
+ }
254
+ void startPlayback();
255
+ return done;
256
+ }
257
+
258
+ // src/video-settings.jsx
259
+ var API = "./api/ekko-skins/";
260
+ var LIMIT = 96 * 1024 * 1024;
261
+ var fields = (entry) => Object.fromEntries(["enabled", "sound", "volume", "fit", "maxSeconds"].map((k) => [k, entry[k]]));
262
+ var draftFrom = (entry) => ({ ...fields(entry), mobileFit: entry.mobile?.fit || entry.fit });
263
+ async function request(path, options = {}) {
264
+ const response = await fetch(API + path, { credentials: "same-origin", ...options });
265
+ let data;
266
+ try {
267
+ data = await response.json();
268
+ } catch {
269
+ throw Error(response.status === 401 ? "\u767B\u5F55\u5DF2\u5931\u6548\uFF0C\u8BF7\u5237\u65B0\u540E\u91CD\u65B0\u767B\u5F55\u3002" : "\u65E0\u6CD5\u8BFB\u53D6\u89C6\u9891\u8BBE\u7F6E\u3002");
270
+ }
271
+ if (!response.ok) throw Error(data.message || "\u89C6\u9891\u64CD\u4F5C\u5931\u8D25\u3002");
272
+ return data;
273
+ }
274
+ var jsonBody = (data) => ({ headers: { "content-type": "application/json" }, body: JSON.stringify(data) });
275
+ function previewError(video) {
276
+ const code = video.error?.code;
277
+ if (code === 2) return "\u89C6\u9891\u9884\u89C8\u52A0\u8F7D\u5931\u8D25\uFF0C\u8BF7\u68C0\u67E5\u7F51\u7EDC\u540E\u91CD\u8BD5\u3002\u5DF2\u4FDD\u5B58\u7684\u89C6\u9891\u4E0D\u4F1A\u88AB\u5220\u9664\u3002";
278
+ if (code === 3 || code === 4) return "\u5F53\u524D\u6D4F\u89C8\u5668\u6682\u65F6\u65E0\u6CD5\u64AD\u653E\u6B64\u89C6\u9891\u3002\u6587\u4EF6\u4ECD\u53EF\u4E0A\u4F20\u548C\u4FDD\u5B58\uFF0C\u53EF\u6362\u6D4F\u89C8\u5668\u9884\u89C8\uFF1B\u82E5\u542F\u52A8\u65F6\u4E5F\u65E0\u6CD5\u64AD\u653E\uFF0C\u8BF7\u8F6C\u6362\u4E3A\u517C\u5BB9\u7684 H.264 MP4 \u6216 WebM\u3002";
279
+ return "\u5F53\u524D\u9884\u89C8\u4E0D\u53EF\u7528\uFF0C\u4E0D\u5F71\u54CD\u4E0A\u4F20\u548C\u4FDD\u5B58\u89C6\u9891\u3002";
280
+ }
281
+ function VideoSettings({ skin }) {
282
+ const [data, setData] = (0, import_react.useState)(null), [draft, setDraft] = (0, import_react.useState)(null), [message, setMessage] = (0, import_react.useState)(""), [busy, setBusy] = (0, import_react.useState)(false), [progress, setProgress] = (0, import_react.useState)(null);
283
+ const [device, setDevice] = (0, import_react.useState)(() => window.matchMedia?.(MOBILE_SCREEN).matches ? "mobile" : "desktop"), [mediaError, setMediaError] = (0, import_react.useState)(""), [sourceIndex, setSourceIndex] = (0, import_react.useState)(0);
284
+ const preview = (0, import_react.useRef)(null), operation = (0, import_react.useRef)(null), uploadId = (0, import_react.useRef)(null), alive = (0, import_react.useRef)(true), input = (0, import_react.useRef)(null);
285
+ const accept = (value, preserve = false) => {
286
+ if (!alive.current) return;
287
+ setData(value);
288
+ if (!preserve) setDraft(draftFrom(value.skins[skin]));
289
+ };
290
+ const load = async () => {
291
+ try {
292
+ accept(await request("boot"));
293
+ } catch (e) {
294
+ if (alive.current) setMessage(e.message);
295
+ }
296
+ };
297
+ const base = data?.skins[skin], entry = selectBootVideo(base, device === "mobile");
298
+ const sources = videoSources(entry, Boolean(document.createElement("video").canPlayType('video/webm; codecs="vp8, opus"'))), source = sources[sourceIndex] || sources[0];
299
+ (0, import_react.useEffect)(() => {
300
+ alive.current = true;
301
+ void load();
302
+ return () => {
303
+ alive.current = false;
304
+ operation.current?.abort();
305
+ if (uploadId.current) void request("upload?action=cancel&id=" + uploadId.current, { method: "POST", ...jsonBody({}) }).catch(() => {
306
+ });
307
+ };
308
+ }, [skin]);
309
+ (0, import_react.useEffect)(() => {
310
+ setSourceIndex(0);
311
+ setMediaError("");
312
+ }, [entry?.url, entry?.fallbackUrl, device]);
313
+ (0, import_react.useEffect)(() => {
314
+ const video = preview.current;
315
+ return () => {
316
+ if (video) {
317
+ video.pause();
318
+ video.removeAttribute("src");
319
+ video.load();
320
+ }
321
+ };
322
+ }, [source, device]);
323
+ (0, import_react.useEffect)(() => {
324
+ if (preview.current) preview.current.volume = draft?.volume ?? 0.65;
325
+ }, [draft?.volume, source, device]);
326
+ const save = async (reset = false) => {
327
+ setBusy(true);
328
+ setMessage("");
329
+ try {
330
+ const patch = reset ? { reset: true } : { ...fields(draft), mobile: { fit: draft.mobileFit } };
331
+ accept(await request("boot-settings", { method: "POST", ...jsonBody({ skin, device, revision: data.revision, patch }) }), reset);
332
+ setMessage(reset ? device === "mobile" ? "\u624B\u673A\u7AEF\u5DF2\u6539\u4E3A\u6CBF\u7528\u7535\u8111\u7AEF\u89C6\u9891\u3002" : "\u7535\u8111\u7AEF\u5DF2\u6062\u590D\u9ED8\u8BA4\u542F\u52A8\u89C6\u9891\u3002" : "\u542F\u52A8\u89C6\u9891\u8BBE\u7F6E\u5DF2\u4FDD\u5B58\u3002");
333
+ } catch (e) {
334
+ setMessage(e.message);
335
+ await load();
336
+ } finally {
337
+ if (alive.current) setBusy(false);
338
+ }
339
+ };
340
+ const upload = async (file) => {
341
+ if (!file) return;
342
+ setMessage("");
343
+ if (!/\.(mp4|webm)$/i.test(file.name) || file.size > LIMIT || file.size < 16) {
344
+ setMessage("\u8BF7\u9009\u62E9\u4E0D\u8D85\u8FC7 96 MiB \u7684 MP4 \u6216 WebM \u89C6\u9891\u3002");
345
+ return;
346
+ }
347
+ const controller = new AbortController();
348
+ operation.current = controller;
349
+ setBusy(true);
350
+ setProgress(0);
351
+ try {
352
+ const start = await request("upload", { method: "POST", signal: controller.signal, ...jsonBody({ skin, device, size: file.size, name: file.name, revision: data.revision }) });
353
+ uploadId.current = start.id;
354
+ for (let offset = 0; offset < file.size; offset += start.chunkSize) {
355
+ const chunk = file.slice(offset, offset + start.chunkSize);
356
+ await request("upload?action=chunk&id=" + start.id + "&offset=" + offset, { method: "POST", signal: controller.signal, headers: { "content-type": "application/octet-stream" }, body: chunk });
357
+ if (alive.current) setProgress(Math.round(Math.min(file.size, offset + chunk.size) / file.size * 100));
358
+ }
359
+ const result = await request("upload?action=complete&id=" + start.id, { method: "POST", signal: controller.signal, ...jsonBody({}) });
360
+ uploadId.current = null;
361
+ accept(result, true);
362
+ if (alive.current) setMessage((device === "mobile" ? "\u624B\u673A\u7AEF" : "\u7535\u8111\u7AEF") + "\u89C6\u9891\u5DF2\u4E0A\u4F20\u5E76\u4FDD\u5B58\uFF0C\u5237\u65B0\u9875\u9762\u65F6\u4F7F\u7528\u3002");
363
+ } catch (e) {
364
+ if (alive.current) setMessage(controller.signal.aborted ? "\u5DF2\u53D6\u6D88\u4E0A\u4F20\u3002" : e.message);
365
+ } finally {
366
+ if (uploadId.current) {
367
+ const id = uploadId.current;
368
+ uploadId.current = null;
369
+ void request("upload?action=cancel&id=" + id, { method: "POST", ...jsonBody({}) }).catch(() => {
370
+ });
371
+ }
372
+ operation.current = null;
373
+ if (alive.current) {
374
+ setBusy(false);
375
+ setProgress(null);
376
+ if (input.current) input.current.value = "";
377
+ }
378
+ }
379
+ };
380
+ if (!data || !draft) return /* @__PURE__ */ import_react.default.createElement("section", { className: "ekko-skin-options ekko-video-settings", "data-testid": "ekko-video-settings" }, /* @__PURE__ */ import_react.default.createElement("h3", null, "\u542F\u52A8\u89C6\u9891"), /* @__PURE__ */ import_react.default.createElement("p", { role: "status" }, message || "\u6B63\u5728\u8BFB\u53D6\u89C6\u9891\u8BBE\u7F6E\u2026"), /* @__PURE__ */ import_react.default.createElement("button", { type: "button", className: "ekko-skin-button", onClick: load }, "\u91CD\u8BD5"));
381
+ const update = (patch) => setDraft({ ...draft, ...patch }), fitKey = device === "mobile" ? "mobileFit" : "fit";
382
+ return /* @__PURE__ */ import_react.default.createElement("section", { className: "ekko-skin-options ekko-video-settings", "data-testid": "ekko-video-settings", "data-device": device }, /* @__PURE__ */ import_react.default.createElement("div", { className: "ekko-skin-toolbar" }, /* @__PURE__ */ import_react.default.createElement("h3", null, skin === "miku" ? "\u521D\u97F3\u672A\u6765" : "\u6709\u83DC\u73AB\u7470", " \xB7 \u542F\u52A8\u89C6\u9891"), /* @__PURE__ */ import_react.default.createElement("span", { className: "ekko-skin-version" }, "\u6BCF\u6B3E\u4E3B\u9898\u5206\u522B\u4FDD\u5B58")), /* @__PURE__ */ import_react.default.createElement("div", { className: "ekko-video-controls" }, /* @__PURE__ */ import_react.default.createElement("label", { className: "ekko-skin-option" }, /* @__PURE__ */ import_react.default.createElement("span", null, "\u542F\u7528\u542F\u52A8\u89C6\u9891", /* @__PURE__ */ import_react.default.createElement("small", null, "\u9875\u9762\u542F\u52A8\u65F6\u64AD\u653E\uFF0C\u53EF\u968F\u65F6\u8DF3\u8FC7")), /* @__PURE__ */ import_react.default.createElement("input", { type: "checkbox", checked: draft.enabled, disabled: busy, onChange: (e) => update({ enabled: e.target.checked }) })), /* @__PURE__ */ import_react.default.createElement("label", { className: "ekko-skin-option" }, /* @__PURE__ */ import_react.default.createElement("span", null, "\u64AD\u653E\u58F0\u97F3", /* @__PURE__ */ import_react.default.createElement("small", null, "\u6D4F\u89C8\u5668\u9650\u5236\u81EA\u52A8\u64AD\u653E\u65F6\u5148\u9759\u97F3\uFF0C\u70B9\u51FB\u53EF\u5F00\u542F\u58F0\u97F3")), /* @__PURE__ */ import_react.default.createElement("input", { type: "checkbox", checked: draft.sound, disabled: busy, onChange: (e) => update({ sound: e.target.checked }) })), /* @__PURE__ */ import_react.default.createElement("label", { className: "ekko-skin-option" }, /* @__PURE__ */ import_react.default.createElement("span", null, "\u97F3\u91CF", /* @__PURE__ */ import_react.default.createElement("small", null, Math.round(draft.volume * 100), "%")), /* @__PURE__ */ import_react.default.createElement("input", { type: "range", min: "0", max: "100", value: Math.round(draft.volume * 100), disabled: busy, onChange: (e) => update({ volume: Number(e.target.value) / 100 }) })), /* @__PURE__ */ import_react.default.createElement("label", { className: "ekko-skin-option" }, /* @__PURE__ */ import_react.default.createElement("span", null, "\u6700\u957F\u64AD\u653E\u65F6\u95F4", /* @__PURE__ */ import_react.default.createElement("small", null, "5\u201360 \u79D2\uFF0C\u5230\u65F6\u81EA\u52A8\u8FDB\u5165\u754C\u9762")), /* @__PURE__ */ import_react.default.createElement("input", { className: "ekko-video-seconds", type: "number", min: "5", max: "60", value: draft.maxSeconds, disabled: busy, onChange: (e) => update({ maxSeconds: Number(e.target.value) }) }))), /* @__PURE__ */ import_react.default.createElement("div", { className: "ekko-video-device-tabs", role: "tablist", "aria-label": "\u542F\u52A8\u89C6\u9891\u5C4F\u5E55\u7C7B\u578B" }, ["desktop", "mobile"].map((value) => /* @__PURE__ */ import_react.default.createElement("button", { key: value, type: "button", role: "tab", id: "ekko-video-tab-" + skin + "-" + value, "aria-selected": device === value, "aria-controls": "ekko-video-panel-" + skin, disabled: busy, className: "ekko-skin-button", "data-active": device === value, onClick: () => {
383
+ setDevice(value);
384
+ setMessage("");
385
+ } }, value === "mobile" ? "\u624B\u673A\u7AEF\u89C6\u9891" : "\u7535\u8111\u7AEF\u89C6\u9891"))), /* @__PURE__ */ import_react.default.createElement("div", { className: "ekko-video-device-panel", role: "tabpanel", id: "ekko-video-panel-" + skin, "aria-labelledby": "ekko-video-tab-" + skin + "-" + device }, /* @__PURE__ */ import_react.default.createElement("p", { className: "ekko-skin-version" }, device === "mobile" ? "\u624B\u673A\u53CA\u7A84\u5C4F\u81EA\u52A8\u4F7F\u7528\u6B64\u89C6\u9891\uFF0C\u63A8\u8350 9:16 \u7AD6\u5C4F\uFF1B\u672A\u4E0A\u4F20\u65F6\u6CBF\u7528\u7535\u8111\u7AEF\u89C6\u9891\u3002" : "\u7535\u8111\u53CA\u5BBD\u5C4F\u4F7F\u7528\u6B64\u89C6\u9891\uFF0C\u63A8\u8350 16:9 \u6A2A\u5C4F\u3002", "\u652F\u6301\u5176\u4ED6\u5206\u8FA8\u7387\u4E0E\u6BD4\u4F8B\u3002"), /* @__PURE__ */ import_react.default.createElement("label", { className: "ekko-skin-option" }, /* @__PURE__ */ import_react.default.createElement("span", null, device === "mobile" ? "\u624B\u673A\u7AEF" : "\u7535\u8111\u7AEF", "\u753B\u9762\u663E\u793A"), /* @__PURE__ */ import_react.default.createElement("select", { "aria-label": (device === "mobile" ? "\u624B\u673A\u7AEF" : "\u7535\u8111\u7AEF") + "\u753B\u9762\u663E\u793A", value: draft[fitKey], disabled: busy, onChange: (e) => update({ [fitKey]: e.target.value }) }, /* @__PURE__ */ import_react.default.createElement("option", { value: "cover" }, "\u94FA\u6EE1\u5C4F\u5E55"), /* @__PURE__ */ import_react.default.createElement("option", { value: "contain" }, "\u5B8C\u6574\u663E\u793A"))), /* @__PURE__ */ import_react.default.createElement("video", { key: device + source, ref: preview, className: "ekko-video-preview", "data-device": device, muted: !draft.sound, controls: true, playsInline: true, preload: "metadata", onLoadedMetadata: () => setMediaError(""), onError: (e) => {
386
+ if (sourceIndex + 1 < sources.length) {
387
+ setSourceIndex(sourceIndex + 1);
388
+ setMediaError("\u6B63\u5728\u5C1D\u8BD5\u53E6\u4E00\u79CD\u89C6\u9891\u7F16\u7801\u2026");
389
+ } else setMediaError(previewError(e.currentTarget));
390
+ }, src: source, "aria-label": (device === "mobile" ? "\u624B\u673A\u7AEF" : "\u7535\u8111\u7AEF") + "\u542F\u52A8\u89C6\u9891\u9884\u89C8" }), /* @__PURE__ */ import_react.default.createElement("p", { className: "ekko-video-name" }, entry.source === "upload" ? entry.name : entry.source === "fallback" ? "\u6CBF\u7528\u7535\u8111\u7AEF\u89C6\u9891" + (base.name ? " \xB7 " + base.name : "") : "\u9ED8\u8BA4\u542F\u52A8\u89C6\u9891"), mediaError && /* @__PURE__ */ import_react.default.createElement("p", { className: "ekko-video-message", role: "status" }, mediaError)), /* @__PURE__ */ import_react.default.createElement("p", { className: "ekko-skin-version" }, "\u652F\u6301 MP4 / WebM\uFF0C\u6BCF\u4E2A\u89C6\u9891\u6700\u5927 96 MiB\u3002\u7535\u8111\u7AEF\u4E0E\u624B\u673A\u7AEF\u5206\u522B\u4FDD\u5B58\uFF0C\u53EF\u5728\u5F53\u524D DSH \u7684\u5176\u4ED6\u8BBE\u5907\u4E0A\u4F7F\u7528\u3002"), /* @__PURE__ */ import_react.default.createElement("input", { ref: input, className: "ekko-video-file", tabIndex: "-1", type: "file", accept: "video/mp4,video/webm,.mp4,.webm", disabled: busy, "aria-label": "\u4E0A\u4F20" + (device === "mobile" ? "\u624B\u673A\u7AEF" : "\u7535\u8111\u7AEF") + "\u542F\u52A8\u89C6\u9891", onChange: (e) => void upload(e.target.files?.[0]) }), progress !== null && /* @__PURE__ */ import_react.default.createElement("div", { role: "status" }, /* @__PURE__ */ import_react.default.createElement("progress", { max: "100", value: progress }), /* @__PURE__ */ import_react.default.createElement("span", null, " \u4E0A\u4F20 ", progress, "%"), /* @__PURE__ */ import_react.default.createElement("button", { type: "button", className: "ekko-skin-button", onClick: () => operation.current?.abort() }, "\u53D6\u6D88\u4E0A\u4F20")), /* @__PURE__ */ import_react.default.createElement("div", { className: "ekko-video-actions" }, /* @__PURE__ */ import_react.default.createElement("button", { type: "button", className: "ekko-skin-button", disabled: busy, onClick: () => input.current?.click() }, "\u4E0A\u4F20", device === "mobile" ? "\u624B\u673A\u7AEF" : "\u7535\u8111\u7AEF", "\u89C6\u9891"), /* @__PURE__ */ import_react.default.createElement("button", { type: "button", className: "ekko-skin-button", disabled: busy, onClick: () => void playBoot(skin, { ...entry, ...fields(draft), fit: draft[fitKey] }, { preview: true }) }, "\u9884\u89C8\u542F\u52A8\u6548\u679C"), /* @__PURE__ */ import_react.default.createElement("button", { type: "button", className: "ekko-skin-button", disabled: busy || entry.source !== "upload", onClick: () => void save(true) }, device === "mobile" ? "\u6CBF\u7528\u7535\u8111\u7AEF\u89C6\u9891" : "\u6062\u590D\u9ED8\u8BA4\u89C6\u9891"), /* @__PURE__ */ import_react.default.createElement("button", { type: "button", className: "ekko-skin-button", "data-active": "true", disabled: busy, onClick: () => void save() }, "\u4FDD\u5B58\u542F\u52A8\u8BBE\u7F6E")), /* @__PURE__ */ import_react.default.createElement("p", { role: "status", className: "ekko-video-message" }, message));
391
+ }
392
+
393
+ // src/controller.mjs
40
394
  var SELECTION_KEY = "dsh:ekko-skins:selection:v1";
41
395
  var SKIN_IDS = ["default", "rose", "miku"];
42
396
  function readSelection(storage, fallback = "rose") {
@@ -160,10 +514,196 @@ function createController({ storage, mount, prepare = async () => {
160
514
  };
161
515
  }
162
516
 
163
- // work/dsh-plugin-ekko-skin-manager/src/generated.mjs
164
- var STYLES = ".ekko-skins{display:grid;gap:18px;color:var(--dsw-alias-label-primary);font:14px/1.6 var(--ds-font-family,system-ui);}\n.ekko-skins h2,.ekko-skins h3,.ekko-skins p{margin:0;}.ekko-skins h2{font-size:20px;line-height:1.5;}.ekko-skins h3{font-size:15px;}\n.ekko-skins-intro{display:grid;gap:5px;}.ekko-skins-intro p,.ekko-skin-version,.ekko-skin-option small{color:var(--dsw-alias-label-secondary);font-size:12px;}\n.ekko-skin-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px;}\n.ekko-skin-card{min-width:0;overflow:hidden;border:1px solid var(--dsw-alias-border-l2);border-radius:16px;background:var(--dsw-alias-bg-layer-1);box-shadow:0 4px 12px #234c6510;}\n.ekko-skin-card[data-active='true']{border-color:var(--dsw-alias-brand-primary);box-shadow:0 0 0 2px color-mix(in srgb,var(--dsw-alias-brand-primary) 18%,transparent);}\n.ekko-skin-cover{display:block;width:100%;aspect-ratio:1400/787;object-fit:cover;border-bottom:1px solid var(--dsw-alias-border-l3);}\n.ekko-skin-description{display:grid;gap:10px;padding:13px;}.ekko-skin-title{display:flex;align-items:center;justify-content:space-between;gap:8px;}.ekko-skin-state{color:var(--dsw-alias-brand-text);font-size:11px;white-space:nowrap;}\n.ekko-skin-button{border:1px solid var(--dsw-alias-border-l2);border-radius:9px;padding:7px 12px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font:inherit;cursor:pointer;}\n.ekko-skin-button:disabled{opacity:.65;cursor:default;}.ekko-skin-button:focus-visible,.ekko-skins input:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:3px;}\n.ekko-skin-button[data-active='true']{background:var(--dsw-alias-brand-primary);border-color:var(--dsw-alias-brand-primary);color:var(--dsw-alias-brand-primary-invert);}\n.ekko-skin-toolbar{display:flex;align-items:center;justify-content:space-between;gap:12px;}.ekko-skin-options{padding:15px;border:1px solid var(--dsw-alias-border-l2);border-radius:14px;background:var(--dsw-alias-bg-layer-1);}\n.ekko-skin-option{display:flex;align-items:center;justify-content:space-between;gap:18px;min-height:40px;padding:8px 0;border-bottom:1px solid var(--dsw-alias-border-l3);}.ekko-skin-option:last-child{border:0;}.ekko-skin-option span{display:grid;gap:2px;}.ekko-skin-option input{accent-color:var(--dsw-alias-brand-primary);}.ekko-skin-option input[type='range']{width:130px;max-width:40%;}.ekko-skin-error{padding:10px 12px;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;color:var(--dsw-alias-state-danger,#c43d3d);font-size:13px;}\n@media(max-width:640px){.ekko-skin-grid{grid-template-columns:1fr;}.ekko-skins h2{font-size:18px;}.ekko-skin-toolbar{align-items:flex-start;}.ekko-skin-description{padding:12px;}}\n/* Keep the native settings shell usable on phones: tabs above the content. */\n@media(max-width:640px){\n [role='dialog']:has(>[class*='_nav']>[class*='_navList']){flex-direction:column;width:calc(100vw - 24px);max-width:calc(100vw - 24px);height:min(800px,calc(100dvh - 48px));}\n [role='dialog']:has(>[class*='_nav']>[class*='_navList'])>[class*='_nav']{width:100%;padding:15px 12px 0;gap:12px;}\n [role='dialog']:has(>[class*='_nav']>[class*='_navList']) [class*='_navTitle']{padding-inline:5px;}\n [role='dialog']:has(>[class*='_nav']>[class*='_navList']) [class*='_navList']{flex-direction:row;gap:4px;overflow:auto;padding-bottom:5px;}\n [role='dialog']:has(>[class*='_nav']>[class*='_navList']) [class*='_navCell']{flex:none;width:max-content;height:36px;padding:7px 10px;font-size:13px;}\n [role='dialog']:has(>[class*='_nav']>[class*='_navList'])>[class*='_content']{min-height:0;width:100%;}\n [role='dialog']:has(>[class*='_nav']>[class*='_navList']) [class*='_header']{position:absolute;top:10px;right:10px;height:auto;padding:0;}\n [role='dialog']:has(>[class*='_nav']>[class*='_navList']) [class*='_options']{padding:12px 12px 18px;min-width:0;}\n .ekko-skins{gap:13px;width:100%;min-width:0;}\n .ekko-skin-grid{grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;}\n .ekko-skin-description{padding:9px;gap:7px;min-width:0;}.ekko-skin-title{gap:4px;flex-wrap:wrap;}.ekko-skin-title h3{font-size:13px;}.ekko-skin-state{font-size:10px;}\n .ekko-skin-button{font-size:12px;padding:6px;min-height:36px;max-width:100%;overflow-wrap:anywhere;}\n .ekko-skin-toolbar{flex-wrap:wrap;min-width:0;}.ekko-skin-options{min-width:0;padding:11px;}.ekko-skin-options .ekko-skin-toolbar h3{font-size:13px;}\n}\n";
517
+ // src/generated.mjs
518
+ var STYLES = ".ekko-skins{display:grid;gap:18px;color:var(--dsw-alias-label-primary);font:14px/1.6 var(--ds-font-family,system-ui);}\n.ekko-skins h2,.ekko-skins h3,.ekko-skins p{margin:0;}.ekko-skins h2{font-size:20px;line-height:1.5;}.ekko-skins h3{font-size:15px;}\n.ekko-skins-intro{display:grid;gap:5px;}.ekko-skins-intro p,.ekko-skin-version,.ekko-skin-option small{color:var(--dsw-alias-label-secondary);font-size:12px;}\n.ekko-skin-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px;}\n.ekko-skin-card{min-width:0;overflow:hidden;border:1px solid var(--dsw-alias-border-l2);border-radius:16px;background:var(--dsw-alias-bg-layer-1);box-shadow:0 4px 12px #234c6510;}\n.ekko-skin-card[data-active='true']{border-color:var(--dsw-alias-brand-primary);box-shadow:0 0 0 2px color-mix(in srgb,var(--dsw-alias-brand-primary) 18%,transparent);}\n.ekko-skin-cover{display:block;width:100%;aspect-ratio:1400/787;object-fit:cover;border-bottom:1px solid var(--dsw-alias-border-l3);}\n.ekko-skin-description{display:grid;gap:10px;padding:13px;}.ekko-skin-title{display:flex;align-items:center;justify-content:space-between;gap:8px;}.ekko-skin-state{color:var(--dsw-alias-brand-text);font-size:11px;white-space:nowrap;}\n.ekko-skin-button{border:1px solid var(--dsw-alias-border-l2);border-radius:9px;padding:7px 12px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font:inherit;cursor:pointer;}\n.ekko-skin-button:disabled{opacity:.65;cursor:default;}.ekko-skin-button:focus-visible,.ekko-skins input:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:3px;}\n.ekko-skin-button[data-active='true']{background:var(--dsw-alias-brand-primary);border-color:var(--dsw-alias-brand-primary);color:var(--dsw-alias-brand-primary-invert);}\n.ekko-skin-option select{min-height:40px;max-width:52%;padding:6px 9px;border:1px solid var(--dsw-alias-border-l2);border-radius:9px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font:inherit;}\n.ekko-skin-option select:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:3px;}\n.ekko-skin-toolbar{display:flex;align-items:center;justify-content:space-between;gap:12px;}.ekko-skin-options{padding:15px;border:1px solid var(--dsw-alias-border-l2);border-radius:14px;background:var(--dsw-alias-bg-layer-1);}\n.ekko-skin-option{display:flex;align-items:center;justify-content:space-between;gap:18px;min-height:40px;padding:8px 0;border-bottom:1px solid var(--dsw-alias-border-l3);}.ekko-skin-option:last-child{border:0;}.ekko-skin-option span{display:grid;gap:2px;}.ekko-skin-option input{accent-color:var(--dsw-alias-brand-primary);}.ekko-skin-option input[type='range']{width:130px;max-width:40%;}.ekko-skin-error{padding:10px 12px;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;color:var(--dsw-alias-state-danger,#c43d3d);font-size:13px;}\n@media(max-width:640px){.ekko-skin-grid{grid-template-columns:1fr;}.ekko-skins h2{font-size:18px;}.ekko-skin-toolbar{align-items:flex-start;}.ekko-skin-description{padding:12px;}}\n/* Keep the native settings shell usable on phones: tabs above the content. */\n@media(max-width:640px){\n [role='dialog']:has(>[class*='_nav']>[class*='_navList']){flex-direction:column;width:calc(100vw - 24px);max-width:calc(100vw - 24px);height:min(800px,calc(100dvh - 48px));}\n [role='dialog']:has(>[class*='_nav']>[class*='_navList'])>[class*='_nav']{width:100%;padding:15px 12px 0;gap:12px;}\n [role='dialog']:has(>[class*='_nav']>[class*='_navList']) [class*='_navTitle']{padding-inline:5px;}\n [role='dialog']:has(>[class*='_nav']>[class*='_navList']) [class*='_navList']{flex-direction:row;gap:4px;overflow:auto;padding-bottom:5px;}\n [role='dialog']:has(>[class*='_nav']>[class*='_navList']) [class*='_navCell']{flex:none;width:max-content;height:36px;padding:7px 10px;font-size:13px;}\n [role='dialog']:has(>[class*='_nav']>[class*='_navList'])>[class*='_content']{min-height:0;width:100%;}\n [role='dialog']:has(>[class*='_nav']>[class*='_navList'])>[class*='_content']>[class*='_header']{position:absolute;top:10px;right:10px;height:auto;padding:0;}\n [role='dialog']:has(>[class*='_nav']>[class*='_navList'])>[class*='_content']>[class*='_header'] button{white-space:nowrap;flex-shrink:0;}\n [role='dialog']:has(>[class*='_nav']>[class*='_navList']) [class*='_options']{padding:12px 12px 18px;min-width:0;}\n .ekko-skins{gap:13px;width:100%;min-width:0;}\n .ekko-skin-grid{grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;}\n .ekko-skin-description{padding:9px;gap:7px;min-width:0;}.ekko-skin-title{gap:4px;flex-wrap:wrap;}.ekko-skin-title h3{font-size:13px;}.ekko-skin-state{font-size:10px;}\n .ekko-skin-button{font-size:12px;padding:6px;min-height:36px;max-width:100%;overflow-wrap:anywhere;}\n .ekko-skin-toolbar{flex-wrap:wrap;min-width:0;}.ekko-skin-options{min-width:0;padding:11px;}.ekko-skin-options .ekko-skin-toolbar h3{font-size:13px;}\n}\n\n.ekko-video-settings{display:grid;gap:10px;min-width:0}.ekko-video-settings .ekko-skin-option{margin:0}.ekko-video-preview{display:block;width:100%;max-height:160px;border:1px solid var(--dsw-alias-border-l2);border-radius:12px;background:#101820;object-fit:contain}.ekko-video-name{font-weight:500;overflow-wrap:anywhere}.ekko-video-file{position:absolute;width:1px;height:1px;opacity:0;overflow:hidden;pointer-events:none}.ekko-video-file::file-selector-button{font:inherit;padding:8px 12px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);margin-right:8px}.ekko-video-actions{display:flex;gap:8px;flex-wrap:wrap}.ekko-video-seconds{width:72px;padding:7px;border-radius:8px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-base);color:inherit;font:inherit}.ekko-video-settings progress{max-width:100%;accent-color:var(--dsw-alias-brand-primary)}.ekko-video-message{font-size:12px;overflow-wrap:anywhere}\n\n.ekko-video-controls{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:0 18px;min-width:0}.ekko-video-controls .ekko-skin-option{min-width:0;gap:10px}.ekko-video-controls .ekko-skin-option:last-child{grid-column:1/-1}.ekko-video-controls .ekko-skin-option span{min-width:0}.ekko-video-controls input[type=range]{width:100px}.ekko-video-actions .ekko-skin-button{min-height:40px}.ekko-video-settings{scroll-margin-top:12px}\n@media(max-width:640px){.ekko-video-controls{grid-template-columns:1fr}.ekko-video-preview{max-height:140px}.ekko-video-actions{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}.ekko-video-actions .ekko-skin-button{width:100%;min-height:42px}.ekko-video-settings .ekko-skin-option small{font-size:11px}}\n\n.ekko-video-device-tabs{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:6px;padding:5px;border:1px solid var(--dsw-alias-border-l3);border-radius:13px;background:var(--dsw-alias-bg-layer-2)}\n.ekko-video-device-tabs .ekko-skin-button{min-height:40px;box-shadow:none;background:transparent;border-color:transparent}\n.ekko-video-device-tabs .ekko-skin-button[data-active='true']{background:var(--dsw-alias-brand-primary);border-color:var(--dsw-alias-brand-primary)}\n.ekko-video-device-panel{display:grid;gap:8px;min-width:0}\n.ekko-video-preview[data-device='mobile']{width:100%;height:210px;max-height:210px;object-fit:contain}\n.ekko-video-device-tabs .ekko-skin-button:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}\n.ekko-video-controls .ekko-skin-option:last-child{grid-column:auto}\n@media(max-width:640px){.ekko-video-preview[data-device='mobile']{height:200px;max-height:200px}.ekko-video-device-tabs{gap:4px;padding:4px}.ekko-video-device-tabs .ekko-skin-button{min-height:40px}}\n\n/* Both phone modes use the active skin's existing artwork and jewellery tokens. */\n.ekko-mobile-navigation{display:none;}\n@media(max-width:700px) and (orientation:portrait){\n body[data-ekko-mobile-nav] .ekko-mobile-navigation{position:fixed;z-index:65;display:flex;box-sizing:border-box;align-items:center;color:var(--dsw-alias-brand-primary);}\n body[data-ekko-mobile-nav] .ekko-mobile-brand,\n body[data-ekko-mobile-nav] .ekko-mobile-action{position:relative;display:flex;flex:none;align-items:center;justify-content:center;box-sizing:border-box;width:44px;height:44px;min-height:44px;padding:8px;border:1px solid var(--ekko-rim-soft);border-radius:14px;background:var(--ekko-panel);box-shadow:inset 0 1px 0 var(--ekko-shine),0 3px 8px var(--ekko-shadow);color:inherit;cursor:pointer;}\n body[data-ekko-mobile-nav] .ekko-mobile-brand{padding:6px;border-color:var(--ekko-rim);}\n body[data-ekko-mobile-nav] .ekko-mobile-brand img{display:block;width:31px;height:31px;object-fit:contain;}\n body[data-ekko-mobile-nav] .ekko-mobile-action :is(svg,img){width:21px;height:21px;flex:none;}\n body[data-ekko-mobile-nav] .ekko-mobile-action[data-ekko-mobile-action='new']{margin-top:4px;border-color:var(--ekko-rim);box-shadow:inset 0 0 0 2px var(--ekko-shine),0 3px 8px var(--ekko-shadow);}\n body[data-ekko-mobile-nav] .ekko-mobile-action[aria-current]{background:var(--ekko-tint);border-color:var(--ekko-rim);}\n body[data-ekko-mobile-nav] .ekko-mobile-action:disabled{opacity:.45;cursor:default;}\n body[data-ekko-mobile-nav] .ekko-mobile-navigation button:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px;}\n body[data-ekko-mobile-nav] .ekko-mobile-navigation button:active{box-shadow:inset 1px 2px 5px var(--ekko-shadow);}\n body[data-ekko-mobile-nav='corner'] .ekko-mobile-navigation{top:calc(env(safe-area-inset-top,0px) + 7px);left:9px;}\n body[data-ekko-mobile-nav='corner'] :is(.ekko-mobile-actions,.ekko-mobile-footer){display:none;}\n /* Set clearance on the content column so native drawer/platform frame rules cannot override it. */\n body[data-ekko-mobile-nav='corner'] [data-ekko-mobile-frame] [data-ekko-mobile-center]{--dsh-frame-leading-clearance:62px;}\n body[data-ekko-mobile-nav='corner'] :is(.miku-welcome-brand,.rose-welcome-brand){padding-left:48px;}\n body[data-ekko-mobile-nav='corner'] :is(.miku-mark,.rose-mark){display:none;}\n body[data-ekko-mobile-nav='rail'] .ekko-mobile-navigation{inset:0 auto 0 0;width:60px;flex-direction:column;gap:10px;padding:calc(env(safe-area-inset-top,0px) + 13px) 7px max(10px,env(safe-area-inset-bottom,0px));background:var(--ekko-panel);border-right:1px solid var(--ekko-rim);box-shadow:inset -2px 0 0 var(--ekko-shine),inset -4px 0 0 var(--ekko-rim-soft);}\n body[data-ekko-mobile-nav='rail'] .ekko-mobile-actions{display:flex;flex:1;min-height:0;flex-direction:column;gap:10px;padding:0 0 4px;overflow:auto;scrollbar-width:none;}\n body[data-ekko-mobile-nav='rail'] .ekko-mobile-footer{display:flex;flex:none;padding-top:5px;border-top:1px solid var(--ekko-rim-soft);}\n body[data-ekko-mobile-nav='rail'] [data-ekko-mobile-frame] [data-ekko-mobile-center]{margin-inline-start:60px;--dsh-frame-leading-clearance:0px;}\n /* The native drawer stays inert while closed. Only our launchers remain visible. */\n body[data-ekko-mobile-nav] [data-ekko-mobile-frame] [data-shell-leading]{display:none;}\n body[data-ekko-mobile-nav] [data-ekko-mobile-frame] [data-ekko-mobile-drawer]{z-index:80;background:var(--ekko-panel);box-shadow:12px 0 30px var(--ekko-shadow);}\n body[data-ekko-mobile-nav][data-ekko-mobile-drawer-open='true'] .ekko-mobile-navigation{visibility:hidden;pointer-events:none;}\n}\n\n/* Keep phone controls aligned when the icon rail reduces the editor width. */\n@media(max-width:700px) and (orientation:portrait){\n body[data-ekko-mobile-nav] [data-ekko-mobile-center] :is([data-miku-editor-root],[data-rose-editor-root]){width:calc(100% + 8px);margin-inline-start:-4px;}\n body[data-ekko-mobile-nav] [data-ekko-mobile-center] [data-composer-card]{container:ekko-composer / inline-size;gap:6px;padding-top:14px;}\n body[data-ekko-mobile-nav] [data-ekko-mobile-center] :is([data-miku-editor-root],[data-rose-editor-root]) [data-composer-card]{min-height:140px;}\n body[data-ekko-mobile-nav] [data-ekko-mobile-center] [data-composer-card]>[class*='_scroll']{flex:1;min-height:56px;margin-right:0;}\n body[data-ekko-mobile-nav] [data-ekko-mobile-center] [data-composer-card] [contenteditable]{padding-inline:12px;}\n body[data-ekko-mobile-nav] [data-ekko-mobile-center] [data-composer-card] button[class*='_primary']{align-self:center;transform:none;}\n}\n@container ekko-composer (max-width:350px){\n body[data-ekko-mobile-nav] [data-ekko-mobile-center] [data-composer-card]>[class*='_row']{display:grid;grid-template-columns:auto minmax(0,1fr) 30px 34px;grid-template-areas:'extras extras extras extras' 'tools model activity send';gap:4px 6px;padding:4px 7px 6px;align-items:center;}\n body[data-ekko-mobile-nav] [data-ekko-mobile-center] [data-composer-card]>[class*='_row']:not(:has([data-slot='conversation.input.right']:not(:empty))){grid-template-areas:'tools model activity send';}\n body[data-ekko-mobile-nav] [data-ekko-mobile-center] [data-composer-card] [class*='_tools']{grid-area:tools;gap:5px;align-items:center;}\n body[data-ekko-mobile-nav] [data-ekko-mobile-center] [data-composer-card] :is([class*='_trailing'],[class*='_standardControls'],[class*='_modes']){display:contents;}\n body[data-ekko-mobile-nav] [data-ekko-mobile-center] [data-composer-card] :is([class*='_tools'],[class*='_standardControls'])[hidden]{display:none!important;}\n body[data-ekko-mobile-nav] [data-ekko-mobile-center] [data-composer-card] [class*='_trailing']>[class*='_activity']{display:contents;}\n body[data-ekko-mobile-nav] [data-ekko-mobile-center] [data-slot='conversation.input.right']{grid-area:extras;display:flex!important;align-items:center;justify-content:flex-start;gap:8px;min-width:0;max-width:100%;overflow-x:auto;scrollbar-width:thin;}\n body[data-ekko-mobile-nav] [data-ekko-mobile-center] [data-slot='conversation.input.right']:empty{display:none!important;}\n body[data-ekko-mobile-nav] [data-ekko-mobile-center] [data-slot='conversation.input.right']>*{flex:none;}\n body[data-ekko-mobile-nav] [data-ekko-mobile-center] [data-slot='conversation.input.model']{grid-area:model;display:flex!important;min-width:0;justify-content:flex-end;align-items:center;}\n body[data-ekko-mobile-nav] [data-ekko-mobile-center] [data-slot='conversation.input.model']>div{min-width:0;max-width:100%;}\n body[data-ekko-mobile-nav] [data-ekko-mobile-center] [data-slot='conversation.input.model'] button{min-width:0;max-width:100%;height:34px;padding-inline:4px;}\n body[data-ekko-mobile-nav] [data-ekko-mobile-center] [data-slot='conversation.input.model'] :is([class*='_triggerLabel'],[class*='_triggerEffort']){display:none;}\n body[data-ekko-mobile-nav] [data-ekko-mobile-center] [data-slot='conversation.input.model'] button>svg:first-child{display:block;flex:none;width:18px;height:18px;}\n body[data-ekko-mobile-nav] [data-ekko-mobile-center] [data-slot='conversation.input.activity']{grid-area:activity;display:flex!important;align-items:center;justify-content:center;}\n body[data-ekko-mobile-nav] [data-ekko-mobile-center] [data-composer-card] button[class*='_primary']{grid-area:send;justify-self:end;}\n body[data-ekko-mobile-nav] [data-ekko-mobile-center] [data-composer-card] button:is([aria-label^='\u8BBF\u95EE\u6A21\u5F0F'],[aria-label^='Access mode']){padding-inline:6px;}\n}\n";
519
+
520
+ // src/mobile-navigation.mjs
521
+ function createMobileNavigation({ layout, document: doc = document }) {
522
+ const view = doc.defaultView, body = doc.body;
523
+ let current = null, nav, brand, actions, footer, frame, sidebar, center, raf = 0, disposed = false;
524
+ const marked = /* @__PURE__ */ new Map(), buttons = /* @__PURE__ */ new Map();
525
+ const mark = (el, name, value = "") => {
526
+ if (!el) return;
527
+ if (!marked.has(el)) marked.set(el, /* @__PURE__ */ new Map());
528
+ const attrs = marked.get(el);
529
+ if (!attrs.has(name)) attrs.set(name, el.getAttribute(name));
530
+ if (el.getAttribute(name) !== value) el.setAttribute(name, value);
531
+ };
532
+ const restore = () => {
533
+ for (const [el, attrs] of marked) for (const [name, value] of attrs) value === null ? el.removeAttribute(name) : el.setAttribute(name, value);
534
+ marked.clear();
535
+ };
536
+ const phone = () => view.innerWidth <= 700 && view.innerHeight >= view.innerWidth;
537
+ const isOpen = () => frame && !frame.hasAttribute("data-sidebar-collapsed");
538
+ const nativeButton = (key) => {
539
+ if (!sidebar) return null;
540
+ if (key === "new") return sidebar.querySelector('button[class*="_newSession"]');
541
+ if (key === "search") return sidebar.querySelector('button[class*="_searchButton"]');
542
+ if (key === "account") return sidebar.querySelector('button[aria-label="\u8D26\u53F7\u83DC\u5355"],button[aria-label="Account menu"]');
543
+ if (key.startsWith("panel:")) return [...sidebar.querySelectorAll('button[class*="_panelRow"]')].find((el) => el.getAttribute("aria-label") === key.slice(6));
544
+ return null;
545
+ };
546
+ function activate(key) {
547
+ const target = nativeButton(key);
548
+ if (!target || target.disabled) return;
549
+ if (key === "search" || key === "account" || target.hasAttribute("aria-haspopup")) {
550
+ if (!isOpen()) layout.toggleSidebar();
551
+ view.requestAnimationFrame(() => {
552
+ if (!disposed) nativeButton(key)?.click();
553
+ });
554
+ } else target.click();
555
+ }
556
+ function makeButton(key, label) {
557
+ const button = doc.createElement("button");
558
+ button.type = "button";
559
+ button.className = "ekko-mobile-action";
560
+ button.dataset.ekkoMobileAction = key;
561
+ button.setAttribute("aria-label", label);
562
+ button.title = label;
563
+ button.addEventListener("click", () => activate(key));
564
+ buttons.set(key, button);
565
+ return button;
566
+ }
567
+ function mount() {
568
+ nav = doc.createElement("nav");
569
+ nav.className = "ekko-mobile-navigation";
570
+ nav.setAttribute("aria-label", "\u624B\u673A\u5BFC\u822A");
571
+ nav.dataset.ekkoMobileNavigation = "";
572
+ brand = doc.createElement("button");
573
+ brand.type = "button";
574
+ brand.className = "ekko-mobile-brand";
575
+ brand.dataset.sidebarOpener = "";
576
+ brand.setAttribute("aria-label", "\u6253\u5F00\u4FA7\u8FB9\u680F");
577
+ brand.title = "\u6253\u5F00\u4FA7\u8FB9\u680F";
578
+ brand.addEventListener("click", () => layout.toggleSidebar());
579
+ actions = doc.createElement("div");
580
+ actions.className = "ekko-mobile-actions";
581
+ footer = doc.createElement("div");
582
+ footer.className = "ekko-mobile-footer";
583
+ nav.append(brand, actions, footer);
584
+ body.prepend(nav);
585
+ }
586
+ function paintButton(key, target, parent) {
587
+ if (!target) return;
588
+ const label = target.getAttribute("aria-label") || target.textContent.trim();
589
+ let button = buttons.get(key);
590
+ if (!button) {
591
+ button = makeButton(key, label);
592
+ parent.append(button);
593
+ }
594
+ button.setAttribute("aria-label", label);
595
+ button.title = label;
596
+ button.disabled = target.disabled;
597
+ const selected = target.getAttribute("aria-current");
598
+ if (selected) button.setAttribute("aria-current", selected);
599
+ else button.removeAttribute("aria-current");
600
+ const icon = target.querySelector("svg,img");
601
+ const signature = icon?.outerHTML || "";
602
+ if (button.dataset.iconSignature !== signature) {
603
+ button.replaceChildren(...icon ? [icon.cloneNode(true)] : []);
604
+ button.dataset.iconSignature = signature;
605
+ }
606
+ }
607
+ function clear() {
608
+ nav?.remove();
609
+ nav = brand = actions = footer = void 0;
610
+ buttons.clear();
611
+ restore();
612
+ frame = sidebar = center = void 0;
613
+ }
614
+ function sync() {
615
+ if (disposed) return;
616
+ if (!current || !phone() || !body.hasAttribute("data-dsh-ekko-" + current.skin)) {
617
+ if (nav || marked.size) clear();
618
+ return;
619
+ }
620
+ const nextSidebar = doc.querySelector('[data-slot="sidebar"]'), nextFrame = nextSidebar?.closest('[class*="_frame"]');
621
+ if (!nextSidebar || !nextFrame) return;
622
+ if (frame && frame !== nextFrame) clear();
623
+ frame = nextFrame;
624
+ sidebar = nextSidebar;
625
+ center = frame.querySelector('[class*="_centerCol"]');
626
+ if (!nav) mount();
627
+ mark(body, "data-ekko-mobile-nav", current.mode === "rail" ? "rail" : "corner");
628
+ mark(body, "data-ekko-mobile-drawer-open", String(isOpen()));
629
+ mark(frame, "data-ekko-mobile-frame");
630
+ mark(center, "data-ekko-mobile-center");
631
+ mark(sidebar.closest('[class*="_sidebarCol"]'), "data-ekko-mobile-drawer");
632
+ mark(sidebar, "id", "ekko-mobile-native-sidebar");
633
+ brand.setAttribute("aria-controls", "ekko-mobile-native-sidebar");
634
+ brand.setAttribute("aria-expanded", String(isOpen()));
635
+ const lang = /^zh/i.test(doc.documentElement.lang || view.navigator.language), openLabel = lang ? "\u6253\u5F00\u4FA7\u8FB9\u680F" : "Open sidebar";
636
+ nav.setAttribute("aria-label", lang ? "\u624B\u673A\u5BFC\u822A" : "Phone navigation");
637
+ brand.setAttribute("aria-label", openLabel);
638
+ brand.title = openLabel;
639
+ if (brand.dataset.mark !== current.mark) {
640
+ const img = doc.createElement("img");
641
+ img.src = current.mark;
642
+ img.alt = "";
643
+ img.draggable = false;
644
+ brand.replaceChildren(img);
645
+ brand.dataset.mark = current.mark;
646
+ }
647
+ const wanted = /* @__PURE__ */ new Set(["new", "search", "account"]);
648
+ paintButton("new", nativeButton("new"), actions);
649
+ for (const target of sidebar.querySelectorAll('button[class*="_panelRow"]')) {
650
+ const label = target.getAttribute("aria-label");
651
+ if (!label) continue;
652
+ const key = "panel:" + label;
653
+ wanted.add(key);
654
+ paintButton(key, target, actions);
655
+ }
656
+ paintButton("search", nativeButton("search"), actions);
657
+ paintButton("account", nativeButton("account"), footer);
658
+ for (const [key, button] of buttons) if (!wanted.has(key) || !nativeButton(key)) {
659
+ button.remove();
660
+ buttons.delete(key);
661
+ }
662
+ }
663
+ function schedule() {
664
+ if (!raf && !disposed) raf = view.requestAnimationFrame(() => {
665
+ raf = 0;
666
+ sync();
667
+ });
668
+ }
669
+ const observer = new view.MutationObserver((records) => {
670
+ if (records.some((record) => {
671
+ const target = record.target.nodeType === 1 ? record.target : record.target.parentElement;
672
+ if (target?.closest("[data-ekko-mobile-navigation]")) return false;
673
+ if (record.type === "attributes") return record.attributeName === "data-sidebar-collapsed" || target?.closest('[data-slot="sidebar"]') || target === doc.documentElement;
674
+ return [...record.addedNodes, ...record.removedNodes].some((node) => node.nodeType === 1 && !node.hasAttribute?.("data-ekko-mobile-navigation"));
675
+ })) schedule();
676
+ });
677
+ observer.observe(doc.documentElement, { subtree: true, childList: true, attributes: true, attributeFilter: ["data-sidebar-collapsed", "aria-current", "aria-label", "disabled", "lang"] });
678
+ const closeAfterSelection = (event) => {
679
+ if (!current || !phone() || !isOpen() || !sidebar?.contains(event.target)) return;
680
+ const target = event.target.closest?.('button,[role="button"],a'), row = event.target.closest?.('[data-row-key^="session:"]');
681
+ if (event.target.closest?.('[aria-haspopup],[role="menu"],input,textarea')) return;
682
+ if (row && target && target !== row) return;
683
+ if (!row && !event.target.closest?.('button[class*="_newSession"]')) return;
684
+ view.requestAnimationFrame(() => {
685
+ if (!disposed && isOpen()) layout.toggleSidebar();
686
+ });
687
+ };
688
+ doc.addEventListener("click", closeAfterSelection, true);
689
+ view.addEventListener("resize", schedule);
690
+ return {
691
+ update(value) {
692
+ current = value;
693
+ sync();
694
+ },
695
+ dispose() {
696
+ disposed = true;
697
+ observer.disconnect();
698
+ doc.removeEventListener("click", closeAfterSelection, true);
699
+ view.removeEventListener("resize", schedule);
700
+ if (raf) view.cancelAnimationFrame(raf);
701
+ clear();
702
+ }
703
+ };
704
+ }
165
705
 
166
- // work/dsh-plugin-ekko-skin-manager/src/client.jsx
706
+ // src/client.jsx
167
707
  var inject = ["slots", "theme", "layout"];
168
708
  var NAMES = { rose: { zh: "\u6709\u83DC\u73AB\u7470", en: "Arina Rose" }, miku: { zh: "\u521D\u97F3\u672A\u6765", en: "Hatsune Miku" } };
169
709
  var COPY = {
@@ -173,7 +713,8 @@ var COPY = {
173
713
  var language = () => /^zh/i.test(document.documentElement.lang || navigator.language) ? "zh" : "en";
174
714
  function apply(ctx) {
175
715
  const storage = window.localStorage, imageCache = /* @__PURE__ */ new Map();
176
- let alive = true, catalog = null, loadError = null, view, preferenceRevision = 0;
716
+ let alive = true, catalog = null, loadError = null, view, preferenceRevision = 0, initialized = false;
717
+ const navigation = createMobileNavigation({ layout: ctx.get("layout") });
177
718
  const listeners = /* @__PURE__ */ new Set();
178
719
  const prepare = (provider) => Promise.all([...new Set(Object.values(provider.assets))].filter((url) => !url.startsWith("data:")).map((url) => {
179
720
  if (!imageCache.has(url)) imageCache.set(url, new Promise((resolve, reject) => {
@@ -201,7 +742,9 @@ function apply(ctx) {
201
742
  return { ready: () => fiber.await(), dispose: () => fiber.dispose() };
202
743
  } });
203
744
  const refresh = () => {
204
- view = { catalog, error: loadError, presentation: controller.getSnapshot(), preferenceRevision };
745
+ const presentation = controller.getSnapshot(), provider = controller.provider(presentation.selected);
746
+ navigation.update(provider ? { skin: provider.id, mode: provider.readSettings(storage).mobileNav, mark: provider.assets.mark || provider.assets.rose } : null);
747
+ view = { catalog, error: loadError, presentation, preferenceRevision };
205
748
  for (const fn of listeners) fn();
206
749
  };
207
750
  const unsubscribe = controller.subscribe(refresh);
@@ -222,7 +765,13 @@ function apply(ctx) {
222
765
  if (!alive) return;
223
766
  catalog = result;
224
767
  refresh();
225
- await controller.initialize(result.defaultSkin, result.skins.filter((s) => s.available).map((s) => s.id));
768
+ if (!initialized) {
769
+ initialized = true;
770
+ await controller.initialize(result.defaultSkin, result.skins.filter((s) => s.available).map((s) => s.id));
771
+ } else {
772
+ const current = controller.getSnapshot().desired;
773
+ if (current !== "default" && !result.skins.some((s) => s.id === current && s.available)) await controller.choose(result.defaultSkin);
774
+ }
226
775
  } catch {
227
776
  if (alive) {
228
777
  loadError = COPY[language()].loadFailed;
@@ -249,20 +798,20 @@ function apply(ctx) {
249
798
  return () => listeners.delete(fn);
250
799
  };
251
800
  function Panel() {
252
- (0, import_react.useEffect)(() => {
801
+ (0, import_react2.useEffect)(() => {
253
802
  if (innerWidth > 640) return;
254
803
  const frame = requestAnimationFrame(() => document.querySelector("[role='dialog'] [class*='_navList'] [class*='_active']")?.scrollIntoView({ block: "nearest", inline: "nearest" }));
255
804
  return () => cancelAnimationFrame(frame);
256
805
  }, []);
257
- const state = (0, import_react.useSyncExternalStore)(subscribe, () => view), lang = language(), c = COPY[lang], p = state.presentation;
806
+ const state = (0, import_react2.useSyncExternalStore)(subscribe, () => view), lang = language(), c = COPY[lang], p = state.presentation;
258
807
  const id = p.selected, provider = controller.provider(id), prefs = provider ? provider.readSettings(storage) : null;
259
808
  const choose = (target) => {
260
809
  void controller.choose(target);
261
810
  };
262
- return /* @__PURE__ */ import_react.default.createElement("section", { className: "ekko-skins", "data-ekko-skin-manager": "", "aria-label": c.nav }, /* @__PURE__ */ import_react.default.createElement("header", { className: "ekko-skins-intro" }, /* @__PURE__ */ import_react.default.createElement("h2", null, c.title), /* @__PURE__ */ import_react.default.createElement("p", null, c.intro)), state.error && /* @__PURE__ */ import_react.default.createElement("div", { className: "ekko-skin-error", role: "alert" }, state.error, " ", /* @__PURE__ */ import_react.default.createElement("button", { type: "button", className: "ekko-skin-button", onClick: load }, c.retry)), !state.catalog && !state.error && /* @__PURE__ */ import_react.default.createElement("p", { role: "status" }, c.loading), /* @__PURE__ */ import_react.default.createElement("div", { className: "ekko-skin-grid" }, ["rose", "miku"].map((skin) => {
811
+ return /* @__PURE__ */ import_react2.default.createElement("section", { className: "ekko-skins", "data-ekko-skin-manager": "", "aria-label": c.nav }, /* @__PURE__ */ import_react2.default.createElement("header", { className: "ekko-skins-intro" }, /* @__PURE__ */ import_react2.default.createElement("h2", null, c.title), /* @__PURE__ */ import_react2.default.createElement("p", null, c.intro)), state.error && /* @__PURE__ */ import_react2.default.createElement("div", { className: "ekko-skin-error", role: "alert" }, state.error, " ", /* @__PURE__ */ import_react2.default.createElement("button", { type: "button", className: "ekko-skin-button", onClick: load }, c.retry)), !state.catalog && !state.error && /* @__PURE__ */ import_react2.default.createElement("p", { role: "status" }, c.loading), /* @__PURE__ */ import_react2.default.createElement("div", { className: "ekko-skin-grid" }, ["rose", "miku"].map((skin) => {
263
812
  const entry = state.catalog?.skins.find((s) => s.id === skin), name = NAMES[skin][lang], active = id === skin, available = entry?.available && p.available.includes(skin);
264
- return /* @__PURE__ */ import_react.default.createElement("article", { key: skin, className: "ekko-skin-card", "data-active": active }, entry?.preview && /* @__PURE__ */ import_react.default.createElement("img", { className: "ekko-skin-cover", src: entry.preview[document.body.hasAttribute("data-ds-dark-theme") ? "dark" : "light"], alt: name, loading: "lazy" }), /* @__PURE__ */ import_react.default.createElement("div", { className: "ekko-skin-description" }, /* @__PURE__ */ import_react.default.createElement("div", { className: "ekko-skin-title" }, /* @__PURE__ */ import_react.default.createElement("h3", null, name), active && /* @__PURE__ */ import_react.default.createElement("span", { className: "ekko-skin-state" }, c.active)), /* @__PURE__ */ import_react.default.createElement("span", { className: "ekko-skin-version" }, entry?.version ? "v" + entry.version : c.unavailable), /* @__PURE__ */ import_react.default.createElement("button", { type: "button", className: "ekko-skin-button", "data-active": active, disabled: !available || active || p.busy, onClick: () => choose(skin) }, p.busy && p.desired === skin ? c.switching : active ? c.active : c.use + " " + name)));
265
- })), /* @__PURE__ */ import_react.default.createElement("div", { className: "ekko-skin-toolbar" }, /* @__PURE__ */ import_react.default.createElement("span", null, p.busy ? c.switching : id === "default" ? c.native : NAMES[id]?.[lang]), /* @__PURE__ */ import_react.default.createElement("button", { type: "button", className: "ekko-skin-button", disabled: id === "default" || p.busy, onClick: () => choose("default") }, c.native)), p.error && /* @__PURE__ */ import_react.default.createElement("div", { className: "ekko-skin-error", role: "alert" }, p.error, /* @__PURE__ */ import_react.default.createElement("button", { type: "button", className: "ekko-skin-button", onClick: () => choose(p.desired) }, c.retry)), prefs && /* @__PURE__ */ import_react.default.createElement("div", { className: "ekko-skin-options" }, /* @__PURE__ */ import_react.default.createElement("div", { className: "ekko-skin-toolbar" }, /* @__PURE__ */ import_react.default.createElement("h3", null, NAMES[id][lang], " \xB7 ", c.options), /* @__PURE__ */ import_react.default.createElement("button", { type: "button", className: "ekko-skin-button", disabled: p.busy, onClick: () => setPreference(id, { artwork: true, companion: true, polaroid: true, opacity: 0.12 }) }, c.reset)), ["artwork", "companion", "polaroid"].map((key) => /* @__PURE__ */ import_react.default.createElement("label", { className: "ekko-skin-option", key }, /* @__PURE__ */ import_react.default.createElement("span", null, c[key]), /* @__PURE__ */ import_react.default.createElement("input", { type: "checkbox", checked: prefs[key], disabled: p.busy, onChange: (e) => setPreference(id, { [key]: e.target.checked }) }))), /* @__PURE__ */ import_react.default.createElement("label", { className: "ekko-skin-option" }, /* @__PURE__ */ import_react.default.createElement("span", null, c.opacity, /* @__PURE__ */ import_react.default.createElement("small", null, Math.round(prefs.opacity * 100), "%")), /* @__PURE__ */ import_react.default.createElement("input", { type: "range", min: ".04", max: ".24", step: ".01", value: prefs.opacity, disabled: p.busy, onChange: (e) => setPreference(id, { opacity: Number(e.target.value) }) }))));
813
+ return /* @__PURE__ */ import_react2.default.createElement("article", { key: skin, className: "ekko-skin-card", "data-active": active }, entry?.preview && /* @__PURE__ */ import_react2.default.createElement("img", { className: "ekko-skin-cover", src: entry.preview[document.body.hasAttribute("data-ds-dark-theme") ? "dark" : "light"], alt: name, loading: "lazy" }), /* @__PURE__ */ import_react2.default.createElement("div", { className: "ekko-skin-description" }, /* @__PURE__ */ import_react2.default.createElement("div", { className: "ekko-skin-title" }, /* @__PURE__ */ import_react2.default.createElement("h3", null, name), active && /* @__PURE__ */ import_react2.default.createElement("span", { className: "ekko-skin-state" }, c.active)), /* @__PURE__ */ import_react2.default.createElement("span", { className: "ekko-skin-version" }, entry?.version ? "v" + entry.version : c.unavailable), /* @__PURE__ */ import_react2.default.createElement("button", { type: "button", className: "ekko-skin-button", "data-active": active, disabled: !available || active || p.busy, onClick: () => choose(skin) }, p.busy && p.desired === skin ? c.switching : active ? c.active : c.use + " " + name)));
814
+ })), /* @__PURE__ */ import_react2.default.createElement("div", { className: "ekko-skin-toolbar" }, /* @__PURE__ */ import_react2.default.createElement("span", null, p.busy ? c.switching : id === "default" ? c.native : NAMES[id]?.[lang]), /* @__PURE__ */ import_react2.default.createElement("button", { type: "button", className: "ekko-skin-button", disabled: id === "default" || p.busy, onClick: () => choose("default") }, c.native)), p.error && /* @__PURE__ */ import_react2.default.createElement("div", { className: "ekko-skin-error", role: "alert" }, p.error, /* @__PURE__ */ import_react2.default.createElement("button", { type: "button", className: "ekko-skin-button", onClick: () => choose(p.desired) }, c.retry)), prefs && /* @__PURE__ */ import_react2.default.createElement("div", { className: "ekko-skin-options" }, /* @__PURE__ */ import_react2.default.createElement("div", { className: "ekko-skin-toolbar" }, /* @__PURE__ */ import_react2.default.createElement("h3", null, NAMES[id][lang], " \xB7 ", c.options), /* @__PURE__ */ import_react2.default.createElement("button", { type: "button", className: "ekko-skin-button", disabled: p.busy, onClick: () => setPreference(id, { artwork: true, companion: true, polaroid: true, opacity: 0.12, mobileNav: "corner" }) }, c.reset)), /* @__PURE__ */ import_react2.default.createElement("label", { className: "ekko-skin-option" }, /* @__PURE__ */ import_react2.default.createElement("span", null, lang === "zh" ? "\u624B\u673A\u5BFC\u822A\u65B9\u5F0F" : "Phone navigation", /* @__PURE__ */ import_react2.default.createElement("small", null, lang === "zh" ? "\u7AD6\u5C4F\u624B\u673A\u751F\u6548\uFF0C\u4E24\u4E2A\u4E3B\u9898\u5206\u522B\u4FDD\u5B58" : "Portrait phones; saved separately for each skin")), /* @__PURE__ */ import_react2.default.createElement("select", { "aria-label": lang === "zh" ? "\u624B\u673A\u5BFC\u822A\u65B9\u5F0F" : "Phone navigation", value: prefs.mobileNav || "corner", disabled: p.busy, onChange: (e) => setPreference(id, { mobileNav: e.target.value }) }, /* @__PURE__ */ import_react2.default.createElement("option", { value: "corner" }, lang === "zh" ? "\u5DE6\u4E0A\u89D2\u6309\u94AE + \u62BD\u5C49" : "Corner button + drawer"), /* @__PURE__ */ import_react2.default.createElement("option", { value: "rail" }, lang === "zh" ? "\u5DE6\u4FA7\u7EB5\u5411\u56FE\u6807\u680F" : "Vertical icon rail"))), ["artwork", "companion", "polaroid"].map((key) => /* @__PURE__ */ import_react2.default.createElement("label", { className: "ekko-skin-option", key }, /* @__PURE__ */ import_react2.default.createElement("span", null, c[key]), /* @__PURE__ */ import_react2.default.createElement("input", { type: "checkbox", checked: prefs[key], disabled: p.busy, onChange: (e) => setPreference(id, { [key]: e.target.checked }) }))), /* @__PURE__ */ import_react2.default.createElement("label", { className: "ekko-skin-option" }, /* @__PURE__ */ import_react2.default.createElement("span", null, c.opacity, /* @__PURE__ */ import_react2.default.createElement("small", null, Math.round(prefs.opacity * 100), "%")), /* @__PURE__ */ import_react2.default.createElement("input", { type: "range", min: ".04", max: ".24", step: ".01", value: prefs.opacity, disabled: p.busy, onChange: (e) => setPreference(id, { opacity: Number(e.target.value) }) }))), prefs && /* @__PURE__ */ import_react2.default.createElement(VideoSettings, { key: id, skin: id }));
266
815
  }
267
816
  function registerSection() {
268
817
  return ctx.slots.inject("settings.section", () => ctx.slots.register({ name: "settings.section", id: "ekko-skins", order: 115, label: () => COPY[language()].nav }, Panel));
@@ -290,14 +839,20 @@ function apply(ctx) {
290
839
  window.addEventListener("storage", onStorage);
291
840
  window.addEventListener("dsh:ekko-skin-preferences", onPreferences);
292
841
  void load();
842
+ const catalogTimer = setInterval(() => void load(), 15e3);
843
+ const reloadCatalog = () => void load();
844
+ window.addEventListener("focus", reloadCatalog);
293
845
  ctx.effect(() => async () => {
294
846
  alive = false;
847
+ clearInterval(catalogTimer);
848
+ window.removeEventListener("focus", reloadCatalog);
295
849
  section?.();
296
850
  locale.disconnect();
297
851
  appearance.disconnect();
298
852
  window.removeEventListener("storage", onStorage);
299
853
  window.removeEventListener("dsh:ekko-skin-preferences", onPreferences);
300
854
  unsubscribe();
855
+ navigation.dispose();
301
856
  await controller.dispose();
302
857
  style.remove();
303
858
  listeners.clear();
@@ -305,4 +860,42 @@ function apply(ctx) {
305
860
  }, "ekko-skins: controller, section and presentation cleanup");
306
861
  }
307
862
 
863
+ // src/shared-lease.mjs
864
+ function acquireShared(ctx, key, plugin, config = {}) {
865
+ const symbol = /* @__PURE__ */ Symbol.for("dsh.ekko-skins.leases." + key);
866
+ const leases = globalThis[symbol] ??= /* @__PURE__ */ new WeakMap(), root = ctx.root;
867
+ if (!root || typeof root.plugin !== "function") throw Error("DSH root context unavailable");
868
+ let state = leases.get(root);
869
+ if (!state) {
870
+ state = { users: 0, fiber: null };
871
+ leases.set(root, state);
872
+ try {
873
+ state.fiber = root.plugin(plugin, config);
874
+ } catch (error) {
875
+ leases.delete(root);
876
+ throw error;
877
+ }
878
+ }
879
+ state.users++;
880
+ let released = false;
881
+ ctx.effect(() => () => {
882
+ if (released) return;
883
+ released = true;
884
+ if (--state.users === 0) {
885
+ leases.delete(root);
886
+ return state.fiber.dispose();
887
+ }
888
+ }, "ekko-skins: shared " + key + " lease");
889
+ return state.fiber;
890
+ }
891
+
892
+ // src/client-entry.mjs
893
+ var manager = { name: "ekko-skins-shared-client-v2", inject, apply };
894
+ function acquireClient(ctx) {
895
+ return acquireShared(ctx, "client-v2", manager);
896
+ }
897
+ function apply2(ctx) {
898
+ acquireClient(ctx);
899
+ }
900
+
308
901
  return module.exports;}});