├── res ├── icon.png ├── background.jpg ├── img_header.png ├── StarRailFont.ttf ├── bubbles │ ├── bubbleStyle6 │ │ ├── main.png │ │ ├── part1.png │ │ └── part2.png │ ├── bubbles.json │ ├── bubbleStyle5.svg │ ├── bubbleStyle4.svg │ ├── bubbleStyle2.svg │ ├── bubbleStyle3.svg │ └── bubbleStyle1.svg ├── icon_tip.svg └── thumb.svg ├── screenshots ├── main.png ├── bubbles.jpg └── settings.jpg ├── manifest.json ├── README.md ├── src ├── preload.js ├── config.js ├── utils.js ├── bubble.js ├── main.js ├── settings.html ├── renderer.js └── style.css ├── LICENSE └── .gitignore /res/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SyrieYume/starrail_ui/HEAD/res/icon.png -------------------------------------------------------------------------------- /res/background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SyrieYume/starrail_ui/HEAD/res/background.jpg -------------------------------------------------------------------------------- /res/img_header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SyrieYume/starrail_ui/HEAD/res/img_header.png -------------------------------------------------------------------------------- /res/StarRailFont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SyrieYume/starrail_ui/HEAD/res/StarRailFont.ttf -------------------------------------------------------------------------------- /screenshots/main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SyrieYume/starrail_ui/HEAD/screenshots/main.png -------------------------------------------------------------------------------- /screenshots/bubbles.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SyrieYume/starrail_ui/HEAD/screenshots/bubbles.jpg -------------------------------------------------------------------------------- /screenshots/settings.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SyrieYume/starrail_ui/HEAD/screenshots/settings.jpg -------------------------------------------------------------------------------- /res/bubbles/bubbleStyle6/main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SyrieYume/starrail_ui/HEAD/res/bubbles/bubbleStyle6/main.png -------------------------------------------------------------------------------- /res/bubbles/bubbleStyle6/part1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SyrieYume/starrail_ui/HEAD/res/bubbles/bubbleStyle6/part1.png -------------------------------------------------------------------------------- /res/bubbles/bubbleStyle6/part2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SyrieYume/starrail_ui/HEAD/res/bubbles/bubbleStyle6/part2.png -------------------------------------------------------------------------------- /res/icon_tip.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 4, 3 | "type": "theme", 4 | "name": "仿星穹铁道短信UI", 5 | "slug": "starrail_ui", 6 | "description": "仿照星穹铁道中的角色短信界面的UI.", 7 | "version": "0.2.1", 8 | "icon": "./res/icon.png", 9 | "thumb": "./res/thumb.svg", 10 | "authors": [ 11 | { 12 | "name": "Syrie", 13 | "link": "https://github.com/SyrieYume" 14 | } 15 | ], 16 | "repository": { 17 | "repo": "SyrieYume/starrail_ui", 18 | "branch": "main", 19 | "release": { 20 | "tag": "latest", 21 | "file": "starrail_ui.zip" 22 | } 23 | }, 24 | "platform": ["win32", "darwin", "linux"], 25 | "injects": { 26 | "renderer": "./src/renderer.js", 27 | "main": "./src/main.js", 28 | "preload": "./src/preload.js" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Star Rail UI 2 | 仿照《崩坏:星穹铁道》中角色短信风格的 LiteLoaderQQNT 主题 3 | 4 | ## 界面展示 5 | ### 主界面 6 | !["main"](screenshots/main.png) 7 | 8 | ### 设置页 9 | ![setting](screenshots/settings.jpg) 10 | 11 | ### 气泡框 12 | ![bubles](screenshots/bubbles.jpg) 13 | 14 | 15 | ## 安装方法 16 | 1. 安装 **[LiteLoaderQQNT](https://liteloaderqqnt.github.io/)** 17 | 1. 从 **[Release](https://github.com/SyrieYume/starrail_ui/releases)** 下载最新的插件版本(或直接 clone 本项目) 18 | 19 | 2. 将下载的插件解压到 LiteLoaderQQNT 目录的 `plugins` 文件夹下 20 | 21 | 3. 重启 QQ 22 | 23 | 24 | ## 版本适配 25 | - 仅适配 **LiteLoaderQQNT 1.0.0** 及以上版本 26 | - 开发基于的QQ版本为 **9.9.12-26466**(目前官网最新版,但是QQ可能会推送更高的版本给部分用户更新,**不保证在更高的版本不出问题**),如果遇到问题,请先尝试 **更新** / **回退** 到该版本。 27 | 28 | 29 | ## 一些注意事项 30 | 1. 聊天界面右上角的工具栏 和 窗口右上角的按钮 会自动隐藏,鼠标移到对应位置就能让它们显现。 31 | 32 | 2. 左侧最近聊天列表的宽度是可以调节的。 33 | 34 | 3. 与 其它主题插件 或者 **对界面有修改的插件** 同时使用可能会导致界面出现无法预知的问题。 35 | 36 | 4. 如果使用的是较新版本的QQ,请在 **超级调色盘** 中使用任意主题色。默认主题里QQ侧边栏的颜色无法改为半透明色,我暂时还没找到解决办法。 -------------------------------------------------------------------------------- /src/preload.js: -------------------------------------------------------------------------------- 1 | // Electron 主进程 与 渲染进程 交互的桥梁 2 | const { contextBridge, ipcRenderer } = require("electron"); 3 | 4 | // 在window对象下导出只读对象 5 | contextBridge.exposeInMainWorld("starrail_ui", { 6 | 7 | onUpdateStyle: (callback) => ipcRenderer.on( 8 | "starrail_ui.updateStyle", 9 | callback 10 | ), 11 | 12 | rendererReady: () => ipcRenderer.send( 13 | "starrail_ui.rendererReady" 14 | ), 15 | 16 | getConfig: () => ipcRenderer.invoke( 17 | "starrail_ui.getConfig" 18 | ), 19 | 20 | getSettingsView: () => ipcRenderer.invoke( 21 | "starrail_ui.getSettingsView" 22 | ), 23 | 24 | updateConfig: (newConfig) => ipcRenderer.send( 25 | "starrail_ui.updateConfig", 26 | newConfig 27 | ), 28 | 29 | getBubbles: () => ipcRenderer.invoke( 30 | "starrail_ui.getBubbles" 31 | ), 32 | 33 | getNewVersion: (nowVersion) => ipcRenderer.invoke( 34 | "starrail_ui.getNewVersion", 35 | nowVersion 36 | ) 37 | }); -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs") 2 | const path = require("path") 3 | 4 | const pluginPath = path.dirname(__dirname).replace(/\\/g, "/") 5 | const configFilePath = `${pluginPath}/config.json` 6 | 7 | const defaultConfig = { 8 | "plugin_path": pluginPath, 9 | "background_img" : `${pluginPath}/res/background.jpg`, 10 | "background_blur": 8, 11 | "background_brightness": 50, 12 | "chatWindow_Opacity": 95, 13 | "bubble": 0, 14 | "displayNickname": true 15 | } 16 | 17 | const config = { ...defaultConfig } 18 | 19 | function saveConfigFile() { 20 | fs.writeFileSync(configFilePath, JSON.stringify(config)) 21 | } 22 | 23 | function applyConfig(styleData) { 24 | return styleData.replace(/\${([^}]+)}/g, (match, key) => config[key]) 25 | } 26 | 27 | if(fs.existsSync(configFilePath)) { 28 | const savedConfig = fs.readFileSync(configFilePath, "utf-8") 29 | Object.assign(config, JSON.parse(savedConfig)) 30 | }else { 31 | saveConfigFile() 32 | } 33 | 34 | module.exports = { 35 | config, 36 | saveConfigFile, 37 | applyConfig 38 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Syrie Yume 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | const { net } = require("electron") 2 | 3 | // 防抖函数 4 | function debounce(fn, time) { 5 | let timer = null 6 | return function (...args) { 7 | if(timer) clearTimeout(timer) 8 | timer = setTimeout(() => { 9 | fn.apply(this, args) 10 | timer = null 11 | }, time) 12 | } 13 | } 14 | 15 | // 比较两个版本的大小 16 | // v1 > v2时返回1,等于时返回0,小于时返回-1 17 | function compareVersions(v1, v2) { 18 | const parts1 = v1.split('.').map(Number) 19 | const parts2 = v2.split('.').map(Number) 20 | 21 | for (let i = 0; i < parts1.length; i++) { 22 | if (parts2.length === i) 23 | return 1 24 | 25 | if (parts1[i] !== parts2[i]) 26 | return parts1[i] > parts2[i] ? 1 : -1; 27 | } 28 | 29 | return parts1.length === parts2.length ? 0 : -1; 30 | } 31 | 32 | // 从网络获取数据 33 | function fetchData(url) { 34 | return new Promise((resolve, reject) => { 35 | const request = net.request({ 36 | method: 'GET', 37 | url: url, 38 | redirect: 'follow' // 处理重定向 39 | }); 40 | 41 | request.on('response', (response) => { 42 | const finalUrl = response.headers.location || response.url; 43 | let data = ''; 44 | 45 | response.on('data', (chunk) => { 46 | data += chunk; 47 | }); 48 | 49 | response.on('end', () => { 50 | resolve({ url: finalUrl, content: data }); 51 | }); 52 | }); 53 | 54 | request.on('error', (error) => { 55 | reject(error); 56 | }); 57 | 58 | request.end(); 59 | }); 60 | } 61 | 62 | 63 | module.exports = { 64 | debounce, 65 | compareVersions, 66 | fetchData 67 | } -------------------------------------------------------------------------------- /res/bubbles/bubbles.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "兔子在哪里?", 4 | "imgPath": "bubbleStyle2.svg", 5 | "textColor": "#f0f0f0", 6 | "css": "border-width: 20px 8px 8px 18px;" 7 | }, 8 | 9 | { 10 | "name": "次元扑满", 11 | "imgPath": "bubbleStyle3.svg", 12 | "textColor": "#f0f0f0", 13 | "css": "border-width: 18px 8px 8px 16px;" 14 | }, 15 | 16 | { 17 | "name": "星体培养皿", 18 | "imgPath": "bubbleStyle1.svg", 19 | "textColor": "#f0f0f0", 20 | "css": "" 21 | }, 22 | 23 | { 24 | "name": "怪物酒馆", 25 | "imgPath": "bubbleStyle4.svg", 26 | "textColor": "#f0f0f0", 27 | "css": "border-width: 24px 16px 16px 16px;" 28 | }, 29 | 30 | { 31 | "name": "影城逐梦记", 32 | "imgPath": "bubbleStyle5.svg", 33 | "textColor": "#f0f0f0", 34 | "css": "border-width: 24px 20px 22px 16px;" 35 | }, 36 | 37 | { 38 | "name": "光阴莫负", 39 | "imgPath": "bubbleStyle6/main.png", 40 | "part1": "bubbleStyle6/part1.png", 41 | "part2": "bubbleStyle6/part2.png", 42 | "textColor": "#864756", 43 | "css": "border: none !important; padding: 22px 30px 16px 42px; background-size: 53px 45px, 27px 36px; margin: 6px 0 2px 0; background-image: url('appimg://${bubblesPath}/${part1}'), url('appimg://${bubblesPath}/${part2}'); background-position: top left, bottom right; background-repeat: no-repeat;", 44 | "before": "border-image: url('appimg://${imgPath}'); content: ''; position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: -1; border-image-slice: 50 60 40 60 fill; border-image-width: 33px 40px 27px 40px; border-width: 16px 16px 12px 32px; pointer-events: none;" 45 | } 46 | ] -------------------------------------------------------------------------------- /src/bubble.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs") 2 | const path = require("path") 3 | 4 | const bubblesPath = path.dirname(__dirname).replace(/\\/g, "/") + "/res/bubbles" 5 | 6 | const bubbles = JSON.parse(fs.readFileSync(`${bubblesPath}/bubbles.json`)) 7 | 8 | bubbles.forEach((bubble) => { 9 | bubble.imgPath = `${bubblesPath}/${bubble.imgPath}` 10 | 11 | bubble.css = bubble.css.replace(/\${bubblesPath}/g, bubblesPath).replace(/\${([^}]+)}/g, (match, key) => bubble[key]) 12 | if("before" in bubble) 13 | bubble.before = bubble.before.replace(/\${bubblesPath}/g, bubblesPath).replace(/\${([^}]+)}/g, (match, key) => bubble[key]) 14 | }) 15 | 16 | function applyBubbleStyle(bubbleIndex, styleData){ 17 | if(bubbleIndex <= 0 || bubbleIndex > bubbles.length) 18 | return styleData 19 | const bubble = bubbles[bubbleIndex - 1] 20 | 21 | return styleData.replace("/* Bubble Style */", ` 22 | background: none; 23 | box-shadow: none; 24 | border: 10px solid transparent; 25 | border-radius: 0; 26 | 27 | border-image: url("appimg://${bubble.imgPath}"); 28 | border-image-slice: 49% fill; 29 | border-image-width: 40px 48px; 30 | border-image-repeat: stretch; 31 | ${bubble.css} 32 | } 33 | .message-container--align-right .message-content__wrapper:has(.msg-content-container:not(.mix-message__container--market-face):not(.mix-message__container--pic)) { 34 | margin-right: -2px; 35 | margin-top: -10px; 36 | } 37 | 38 | .message-container--align-right .message-content, .reply-element--self { 39 | color: ${bubble.textColor} !important; 40 | ` + ("before" in bubble ? `}.container--self:not(.mix-message__container--market-face):not(.mix-message__container--pic)::before { ${bubble.before}`: "") 41 | ) 42 | 43 | } 44 | 45 | module.exports = { 46 | bubbles, 47 | applyBubbleStyle 48 | } -------------------------------------------------------------------------------- /res/thumb.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | config.json 2 | releases 3 | 4 | # Logs 5 | logs 6 | *.log 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | lerna-debug.log* 11 | .pnpm-debug.log* 12 | 13 | # Diagnostic reports (https://nodejs.org/api/report.html) 14 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 15 | 16 | # Runtime data 17 | pids 18 | *.pid 19 | *.seed 20 | *.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | lib-cov 24 | 25 | # Coverage directory used by tools like istanbul 26 | coverage 27 | *.lcov 28 | 29 | # nyc test coverage 30 | .nyc_output 31 | 32 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 33 | .grunt 34 | 35 | # Bower dependency directory (https://bower.io/) 36 | bower_components 37 | 38 | # node-waf configuration 39 | .lock-wscript 40 | 41 | # Compiled binary addons (https://nodejs.org/api/addons.html) 42 | build/Release 43 | 44 | # Dependency directories 45 | node_modules/ 46 | jspm_packages/ 47 | 48 | # Snowpack dependency directory (https://snowpack.dev/) 49 | web_modules/ 50 | 51 | # TypeScript cache 52 | *.tsbuildinfo 53 | 54 | # Optional npm cache directory 55 | .npm 56 | 57 | # Optional eslint cache 58 | .eslintcache 59 | 60 | # Optional stylelint cache 61 | .stylelintcache 62 | 63 | # Microbundle cache 64 | .rpt2_cache/ 65 | .rts2_cache_cjs/ 66 | .rts2_cache_es/ 67 | .rts2_cache_umd/ 68 | 69 | # Optional REPL history 70 | .node_repl_history 71 | 72 | # Output of 'npm pack' 73 | *.tgz 74 | 75 | # Yarn Integrity file 76 | .yarn-integrity 77 | 78 | # dotenv environment variable files 79 | .env 80 | .env.development.local 81 | .env.test.local 82 | .env.production.local 83 | .env.local 84 | 85 | # parcel-bundler cache (https://parceljs.org/) 86 | .cache 87 | .parcel-cache 88 | 89 | # Next.js build output 90 | .next 91 | out 92 | 93 | # Nuxt.js build / generate output 94 | .nuxt 95 | dist 96 | 97 | # Gatsby files 98 | .cache/ 99 | # Comment in the public line in if your project uses Gatsby and not Next.js 100 | # https://nextjs.org/blog/next-9-1#public-directory-support 101 | # public 102 | 103 | # vuepress build output 104 | .vuepress/dist 105 | 106 | # vuepress v2.x temp and cache directory 107 | .temp 108 | .cache 109 | 110 | # Docusaurus cache and generated files 111 | .docusaurus 112 | 113 | # Serverless directories 114 | .serverless/ 115 | 116 | # FuseBox cache 117 | .fusebox/ 118 | 119 | # DynamoDB Local files 120 | .dynamodb/ 121 | 122 | # TernJS port file 123 | .tern-port 124 | 125 | # Stores VSCode versions used for testing VSCode extensions 126 | .vscode-test 127 | 128 | # yarn v2 129 | .yarn/cache 130 | .yarn/unplugged 131 | .yarn/build-state.yml 132 | .yarn/install-state.gz 133 | .pnp.* 134 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs") 2 | const path = require("path") 3 | const { BrowserWindow, ipcMain } = require("electron") 4 | const { debounce, compareVersions, fetchData } = require("./utils.js") 5 | let { config, saveConfigFile, applyConfig } = require("./config.js") 6 | let { bubbles, applyBubbleStyle } = require("./bubble.js") 7 | 8 | 9 | 10 | // 更新样式 11 | function updateStyle(webContents) { 12 | if(/^app:\/\/\.\/renderer\/index\.html.+\/main\/message$/.test(webContents.getURL())){ 13 | console.log("[starrail_ui] updateStyle") 14 | let styleData = fs.readFileSync(path.join(__dirname, "style.css"), "utf-8") 15 | styleData = applyConfig(styleData) 16 | styleData = applyBubbleStyle(config.bubble, styleData) 17 | webContents.send("starrail_ui.updateStyle", styleData) 18 | return true 19 | } 20 | return false 21 | } 22 | 23 | // 更新配置 24 | function updateConfig(webContents, newConfig) { 25 | Object.assign(config, newConfig) 26 | updateStyle(webContents) 27 | saveConfig() 28 | } 29 | 30 | // 监听文件修改,方便开发时调试 31 | function watchFileChange(webContents) { 32 | fs.watch(path.join(__dirname, "style.css"), "utf-8", 33 | debounce(() => { 34 | updateStyle(webContents) 35 | }, 400) 36 | ) 37 | } 38 | 39 | // 保存配置文件(限制每秒最多只能写入一次) 40 | const saveConfig = debounce(saveConfigFile, 1000) 41 | 42 | // 监听渲染进程的事件 43 | ipcMain.on("starrail_ui.rendererReady", async (event, message) => { 44 | let count = 0 45 | let intervalId = setInterval(() => { 46 | const window = BrowserWindow.fromWebContents(event.sender) 47 | if((count++) > 10 || updateStyle(window.webContents)) 48 | clearInterval(intervalId) 49 | }, 1000) 50 | }) 51 | 52 | ipcMain.handle("starrail_ui.getConfig", (event, message) => { 53 | return config 54 | }) 55 | 56 | ipcMain.handle("starrail_ui.getBubbles", (event, message) => { 57 | return bubbles 58 | }) 59 | 60 | ipcMain.handle("starrail_ui.getSettingsView", (event, message) => { 61 | return fs.readFileSync(`${config.plugin_path}/src/settings.html`, "utf-8") 62 | }) 63 | 64 | ipcMain.handle("starrail_ui.getNewVersion", async (event, nowVersion) => { 65 | let githubReleaseWeb 66 | try{ 67 | githubReleaseWeb = await fetchData("https://github.com/SyrieYume/starrail_ui/releases/latest") 68 | } 69 | catch(error){ 70 | return { hasNewVersion: false, tip: `网络不佳,检查更新失败,${error}` } 71 | } 72 | 73 | const versionMatch = githubReleaseWeb.content.match(/\/releases\/tag\/v(\d+\.\d+\.\d+)/) 74 | 75 | if(versionMatch){ 76 | const version = versionMatch[1] 77 | if(compareVersions(version, nowVersion) > 0) 78 | return { hasNewVersion: true, tip: `新版本 ${version} 已发布` } 79 | else 80 | return { hasNewVersion: false, tip: `当前已是最新版本` } 81 | } 82 | 83 | return { hasNewVersion: false, tip: "检查更新失败" } 84 | }) 85 | 86 | 87 | // 创建窗口时触发 88 | module.exports.onBrowserWindowCreated = window => { 89 | // window 为 Electron 的 BrowserWindow 实例 90 | 91 | window.on("ready-to-show", () => { 92 | const url = window.webContents.getURL() 93 | if (url.startsWith("app://./renderer/index.html")) 94 | // 监听配置更新 95 | ipcMain.on("starrail_ui.updateConfig", 96 | debounce((event, newConfig) => { 97 | updateConfig(window.webContents, newConfig) 98 | }, 400)) 99 | 100 | // watchFileChange(window.webContents) 101 | }); 102 | } 103 | -------------------------------------------------------------------------------- /src/settings.html: -------------------------------------------------------------------------------- 1 | 86 | 87 | 88 | 89 | 90 | 91 |
92 | 背景图片 93 | 插件目录/res/background.jpg 94 |
95 | 96 | 97 | 100 |
101 | 102 | 103 |
104 | 背景图片模糊 105 | 默认值: 8, 单位为px 106 |
107 | 108 | 109 |
110 | 111 | 112 |
113 | 背景图片亮度 114 | 默认值: 50, 单位为% 115 |
116 | 117 | 118 |
119 | 120 | 121 |
122 | 聊天窗口不透明度 123 | 默认值: 95, 单位为% 124 |
125 | 126 | 127 |
128 |
129 |
130 |
131 | 132 | 133 |
134 | 135 | 136 | 137 | 138 |
139 |
140 |
你好
141 | 语言的艺术 142 |
143 |
144 |
145 | 146 | 147 |
148 |
149 | 150 | 151 | 152 |

153 | 154 | 155 | 156 | 157 | 158 |
159 | 私聊中显示昵称 160 | 重启后生效 161 |
162 | 163 |
164 | 165 |
166 |
167 |
168 | 169 | 170 |
171 | 172 | 173 | 174 | 175 | 176 |
177 | 版本 178 | 0.1.0 179 |
180 | 181 | 检查更新 182 |
183 | 184 | 185 |
186 | Github仓库 187 | https://github.com/SyrieYume/starrail_ui 188 |
189 | 190 | 前往 191 | 192 |
193 |
194 |
195 |
196 | 197 | 198 |

-------------------------------------------------------------------------------- /src/renderer.js: -------------------------------------------------------------------------------- 1 | 2 | // 添加样式 3 | const element = document.createElement("style") 4 | element.class = "starrail_ui" 5 | document.body.appendChild(element) 6 | 7 | starrail_ui.onUpdateStyle((event, styleData) => { 8 | element.textContent = styleData 9 | }) 10 | 11 | starrail_ui.rendererReady() 12 | 13 | let config = await starrail_ui.getConfig() 14 | 15 | // 监听添加元素事件 16 | if(config["displayNickname"]){ 17 | const observer = new MutationObserver(mutations => { 18 | mutations.forEach(mutation => { 19 | if (mutation.type === 'childList') { 20 | const addedNodes = Array.from(mutation.addedNodes) 21 | addedNodes.forEach(node => { 22 | if (node.className == "ml-item") { 23 | const msgContainer = node.querySelector(".message-container") 24 | if(msgContainer && !msgContainer.querySelector(".user-name")){ 25 | const nicknameDiv = document.createElement('div') 26 | nicknameDiv.classList.add("user-name", "no-copy", "text-ellipsis") 27 | const span = document.createElement('span') 28 | span.classList.add("text-ellipsis") 29 | const nickname = msgContainer.querySelector(".avatar-span").getAttribute("aria-label") 30 | if(nickname) 31 | span.textContent = nickname 32 | nicknameDiv.appendChild(span) 33 | msgContainer.appendChild(nicknameDiv) 34 | } 35 | 36 | } 37 | }) 38 | } 39 | }) 40 | }) 41 | 42 | // 监控整个文档 43 | observer.observe(document.body, { childList: true, subtree: true }); 44 | } 45 | 46 | 47 | 48 | // 打开设置界面时触发 49 | export const onSettingWindowCreated = async view => { 50 | // view 为 Element 对象,修改将同步到插件设置界面 51 | view.innerHTML = await starrail_ui.getSettingsView() 52 | 53 | config = await starrail_ui.getConfig() 54 | 55 | // 选择背景图片 56 | const text_bgPath = view.querySelector("#text_bgPath") 57 | text_bgPath.textContent = config.background_img 58 | 59 | view.querySelector("#input_selectBg").addEventListener("change", (event) => { 60 | const path = event.target.files[0].path.replace(/\\/g, "/") 61 | config["background_img"] = path 62 | text_bgPath.textContent = path 63 | starrail_ui.updateConfig(config) 64 | }) 65 | 66 | // 设置背景模糊 67 | const input_setBgBlur = view.querySelector("#input_setBgBlur") 68 | input_setBgBlur.value = config["background_blur"] 69 | input_setBgBlur.addEventListener("change", (event) => { 70 | config["background_blur"] = event.target.value 71 | starrail_ui.updateConfig(config) 72 | }) 73 | 74 | // 设置背景亮度 75 | const input_setBgBrightness = view.querySelector("#input_setBgBrightness") 76 | input_setBgBrightness.value = config["background_brightness"] 77 | input_setBgBrightness.addEventListener("change", (event) => { 78 | config["background_brightness"] = event.target.value 79 | starrail_ui.updateConfig(config) 80 | }) 81 | 82 | // 设置聊天窗口不透明度 83 | const input_setChatWindowOpacity = view.querySelector("#input_setChatWindowOpacity") 84 | input_setChatWindowOpacity.value = config["chatWindow_Opacity"] 85 | input_setChatWindowOpacity.addEventListener("change", (event) => { 86 | config["chatWindow_Opacity"] = event.target.value 87 | starrail_ui.updateConfig(config) 88 | }) 89 | 90 | // 选择气泡框 91 | let bubbles = await starrail_ui.getBubbles() 92 | 93 | const bubblesList = view.querySelector("#bubblesList") 94 | const defaultBubble = view.querySelector("#defaultBubble") 95 | defaultBubble.value = 0 96 | 97 | let selectedBubble = defaultBubble 98 | 99 | const selectBubble = (bubbleView) => { 100 | bubbleView.classList.add("select") 101 | selectedBubble.classList.remove("select") 102 | selectedBubble = bubbleView 103 | } 104 | 105 | const onBubbleClicked = (event) => { 106 | selectBubble(event.target) 107 | config.bubble = event.target.value 108 | starrail_ui.updateConfig(config) 109 | } 110 | 111 | defaultBubble.addEventListener("click", onBubbleClicked) 112 | 113 | for(let i = 0; i< bubbles.length; i++){ 114 | const bubbleContainerDiv = document.createElement("div") 115 | bubbleContainerDiv.classList.add("bubbleContainer") 116 | bubbleContainerDiv.value = i + 1 117 | if( (i+1) == config.bubble ) 118 | selectBubble(bubbleContainerDiv) 119 | bubbleContainerDiv.addEventListener("click", onBubbleClicked) 120 | 121 | const bubbleWrapper = document.createElement("div") 122 | bubbleWrapper.classList.add("bubbleWrapper") 123 | 124 | const bubbleDiv = document.createElement("div") 125 | bubbleDiv.classList.add("bubble") 126 | bubbleDiv.style = ` 127 | color: ${bubbles[i].textColor}; 128 | border-image: url("appimg://${bubbles[i].imgPath}"); 129 | ${bubbles[i].css} 130 | ` 131 | bubbleDiv.textContent = "你好" 132 | 133 | const bubbleName = document.createElement("b") 134 | bubbleName.classList.add("bubbleName") 135 | bubbleName.textContent = bubbles[i].name 136 | 137 | if("before" in bubbles[i]){ 138 | const bubbleBefore = document.createElement("div") 139 | bubbleBefore.style = bubbles[i].before 140 | bubbleWrapper.appendChild(bubbleBefore) 141 | } 142 | 143 | bubbleWrapper.appendChild(bubbleDiv) 144 | bubbleContainerDiv.appendChild(bubbleWrapper) 145 | bubbleContainerDiv.appendChild(bubbleName) 146 | bubblesList.appendChild(bubbleContainerDiv) 147 | } 148 | 149 | // 检查更新 150 | const versionText = view.querySelector("#versionText") 151 | const checkVersionBtn = view.querySelector("#checkVersionBtn") 152 | const updateBtn = view.querySelector("#updateBtn") 153 | 154 | const nowVersion = LiteLoader.plugins["starrail_ui"].manifest.version 155 | versionText.textContent = nowVersion 156 | 157 | updateBtn.addEventListener("click", (event) => { 158 | plugininstaller.updateBySlug("starrail_ui") 159 | }) 160 | 161 | checkVersionBtn.addEventListener("click", async (event) => { 162 | versionText.textContent = `正在检查更新中...` 163 | const newVersion = await starrail_ui.getNewVersion(nowVersion) 164 | versionText.textContent = `${nowVersion} (${newVersion.tip})` 165 | 166 | // 判断 plugininstaller 插件是否存在并启用 167 | if(newVersion.hasNewVersion && LiteLoader.plugins["plugininstaller"] && !LiteLoader.plugins["plugininstaller"].disabled){ 168 | updateBtn.style.display = "inline-block" 169 | checkVersionBtn.style.display = "none" 170 | } 171 | 172 | }) 173 | 174 | // 前往github仓库 175 | view.querySelector("#gotoGithub").addEventListener("click", (event) => { 176 | LiteLoader.api.openExternal("https://github.com/SyrieYume/starrail_ui") 177 | }) 178 | 179 | // 是否在私聊中显示昵称 180 | const displayNicknameSwitch = view.querySelector("#displayNickname") 181 | if(config.displayNickname) 182 | displayNicknameSwitch.setAttribute("is-active", "") 183 | displayNicknameSwitch.addEventListener("click", (event) => { 184 | config.displayNickname = !config.displayNickname 185 | if(config.displayNickname) 186 | displayNicknameSwitch.setAttribute("is-active", "") 187 | else 188 | displayNicknameSwitch.removeAttribute("is-active") 189 | starrail_ui.updateConfig(config) 190 | }) 191 | } -------------------------------------------------------------------------------- /res/bubbles/bubbleStyle5.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /res/bubbles/bubbleStyle4.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/style.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "StarRailFont"; 3 | src: url("appimg://${plugin_path}/res/StarRailFont.ttf") format("truetype"); 4 | } 5 | 6 | /* 设置所有字体为星穹铁道同款 7 | * 字体来自星穹铁道游戏目录下的 StarRail_Data\StreamingAssets\MiHoYoSDKRes\HttpServerResources\font\zh-cn.ttf */ 8 | * { 9 | font-family: "StarRailFont"; 10 | } 11 | 12 | /* 隐藏部分组件的背景色 */ 13 | .tab-container,.two-col-layout,.two-col-layout__aside,.recent-contact,.list-toggler,.recent-contact-list,.chat-header,.chat-header .background-mask, .viewport-list__inner { 14 | background: none !important; 15 | } 16 | 17 | #app > .container { 18 | position: relative; 19 | overflow-y: hidden; 20 | } 21 | 22 | /* 窗口的背景图 */ 23 | #app > .container::before { 24 | content: " "; 25 | position: absolute; 26 | top: -5%; 27 | left: -5%; 28 | width: 110%; 29 | height: 110%; 30 | background-image: url("appimg://${background_img}"); 31 | background-size: cover; 32 | background-position: center; 33 | background-repeat: no-repeat; 34 | filter: blur(${background_blur}px) brightness(${background_brightness}%); 35 | z-index: -1; 36 | } 37 | 38 | /* 窗口右上角的按钮 自动隐藏 */ 39 | .window-control-area { 40 | opacity: 0; 41 | border-radius: 4px; 42 | margin: 4px; 43 | transition: all .7s ease; 44 | } 45 | 46 | .window-control-area:hover { 47 | opacity: 100%; 48 | background-color: rgb(255,255,255,0.75); 49 | } 50 | 51 | .window-control-area .q-icon { 52 | color: black; 53 | } 54 | 55 | 56 | /* 聊天界面 */ 57 | .two-col-layout__main { 58 | position: relative; 59 | margin: 20px; 60 | border-radius: 1px; 61 | border-top-right-radius: 15px; 62 | box-shadow: 0 0 3px rgb(0,0,0,0.5); 63 | } 64 | 65 | /* 聊天界面顶部区域 */ 66 | .chat-header { 67 | margin-top: -12.5px; 68 | height: 75px !important; 69 | } 70 | 71 | .chat-header .background-mask { 72 | box-shadow: 0 1.2px rgb(172, 172, 172, 0.5); 73 | } 74 | 75 | /* 聊天界面顶部区域 左边的群名或者好友名 */ 76 | .chat-header__contact-name span { 77 | color: black !important; 78 | font-weight: bolder; 79 | font-size: large; 80 | line-height: unset !important; 81 | } 82 | 83 | 84 | /* 聊天界面顶部区域 右边的工具栏 自动隐藏 */ 85 | .chat-header .bar-icon .q-icon{ 86 | opacity: 0; 87 | } 88 | 89 | .chat-header:hover .bar-icon .q-icon{ 90 | opacity: 100%; 91 | transition: all .7s ease; 92 | } 93 | 94 | 95 | /* 聊天界面背后的那个框框 */ 96 | .two-col-layout__main::before { 97 | content: " "; 98 | z-index: -1; 99 | position: absolute; 100 | top: 5px; 101 | left: -6px; 102 | display: block; 103 | width: 100%; 104 | height: 100%; 105 | border: 1.5px solid rgb(172, 172, 172, 0.5); 106 | } 107 | 108 | /* 聊天界面左上角的图形 */ 109 | .two-col-layout__main::after { 110 | content: " "; 111 | position: absolute; 112 | top: 0; 113 | left: 0; 114 | opacity: 0.75; 115 | display: block; 116 | width: 130px; 117 | height: 130px; 118 | background-image: url("appimg://${plugin_path}/res/img_header.png"); 119 | background-position: 0 0; 120 | background-size: 130px 130px; 121 | background-repeat: no-repeat; 122 | } 123 | 124 | /* 聊天界面背景色 */ 125 | .two-col-layout__main { 126 | background-color: rgb(221, 221, 221, ${chatWindow_Opacity}%) !important; 127 | } 128 | 129 | /* 聊天界面头像大小以及消息的外边距 */ 130 | .message-container { 131 | --avatar_size_2: 60px !important; 132 | margin: 3px 32px; 133 | padding: 0px; 134 | } 135 | 136 | /* 聊天气泡 */ 137 | .msg-content-container:not(.mix-message__container--market-face):not(.mix-message__container--pic) { 138 | background-color: rgb(248, 248, 248); 139 | border-radius: 13px; 140 | border-top-left-radius: 2px; 141 | padding: 11px 17px 9px 17px; 142 | box-shadow: -1px 2px rgb(156,156,156); 143 | } 144 | 145 | .msg-content-container { 146 | margin: 2px 6px; 147 | } 148 | 149 | /* 聊天气泡里面的文字样式 */ 150 | .message-content__wrapper span, .markdown-element p, .message-content{ 151 | color: rgb(0,0,0); 152 | font-size: calc(var(--font_size_3) * 1.3) !important; 153 | line-height: unset !important; 154 | font-weight: bold !important; 155 | } 156 | 157 | 158 | .container--self:focus { 159 | background-color: rgb(182, 182, 182, 0.55) !important; 160 | } 161 | 162 | /* 右侧聊天气泡,也就是自己发的内容 */ 163 | .container--self:not(.mix-message__container--market-face):not(.mix-message__container--pic) { 164 | background-color: rgb(209,189,149); 165 | border-top-left-radius: 12px; 166 | border-top-right-radius: 2px; 167 | box-shadow: 1px 3px rgb(156,156,156); 168 | color: rgb(40,40,40); 169 | 170 | /* 注意下面这段注释不能删!!! */ 171 | /* Bubble Style */ 172 | } 173 | 174 | /* 右侧聊天气泡里面"@XXX"的文字样式 */ 175 | .message-container--align-right .message-content__wrapper span { 176 | color: inherit !important; 177 | } 178 | 179 | .message-container:not(:has(.user-name)) .message-content__wrapper { 180 | margin-top: calc(var(--avatar_size_2) / 2 + 2px) !important; 181 | } 182 | 183 | /* 聊天气泡里 "@XXX" 的文本样式 */ 184 | .text-element--at { 185 | color: var(--brand_standard) !important; 186 | } 187 | 188 | /* 聊天气泡上面显示的昵称 */ 189 | .user-name .text-ellipsis { 190 | color: rgb(100, 100, 100) !important; 191 | font-weight: bold; 192 | font-size: 0.95rem; 193 | line-height: unset !important; 194 | } 195 | 196 | .user-name { 197 | margin: 4px 6px; 198 | } 199 | 200 | /* 一些提示文本,比如"XXX 撤回了一条消息" */ 201 | .gray-tip-content.gray-tip-element { 202 | font-size: larger; 203 | line-height: unset !important; 204 | font-weight: bold; 205 | color: rgb(108, 108, 108); 206 | margin: 2px; 207 | } 208 | 209 | /* 消息上方显示的一整行的时间 */ 210 | .message__timestamp { 211 | padding: 0; 212 | margin: 2px 0 !important; 213 | } 214 | 215 | /* 提示文本左侧的图标 */ 216 | .gray-tip-content.gray-tip-element::before { 217 | content: " "; 218 | display: inline-block; 219 | vertical-align: middle; 220 | width: 26px; 221 | height: 26px; 222 | margin-right: 10px; 223 | background-image: url("appimg://${plugin_path}/res/icon_tip.svg"); 224 | background-size: cover; 225 | background-position: center; 226 | background-repeat: no-repeat; 227 | } 228 | 229 | /* 输入区域 */ 230 | .chat-input-area { 231 | box-shadow: inset 0px 2px rgb(172, 172, 172, 0.25) !important; 232 | background-color: rgb(100,100,100,0.05) !important; 233 | } 234 | 235 | /* 输入区域文本框内的文字样式 */ 236 | .qq-editor > *{ 237 | color: black; 238 | caret-color: black; 239 | } 240 | 241 | /* 输入区域上方的工具图标,比如表情,文件,截图按钮 */ 242 | .chat-func-bar .q-icon, .func-bar .q-icon{ 243 | color: rgb(62, 46, 14) !important; 244 | } 245 | 246 | /* 发送按钮 */ 247 | .send-btn-wrap { 248 | border-radius: 1px !important; 249 | box-shadow: 1px 1px 2px rgb(172, 172, 172, 0.75) !important; 250 | background-color: rgb(234,234,234) !important; 251 | } 252 | 253 | .send-setting__wrapper i, .send-msg { 254 | color: rgb(18,18,18) !important; 255 | font-weight: bold; 256 | font-size: 0.95rem; 257 | line-height: unset !important; 258 | } 259 | 260 | /* 回到底部按钮 */ 261 | .q-notification.chat-msg-area__tip--bottom { 262 | position: absolute; 263 | bottom: 0; 264 | left: 50%; 265 | transform: translateX(-50%); 266 | background: none !important; 267 | box-shadow: none; 268 | outline: none !important; 269 | } 270 | 271 | /* 左侧最近聊天列表的外边距 */ 272 | .two-col-layout__aside { 273 | margin: 10px; 274 | margin-left: 20px; 275 | box-sizing: content-box; 276 | } 277 | 278 | /* 最近聊天列表上方的搜索框和添加按钮 */ 279 | .search-input__icon,.q-input__inner,.contact-adder-btn, .top-bar__adder i { 280 | color: rgb(200,200,200) !important; 281 | } 282 | 283 | /* 最近聊天列表中单个元素的样式 */ 284 | .recent-contact-item { 285 | --on_brand_primary: rgb(240,240,240) !important; 286 | --on_brand_secondary: rgb(200,200,200) !important; 287 | --on_bg_text: rgb(200,200,200) !important; 288 | border: 2px solid rgb(172, 172, 172,0.35); 289 | border-radius: 2px; 290 | margin-bottom: 8px; 291 | background-color: rgb(0,0,0, 0.4); 292 | transition: all .5s ease; 293 | } 294 | 295 | /* 最近聊天列表 头像大小 */ 296 | .recent-contact-item .avatar { 297 | --avatar_size_3: 52px; 298 | } 299 | 300 | /* 最近聊天列表 鼠标悬浮时出现的框框 */ 301 | .recent-contact-item:hover { 302 | border: 2px solid rgb(231,231,234, 0.75); 303 | background-color: rgb(0,0,0, 0.1); 304 | } 305 | 306 | /* 最近聊天列表 每个元素的内边距和高度 */ 307 | .recent-contact-list .list-item .list-item__content { 308 | padding: 100px !important; 309 | height: calc(var(--avatar_size_3) + 30px) !important; 310 | } 311 | 312 | 313 | .recent-contact-item.list-item--selected .list-item__content { 314 | background-color: rgb(0,0,0,0.6); 315 | } 316 | 317 | /* 最近聊天列表 好友昵称/群名 */ 318 | .recent-contact-item .list-item__title .main-info { 319 | color: rgb(200,200,200) !important; 320 | font-size: calc(var(--font_size_3) * 1.08); 321 | line-height: unset !important; 322 | } 323 | 324 | /* 最左侧侧边栏 */ 325 | .sidebar-nav { 326 | background-color: rgb(0,0,0,0.5) !important; 327 | } 328 | 329 | /* 最左侧侧边栏 里面的图标 */ 330 | .sidebar-nav .q-icon { 331 | color: rgb(200,200,200); 332 | } 333 | 334 | /* QQ超级调色盘背景 隐藏 */ 335 | .q-miracle-background { 336 | display: none; 337 | } -------------------------------------------------------------------------------- /res/bubbles/bubbleStyle2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /res/bubbles/bubbleStyle3.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /res/bubbles/bubbleStyle1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | --------------------------------------------------------------------------------