diff --git a/package.json b/package.json index eecc6251..e107c502 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "build:prod": "vite build --mode production", "build": "npm run build:dev", "build:core": "cd ./src/core && npm run build && cd ../.. && node ./script/copy-core.cjs", + "build:webui": "cd ./src/webui && vite build", "watch": "npm run watch:dev", "debug-win": "powershell dist/napcat.ps1", "lint": "eslint --fix src/**/*.{js,ts}", diff --git a/static/components/NapCat.ts b/src/webui/src/NapCat.ts similarity index 97% rename from static/components/NapCat.ts rename to src/webui/src/NapCat.ts index 417a52c6..9ffcaa3d 100644 --- a/static/components/NapCat.ts +++ b/src/webui/src/NapCat.ts @@ -1,9 +1,9 @@ -import { SettingList } from "./SettingList"; -import { SettingItem } from "./SettingItem"; -import { SettingButton } from "./SettingButton"; -import { SettingSwitch } from "./SettingSwitch"; -import { SettingSelect } from "./SettingSelect"; -import { WebUiApi } from "./WebApi" +import { SettingList } from "./components/SettingList"; +import { SettingItem } from "./components/SettingItem"; +import { SettingButton } from "./components/SettingButton"; +import { SettingSwitch } from "./components/SettingSwitch"; +import { SettingSelect } from "./components/SettingSelect"; +import { WebUiApi } from "./components/WebApi" async function onSettingWindowCreated(view: Element) { const isEmpty = (value: any) => value === undefined || value === undefined || value === ''; let ob11Config = await WebUiApi.getOB11Config(); diff --git a/static/components/SettingButton.ts b/src/webui/src/components/SettingButton.ts similarity index 100% rename from static/components/SettingButton.ts rename to src/webui/src/components/SettingButton.ts diff --git a/static/components/SettingItem.ts b/src/webui/src/components/SettingItem.ts similarity index 100% rename from static/components/SettingItem.ts rename to src/webui/src/components/SettingItem.ts diff --git a/static/components/SettingList.ts b/src/webui/src/components/SettingList.ts similarity index 100% rename from static/components/SettingList.ts rename to src/webui/src/components/SettingList.ts diff --git a/static/components/SettingOption.ts b/src/webui/src/components/SettingOption.ts similarity index 100% rename from static/components/SettingOption.ts rename to src/webui/src/components/SettingOption.ts diff --git a/static/components/SettingSelect.ts b/src/webui/src/components/SettingSelect.ts similarity index 100% rename from static/components/SettingSelect.ts rename to src/webui/src/components/SettingSelect.ts diff --git a/static/components/SettingSwitch.ts b/src/webui/src/components/SettingSwitch.ts similarity index 100% rename from static/components/SettingSwitch.ts rename to src/webui/src/components/SettingSwitch.ts diff --git a/static/components/WebApi.ts b/src/webui/src/components/WebApi.ts similarity index 100% rename from static/components/WebApi.ts rename to src/webui/src/components/WebApi.ts diff --git a/src/webui/vite.config.ts b/src/webui/vite.config.ts new file mode 100644 index 00000000..56a17370 --- /dev/null +++ b/src/webui/vite.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from 'vite' + +export default defineConfig({ + build:{ + target: 'esnext', + minify: false, + lib: { + entry: 'src/NapCat.ts', + formats: ['cjs'], + fileName: () => 'renderer.js', + } + } +}); \ No newline at end of file diff --git a/static/assets/renderer.js b/static/assets/renderer.js new file mode 100644 index 00000000..b28e910a --- /dev/null +++ b/static/assets/renderer.js @@ -0,0 +1,386 @@ +'use strict'; + +Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); + +const SettingList = (items, title, isCollapsible = false, direction = "column") => { + return ` + + + ${items.join("")} + + + `; +}; + +const SettingItem = (title, subtitle, action, id, visible = true) => { + return ` +
+ ${title} + ${subtitle ? `${subtitle}` : ""} +
+ ${action ? `
${action}
` : ""} +
`; +}; + +const SettingButton = (text, id, type = "secondary") => { + return `${text}`; +}; + +const SettingSwitch = (configKey, isActive = false, extraData) => { + return ` `data-${key}="${extraData[key]}"`) : ""} + > + `; +}; + +const SettingOption = (text, value, isSelected = false) => { + return `${text}`; +}; + +const SelectTemplate = document.createElement("template"); +SelectTemplate.innerHTML = ` +
+
+ + + + +
+ +
`; +window.customElements.define( + "ob-setting-select", + class extends HTMLElement { + _button; + _text; + _context; + constructor() { + super(); + this.attachShadow({ mode: "open" }); + this.shadowRoot?.append(SelectTemplate.content.cloneNode(true)); + this._button = this.shadowRoot.querySelector('div[part="button"]'); + this._text = this.shadowRoot.querySelector('input[part="current-text"]'); + this._context = this.shadowRoot.querySelector('ul[part="option-list"]'); + const buttonClick = () => { + const isHidden = this._context.classList.toggle("hidden"); + window[`${isHidden ? "remove" : "add"}EventListener`]("pointerdown", windowPointerDown); + }; + const windowPointerDown = ({ target }) => { + if (!this.contains(target)) + buttonClick(); + }; + this._button.addEventListener("click", buttonClick); + this._context.addEventListener("click", ({ target }) => { + if (target.tagName !== "SETTING-OPTION") + return; + buttonClick(); + if (target.hasAttribute("is-selected")) + return; + this.querySelectorAll("setting-option[is-selected]").forEach((dom) => dom.toggleAttribute("is-selected")); + target.toggleAttribute("is-selected"); + this._text.value = target.textContent; + this.dispatchEvent( + new CustomEvent("selected", { + bubbles: true, + composed: true, + detail: { + name: target.textContent, + value: target.dataset.value + } + }) + ); + }); + this._text.value = this.querySelector("setting-option[is-selected]")?.textContent; + } + } +); +const SettingSelect = (items, configKey, configValue) => { + return ` + ${items.map((e, i) => { + return SettingOption(e.text, e.value, configKey && configValue ? configValue === e.value : i === 0); + }).join("")} +`; +}; + +class WebUiApiWrapper { + token = ""; + async setOB11Config(config) { + } + async getOB11Config() { + return { + httpHost: "", + httpPort: 3e3, + httpPostUrls: [], + httpSecret: "", + wsHost: "", + wsPort: 3e3, + wsReverseUrls: [], + enableHttp: false, + enableHttpHeart: false, + enableHttpPost: false, + enableWs: false, + enableWsReverse: false, + messagePostFormat: "array", + reportSelfMessage: false, + enableLocalFile2Url: false, + debug: false, + heartInterval: 6e4, + token: "", + musicSignUrl: "" + }; + } +} +const WebUiApi = new WebUiApiWrapper(); + +async function onSettingWindowCreated(view) { + const isEmpty = (value) => value === void 0 || value === void 0 || value === ""; + let ob11Config = await WebUiApi.getOB11Config(); + const setOB11Config = (key, value) => { + }; + const parser = new DOMParser(); + const doc = parser.parseFromString( + [ + "
", + ` +
+
`, + SettingList([ + SettingItem( + '正在检查 Napcat 更新', + void 0, + SettingButton("请稍候", "napcat-update-button", "secondary") + ) + ]), + SettingList([ + SettingItem( + "启用 HTTP 服务", + void 0, + SettingSwitch("ob11.enableHttp", ob11Config.enableHttp, { "control-display-id": "config-ob11-httpPort" }) + ), + SettingItem( + "HTTP 服务监听端口", + void 0, + `
`, + "config-ob11-httpPort", + ob11Config.enableHttp + ), + SettingItem( + "启用 HTTP 心跳", + void 0, + SettingSwitch("ob11.enableHttpHeart", ob11Config.enableHttpHeart, { + "control-display-id": "config-ob11-enableHttpHeart" + }) + ), + SettingItem( + "启用 HTTP 事件上报", + void 0, + SettingSwitch("ob11.enableHttpPost", ob11Config.enableHttpPost, { + "control-display-id": "config-ob11-httpHosts" + }) + ), + `
+ +
+ HTTP 事件上报密钥 +
+
+ +
+
+ +
+ HTTP 事件上报地址 +
+ 添加 +
+
+
`, + SettingItem( + "启用正向 WebSocket 服务", + void 0, + SettingSwitch("ob11.enableWs", ob11Config.enableWs, { "control-display-id": "config-ob11-wsPort" }) + ), + SettingItem( + "正向 WebSocket 服务监听端口", + void 0, + `
`, + "config-ob11-wsPort", + ob11Config.enableWs + ), + SettingItem( + "启用反向 WebSocket 服务", + void 0, + SettingSwitch("ob11.enableWsReverse", ob11Config.enableWsReverse, { + "control-display-id": "config-ob11-wsHosts" + }) + ), + `
+ +
+ 反向 WebSocket 监听地址 +
+ 添加 +
+
+
`, + SettingItem( + " WebSocket 服务心跳间隔", + "控制每隔多久发送一个心跳包,单位为毫秒", + `
` + ), + SettingItem( + "Access token", + void 0, + `
` + ), + SettingItem( + "新消息上报格式", + `如客户端无特殊需求推荐保持默认设置,两者的详细差异可参考 OneBot v11 文档`, + SettingSelect( + [ + { text: "消息段", value: "array" }, + { text: "CQ码", value: "string" } + ], + "ob11.messagePostFormat", + ob11Config.messagePostFormat + ) + ), + SettingItem( + "音乐卡片签名地址", + void 0, + `
`, + "config-musicSignUrl" + ), + SettingItem("", void 0, SettingButton("保存", "config-ob11-save", "primary")) + ]), + SettingList([ + SettingItem( + "上报 Bot 自身发送的消息", + "上报 event 为 message_sent", + SettingSwitch("reportSelfMessage", ob11Config.reportSelfMessage) + ) + ]), + SettingList([ + SettingItem("GitHub 仓库", `https://github.com/`, SettingButton("点个星星", "open-github")), + SettingItem("NapCat 文档", `https://`, SettingButton("看看文档", "open-docs")), + SettingItem("Telegram 群", `https://t.me/+nLZEnpne-pQ1OWFl`, SettingButton("进去逛逛", "open-telegram")), + SettingItem("QQ 群", `545402644`, SettingButton("我要进去", "open-qq-group")) + ]), + "
" + ].join(""), + "text/html" + ); + doc.querySelector("#open-github")?.addEventListener("click", () => { + window.open("https://github.com/", "_blank"); + }); + doc.querySelector("#open-telegram")?.addEventListener("click", () => { + window.open("https://t.me/+nLZEnpne-pQ1OWFl"); + }); + doc.querySelector("#open-qq-group")?.addEventListener("click", () => { + window.open("https://qm.qq.com/q/bDnHRG38aI"); + }); + doc.querySelector("#open-docs")?.addEventListener("click", () => { + window.open("https://github.io/"); + }); + const buildHostListItem = (type, host, index, inputAttrs = {}) => { + const dom = { + container: document.createElement("setting-item"), + input: document.createElement("input"), + inputContainer: document.createElement("div"), + deleteBtn: document.createElement("setting-button") + }; + dom.container.classList.add("setting-host-list-item"); + dom.container.dataset.direction = "row"; + Object.assign(dom.input, inputAttrs); + dom.input.classList.add("q-input__inner"); + dom.input.type = "url"; + dom.input.value = host; + dom.input.addEventListener("input", () => { + ob11Config[type][index] = dom.input.value; + }); + dom.inputContainer.classList.add("q-input"); + dom.inputContainer.appendChild(dom.input); + dom.deleteBtn.innerHTML = "删除"; + dom.deleteBtn.dataset.type = "secondary"; + dom.deleteBtn.addEventListener("click", () => { + ob11Config[type].splice(index, 1); + initReverseHost(type); + }); + dom.container.appendChild(dom.inputContainer); + dom.container.appendChild(dom.deleteBtn); + return dom.container; + }; + const buildHostList = (hosts, type, inputAttr = {}) => { + const result = []; + hosts.forEach((host, index) => { + result.push(buildHostListItem(type, host, index, inputAttr)); + }); + return result; + }; + const addReverseHost = (type, doc2 = document, inputAttr = {}) => { + const hostContainerDom = doc2.body.querySelector(`#config-ob11-${type}-list`); + hostContainerDom?.appendChild(buildHostListItem(type, "", ob11Config[type].length, inputAttr)); + ob11Config[type].push(""); + }; + const initReverseHost = (type, doc2 = document) => { + const hostContainerDom = doc2.body?.querySelector(`#config-ob11-${type}-list`); + [...hostContainerDom.childNodes].forEach((dom) => dom.remove()); + buildHostList(ob11Config[type], type).forEach((dom) => { + hostContainerDom?.appendChild(dom); + }); + }; + initReverseHost("httpHosts", doc); + initReverseHost("wsHosts", doc); + doc.querySelector("#config-ob11-httpHosts-add")?.addEventListener( + "click", + () => addReverseHost("httpHosts", document, { placeholder: "如:http://127.0.0.1:5140/onebot" }) + ); + doc.querySelector("#config-ob11-wsHosts-add")?.addEventListener( + "click", + () => addReverseHost("wsHosts", document, { placeholder: "如:ws://127.0.0.1:5140/onebot" }) + ); + doc.querySelector("#config-ffmpeg-select")?.addEventListener("click", () => { + }); + doc.querySelector("#config-open-log-path")?.addEventListener("click", () => { + }); + doc.querySelectorAll("setting-switch[data-config-key]").forEach((dom) => { + dom.addEventListener("click", () => { + const active = dom.getAttribute("is-active") === void 0; + setOB11Config(dom.dataset.configKey); + if (active) + dom.setAttribute("is-active", ""); + else + dom.removeAttribute("is-active"); + if (!isEmpty(dom.dataset.controlDisplayId)) { + const displayDom = document.querySelector(`#${dom.dataset.controlDisplayId}`); + if (active) + displayDom?.removeAttribute("is-hidden"); + else + displayDom?.setAttribute("is-hidden", ""); + } + }); + }); + doc.querySelectorAll("setting-item .q-input input.q-input__inner[data-config-key]").forEach((dom) => { + dom.addEventListener("input", () => { + const Type = dom.getAttribute("type"); + dom.dataset.configKey; + Type === "number" ? parseInt(dom.value) >= 1 ? parseInt(dom.value) : 1 : dom.value; + }); + }); + doc.querySelectorAll("ob-setting-select[data-config-key]").forEach((dom) => { + dom?.addEventListener("selected", (e) => { + dom.dataset.configKey; + e.detail.value; + }); + }); + doc.querySelector("#config-ob11-save")?.addEventListener("click", () => { + WebUiApi.setOB11Config(ob11Config); + alert("保存成功"); + }); +} + +exports.onSettingWindowCreated = onSettingWindowCreated; diff --git a/static/index.html b/static/index.html index 95f91129..7670f6fd 100644 --- a/static/index.html +++ b/static/index.html @@ -12,6 +12,12 @@ + + + NapCat-WebUi diff --git a/vite.config.ts b/vite.config.ts index be4294b3..d75f68b1 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -43,6 +43,7 @@ const baseConfigPlugin: PluginOption[] = [ targets: [ // ...external.map(genCpModule), { src: './src/napcat.json', dest: 'dist/config/' }, + { src: './static/', dest: 'dist/static/', flatten: false }, { src: './src/onebot11/onebot11.json', dest: 'dist/config/' }, { src: './package.json', dest: 'dist' }, { src: './README.md', dest: 'dist' },