Repository: MUKAPP/LiteLoaderQQNT-MSpring-Theme Branch: v5 Commit: 266a1237b0af Files: 40 Total size: 119.6 KB Directory structure: gitextract_yk4n_yry/ ├── .github/ │ └── workflows/ │ └── build.yml ├── .gitignore ├── AGENTS.md ├── LICENSE ├── README.md ├── main/ │ ├── index.js │ ├── liteloader.js │ └── qwqnt.js ├── manifest.json ├── pack.js ├── package.json ├── postcss.config.js ├── preload/ │ └── index.js ├── renderer/ │ ├── features.js │ ├── index.js │ ├── liteloader.js │ ├── qwqnt.js │ ├── settings.js │ └── theme.js ├── res/ │ └── github/ │ ├── protocio-install.html │ └── qq.css ├── src/ │ ├── scss/ │ │ ├── _base.scss │ │ ├── _channels.scss │ │ ├── _chat.scss │ │ ├── _contact-management.scss │ │ ├── _file-manager.scss │ │ ├── _frame.scss │ │ ├── _lists.scss │ │ ├── _login.scss │ │ ├── _modals.scss │ │ ├── _plugins.scss │ │ ├── _settings.scss │ │ └── _variables.scss │ ├── settings.html │ └── style.scss └── vendor/ └── heti/ ├── heti-m.scss ├── js/ │ ├── heti-addon.js │ └── heti-entry.js └── lib/ ├── _variables.scss └── helpers/ └── _add-on.scss ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/build.yml ================================================ name: Build Plugin on: push: branches: [ "v5" ] workflow_dispatch: jobs: build: runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v4 - name: Set up Node.js uses: actions/setup-node@v4 with: node-version: '20' cache: 'npm' - name: 'Install dependencies' run: npm install - name: Build plugin run: npm run build - name: Unzip artifact for clean upload run: | mkdir -p release unzip mspring-theme.zip -d release - name: Upload artifact uses: actions/upload-artifact@v4 with: name: mspring-theme path: release/ ================================================ FILE: .gitignore ================================================ # 忽略编译输出目录 /src/style.min.css /src/heti-addon.js /src/heti-m.css mspring-theme.zip # 忽略 Node.js 模块 /node_modules # 忽略日志文件 *.log npm-debug.log* yarn-debug.log* yarn-error.log* # 忽略 IDE 和操作系统文件 .idea/ .vscode/ .DS_Store Thumbs.db # gemini cli 上下文文件 GEMINI.md .continue/ ================================================ FILE: AGENTS.md ================================================ # AGENTS.md 本文件用于说明本仓库在使用自动化代理/AI 助手(例如 Copilot、ChatGPT、Claude、Continue 等)协作开发时的约定与边界,便于贡献者与自动化工具遵循一致的工作流。 ## 仓库概览 - 项目:MSpring Theme - 类型:LiteLoaderQQNT / QwQNT 主题(theme) - 打包:通过 `npm run build` 生成 `mspring-theme.zip`,并发布到 GitHub Release ## 目录结构(简述) - `src/`:主题样式与相关静态资源(`style.scss` 源码、`style.min.css` 构建产物等) - `renderer/`、`preload/`、`main/`:注入脚本 - `res/`:图标与 README 截图等资源 - `manifest.json`:LiteLoaderQQNT 插件清单 - `package.json`:构建脚本与 QwQNT 元信息(含 `qwqnt.dependencies`) - `pack.js`:打包脚本(输出 `mspring-theme.zip`) ## 版本与发布 ### 版本号规则 - 发布前需要同步更新: - `package.json` 的 `version` - `manifest.json` 的 `version` - Tag 规则:`vX.Y.Z`(例如 `v2.0.1`) ### 发布流程(人工或自动化工具执行) 1. 确认工作区干净(`git status`) 2. 更新版本号并提交 3. 构建与打包: - 推荐:`npm run build`(包含清理旧产物、重新构建 CSS、执行打包) 4. 创建 tag 并推送 5. 创建 GitHub Release 并上传 `mspring-theme.zip` > 注意:Release 资产文件名固定为 `mspring-theme.zip`(见 `manifest.json` 的 `repository.release.file`)。 ## 依赖与兼容性 - QwQNT 侧依赖在 `package.json -> qwqnt.dependencies` 中维护。 - 如需迁移依赖或更改最低版本要求,请同时更新 README 的安装/前置信息。 ## 代码与样式约定 ### 样式 - 样式源文件:`src/style.scss` - 构建产物:`src/style.min.css`(由构建脚本生成;不建议手工编辑) ### 注释与文案 - 除非特殊要求,仓库内注释与字符串建议使用中文,保持与现有风格一致。 ## 自动化代理工作约定 ### 可以做的事 - 按 issue/PR 描述修改代码、样式、文档 - 运行 `npm run build` 验证打包 - 更新版本号、补充 changelog/release notes(按维护者要求) ### 不应该做的事 - 不要提交隐私/敏感信息(token、cookie、个人账号数据) - 不要引入未说明用途的大型依赖 - 不要在没有说明的情况下大范围重构/格式化(避免产生噪音 diff) ### 提交信息(建议) - `feat:` 新功能 - `fix:` 修复问题 - `docs:` 文档 - `chore:` 杂项(依赖、脚本、版本号等) - `refactor:` 重构 ## 联系与反馈 - 作者:MUKAPP - 问题反馈:优先使用 GitHub Issues ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2023 MUK Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # MSpring Theme [LiteLoaderQQNT](https://github.com/LiteLoaderQQNT/LiteLoaderQQNT)/[QwQNT](https://qwqnt-community.github.io/docs/) 主题,优雅 · 粉粉 · 细致 目前适配了大部分日间模式以及夜间模式场景,对很多地方的细节都进行了处理,欢迎使用 & star MUK 的 TG 频道:[MUKAPP](https://t.me/MUKAPP_Personal) ## 目录 - [MSpring Theme](#mspring-theme) - [目录](#目录) - [注意事项](#注意事项) - [截图](#截图) - [日间模式](#日间模式) - [夜间模式](#夜间模式) - [优化消息排版演示](#优化消息排版演示) - [安装方法](#安装方法) - [LiteLoaderQQNT](#liteloaderqqnt) - [管理器安装](#管理器安装) - [插件列表查看](#插件列表查看) - [PluginInstaller](#plugininstaller) - [QwQNT](#qwqnt) - [插件冲突分析](#插件冲突分析) - [其他](#其他) ## 注意事项 > [!TIP] > Windows11 22H2 及以上的版本推荐搭配 [More Materials](https://github.com/mo-jinran/More-Materials) 插件使用。\ > Linux (KDE) 请配合 [More Materials](https://github.com/mo-jinran/More-Materials) 插件使用。\ > Linux 更改主题颜色后需要重启 QQ 才能生效\ > 其他平台请自行尝试 > [!CAUTION] > **不要在 QQ 官方群聊发送*任何*可以看出你使用了第三方插件的截图,不论是本主题还是其他主题其他插件** 作者之前没怎么接触过前端开发,所以非常菜鸡()\ 如果你发现哪里还没有适配或者哪里有问题,欢迎提 Issues ## 截图 ### 日间模式 ![总览](./res/github/screenshots/1.png "总览") ![更换主题色](./res/github/screenshots/7.png "更换主题色") ![独立聊天窗口](./res/github/screenshots/2.png "独立聊天窗口") ![设置界面](./res/github/screenshots/3.png "设置界面") ### 夜间模式 ![总览](./res/github/screenshots/4.png "总览") ![独立聊天窗口](./res/github/screenshots/5.png "独立聊天窗口") ![设置界面](./res/github/screenshots/6.png "设置界面") ### 优化消息排版演示 ![优化消息排版](./res/github/screenshots/8.png "优化消息排版") ## 安装方法 ### LiteLoaderQQNT #### 管理器安装 下载 [release](https://github.com/MUKAPP/mspring-theme/releases/latest) 内的 `mspring-theme.zip`,进入 LiteLoaderQQNT 设置,找到“安装新插件”右侧的“选择文件”按钮,选择刚刚下载的 zip 文件。 #### 插件列表查看 安装 [插件列表查看](https://github.com/ltxhhz/LL-plugin-list-viewer) 插件,打开对应设置页面,找到本插件,点击 **安装** - **使用 Release 包**,安装完成之后重启 #### PluginInstaller 安装 [PluginInstaller](https://github.com/xinyihl/LiteLoaderQQNT-PluginInstaller) 插件,打开对应设置页面,在安装插件输入框内输入 `https://raw.githubusercontent.com/MUKAPP/mspring-theme/v5/manifest.json`,点击确定按钮\ 或者也可以同时安装 [Protocio](https://github.com/PRO-2684/protocio) 插件,然后点击 [该链接](https://mukapp.github.io/mspring-theme/res/github/protocio-install.html) 直接拉起 PluginInstaller 安装插件 ### QwQNT 前置插件:[qwqnt-hako](https://github.com/qwqnt-community/qwqnt-hako) 下载 [release](https://github.com/MUKAPP/mspring-theme/releases/latest) 内的 `mspring-theme.zip`,将下载的 zip 文件解压,解压出的文件夹移动至 `qwqnt/qwqnt-storage/plugins/` 内,重启 QQ 即可\ (如果解压之后不是一个文件夹,而是几个文件夹和几个文件,那么请创建一个文件夹,将解压出来的文件夹和文件放进去,然后再移动到上述路径内) ## 插件冲突分析 - **优化消息排版** 功能在 [**Markdown 插件**](https://github.com/d0j1a1701/LiteLoaderQQNT-Markdown) 开启时会失效,Markdown 插件自带了 `pangu.js` 的中英混排优化,开启二者之一就可以了 ## 其他 Star History Chart ================================================ FILE: main/index.js ================================================ const fs = require("fs"); const fsPromises = require("fs").promises; const path = require("path"); const { BrowserWindow, ipcMain, shell, net } = require("electron"); // 框架类型检测 const frameworkType = global.MSPRING_FRAMEWORK_TYPE function log(...args) { console.log(`[MSpring Theme]`, ...args); } // log("框架类型", frameworkType); function openWeb(url) { shell.openExternal(url); } function fetchData(url) { return new Promise((resolve, reject) => { const request = net.request({ method: 'GET', url: url, redirect: 'follow' // 处理重定向 }); request.on('response', (response) => { const finalUrl = response.headers.location || response.url; let data = ''; response.on('data', (chunk) => { data += chunk; }); response.on('end', () => { resolve({ url: finalUrl, content: data }); }); }); request.on('error', (error) => { reject(error); }); request.end(); }); } // 防抖函数 function debounce(fn, time) { let timer = null; return function (...args) { timer && clearTimeout(timer); timer = setTimeout(() => { fn.apply(this, args); }, time); } } /** * 将十六进制颜色值转换为 RGB 值 * @param {string} hex 表示十六进制颜色的字符串,例如 "#ff0000" * @returns {Array} 包含 RGB 值的数组,例如 [255, 0, 0] */ function hexToRGB(hex) { // 分别提取并转换红色、绿色、蓝色的十六进制值到整数 var r = parseInt(hex.slice(1, 3), 16); var g = parseInt(hex.slice(3, 5), 16); var b = parseInt(hex.slice(5, 7), 16); return [r, g, b]; } /** * 将 RGB 值转换为十六进制颜色值 * @param {Array} rgb 包含 RGB 值的数组,例如 [255, 0, 0] * @returns {string} 表示十六进制颜色的字符串,例如 "#ff0000" */ function RGBToHex(rgb) { var r = rgb[0].toString(16).padStart(2, '0'); var g = rgb[1].toString(16).padStart(2, '0'); var b = rgb[2].toString(16).padStart(2, '0'); return '#' + r + g + b; } /** * 混合两个颜色的 RGB 值 * @param {Array} color1 第一个颜色的 RGB 值数组 * @param {Array} color2 第二个颜色的 RGB 值数组 * @param {number} ratio 混合比率,范围在 0 到 1 之间 * @returns {Array} 混合后的 RGB 值数组 */ function blendColors(color1, color2, ratio) { var blendedColor = []; for (var i = 0; i < 3; i++) { blendedColor[i] = Math.round(color1[i] * (1 - ratio) + color2[i] * ratio); } return blendedColor; } /** * 计算给定RGB颜色的亮度 * @param {number} r - 红色通道的值(0-255) * @param {number} g - 绿色通道的值(0-255) * @param {number} b - 蓝色通道的值(0-255) * @returns {number} - 该颜色的亮度值 */ function luminance(r, g, b) { const a = [r, g, b].map(function (v) { v /= 255; return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4); }); return a[0] * 0.2126 + a[1] * 0.7152 + a[2] * 0.0722; } /** * 根据背景颜色的十六进制值判断最佳文字颜色(黑色或白色) * @param {string} hexColor - 十六进制表示的背景颜色,如 "#FFFFFF" * @returns {string} - 最佳文字颜色,'black' 或 'white' */ function getBestTextColor(hexColor) { const [r, g, b] = hexToRGB(hexColor); const lum = luminance(r, g, b); return lum > 0.5 ? 'black' : 'white'; } // 更新样式 async function updateStyle(webContents, settingsPath) { try { // 检查 webContents 是否仍然有效 if (!webContents || webContents.isDestroyed()) { log("WebContents 已被销毁,跳过样式更新"); return; } // 读取settings.json const data = await fsPromises.readFile(settingsPath, "utf-8"); const config = JSON.parse(data); const themeColor = config.themeColor; // 将themeColorDark1设置成themeColor和10%的黑色的混合色 // const themeColorDark1 = RGBToHex(blendColors(hexToRGB(themeColor), [0, 0, 0], 0.1)); // 将themeColorDark2设置成themeColor和20%的黑色的混合色 // const themeColorDark2 = RGBToHex(blendColors(hexToRGB(themeColor), [0, 0, 0], 0.2)); const backgroundOpacity = config.backgroundOpacity; // 将backgroundOpacity(是个0-100的整数值)转为两位hex值作为RGBA的透明度(注意不要出现小数) const backgroundOpacityHex = Math.round(backgroundOpacity * 2.55).toString(16).padStart(2, "0"); const onThemeTextColor = getBestTextColor(themeColor) === "black" ? "#000000" : "#FFFFFF"; const csspath = path.join(frameworkType === "liteloader" ? LiteLoader.plugins["mspring-theme"].path.plugin : qwqnt.framework.plugins["mspring-theme"].meta.path, "src/style.min.css"); const cssData = await fsPromises.readFile(csspath, "utf-8"); // --theme-color-dark1: ${themeColorDark1}; // --theme-color-dark2: ${themeColorDark2}; // --theme-color-alpha: ${themeColor + "3f"}; // --text-selected-color: ${themeColor + "7f"}; // --theme-tag-color: ${themeColor + "3f"}; let preloadString = `:root { --theme-color: ${themeColor}; --background-color-light: #FFFFFF${backgroundOpacityHex}; --background-color-dark: #171717${backgroundOpacityHex}; --on-theme-text-color: ${onThemeTextColor}; }` // 在发送之前再次检查 if (!webContents.isDestroyed()) { webContents.send( "mspring_theme.updateStyle", // 将主题色插入到style.css中 preloadString + "\n\n" + cssData ); } else { log("WebContents 在处理过程中被销毁,跳过发送"); } } catch (err) { log("更新样式出错", err); } } // 存储监听器的 Map,用于清理 const watchersMap = new WeakMap(); // 监听CSS修改-开发时候用的 function watchCSSChange(webContents, settingsPath) { const cssFilePath = path.join(frameworkType === "liteloader" ? LiteLoader.plugins["mspring-theme"].path.plugin : qwqnt.framework.plugins["mspring-theme"].meta.path, "src/style.min.css"); const watcher = fs.watch(cssFilePath, debounce((eventType, filename) => { log(`CSS文件 ${filename} 发生变动, 重新加载样式...`); if (!webContents.isDestroyed()) { updateStyle(webContents, settingsPath); } else { log("WebContents 已被销毁,清理 CSS 监听器"); watcher.close(); } }, 100)); // 存储监听器以便后续清理 if (!watchersMap.has(webContents)) { watchersMap.set(webContents, []); } watchersMap.get(webContents).push(watcher); } // 监听配置文件修改 function watchSettingsChange(webContents, settingsPath) { const watcher = fs.watch(settingsPath, "utf-8", debounce(() => { if (!webContents.isDestroyed()) { updateStyle(webContents, settingsPath); } else { log("WebContents 已被销毁,清理设置监听器"); watcher.close(); } }, 100)); // 存储监听器以便后续清理 if (!watchersMap.has(webContents)) { watchersMap.set(webContents, []); } watchersMap.get(webContents).push(watcher); } // 加载插件时触发 const pluginDataPath = frameworkType === "liteloader" ? LiteLoader.plugins["mspring-theme"].path.data : path.join(qwqnt.framework.paths.configs, "mspring-theme"); const settingsPath = path.join(pluginDataPath, "settings.json"); // 判断插件路径是否存在,如果不存在则创建(同时创建父目录(如果不存在的话)) if (!fs.existsSync(pluginDataPath)) { fs.mkdirSync(pluginDataPath, { recursive: true }); } // 迁移配置 if (frameworkType === "qwqnt") { const oldPluginDataPath = path.join(qwqnt.framework.paths.data, "mspring-theme"); const oldSettingsPath = path.join(oldPluginDataPath, "settings.json"); const oldSettingsPathExists = fs.existsSync(oldSettingsPath); if (!fs.existsSync(settingsPath) && oldSettingsPathExists) { try { const oldData = fs.readFileSync(oldSettingsPath, "utf-8"); fs.writeFileSync(settingsPath, oldData, "utf-8"); log("已从旧路径迁移 settings.json"); fs.unlinkSync(oldSettingsPath); log("已删除旧的 settings.json"); const remainingFiles = fs.readdirSync(oldPluginDataPath); // 如果目录为空,则删除它 if (remainingFiles.length === 0) { fs.rmdirSync(oldPluginDataPath); log("旧插件目录为空,已删除"); } else { log("旧插件目录中仍有其他文件,不删除目录"); } } catch (error) { log("迁移旧 settings.json 时出错", error); } } } const defaultConfig = { "themeColor": "#cb82be", "backgroundOpacity": "70", "heti": false, "forceHostBubbleColor": false, "forceNightModeBackground": true, "logToMain": false, }; // 检查和更新配置文件 try { if (!fs.existsSync(settingsPath)) { // 如果文件不存在,直接写入默认配置 fs.writeFileSync(settingsPath, JSON.stringify(defaultConfig, null, 4), "utf-8"); } else { // 如果文件存在,读取并检查缺失的键 const data = fs.readFileSync(settingsPath, "utf-8"); const config = JSON.parse(data); let updated = false; // 遍历默认配置的键,如果当前配置中不存在,则添加它 for (const key in defaultConfig) { if (config[key] === undefined) { config[key] = defaultConfig[key]; updated = true; } } // 如果有任何更新,则只写一次文件 if (updated) { fs.writeFileSync(settingsPath, JSON.stringify(config, null, 4), "utf-8"); } } } catch (error) { log("更新 settings.json 时出错", error); // 如果发生错误(如JSON损坏),可以用默认配置覆盖它 fs.writeFileSync(settingsPath, JSON.stringify(defaultConfig, null, 4), "utf-8"); } ipcMain.on( "mspring_theme.rendererReady", (event, message) => { const window = BrowserWindow.fromWebContents(event.sender); updateStyle(window.webContents, settingsPath); } ); // 监听渲染进程的updateStyle事件 ipcMain.on( "mspring_theme.updateStyle", (event, settingsPath) => { const window = BrowserWindow.fromWebContents(event.sender); updateStyle(window.webContents, settingsPath); }); // 监听渲染进程的watchCSSChange事件 ipcMain.on( "mspring_theme.watchCSSChange", (event, settingsPath) => { const window = BrowserWindow.fromWebContents(event.sender); watchCSSChange(window.webContents, settingsPath); }); // 监听渲染进程的watchSettingsChange事件 ipcMain.on( "mspring_theme.watchSettingsChange", (event, settingsPath) => { const window = BrowserWindow.fromWebContents(event.sender); watchSettingsChange(window.webContents, settingsPath); }); ipcMain.handle( "mspring_theme.getFrameworkType", (event) => { return frameworkType; } ); ipcMain.handle( "mspring_theme.getSettings", async (event, message) => { try { const data = await fsPromises.readFile(settingsPath, "utf-8"); const config = JSON.parse(data); return config; } catch (error) { log(error); return {}; } } ); ipcMain.handle( "mspring_theme.setSettings", (event, content) => { try { const new_config = JSON.stringify(content); fs.writeFileSync(settingsPath, new_config, "utf-8"); } catch (error) { log(error); } } ); ipcMain.on("mspring_theme.openWeb", (event, ...message) => openWeb(...message) ); ipcMain.handle("mspring_theme.logToMain", (event, ...args) => { log(...args); } ); ipcMain.handle("mspring_theme.fetchData", (event, url) => { return fetchData(url); }); ipcMain.handle("mspring_theme.readFile", async (event, filePath) => { try { return await fsPromises.readFile(filePath, "utf-8"); } catch (error) { log("读取文件时出错", error); return null; // 或返回错误信息 } }); // 监听来自设置窗口强行覆盖气泡颜色的通知 ipcMain.on("mspring_theme.updateForceBubbleColor", (event, state) => { // 遍历所有窗口 for (const window of BrowserWindow.getAllWindows()) { // 向每个窗口的渲染进程发送应用样式的指令 window.webContents.send("mspring_theme.applyForceBubbleColor", state); } }); // 监听来自设置窗口强制覆盖夜间模式背景的通知 ipcMain.on("mspring_theme.updateForceNightModeBackground", (event, state) => { for (const window of BrowserWindow.getAllWindows()) { window.webContents.send("mspring_theme.applyForceNightModeBackground", state); } }); // 清理监听器 function cleanupWatchers(webContents) { const watchers = watchersMap.get(webContents); if (watchers) { watchers.forEach(watcher => { try { watcher.close(); log("已清理文件监听器"); } catch (error) { log("清理监听器时出错", error); } }); watchersMap.delete(webContents); } } // 创建窗口时触发 function browserWindowCreated(window) { const settingsPath = path.join(pluginDataPath, "settings.json"); window.on("ready-to-show", () => { const url = window.webContents.getURL(); if (url.includes("app://./renderer/")) { watchCSSChange(window.webContents, settingsPath); watchSettingsChange(window.webContents, settingsPath); } }); // 窗口关闭时清理监听器 window.on("closed", () => { log("窗口被关闭,清理监听器"); cleanupWatchers(window.webContents); }); // webContents 销毁时也清理监听器 window.webContents.on("destroyed", () => { log("webContents被销毁,清理监听器"); cleanupWatchers(window.webContents); }); } if (frameworkType === "liteloader") { module.exports.onBrowserWindowCreated = window => { browserWindowCreated(window); } } else { qwqnt.main.hooks.whenBrowserWindowCreated.peek((window) => { browserWindowCreated(window); }); } ================================================ FILE: main/liteloader.js ================================================ // LiteLoader 框架入口文件 global.MSPRING_FRAMEWORK_TYPE = 'liteloader'; module.exports = require('./index.js'); ================================================ FILE: main/qwqnt.js ================================================ // QwQNT 框架入口文件 global.MSPRING_FRAMEWORK_TYPE = 'qwqnt'; module.exports = require('./index.js'); ================================================ FILE: manifest.json ================================================ { "manifest_version": 4, "type": "theme", "name": "MSpring Theme", "slug": "mspring-theme", "description": "LiteLoaderQQNT 主题,优雅 · 粉粉 · 细致", "version": "2.0.3", "icon": "./res/icon.png", "thumb": "./res/brush_FILL0_wght300_GRAD0_opsz24.svg", "authors": [ { "name": "MUKAPP", "link": "https://github.com/MUKAPP" } ], "platform": [ "win32", "linux", "darwin" ], "injects": { "renderer": "./renderer/liteloader.js", "main": "./main/liteloader.js", "preload": "./preload/index.js" }, "repository": { "repo": "MUKAPP/mspring-theme", "branch": "v5", "release": { "tag": "latest", "file": "mspring-theme.zip" } } } ================================================ FILE: pack.js ================================================ const fs = require('fs'); const archiver = require('archiver'); console.log('Creating release zip...'); const output = fs.createWriteStream('mspring-theme.zip'); const archive = archiver('zip', { zlib: { level: 9 } }); output.on('close', function () { console.log(archive.pointer() + ' total bytes'); console.log('Release zip created successfully: mspring-theme.zip'); }); archive.on('warning', function (err) { if (err.code === 'ENOENT') { console.warn(err); } else { throw err; } }); archive.on('error', function (err) { throw err; }); archive.pipe(output); // 1. 添加完整的源码目录 archive.directory('main/', 'main'); archive.directory('preload/', 'preload'); archive.directory('renderer/', 'renderer'); // 2. 精细化添加 src 目录中的文件 // 使用 glob 模式匹配 src 下的所有文件,但排除不需要的 archive.glob('**/*', { cwd: 'src', ignore: [ 'scss/**', // 排除 css 文件夹及其内容 'style.scss', // 排除 style.scss 源文件 '*.map' // 可选:排除可能产生的 sourcemap 文件 ] }, { prefix: 'src' }); // 3. 添加 res 目录 archive.glob('**/*', { cwd: 'res', ignore: ['github/**'] }, { prefix: 'res' }); // 4. 添加根目录下的关键文件 archive.file('manifest.json', { name: 'manifest.json' }); archive.file('package.json', { name: 'package.json' }); // 如果有 README 或 LICENSE 也可以加进来 archive.file('README.md', { name: 'README.md' }); archive.file('LICENSE', { name: 'LICENSE' }); archive.finalize(); ================================================ FILE: package.json ================================================ { "$schema": "https://raw.githubusercontent.com/qwqnt-community/types/main/plugin.schema.json", "name": "mspring-theme", "version": "2.0.3", "description": "QwQNT 主题,优雅 · 粉粉 · 细致", "main": "./main/qwqnt.js", "type": "commonjs", "author": "MUKAPP", "license": "MIT", "scripts": { "clean": "rimraf dist src/style.min.css src/heti-addon.js src/heti-m.css mspring-theme.zip", "build:heti:css": "sass --silence-deprecation=import,global-builtin --no-source-map vendor/heti/heti-m.scss src/heti-m.css", "build:heti:js": "esbuild vendor/heti/js/heti-entry.js --bundle --format=iife --platform=browser --charset=utf8 --outfile=src/heti-addon.js", "build:heti": "npm run build:heti:css && npm run build:heti:js", "build:css": "sass src/style.scss | postcss -o src/style.min.css", "build": "npm run clean && npm run build:heti && npm run build:css && node pack.js", "dev": "npm run build:heti && npm run build:css && nodemon --watch src/style.scss --watch src/scss --watch vendor/heti -e scss,js --ignore src/heti-addon.js --ignore src/style.min.css --ignore src/heti-m.css --exec \"npm run build:heti && npm run build:css\"" }, "qwqnt": { "name": "MSpring Theme", "icon": "./res/brush_FILL0_wght300_GRAD0_opsz24.svg", "inject": { "preload": "./preload/index.js", "renderer": "./renderer/qwqnt.js" }, "dependencies": { "qwqnt-hako": "^1.0.1" } }, "devDependencies": { "archiver": "^7.0.1", "autoprefixer": "^10.4.21", "cssnano": "^7.1.2", "esbuild": "^0.28.0", "heti-findandreplacedomtext": "^0.5.0", "nodemon": "^3.1.10", "postcss": "^8.5.6", "postcss-cli": "^11.0.1", "rimraf": "^6.1.0", "sass": "^1.93.3" } } ================================================ FILE: postcss.config.js ================================================ module.exports = { plugins: [ require('autoprefixer'), require('cssnano')({ preset: 'default', }), ], }; ================================================ FILE: preload/index.js ================================================ const { contextBridge, ipcRenderer } = require("electron"); contextBridge.exposeInMainWorld("mspring_theme", { updateStyle: (callback) => ipcRenderer.on( "mspring_theme.updateStyle", callback ), rendererReady: () => ipcRenderer.send( "mspring_theme.rendererReady" ), getFrameworkType: () => ipcRenderer.invoke( "mspring_theme.getFrameworkType" ), getPlatform: () => process.platform, getSettings: () => ipcRenderer.invoke( "mspring_theme.getSettings" ), setSettings: content => ipcRenderer.invoke( "mspring_theme.setSettings", content ), logToMain: (...args) => ipcRenderer.invoke( "mspring_theme.logToMain", ...args ), openWeb: (url) => ipcRenderer.send("mspring_theme.openWeb", url), fetchData: (url) => ipcRenderer.invoke("mspring_theme.fetchData", url), readFile: (path) => ipcRenderer.invoke("mspring_theme.readFile", path), // 强制覆盖气泡颜色 从设置页通知主进程 updateForceBubbleColor: (state) => ipcRenderer.send( "mspring_theme.updateForceBubbleColor", state ), // 强制覆盖气泡颜色 在所有窗口接收主进程的通知 onApplyForceBubbleColor: (callback) => ipcRenderer.on( "mspring_theme.applyForceBubbleColor", callback ), // 强制覆盖夜间模式背景 从设置页通知主进程 updateForceNightModeBackground: (state) => ipcRenderer.send( "mspring_theme.updateForceNightModeBackground", state ), // 强制覆盖夜间模式背景 在所有窗口接收主进程的通知 onApplyForceNightModeBackground: (callback) => ipcRenderer.on( "mspring_theme.applyForceNightModeBackground", callback ), }); ================================================ FILE: renderer/features.js ================================================ async function insertHeti(messageListElement, selector, mspring_theme, plugin_path) { // 在页面header插入heti的css和js const hetiAddonCSS = await mspring_theme.readFile(`${plugin_path}/src/heti-m.css`); const hetiStyleElement = document.createElement("style"); hetiStyleElement.textContent = hetiAddonCSS; document.head.appendChild(hetiStyleElement); const hetiAddonJS = await mspring_theme.readFile(`${plugin_path}/src/heti-addon.js`); const hetiScriptElement = document.createElement("script"); hetiScriptElement.textContent = hetiAddonJS; document.head.appendChild(hetiScriptElement); const hetiSpacingElementScriptElement = document.createElement("script"); hetiSpacingElementScriptElement.textContent = ` function hetiSpacingElement(element) { let heti = new Heti(); heti.spacingElement(element); } `; document.head.appendChild(hetiSpacingElementScriptElement); function applyHetiToElement(element) { if (!element.matches(selector) || element.querySelector("heti-spacing, heti-adjacent, heti-close")) { return; } element.classList.add("heti"); hetiSpacingElement(element); } // 页面变化时,遍历 class 中包含 text-normal 的所有元素 function processHeti(rootElement) { if (!rootElement || rootElement.nodeType !== 1) { return; } applyHetiToElement(rootElement); rootElement.querySelectorAll(selector).forEach(applyHetiToElement); } let processTimer = null; const pendingRoots = new Set(); function scheduleHetiProcess(element) { if (!element || element.nodeType !== 1) { return; } pendingRoots.add(element); if (processTimer !== null) { return; } processTimer = requestAnimationFrame(() => { pendingRoots.forEach(processHeti); pendingRoots.clear(); processTimer = null; }); } // 处理页面上已经存在的元素 processHeti(messageListElement); // 设置监听器,处理新增元素和后续文本重渲染 const observer = new MutationObserver((mutationsList) => { for (const mutation of mutationsList) { if (mutation.type === "childList") { if (mutation.target.nodeType === 1) { scheduleHetiProcess(mutation.target); } mutation.addedNodes.forEach(node => { if (node.nodeType === 1) { scheduleHetiProcess(node); } }); } else if (mutation.type === "characterData" && mutation.target.parentElement) { scheduleHetiProcess(mutation.target.parentElement); } } }); observer.observe(messageListElement, { childList: true, subtree: true, characterData: true }); } function compareVersions(v1, v2) { const parts1 = v1.split('.').map(Number); const parts2 = v2.split('.').map(Number); for (let i = 0; i < parts1.length; i++) { if (parts2.length === i) { return 1; } if (parts1[i] !== parts2[i]) { return parts1[i] > parts2[i] ? 1 : -1; } } return parts1.length === parts2.length ? 0 : -1; } async function setupThemeFeatures(settings, { log, mspring_theme, frameworkType, plugin_path, observeElement }) { try { // 判断操作系统类型 var osType = ""; const platform = mspring_theme.getPlatform(); if (platform === "win32") { osType = "windows"; } else if (platform === "linux") { osType = "linux"; } else if (platform === "darwin") { osType = "mac"; } document.documentElement.classList.add(osType); // 判断是否强制覆盖自己的气泡颜色 if (settings.forceHostBubbleColor) { document.documentElement.classList.add("mspring_force_host_bubble_color"); } // 强制覆盖气泡颜色 设置监听 mspring_theme.onApplyForceBubbleColor((event, state) => { const docElement = document.documentElement; log(`设置修改强制气泡颜色: ${state}`); if (state) { docElement.classList.add("mspring_force_host_bubble_color"); } else { docElement.classList.remove("mspring_force_host_bubble_color"); } }); // 判断是否强制覆盖夜间模式主页背景 if (settings.forceNightModeBackground) { document.documentElement.classList.add("mspring_force_night_mode_background"); } // 强制覆盖夜间模式背景 设置监听 mspring_theme.onApplyForceNightModeBackground((event, state) => { const docElement = document.documentElement; log(`设置修改强制夜间模式背景: ${state}`); if (state) { docElement.classList.add("mspring_force_night_mode_background"); } else { docElement.classList.remove("mspring_force_night_mode_background"); } }); if (frameworkType === "liteloader") { // 判断插件background_plugin是否存在且启用 if (LiteLoader.plugins["background_plugin"] && !LiteLoader.plugins["background_plugin"].disabled) { log("[检测]", "已启用背景插件"); document.documentElement.classList.add("mspring_background_plugin_enabled"); } // 判断插件lite_tools是否存在且启用 if (LiteLoader.plugins["lite_tools"] && !LiteLoader.plugins["lite_tools"].disabled) { log("[检测]", "已启用轻量工具箱"); const ltData = await mspring_theme.readFile(LiteLoader.plugins["lite_tools"].path.data + "/config.json"); const ltOptions = JSON.parse(ltData); if (ltOptions && ltOptions.background) { if (ltOptions.background.enabled) { log("[检测]", "已启用轻量工具箱-自定义背景"); document.documentElement.classList.add("mspring_lite_tool_background_enabled"); } } } log(document.documentElement.classList); } let more_materials_enabled = frameworkType === "liteloader" ? LiteLoader.plugins["more_materials"] && !LiteLoader.plugins["more_materials"].disabled : false; if (more_materials_enabled) { log("[检测]", "已启用 More Materials"); } const url = window.location.href; log("[检测]", "当前页面", url); if (url.startsWith("app://./renderer/login.html")) { log("[检测]", "登录页面"); // 判断窗口是否是夜间模式 let isDarkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; let colorKey = isDarkMode ? 'dark' : 'light'; let defaultColor = isDarkMode ? '#171717' : '#ffffff'; document.body.style.backgroundColor = more_materials_enabled ? `var(--background-color-${colorKey})` : defaultColor; } // Heti 功能初始化 let applyHeti = null; if (settings.heti && url.startsWith("app://./renderer/index.html")) { let hetiApplied = false; applyHeti = () => { if (hetiApplied) { return; } const currentHash = window.location.hash; if (currentHash.includes("#/main/message") || currentHash.includes("#/forward")) { log("[设置]", "应用赫蹏,聊天页面,hash:", currentHash); try { observeElement('#ml-root', (element) => { insertHeti(element, ".text-normal", mspring_theme, plugin_path); }, false); hetiApplied = true; } catch (error) { log("[错误]", "赫蹏加载出错", error); } } }; // 立即尝试应用(如果已经在目标页面) applyHeti(); } // 通用 URL 变化监听 const wrapHistory = (type) => { const original = history[type]; return function (...args) { const result = original.apply(this, args); // 创建并派发一个自定义事件 const event = new Event(type); event.arguments = args; window.dispatchEvent(event); return result; }; }; // 重写 pushState 和 replaceState history.pushState = wrapHistory('pushState'); history.replaceState = wrapHistory('replaceState'); // URL 变化时的通用处理 const onUrlChange = () => { log("[检测]", "URL 变化:", window.location.href); // 调用各个功能的处理函数 if (applyHeti) { applyHeti(); } }; // 监听所有可能导致 URL 变化的情况 window.addEventListener('hashchange', onUrlChange); // 手动修改 hash window.addEventListener('popstate', onUrlChange); // 浏览器前进/后退 window.addEventListener('pushState', onUrlChange); // 代码调用 pushState window.addEventListener('replaceState', onUrlChange); // 代码调用 replaceState } catch (error) { log("[渲染进程错误]", error); } } export { insertHeti, compareVersions, setupThemeFeatures }; ================================================ FILE: renderer/index.js ================================================ import { initTheme } from './theme.js'; import { setupThemeFeatures } from './features.js'; import { settingWindowCreated } from './settings.js'; // --- Shared State & Utilities --- let themeSettings = {}; function log(...args) { console.log(`[MSpring Theme]`, ...args); if (themeSettings && themeSettings.logToMain) { mspring_theme.logToMain(...args); } } const frameworkType = await mspring_theme.getFrameworkType(); const plugin_path = frameworkType === "liteloader" ? LiteLoader.plugins["mspring-theme"].path.plugin : qwqnt.framework.plugins["mspring-theme"].meta.path; function observeElement(selector, callback, callbackEnable = true, interval = 100) { const timer = setInterval(function () { const element = document.querySelector(selector); if (element) { if (callbackEnable) { callback(); } else { callback(element); } log("已检测到", selector); clearInterval(timer); } }, interval); } // --- Initialization Logic --- const dependencyContext = { log, mspring_theme, frameworkType, plugin_path, observeElement }; async function main() { const settings = await initTheme(dependencyContext); themeSettings = settings; // Update shared settings object await setupThemeFeatures(settings, dependencyContext); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', main); } else { main(); } // --- Exports for Entry Points --- // This function will be called by settingWindowCreated from the outside async function handleSettingWindowCreated(view) { // We need to re-fetch settings within the settings window context const currentSettings = await mspring_theme.getSettings(); themeSettings = currentSettings; // Pass the full context to the settings handler await settingWindowCreated(view, dependencyContext); } export { log, observeElement, initTheme, setupThemeFeatures, handleSettingWindowCreated as settingWindowCreated, // Rename for clarity plugin_path, frameworkType }; ================================================ FILE: renderer/liteloader.js ================================================ // LiteLoader 框架入口文件 import { settingWindowCreated } from './index.js'; export const onSettingWindowCreated = async (view) => { settingWindowCreated(view); }; ================================================ FILE: renderer/qwqnt.js ================================================ // QWQNT框架入口文件 const sharedModulePromise = import('./index.js'); RendererEvents.onSettingsWindowCreated(async () => { const { settingWindowCreated, log } = await sharedModulePromise; try { const packageJson = __self ? __self.meta.packageJson : {}; const view = await PluginSettings.renderer.registerPluginSettings(packageJson); settingWindowCreated(view); } catch (error) { log('[MSpring Theme] 设置窗口创建时出错:', error); } }); ================================================ FILE: renderer/settings.js ================================================ import { compareVersions } from "./features.js"; async function settingWindowCreated(view, { log, mspring_theme, frameworkType, plugin_path }) { log("[设置]", "打开设置界面"); try { const htmlContent = await mspring_theme.readFile(`${plugin_path}/src/settings.html`); view.innerHTML = htmlContent; // 获取设置 const settings = await mspring_theme.getSettings(); const themeColor = settings.themeColor; // 给pick-color(input)设置默认颜色 const pickColor = view.querySelector(".pick-color"); pickColor.value = themeColor; // 给pick-color(input)添加事件监听 pickColor.addEventListener("change", (event) => { // 修改settings的themeColor值 settings.themeColor = event.target.value; // 将修改后的settings保存到settings.json mspring_theme.setSettings(settings); }); // 背景颜色透明 const backgroundOpacity = settings.backgroundOpacity; // 给pick-opacity(input)设置默认值 const pickOpacity = view.querySelector(".pick-opacity"); pickOpacity.value = backgroundOpacity; // 给pick-opacity(input)添加事件监听 pickOpacity.addEventListener("change", (event) => { // 修改settings的backgroundOpacity值 settings.backgroundOpacity = event.target.value; // 将修改后的settings保存到settings.json mspring_theme.setSettings(settings); }); // 选择id为heti的setting-switch const hetiSwitch = view.querySelector("#heti"); if (settings.heti) { hetiSwitch.setAttribute("is-active", ""); } // 给hetiSwitch添加点击监听 hetiSwitch.addEventListener("click", (event) => { const isActive = event.currentTarget.hasAttribute("is-active"); if (isActive) { event.currentTarget.removeAttribute("is-active") // 修改settings的heti值为false settings.heti = false; } else { event.currentTarget.setAttribute("is-active", ""); // 修改settings的heti值为true settings.heti = true; } // 将修改后的settings保存到settings.json mspring_theme.setSettings(settings); }); // 选择id为force-host-bubble-color的setting-switch const forceHostBubbleColorSwitch = view.querySelector("#force-host-bubble-color"); if (settings.forceHostBubbleColor) { forceHostBubbleColorSwitch.setAttribute("is-active", ""); } // 给forceHostBubbleColorSwitch添加点击监听 forceHostBubbleColorSwitch.addEventListener("click", (event) => { const isActive = event.currentTarget.hasAttribute("is-active"); if (isActive) { event.currentTarget.removeAttribute("is-active"); settings.forceHostBubbleColor = false; mspring_theme.updateForceBubbleColor(false); } else { event.currentTarget.setAttribute("is-active", ""); settings.forceHostBubbleColor = true; mspring_theme.updateForceBubbleColor(true); } // 将修改后的settings保存到settings.json mspring_theme.setSettings(settings); }); // 选择 id 为 force-night-mode-background 的 setting-switch const forceNightModeBackgroundSwitch = view.querySelector("#force-night-mode-background"); if (settings.forceNightModeBackground) { forceNightModeBackgroundSwitch.setAttribute("is-active", ""); } // 添加点击监听 forceNightModeBackgroundSwitch.addEventListener("click", (event) => { const isActive = event.currentTarget.hasAttribute("is-active"); if (isActive) { event.currentTarget.removeAttribute("is-active"); settings.forceNightModeBackground = false; mspring_theme.updateForceNightModeBackground(false); } else { event.currentTarget.setAttribute("is-active", ""); settings.forceNightModeBackground = true; mspring_theme.updateForceNightModeBackground(true); } mspring_theme.setSettings(settings); }); // 版本更新 const version = view.querySelector("#mst-settings-version"); version.textContent = frameworkType === "liteloader" ? LiteLoader.plugins["mspring-theme"].manifest.version : qwqnt.framework.plugins["mspring-theme"].meta.packageJson.version const updateButton = view.querySelector("#mst-settings-go-to-update"); updateButton.style.display = "none"; mspring_theme.fetchData("https://github.com/MUKAPP/LiteLoaderQQNT-MSpring-Theme/releases/latest") .then(({ url, content }) => { const versionMatch = content.match(/\/releases\/tag\/v(\d+\.\d+\.\d+)/); const urlMatch = content.match(/https:\/\/github\.com\/[\w-]+\/[\w-]+\/releases\/tag\/v\d+\.\d+\.\d+/); log("urlMatch", urlMatch[0]); if (versionMatch) { const new_version = versionMatch[1]; log("[版本]", "最新版本", new_version); if (compareVersions(new_version, frameworkType === "liteloader" ? LiteLoader.plugins["mspring-theme"].manifest.version : qwqnt.framework.plugins["mspring-theme"].meta.packageJson.version) > 0) { updateButton.style.display = "block"; version.innerHTML += ` (有新版本: ${new_version})`; // 判断 plugininstaller 插件是否存在并启用 if (frameworkType === "liteloader") { if (LiteLoader.plugins["plugininstaller"] && !LiteLoader.plugins["plugininstaller"].disabled) { updateButton.addEventListener("click", () => { plugininstaller.updateBySlug("mspring-theme"); }); } else { version.innerHTML += "
未安装PluginInstaller,安装之后可以一键更新,当前需要手动更新" updateButton.addEventListener("click", () => { mspring_theme.openWeb(urlMatch[0]); }); } } else { // version.innerHTML += "
未安装PluginInstaller,安装之后可以一键更新,当前需要手动更新" updateButton.addEventListener("click", () => { mspring_theme.openWeb(urlMatch[0]); }); } } else { version.innerHTML += ` (已是最新版本)`; } } else { version.innerHTML += ` (版本更新检查失败)`; log("版本更新检查失败", content); } }) .catch((error) => { version.innerHTML += ` (版本更新检查失败: ${error.message})`; log("版本更新检查失败", error); }); // tg 频道 const tgChannel = view.querySelector("#msp-tg-channel"); if (tgChannel) { tgChannel.addEventListener("click", () => { mspring_theme.openWeb("https://t.me/MUKAPP_Personal"); }); } // 输出日志到主进程 const logToMainSwitch = view.querySelector("#msp-log-to-main"); if (settings.logToMain) { logToMainSwitch.setAttribute("is-active", ""); } logToMainSwitch.addEventListener("click", (event) => { const isActive = event.currentTarget.hasAttribute("is-active"); if (isActive) { event.currentTarget.removeAttribute("is-active"); settings.logToMain = false; } else { event.currentTarget.setAttribute("is-active", ""); settings.logToMain = true; } mspring_theme.setSettings(settings); }); /** * 为单个组件元素注入样式的函数 * @param {HTMLElement} element - 要注入样式的组件元素,如 */ function applyCustomStyles(element) { const shadow = element.shadowRoot; // 如果没有 shadow root 或者已经注入过样式,则直接返回,防止重复操作 if (!shadow || shadow.querySelector('.custom-style-injected')) { return; } // 创建一个新的 若使用新版 QQ 请在 超级调色盘 中使用 极简白 主题颜色
背景颜色透明度 该选项在启用 轻量工具箱-自定义背景 之后不会生效
强制覆盖自己发送的气泡颜色 默认情况下不需要开启。如果想要在开启其他修改背景的插件时,让自己的气泡背景显示为主题色,就需要开启该功能
强制覆盖夜间模式主页背景 默认开启。由于新版 QQ 夜间模式下会使用 !important 覆盖部分属性,所以主题只能强行覆盖。如果影响到其他插件的效果,请关闭该功能
优化消息排版 使用 赫蹏,提供中英文混排美化、标点挤压 (修改之后重启生效)
版本号 没问题的话这里显示的是版本号
去更新
Telegram 频道 https://t.me/MUKAPP_Personal
进去瞅瞅
输出日志到主进程 非调试不建议开启
================================================ FILE: src/style.scss ================================================ @charset "UTF-8"; @use "./scss/variables"; @use "./scss/base"; @use "./scss/frame"; @use "./scss/lists"; @use "./scss/chat"; @use "./scss/channels"; @use "./scss/settings"; @use "./scss/login"; @use "./scss/modals"; @use "./scss/file-manager"; @use "./scss/contact-management"; @use "./scss/plugins"; ================================================ FILE: vendor/heti/heti-m.scss ================================================ /*! * Project: Heti * URL: https://github.com/sivan/heti * Author: Sivan [sun.sivan@gmail.com] * MUKAPP 修改:聊天场景使用的精简样式入口 */ @import "lib/variables"; @import "lib/helpers/add-on"; #{$root-selector} { overflow-wrap: break-word; word-wrap: break-word; hyphens: auto; letter-spacing: $letter-spacing-medium; p { text-align: justify; @include non-cjk-block { text-align: start; } } pre { overflow: auto; white-space: pre; word-wrap: normal; } @include non-cjk-block { letter-spacing: $letter-spacing-normal; } a, abbr, code, heti-spacing, [lang="en-US"] { letter-spacing: normal; } table { word-break: break-word; } q { quotes: map-get(map-get($chinese-quote-presets, $chinese-quote-set), "horizontal"); @include non-cjk-block { quotes: initial; quotes: auto; } } @include hetiAddOns(); } ================================================ FILE: vendor/heti/js/heti-addon.js ================================================ /** * Heti add-on v0.1.0 * Add right spacing between CJK & ANS characters * * 源码基于:https://github.com/sivan/heti/blob/master/js/heti-addon.js * 本地补丁:将 heti-adjacent 加入跳过列表,避免在聊天场景重复处理时出现多层嵌套包裹。 */ import Finder from 'heti-findandreplacedomtext'; const hasOwn = {}.hasOwnProperty; const HETI_NON_CONTIGUOUS_ELEMENTS = Object.assign({}, Finder.NON_CONTIGUOUS_PROSE_ELEMENTS, { ins: 1, del: 1, s: 1, a: 1, }); const HETI_SKIPPED_ELEMENTS = Object.assign({}, Finder.NON_PROSE_ELEMENTS, { pre: 1, code: 1, sup: 1, sub: 1, 'heti-spacing': 1, 'heti-close': 1, 'heti-adjacent': 1, }); const HETI_SKIPPED_CLASS = 'heti-skip'; // 部分正则表达式修改自 pangu.js // https://github.com/vinta/pangu.js const CJK = '\u2e80-\u2eff\u2f00-\u2fdf\u3040-\u309f\u30a0-\u30fa\u30fc-\u30ff\u3100-\u312f\u3200-\u32ff\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff'; const A = 'A-Za-z\u0080-\u00ff\u0370-\u03ff'; const N = '0-9'; const S = '`\~!@#\\$%\\^&\\*\\(\\)-_=\\+\\[\\]{}\\\\\\|;:\'",<.>\\/\\?'; const ANS = `${A}${N}${S}`; const REG_CJK_FULL = `(?<=[${CJK}])( *[${ANS}]+(?: +[${ANS}]+)* *)(?=[${CJK}])`; const REG_CJK_START = `([${ANS}]+(?: +[${ANS}]+)* *)(?=[${CJK}])`; const REG_CJK_END = `(?<=[${CJK}])( *[${ANS}]+(?: +[${ANS}]+)*)`; const REG_CJK_FULL_WITHOUT_LOOKBEHIND = `(?:[${CJK}])( *[${ANS}]+(?: +[${ANS}]+)* *)(?=[${CJK}])`; const REG_CJK_END_WITHOUT_LOOKBEHIND = `(?:[${CJK}])( *[${ANS}]+(?: +[${ANS}]+)*)`; const REG_BD_STOP = `。.,、:;!‼?⁇`; const REG_BD_SEP = `·・‧`; const REG_BD_OPEN = `「『(《〈【〖〔[{`; const REG_BD_CLOSE = `」』)》〉】〗〕]}`; const REG_BD_START = `${REG_BD_OPEN}${REG_BD_CLOSE}`; const REG_BD_END = `${REG_BD_STOP}${REG_BD_OPEN}${REG_BD_CLOSE}`; const REG_BD_HALF_OPEN = `“‘`; const REG_BD_HALF_CLOSE = `”’`; const REG_BD_HALF_START = `${REG_BD_HALF_OPEN}${REG_BD_HALF_CLOSE}`; class Heti { constructor(rootSelector) { let supportLookBehind = true; try { new RegExp(`(?<=\\d)\\d`, 'g').test(''); } catch (error) { console.info(error.name, '该浏览器尚未实现 RegExp positive lookbehind'); supportLookBehind = false; } this.rootSelector = rootSelector || '.heti'; this.REG_FULL = new RegExp( supportLookBehind ? REG_CJK_FULL : REG_CJK_FULL_WITHOUT_LOOKBEHIND, 'g' ); this.REG_START = new RegExp(REG_CJK_START, 'g'); this.REG_END = new RegExp( supportLookBehind ? REG_CJK_END : REG_CJK_END_WITHOUT_LOOKBEHIND, 'g' ); this.offsetWidth = supportLookBehind ? 0 : 1; this.funcForceContext = function forceContext(element) { return hasOwn.call(HETI_NON_CONTIGUOUS_ELEMENTS, element.nodeName.toLowerCase()); }; this.funcFilterElements = function filterElements(element) { return !( (element.classList && element.classList.contains(HETI_SKIPPED_CLASS)) || hasOwn.call(HETI_SKIPPED_ELEMENTS, element.nodeName.toLowerCase()) ); }; } spacingElements(elementList) { for (const rootElement of elementList) { this.spacingElement(rootElement); } } spacingElement(element) { const commonConfig = { forceContext: this.funcForceContext, filterElements: this.funcFilterElements, }; const getWrapper = function (elementName, classList, text) { const wrapper = document.createElement(elementName); wrapper.className = classList; wrapper.textContent = text.trim(); return wrapper; }; Finder(element, Object.assign({}, commonConfig, { find: this.REG_FULL, replace: portion => getWrapper('heti-spacing', 'heti-spacing-start heti-spacing-end', portion.text), offset: this.offsetWidth, })); Finder(element, Object.assign({}, commonConfig, { find: this.REG_START, replace: portion => getWrapper('heti-spacing', 'heti-spacing-start', portion.text), })); Finder(element, Object.assign({}, commonConfig, { find: this.REG_END, replace: portion => getWrapper('heti-spacing', 'heti-spacing-end', portion.text), offset: this.offsetWidth, })); Finder(element, Object.assign({}, commonConfig, { find: new RegExp( `([${REG_BD_STOP}])(?=[${REG_BD_START}])|([${REG_BD_OPEN}])(?=[${REG_BD_OPEN}])|([${REG_BD_CLOSE}])(?=[${REG_BD_END}])`, 'g' ), replace: portion => getWrapper('heti-adjacent', 'heti-adjacent-half', portion.text), offset: this.offsetWidth, })); Finder(element, Object.assign({}, commonConfig, { find: new RegExp( `([${REG_BD_SEP}])(?=[${REG_BD_OPEN}])|([${REG_BD_CLOSE}])(?=[${REG_BD_SEP}])`, 'g' ), replace: portion => getWrapper('heti-adjacent', 'heti-adjacent-quarter', portion.text), offset: this.offsetWidth, })); // 使用弯引号时,在停顿符号接弯引号或弯引号接全角开引号时,间距缩进调整到四分之一。 Finder(element, Object.assign({}, commonConfig, { find: new RegExp( `([${REG_BD_STOP}])(?=[${REG_BD_HALF_START}])|([${REG_BD_HALF_OPEN}])(?=[${REG_BD_OPEN}])`, 'g' ), replace: portion => getWrapper('heti-adjacent', 'heti-adjacent-quarter', portion.text), offset: this.offsetWidth, })); } autoSpacing() { const callback = () => { const rootList = document.querySelectorAll(this.rootSelector); for (const rootElement of rootList) { this.spacingElement(rootElement); } }; if (document.readyState === 'complete') { setTimeout(callback); } else { document.addEventListener('DOMContentLoaded', callback); } } } export default Heti; ================================================ FILE: vendor/heti/js/heti-entry.js ================================================ /** * Heti 浏览器注入入口。 * 将默认导出挂到全局,保持与当前主题脚本注入方式兼容。 */ import Heti from './heti-addon.js'; globalThis.Heti = Heti; ================================================ FILE: vendor/heti/lib/_variables.scss ================================================ // Author: Sivan [sun.sivan@gmail.com] // Description: define variables, alias etc. // 定义赫蹏根 class 名 $root-selector: '.heti' !default; $darkmode: true !default; // true | false | 'manual' $manualmode-auto-selector: '[data-darkmode="auto"] &' !default; $manualmode-dark-selector: '[data-darkmode="dark"] &' !default; // 字体 Fonts // 字体栈 Font Stacks $_font-stack-sans: "Helvetica Neue", helvetica, arial !default; $_font-stack-serif: "Times New Roman", times !default; $_font-stack-mono: "SFMono-Regular", consolas, "Liberation Mono", menlo, courier !default; $_font-stack-symbol: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol" !default; // 字体族 Font Families $font-family-hei: $_font-stack-sans, "Heti Hei", sans-serif, $_font-stack-symbol !default; $font-family-song: $_font-stack-serif, "Heti Song", serif, $_font-stack-symbol !default; $font-family-kai: $_font-stack-serif, "Heti Kai", serif, $_font-stack-symbol !default; $font-family-hei-black: $_font-stack-sans, "Heti Hei Black", sans-serif, $_font-stack-symbol !default; $font-family-song-black: $_font-stack-serif, "Heti Song Black", serif, $_font-stack-symbol !default; $font-family-kai-black: $_font-stack-serif, "Heti Kai Black", serif, $_font-stack-symbol !default; $font-family-mono: $_font-stack-mono, monospace, $_font-stack-symbol !default; // 字重 Font Weights $font-weight-bolder: 800 !default; $font-weight-bold: 600 !default; $font-weight-normal: 400 !default; $font-weight-lighter: 200 !default; // 字号 Font Sizes $font-size-normal: 16px !default; $font-size-x-large: 20px !default; $font-size-large: 18px !default; $font-size-small: 14px !default; $font-size-x-small: 12px !default; $font-size-h1: 32px !default; $font-size-h2: 24px !default; $font-size-h3: 20px !default; $font-size-h4: 18px !default; $font-size-h5: 16px !default; $font-size-h6: 14px !default; // 行 Lines // 行宽 $line-length: 42em !default; // 行高 Line Heights $line-height-normal: 1.5 !default; $line-height-expanded-ultra: 2.25 !default; // 字符间距 $letter-spacing-normal: 0 !default; $letter-spacing-small: 0.01em !default; $letter-spacing-medium: 0.02em !default; $letter-spacing-large: 0.05em !default; $line-height-size-normal: $font-size-normal * $line-height-normal !default; $line-height-size-large: $line-height-size-normal !default; $line-height-size-x-large: $font-size-x-large * $line-height-normal !default; $line-height-size-small: $line-height-size-normal !default; $line-height-size-x-small: 18px !default; $line-height-size-h1: 48px !default; $line-height-size-h2: 36px !default; $line-height-size-h3: 36px !default; $line-height-size-h4: 24px !default; $line-height-size-h5: 24px !default; $line-height-size-h6: 24px !default; // 段落 Paragraphs // 标准网格单位变量 Standard Length $std-block-unit: $line-height-size-normal !default; $std-inline-unit: $font-size-normal !default; // 示例:缩进单位 = 二倍文字宽度 $text-indent-length: 2em !default; $text-indent-size: $font-size-normal * 2 !default; // 中文引号 Chinese Quote Set $chinese-quote-presets: ( "cn": ( "horizontal": "“" "”" "‘" "’", "vertical": "『" "』" "「" "」" ), "tw": ( "horizontal": "「" "」" "『" "』", "vertical": "「" "」" "『" "』" ), "common": ( "horizontal": "「" "」" "『" "』", "vertical": "「" "」" "『" "』" ) ) !default; $chinese-quote-set: "common" !default; // 栏 Columns $column-count-list: (1, 2, 3, 4) !default; $column-width-list: (16em, 20em, 24em, 28em, 32em, 36em, 40em, 44em, 48em) !default; // 开发用配置项 Develop Configs $_css-reset-scheme: "reset"; // 混合 Mix-ins @mixin clear-float { &::before, &::after { content: ""; display: table; } &::after { clear: both; } } @mixin non-cjk-block { &:not(:lang(zh)):not(:lang(ja)):not(:lang(ko)), &:not(:lang(zh)) { @content; } } @mixin hang { position: absolute; line-height: inherit; text-indent: 0; } @mixin darkmode-style( $darkmode: $darkmode, $dark-selector: $manualmode-dark-selector, $auto-selector: $manualmode-auto-selector ) { @if $darkmode == 'manual' { #{$dark-selector} { @content; } @media (prefers-color-scheme: dark) { #{$auto-selector} { @content; } } } @else if $darkmode { @media (prefers-color-scheme: dark) { @content; } } } // 函数 Functions @function batch-fix-list($list, $prefix: '', $suffix: '') { $_list: () !default; @each $item in $list { $_list: append($_list, #{$prefix}#{$item}#{$suffix}, comma); } @return $_list; } ================================================ FILE: vendor/heti/lib/helpers/_add-on.scss ================================================ // Author: Sivan [sun.sivan@gmail.com] // Description: define add-ons. @import "../variables"; @mixin hetiAddOns { // 用于中西文混排增加边距 heti-spacing { display: inline; & + sup, & + sub { margin-inline-start: 0; } } .heti-spacing-start { margin-inline-end: 0.25em; } .heti-spacing-end { margin-inline-start: 0.25em; } heti-adjacent { display: inline; text-spacing-trim: space-all; unicode-bidi: isolate; // 解决苹方等字体下标点过度挤压的问题 } .heti-adjacent-half { margin-inline-end: -0.5em; } .heti-adjacent-quarter { margin-inline-end: -0.25em; } }