From a4afde9353fccc6708fc6051e491754fccbf763f Mon Sep 17 00:00:00 2001 From: Aisland Date: Tue, 1 Sep 2026 21:13:58 +0800 Subject: [PATCH] =?UTF-8?q?feat(console):=20=E5=AA=92=E4=BD=93=E5=BA=93?= =?UTF-8?q?=E7=BB=9F=E4=B8=80=EF=BC=88=E7=85=A7=E7=89=87+=E8=A7=86?= =?UTF-8?q?=E9=A2=91=EF=BC=8C=E5=90=8C=E5=BA=93=E7=AE=A1=E7=90=86/?= =?UTF-8?q?=E5=88=87=E6=8D=A2/=E5=88=A0=E9=99=A4/=E6=BB=A4=E9=95=9C?= =?UTF-8?q?=EF=BC=8C=E8=A7=86=E9=A2=91=E8=83=8C=E6=99=AF=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E6=92=AD=E6=94=BE=EF=BC=89=EF=BC=9B=E5=AD=90=E8=8F=9C=E5=8D=95?= =?UTF-8?q?50/50=E8=A1=8C=EF=BC=9B=E6=95=B0=E5=AD=97=E8=BE=93=E5=85=A5?= =?UTF-8?q?=E5=8E=BBspinner=E5=8A=A0=E5=AE=BD=EF=BC=9B=E7=85=A7=E7=89=87?= =?UTF-8?q?=E6=A8=A1=E5=BC=8F=E7=BD=91=E6=A0=BC=E7=BA=BF=E5=8A=A0=E5=BC=BA?= =?UTF-8?q?=E4=B8=94=E5=BC=80=E5=85=B3=E7=94=9F=E6=95=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 01_content/lib/client.js | 97 +++++++++++++++++++---------- 01_content/src/client/index.tsx | 6 +- 01_content/src/client/locales.ts | 4 +- 01_content/src/client/photoStore.ts | 13 ++-- 01_content/src/client/split.tsx | 53 +++++++++------- 01_content/src/client/styles.ts | 9 ++- 6 files changed, 115 insertions(+), 67 deletions(-) diff --git a/01_content/lib/client.js b/01_content/lib/client.js index 2f2b1e3..4e41238 100644 --- a/01_content/lib/client.js +++ b/01_content/lib/client.js @@ -8193,7 +8193,8 @@ var css = xterm_default + "\n" + [ ".dsh-wt_switchKnob{position:absolute;top:2px;left:2px;width:13px;height:13px;border-radius:50%;background:#fff;box-shadow:0 1px 3px rgba(0,0,0,.4);transition:left .18s cubic-bezier(.22,.61,.36,1)}", ".dsh-wt_switchOn .dsh-wt_switchKnob{left:15px}", ".dsh-wt_console[data-wt-theme=light] .dsh-wt_switch{background:rgba(27,31,36,.15)}", - ".dsh-wt_console[data-wt-grid=off] .dsh-wt_consoleBg::after{display:none}", + ".dsh-wt_console[data-wt-bg=photo][data-wt-grid=on] .dsh-wt_consoleBg::after{background-image:linear-gradient(rgba(255,255,255,.16) 1px,transparent 1px),linear-gradient(90deg,rgba(255,255,255,.16) 1px,transparent 1px);background-size:30px 30px}", + ".dsh-wt_console[data-wt-bg=photo][data-wt-grid=off] .dsh-wt_consoleBg::after{display:none}", ".dsh-wt_hslHead{display:flex;align-items:center;gap:4px;padding:2px 4px 4px}", ".dsh-wt_hslBack{flex:none;width:20px;height:20px;padding:0;border:none;border-radius:6px;background:transparent;color:var(--wt-text3);cursor:pointer;font-size:14px;line-height:1;display:inline-flex;align-items:center;justify-content:center}", ".dsh-wt_hslBack:hover{background:var(--wt-chip);color:var(--wt-text)}", @@ -8201,7 +8202,8 @@ var css = xterm_default + "\n" + [ ".dsh-wt_hslRow{display:flex;align-items:center;gap:4px;padding:2px 3px;width:100%;box-sizing:border-box}", ".dsh-wt_hslLabel{flex:none;width:10px;font-size:9px;font-weight:600;color:var(--wt-text3);text-align:center}", ".dsh-wt_hslSlider{flex:1;min-width:0;height:14px;accent-color:#4f8ef7;cursor:pointer}", - ".dsh-wt_hslInput{flex:none;width:36px;height:20px;padding:0 4px;border:1px solid var(--wt-border);border-radius:6px;background:var(--wt-chip);color:var(--wt-text2);font:inherit;font-size:10px;text-align:right;outline:none;box-sizing:border-box;font-variant-numeric:tabular-nums}", + ".dsh-wt_hslInput{flex:none;width:42px;height:20px;padding:0 4px;border:1px solid var(--wt-border);border-radius:6px;background:var(--wt-chip);color:var(--wt-text2);font:inherit;font-size:10px;text-align:right;outline:none;box-sizing:border-box;font-variant-numeric:tabular-nums;-moz-appearance:textfield;appearance:textfield}", + ".dsh-wt_hslInput::-webkit-outer-spin-button,.dsh-wt_hslInput::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}", ".dsh-wt_hslInput:focus{border-color:#4f8ef7;color:var(--wt-text)}", ".dsh-wt_hslReset{margin:4px 4px 2px;padding:5px 9px;border:none;border-radius:7px;background:var(--wt-chip);color:var(--wt-text2);font:inherit;font-size:12px;line-height:17px;cursor:pointer;text-align:center}", ".dsh-wt_hslReset:hover{background:rgba(79,142,247,.22);color:var(--wt-text)}", @@ -8212,6 +8214,9 @@ var css = xterm_default + "\n" + [ ".dsh-wt_photoName{flex:1;min-width:0;font-size:11px;line-height:16px;color:var(--wt-text2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}", ".dsh-wt_photoEmpty{padding:4px 4px 2px;font-size:11px;line-height:16px;color:var(--wt-text3)}", ".dsh-wt_hslDivider{height:1px;margin:5px 4px;background:var(--wt-border);opacity:.5}", + ".dsh-wt_consoleVid{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}", + ".dsh-wt_gridHalf{flex:1;min-width:0;display:flex;align-items:center;justify-content:space-between;gap:4px;padding:0 4px;box-sizing:border-box}", + ".dsh-wt_photoThumb video,.dsh-wt_photoThumb{display:block}", ".dsh-wt_dropItemOn{background:var(--wt-chip);color:var(--wt-text);font-weight:600}", ".dsh-wt_hbar{position:relative;width:130px;height:26px;margin:4px auto;border-radius:999px;background:rgba(255,255,255,.08)}", ".dsh-wt_hbarFill{position:absolute;top:3px;bottom:3px;left:0;border-radius:999px;background:linear-gradient(90deg,#4f8ef7,#3b82f6);pointer-events:none;transition:width .25s cubic-bezier(.22,.61,.36,1)}", @@ -8453,7 +8458,7 @@ var zh = { "console.bgLabel": "\u80CC\u666F", "console.bgPlain": "\u7EAF\u8272\u80CC\u666F", "console.bgGlow": "\u6D41\u5149\u80CC\u666F", - "console.bgPhoto": "\u4E0A\u4F20\u7167\u7247", + "console.bgPhoto": "\u4E0A\u4F20\u5A92\u4F53", "console.bgEdit": "\u80CC\u666F\u8BBE\u7F6E", "console.bgEditPlain": "\u7EAF\u8272\u80CC\u666F\u8BBE\u7F6E", "console.bgEditGlow": "\u6D41\u5149\u80CC\u666F\u8BBE\u7F6E", @@ -8647,7 +8652,7 @@ var en = { "console.bgLabel": "Background", "console.bgPlain": "Plain", "console.bgGlow": "Glow", - "console.bgPhoto": "Photo", + "console.bgPhoto": "Upload media", "console.bgEdit": "Background settings", "console.bgEditPlain": "Plain background", "console.bgEditGlow": "Glow background", @@ -17379,6 +17384,7 @@ function ConsolePane() { const [bgPhoto, setBgPhoto] = (0, import_react.useState)(""); const [photoId, setPhotoIdLocal] = (0, import_react.useState)(""); const [photoList, setPhotoList] = (0, import_react.useState)([]); + const [bgVideo, setBgVideo] = (0, import_react.useState)(""); const [photoGrid, setPhotoGridState] = (0, import_react.useState)(() => splitEnv?.console?.getPhotoGrid?.() ?? true); const photoUrlRef = (0, import_react.useRef)(""); const [openMenu, setOpenMenu] = (0, import_react.useState)(null); @@ -17409,7 +17415,7 @@ function ConsolePane() { const pickPhotoFile = () => { const input = document.createElement("input"); input.type = "file"; - input.accept = "image/*"; + input.accept = "image/*,video/*"; input.onchange = () => { const file = input.files?.[0]; if (!file) return; @@ -17420,17 +17426,25 @@ function ConsolePane() { const onFilePhoto = async (file) => { let id = ""; let url = ""; + let kind = file.type && file.type.indexOf("video/") === 0 ? "video" : "photo"; const rec = await (splitEnv?.console?.addPhoto?.(file) ?? Promise.resolve(null)).catch(() => null); if (rec) { id = rec.id; url = rec.url; + kind = rec.kind; } else { url = URL.createObjectURL(file); } photoUrlRef.current = url; setPhotoIdLocal(id); - if (id) setPhotoList((prev) => [{ id, url }, ...prev]); - setBgPhoto(url); + if (id) setPhotoList((prev) => [{ id, kind, url }, ...prev]); + if (kind === "video") { + setBgPhoto(""); + setBgVideo(url); + } else { + setBgVideo(""); + setBgPhoto(url); + } setBgState("photo"); splitEnv?.console?.setBg?.("photo"); }; @@ -17451,13 +17465,15 @@ function ConsolePane() { setPhotoIdLocal(next.id); splitEnv?.console?.setPhotoId?.(next.id); photoUrlRef.current = next.url; - setBgPhoto(next.url); + setBgVideo(next.kind === "video" ? next.url : ""); + setBgPhoto(next.kind === "video" ? "" : next.url); setBgState("photo"); splitEnv?.console?.setBg?.("photo"); } else { setPhotoIdLocal(""); photoUrlRef.current = ""; setBgPhoto(""); + setBgVideo(""); setBgState("glow"); splitEnv?.console?.setBg?.("glow"); } @@ -17472,7 +17488,13 @@ function ConsolePane() { setPhotoIdLocal(p.id); splitEnv?.console?.setPhotoId?.(p.id); photoUrlRef.current = p.url; - setBgPhoto(p.url); + if (p.kind === "video") { + setBgPhoto(""); + setBgVideo(p.url); + } else { + setBgVideo(""); + setBgPhoto(p.url); + } setBgState("photo"); splitEnv?.console?.setBg?.("photo"); }; @@ -17587,7 +17609,8 @@ function ConsolePane() { if (active) { setPhotoIdLocal(active.id); photoUrlRef.current = active.url; - setBgPhoto(active.url); + setBgVideo(active.kind === "video" ? active.url : ""); + setBgPhoto(active.kind === "video" ? "" : active.url); } }).catch(() => { }); @@ -17641,13 +17664,16 @@ function ConsolePane() { { mode: "light", key: "console.themeLight" }, { mode: "system", key: "console.themeSystem" } ]; - return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-wt_console", "data-wt-theme": resolvedTheme, "data-wt-shape": shape, "data-wt-grid": bg === "photo" && !photoGrid ? "off" : "on", style: bg === "plain" ? { ["--wt-bg"]: "hsl(" + plainHsl.h + ", " + plainHsl.s + "%, " + plainHsl.l + "%)" } : void 0, children: [ - /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dsh-wt_consoleBg", "aria-hidden": true, style: bg === "photo" && bgPhoto ? { backgroundImage: 'url("' + bgPhoto + '")', backgroundSize: "cover", backgroundPosition: "center", ...photoHsl.h !== 0 || photoHsl.s !== 100 || photoHsl.l !== 100 ? { filter: "hue-rotate(" + photoHsl.h + "deg) saturate(" + photoHsl.s + "%) brightness(" + photoHsl.l + "%)" } : {} } : bg === "glow" && (glowHsl.h !== 0 || glowHsl.s !== 100 || glowHsl.l !== 100) ? { filter: "hue-rotate(" + glowHsl.h + "deg) saturate(" + glowHsl.s + "%) brightness(" + glowHsl.l + "%)" } : void 0, children: bg === "glow" && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [ - /* @__PURE__ */ (0, import_jsx_runtime.jsx)("i", { className: "dsh-wt_blob dsh-wt_blob1" }), - /* @__PURE__ */ (0, import_jsx_runtime.jsx)("i", { className: "dsh-wt_blob dsh-wt_blob2" }), - /* @__PURE__ */ (0, import_jsx_runtime.jsx)("i", { className: "dsh-wt_blob dsh-wt_blob3" }), - /* @__PURE__ */ (0, import_jsx_runtime.jsx)("i", { className: "dsh-wt_blob dsh-wt_blob4" }) - ] }) }), + return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-wt_console", "data-wt-theme": resolvedTheme, "data-wt-shape": shape, "data-wt-bg": bg, "data-wt-grid": bg === "photo" && !photoGrid ? "off" : "on", style: bg === "plain" ? { ["--wt-bg"]: "hsl(" + plainHsl.h + ", " + plainHsl.s + "%, " + plainHsl.l + "%)" } : void 0, children: [ + /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "dsh-wt_consoleBg", "aria-hidden": true, style: bg === "photo" && bgPhoto ? { backgroundImage: 'url("' + bgPhoto + '")', backgroundSize: "cover", backgroundPosition: "center", ...photoHsl.h !== 0 || photoHsl.s !== 100 || photoHsl.l !== 100 ? { filter: "hue-rotate(" + photoHsl.h + "deg) saturate(" + photoHsl.s + "%) brightness(" + photoHsl.l + "%)" } : {} } : bg === "glow" && (glowHsl.h !== 0 || glowHsl.s !== 100 || glowHsl.l !== 100) ? { filter: "hue-rotate(" + glowHsl.h + "deg) saturate(" + glowHsl.s + "%) brightness(" + glowHsl.l + "%)" } : void 0, children: [ + bg === "photo" && bgVideo && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("video", { className: "dsh-wt_consoleVid", src: bgVideo, autoPlay: true, muted: true, loop: true, playsInline: true, style: photoHsl.h !== 0 || photoHsl.s !== 100 || photoHsl.l !== 100 ? { filter: "hue-rotate(" + photoHsl.h + "deg) saturate(" + photoHsl.s + "%) brightness(" + photoHsl.l + "%)" } : void 0 }), + bg === "glow" && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [ + /* @__PURE__ */ (0, import_jsx_runtime.jsx)("i", { className: "dsh-wt_blob dsh-wt_blob1" }), + /* @__PURE__ */ (0, import_jsx_runtime.jsx)("i", { className: "dsh-wt_blob dsh-wt_blob2" }), + /* @__PURE__ */ (0, import_jsx_runtime.jsx)("i", { className: "dsh-wt_blob dsh-wt_blob3" }), + /* @__PURE__ */ (0, import_jsx_runtime.jsx)("i", { className: "dsh-wt_blob dsh-wt_blob4" }) + ] }) + ] }), openMenu !== null && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-wt_dropMask", onClick: () => setOpenMenu(null) }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-wt_consoleScroll", children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { ref: gridRef, className: "dsh-wt_consoleGrid", style: { ["--wt-cols"]: cols }, children: [ cards.map((c) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)( @@ -17780,20 +17806,22 @@ function ConsolePane() { /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dsh-wt_hslTitle", children: bgEdit === "plain" ? T("console.bgEditPlain") : bgEdit === "glow" ? T("console.bgEditGlow") : T("console.bgEditPhoto") }) ] }), bgEdit === "photo" && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [ - /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", { type: "button", className: "dsh-wt_dropItem", onClick: pickPhotoFile, children: [ - /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { width: "13", height: "13", viewBox: "0 0 16 16", "aria-hidden": true, children: [ - /* @__PURE__ */ (0, import_jsx_runtime.jsx)("rect", { x: "2.5", y: "3.5", width: "11", height: "9", rx: "1.5", fill: "none", stroke: "currentColor", strokeWidth: "1.2" }), - /* @__PURE__ */ (0, import_jsx_runtime.jsx)("circle", { cx: "5.8", cy: "6.6", r: "1.1", fill: "currentColor" }), - /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M3.2 11.2l2.8-2.8 2.2 2.2 1.8-1.8 2.8 2.4", fill: "none", stroke: "currentColor", strokeWidth: "1.2" }) - ] }), - T("console.bgPhoto") - ] }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-wt_dropRow", children: [ - /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dsh-wt_gridLabel", children: T("console.bgGridLabel") }), - /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "dsh-wt_switch" + (photoGrid ? " dsh-wt_switchOn" : ""), "aria-pressed": photoGrid, "aria-label": T("console.bgGridLabel"), onClick: togglePhotoGrid, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dsh-wt_switchKnob" }) }) + /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", { type: "button", className: "dsh-wt_dropItem", onClick: pickPhotoFile, children: [ + /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { width: "13", height: "13", viewBox: "0 0 16 16", "aria-hidden": true, children: [ + /* @__PURE__ */ (0, import_jsx_runtime.jsx)("rect", { x: "2.5", y: "3.5", width: "11", height: "9", rx: "1.5", fill: "none", stroke: "currentColor", strokeWidth: "1.2" }), + /* @__PURE__ */ (0, import_jsx_runtime.jsx)("circle", { cx: "5.8", cy: "6.6", r: "1.1", fill: "currentColor" }), + /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { d: "M3.2 11.2l2.8-2.8 2.2 2.2 1.8-1.8 2.8 2.4", fill: "none", stroke: "currentColor", strokeWidth: "1.2" }) + ] }), + T("console.bgPhoto") + ] }), + /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-wt_gridHalf", children: [ + /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dsh-wt_gridLabel", children: T("console.bgGridLabel") }), + /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "dsh-wt_switch" + (photoGrid ? " dsh-wt_switchOn" : ""), "aria-pressed": photoGrid, "aria-label": T("console.bgGridLabel"), onClick: togglePhotoGrid, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dsh-wt_switchKnob" }) }) + ] }) ] }), photoList.length > 0 ? photoList.slice(0, 4).map((p) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-wt_dropRow dsh-wt_photoRow" + (photoId === p.id ? " dsh-wt_photoRowOn" : ""), onClick: () => selectPhoto(p), children: [ - /* @__PURE__ */ (0, import_jsx_runtime.jsx)("img", { className: "dsh-wt_photoThumb", src: p.url, alt: "" }), + p.kind === "video" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("video", { className: "dsh-wt_photoThumb", src: p.url, muted: true, playsInline: true, preload: "metadata" }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("img", { className: "dsh-wt_photoThumb", src: p.url, alt: "" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "dsh-wt_photoName", children: photoId === p.id ? T("console.bgPhotoCurrent") : T("console.bgPhotoUse") }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "dsh-wt_dropTrash", title: T("console.bgPhotoDelete"), "aria-label": T("console.bgPhotoDelete"), onClick: (e) => { e.stopPropagation(); @@ -18851,6 +18879,7 @@ var DB_NAME = "dsh-worktable"; var STORE = "photoRecords"; var LEGACY_STORE = "consoleBgPhoto"; var LEGACY_KEY = "original"; +var kindOf = (blob) => blob.type && blob.type.indexOf("video/") === 0 ? "video" : "photo"; function openDb() { return new Promise((resolve, reject) => { const req = indexedDB.open(DB_NAME, 2); @@ -18871,7 +18900,7 @@ function getAll(db) { for (const v of req.result) { const r = v; if (r && typeof r === "object" && typeof r.id === "string" && r.blob instanceof Blob) { - arr.push(r); + arr.push({ id: r.id, createdAt: r.createdAt, kind: kindOf(r.blob), blob: r.blob }); } } arr.sort((a, b) => b.createdAt - a.createdAt); @@ -18899,7 +18928,7 @@ var photoStore = { req.onerror = () => reject(req.error); }); if (blob) { - const rec = { id: "legacy", createdAt: Date.now(), blob }; + const rec = { id: "legacy", createdAt: Date.now(), kind: kindOf(blob), blob }; await new Promise((resolve, reject) => { const tx = db.transaction(STORE, "readwrite"); tx.objectStore(STORE).put(rec, rec.id); @@ -18915,11 +18944,11 @@ var photoStore = { db.close(); return []; }, - /** 新增一张原始照片,返回其 id(自动成为最新) */ + /** 新增一条媒体(照片/视频),返回其 id(自动成为最新) */ async add(blob) { const db = await openDb(); const id = Date.now().toString(36) + Math.random().toString(36).slice(2, 7); - const rec = { id, createdAt: Date.now(), blob }; + const rec = { id, createdAt: Date.now(), kind: kindOf(blob), blob }; return new Promise((resolve, reject) => { const tx = db.transaction(STORE, "readwrite"); tx.objectStore(STORE).put(rec, rec.id); @@ -20202,7 +20231,7 @@ function WorktableSection(props) { const stored = viewRef.current.consoleBgPhotoId ?? null; const activeId = stored && records.some((r) => r.id === stored) ? stored : null; return { - list: records.map((r) => ({ id: r.id, url: URL.createObjectURL(r.blob) })), + list: records.map((r) => ({ id: r.id, kind: r.kind, url: URL.createObjectURL(r.blob) })), activeId }; }, @@ -20213,7 +20242,7 @@ function WorktableSection(props) { localStorage.removeItem("dsh.worktable.consoleBgPhoto.v1"); } catch { } - return { id, url: URL.createObjectURL(blob) }; + return { id, kind: kindOf(blob), url: URL.createObjectURL(blob) }; }, setPhotoId: (id) => persistView({ consoleBgPhotoId: id }), removePhoto: async (id) => { diff --git a/01_content/src/client/index.tsx b/01_content/src/client/index.tsx index 0d1a3f0..3ead197 100644 --- a/01_content/src/client/index.tsx +++ b/01_content/src/client/index.tsx @@ -3,7 +3,7 @@ import { css } from './styles' import { NS, zh, en, type WorktableKey } from './locales' import { isAbs, joinPath, parentPathOf, basenameOf } from './pathutil' import { splitStore, SplitWorkspace, setSplitT, setSplitEnv, type LayoutSpec, type SplitPane, type ConsoleCardData } from './split' -import { photoStore } from './photoStore' +import { photoStore, kindOf } from './photoStore' /** * dsh-worktable 客户端(v2):侧边栏底部「工作台」区块。 @@ -1386,7 +1386,7 @@ function WorktableSection(props: any) { const stored = viewRef.current.consoleBgPhotoId ?? null const activeId = stored && records.some((r) => r.id === stored) ? stored : null return { - list: records.map((r) => ({ id: r.id, url: URL.createObjectURL(r.blob) })), + list: records.map((r) => ({ id: r.id, kind: r.kind, url: URL.createObjectURL(r.blob) })), activeId, } }, @@ -1394,7 +1394,7 @@ function WorktableSection(props: any) { const id = await photoStore.add(blob) persistView({ consoleBgPhotoId: id }) try { localStorage.removeItem('dsh.worktable.consoleBgPhoto.v1') } catch {} - return { id, url: URL.createObjectURL(blob) } + return { id, kind: kindOf(blob), url: URL.createObjectURL(blob) } }, setPhotoId: (id: string) => persistView({ consoleBgPhotoId: id }), removePhoto: async (id: string) => { await photoStore.remove(id) }, diff --git a/01_content/src/client/locales.ts b/01_content/src/client/locales.ts index 7f31a94..d54017c 100644 --- a/01_content/src/client/locales.ts +++ b/01_content/src/client/locales.ts @@ -122,7 +122,7 @@ export const zh = { 'console.bgLabel': '背景', 'console.bgPlain': '纯色背景', 'console.bgGlow': '流光背景', - 'console.bgPhoto': '上传照片', + 'console.bgPhoto': '上传媒体', 'console.bgEdit': '背景设置', 'console.bgEditPlain': '纯色背景设置', 'console.bgEditGlow': '流光背景设置', @@ -321,7 +321,7 @@ export const en = { 'console.bgLabel': 'Background', 'console.bgPlain': 'Plain', 'console.bgGlow': 'Glow', - 'console.bgPhoto': 'Photo', + 'console.bgPhoto': 'Upload media', 'console.bgEdit': 'Background settings', 'console.bgEditPlain': 'Plain background', 'console.bgEditGlow': 'Glow background', diff --git a/01_content/src/client/photoStore.ts b/01_content/src/client/photoStore.ts index e34addf..7c6e573 100644 --- a/01_content/src/client/photoStore.ts +++ b/01_content/src/client/photoStore.ts @@ -6,7 +6,10 @@ const STORE = 'photoRecords' const LEGACY_STORE = 'consoleBgPhoto' const LEGACY_KEY = 'original' -export type PhotoRecord = { id: string; createdAt: number; blob: Blob } +export type MediaKind = 'photo' | 'video' +export type PhotoRecord = { id: string; createdAt: number; kind: MediaKind; blob: Blob } + +export const kindOf = (blob: Blob): MediaKind => (blob.type && blob.type.indexOf('video/') === 0 ? 'video' : 'photo') function openDb(): Promise { return new Promise((resolve, reject) => { @@ -29,7 +32,7 @@ function getAll(db: IDBDatabase): Promise { for (const v of req.result as unknown[]) { const r = v as PhotoRecord | null if (r && typeof r === 'object' && typeof (r as any).id === 'string' && (r as any).blob instanceof Blob) { - arr.push(r) + arr.push({ id: r.id, createdAt: r.createdAt, kind: kindOf(r.blob), blob: r.blob }) } } arr.sort((a, b) => b.createdAt - a.createdAt) @@ -56,7 +59,7 @@ export const photoStore = { req.onerror = () => reject(req.error) }) if (blob) { - const rec: PhotoRecord = { id: 'legacy', createdAt: Date.now(), blob } + const rec: PhotoRecord = { id: 'legacy', createdAt: Date.now(), kind: kindOf(blob), blob } await new Promise((resolve, reject) => { const tx = db.transaction(STORE, 'readwrite') tx.objectStore(STORE).put(rec, rec.id) @@ -71,11 +74,11 @@ export const photoStore = { db.close() return [] }, - /** 新增一张原始照片,返回其 id(自动成为最新) */ + /** 新增一条媒体(照片/视频),返回其 id(自动成为最新) */ async add(blob: Blob): Promise { const db = await openDb() const id = Date.now().toString(36) + Math.random().toString(36).slice(2, 7) - const rec: PhotoRecord = { id, createdAt: Date.now(), blob } + const rec: PhotoRecord = { id, createdAt: Date.now(), kind: kindOf(blob), blob } return new Promise((resolve, reject) => { const tx = db.transaction(STORE, 'readwrite') tx.objectStore(STORE).put(rec, rec.id) diff --git a/01_content/src/client/split.tsx b/01_content/src/client/split.tsx index c66cd40..893e215 100644 --- a/01_content/src/client/split.tsx +++ b/01_content/src/client/split.tsx @@ -247,8 +247,8 @@ type SplitEnv = { setShape: (s: 'square' | 'circle') => void getBg: () => 'plain' | 'glow' | 'photo' setBg: (m: 'plain' | 'glow' | 'photo') => void - getPhotoLib: () => Promise<{ list: { id: string; url: string }[]; activeId: string | null }> - addPhoto: (blob: Blob) => Promise<{ id: string; url: string }> + getPhotoLib: () => Promise<{ list: { id: string; kind: 'photo' | 'video'; url: string }[]; activeId: string | null }> + addPhoto: (blob: Blob) => Promise<{ id: string; kind: 'photo' | 'video'; url: string }> setPhotoId: (id: string) => void getPhotoHsl: (id: string) => { h: number; s: number; l: number } setPhotoHsl: (id: string, v: { h: number; s: number; l: number }) => void @@ -1089,7 +1089,8 @@ function ConsolePane() { const [bg, setBgState] = useState<'plain' | 'glow' | 'photo'>(() => splitEnv?.console?.getBg?.() ?? 'glow') const [bgPhoto, setBgPhoto] = useState('') const [photoId, setPhotoIdLocal] = useState('') - const [photoList, setPhotoList] = useState<{ id: string; url: string }[]>([]) + const [photoList, setPhotoList] = useState<{ id: string; kind: 'photo' | 'video'; url: string }[]>([]) + const [bgVideo, setBgVideo] = useState('') const [photoGrid, setPhotoGridState] = useState(() => splitEnv?.console?.getPhotoGrid?.() ?? true) const photoUrlRef = useRef('') const [openMenu, setOpenMenu] = useState(null) @@ -1121,7 +1122,7 @@ function ConsolePane() { const pickPhotoFile = () => { const input = document.createElement('input') input.type = 'file' - input.accept = 'image/*' + input.accept = 'image/*,video/*' input.onchange = () => { const file = input.files?.[0] if (!file) return @@ -1129,23 +1130,24 @@ function ConsolePane() { } input.click() } - /** 新增照片:原始 Blob 存 IndexedDB(零压缩),显示用对象 URL;自动设为当前 */ + /** 新增媒体(照片/视频):原始 Blob 存 IndexedDB(零压缩),显示用对象 URL;自动设为当前 */ const onFilePhoto = async (file: Blob) => { let id = '' let url = '' + let kind: 'photo' | 'video' = file.type && file.type.indexOf('video/') === 0 ? 'video' : 'photo' const rec = await (splitEnv?.console?.addPhoto?.(file) ?? Promise.resolve(null)).catch(() => null) - if (rec) { id = rec.id; url = rec.url } else { + if (rec) { id = rec.id; url = rec.url; kind = rec.kind } else { url = URL.createObjectURL(file) } photoUrlRef.current = url setPhotoIdLocal(id) - if (id) setPhotoList((prev) => [{ id, url }, ...prev]) - setBgPhoto(url) + if (id) setPhotoList((prev) => [{ id, kind, url }, ...prev]) + if (kind === 'video') { setBgPhoto(''); setBgVideo(url) } else { setBgVideo(''); setBgPhoto(url) } setBgState('photo') splitEnv?.console?.setBg?.('photo') } - /** 删除一张照片:当前使用的那张被删 → 切到最新一张;删光 → 回到流光背景 */ - const removePhotoById = async (p: { id: string; url: string }) => { + /** 删除一条媒体:当前使用的那条被删 → 切到最新一条;删光 → 回到流光背景 */ + const removePhotoById = async (p: { id: string; kind: 'photo' | 'video'; url: string }) => { try { await splitEnv?.console?.removePhoto?.(p.id) } catch {} try { URL.revokeObjectURL(p.url) } catch {} const nextList = photoList.filter((x) => x.id !== p.id) @@ -1156,13 +1158,15 @@ function ConsolePane() { setPhotoIdLocal(next.id) splitEnv?.console?.setPhotoId?.(next.id) photoUrlRef.current = next.url - setBgPhoto(next.url) + setBgVideo(next.kind === 'video' ? next.url : '') + setBgPhoto(next.kind === 'video' ? '' : next.url) setBgState('photo') splitEnv?.console?.setBg?.('photo') } else { setPhotoIdLocal('') photoUrlRef.current = '' setBgPhoto('') + setBgVideo('') setBgState('glow') splitEnv?.console?.setBg?.('glow') } @@ -1173,12 +1177,12 @@ function ConsolePane() { setPhotoGridState(next) splitEnv?.console?.setPhotoGrid?.(next) } - /** 在照片库中选中某张 */ - const selectPhoto = (p: { id: string; url: string }) => { + /** 在媒体库中选中某条 */ + const selectPhoto = (p: { id: string; kind: 'photo' | 'video'; url: string }) => { setPhotoIdLocal(p.id) splitEnv?.console?.setPhotoId?.(p.id) photoUrlRef.current = p.url - setBgPhoto(p.url) + if (p.kind === 'video') { setBgPhoto(''); setBgVideo(p.url) } else { setBgVideo(''); setBgPhoto(p.url) } setBgState('photo') splitEnv?.console?.setBg?.('photo') } @@ -1271,7 +1275,8 @@ function ConsolePane() { if (active) { setPhotoIdLocal(active.id) photoUrlRef.current = active.url - setBgPhoto(active.url) + setBgVideo(active.kind === 'video' ? active.url : '') + setBgPhoto(active.kind === 'video' ? '' : active.url) } }).catch(() => {}) return () => { alive = false } @@ -1315,8 +1320,10 @@ function ConsolePane() { { mode: 'system', key: 'console.themeSystem' }, ] return ( -
- {bg === 'glow' && (<>)} +
+ + {bg === 'photo' && bgVideo && {openMenu !== null &&
setOpenMenu(null)} />}
@@ -1410,15 +1417,19 @@ function ConsolePane() {
{bgEdit === 'photo' && ( <> -
- {T('console.bgGridLabel')} - + +
+ {T('console.bgGridLabel')} + +
{photoList.length > 0 ? ( photoList.slice(0, 4).map((p) => (
selectPhoto(p)}> - + {p.kind === 'video' + ?
diff --git a/01_content/src/client/styles.ts b/01_content/src/client/styles.ts index cda30ef..b792560 100644 --- a/01_content/src/client/styles.ts +++ b/01_content/src/client/styles.ts @@ -441,7 +441,8 @@ export const css = xtermCss + '\n' + [ '.dsh-wt_switchKnob{position:absolute;top:2px;left:2px;width:13px;height:13px;border-radius:50%;background:#fff;box-shadow:0 1px 3px rgba(0,0,0,.4);transition:left .18s cubic-bezier(.22,.61,.36,1)}', '.dsh-wt_switchOn .dsh-wt_switchKnob{left:15px}', '.dsh-wt_console[data-wt-theme=light] .dsh-wt_switch{background:rgba(27,31,36,.15)}', - '.dsh-wt_console[data-wt-grid=off] .dsh-wt_consoleBg::after{display:none}', + '.dsh-wt_console[data-wt-bg=photo][data-wt-grid=on] .dsh-wt_consoleBg::after{background-image:linear-gradient(rgba(255,255,255,.16) 1px,transparent 1px),linear-gradient(90deg,rgba(255,255,255,.16) 1px,transparent 1px);background-size:30px 30px}', + '.dsh-wt_console[data-wt-bg=photo][data-wt-grid=off] .dsh-wt_consoleBg::after{display:none}', '.dsh-wt_hslHead{display:flex;align-items:center;gap:4px;padding:2px 4px 4px}', '.dsh-wt_hslBack{flex:none;width:20px;height:20px;padding:0;border:none;border-radius:6px;background:transparent;color:var(--wt-text3);cursor:pointer;font-size:14px;line-height:1;display:inline-flex;align-items:center;justify-content:center}', '.dsh-wt_hslBack:hover{background:var(--wt-chip);color:var(--wt-text)}', @@ -449,7 +450,8 @@ export const css = xtermCss + '\n' + [ '.dsh-wt_hslRow{display:flex;align-items:center;gap:4px;padding:2px 3px;width:100%;box-sizing:border-box}', '.dsh-wt_hslLabel{flex:none;width:10px;font-size:9px;font-weight:600;color:var(--wt-text3);text-align:center}', '.dsh-wt_hslSlider{flex:1;min-width:0;height:14px;accent-color:#4f8ef7;cursor:pointer}', - '.dsh-wt_hslInput{flex:none;width:36px;height:20px;padding:0 4px;border:1px solid var(--wt-border);border-radius:6px;background:var(--wt-chip);color:var(--wt-text2);font:inherit;font-size:10px;text-align:right;outline:none;box-sizing:border-box;font-variant-numeric:tabular-nums}', + '.dsh-wt_hslInput{flex:none;width:42px;height:20px;padding:0 4px;border:1px solid var(--wt-border);border-radius:6px;background:var(--wt-chip);color:var(--wt-text2);font:inherit;font-size:10px;text-align:right;outline:none;box-sizing:border-box;font-variant-numeric:tabular-nums;-moz-appearance:textfield;appearance:textfield}', + '.dsh-wt_hslInput::-webkit-outer-spin-button,.dsh-wt_hslInput::-webkit-inner-spin-button{-webkit-appearance:none;margin:0}', '.dsh-wt_hslInput:focus{border-color:#4f8ef7;color:var(--wt-text)}', '.dsh-wt_hslReset{margin:4px 4px 2px;padding:5px 9px;border:none;border-radius:7px;background:var(--wt-chip);color:var(--wt-text2);font:inherit;font-size:12px;line-height:17px;cursor:pointer;text-align:center}', '.dsh-wt_hslReset:hover{background:rgba(79,142,247,.22);color:var(--wt-text)}', @@ -460,6 +462,9 @@ export const css = xtermCss + '\n' + [ '.dsh-wt_photoName{flex:1;min-width:0;font-size:11px;line-height:16px;color:var(--wt-text2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}', '.dsh-wt_photoEmpty{padding:4px 4px 2px;font-size:11px;line-height:16px;color:var(--wt-text3)}', '.dsh-wt_hslDivider{height:1px;margin:5px 4px;background:var(--wt-border);opacity:.5}', + '.dsh-wt_consoleVid{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}', + '.dsh-wt_gridHalf{flex:1;min-width:0;display:flex;align-items:center;justify-content:space-between;gap:4px;padding:0 4px;box-sizing:border-box}', + '.dsh-wt_photoThumb video,.dsh-wt_photoThumb{display:block}', '.dsh-wt_dropItemOn{background:var(--wt-chip);color:var(--wt-text);font-weight:600}', '.dsh-wt_hbar{position:relative;width:130px;height:26px;margin:4px auto;border-radius:999px;background:rgba(255,255,255,.08)}', '.dsh-wt_hbarFill{position:absolute;top:3px;bottom:3px;left:0;border-radius:999px;background:linear-gradient(90deg,#4f8ef7,#3b82f6);pointer-events:none;transition:width .25s cubic-bezier(.22,.61,.36,1)}',