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/README.md +16 -66
- package/THIRD_PARTY_LICENSES/dsh-video-skin-LICENSE +21 -0
- package/design-qa.md +51 -8
- package/lib/client.js +610 -17
- package/lib/index.js +386 -14
- package/package.json +8 -5
- package/preview/miku-settings.png +0 -0
- package/preview/startup-settings.png +0 -0
- package/screenshots.json +4 -0
- package/scripts/build.mjs +5 -3
- package/src/boot-entry.mjs +2 -0
- package/src/boot-generated.mjs +1 -0
- package/src/boot-media.mjs +160 -0
- package/src/boot-player.mjs +90 -0
- package/src/client-entry.mjs +6 -0
- package/src/client.jsx +11 -5
- package/src/generated.mjs +1 -1
- package/src/host-entry.mjs +6 -0
- package/src/index.mjs +20 -2
- package/src/mobile-composer.css +28 -0
- package/src/mobile-navigation.css +29 -0
- package/src/mobile-navigation.mjs +108 -0
- package/src/shared-lease.mjs +11 -0
- package/src/style.css +18 -1
- package/src/vendor/video-route.mjs +158 -0
- package/src/video-settings.jsx +90 -0
- package/test/boot-media.test.mjs +95 -0
- package/test/boot-player.test.mjs +56 -0
- package/test/native-integration.test.mjs +44 -0
- package/test/shared-lease.test.mjs +15 -0
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
|
-
//
|
|
31
|
-
var
|
|
32
|
-
__export(
|
|
33
|
-
|
|
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(
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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__ */
|
|
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__ */
|
|
265
|
-
})), /* @__PURE__ */
|
|
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;}});
|