├── .github └── workflows │ └── main.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── README.md ├── icon.png ├── manifest.json ├── package.json └── src ├── config ├── view.css └── view.html ├── handler └── handler.js ├── logger.js ├── main.js ├── remote ├── download.js └── remote.js ├── renderer ├── config │ └── config.js ├── preload.js ├── renderer.js └── sticker │ ├── addMenu.js │ ├── panel.js │ └── sticker.js └── sticker.css /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Sticker++ Plugin Release 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - 'main' 7 | types: 8 | - closed 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v3 17 | 18 | - name: Zip the Build 19 | run: zip -r stickerpp.zip ./ 20 | 21 | - name: Commit SHA 22 | run: echo ${{github.sha}} > Release.txt 23 | 24 | - name: Test 25 | run: cat Release.txt 26 | 27 | - name: Create Release and Upload Release Asset 28 | uses: softprops/action-gh-release@v1 29 | if: startsWith(github.ref, 'refs/tags/') 30 | with: 31 | files: | 32 | stickerpp.zip 33 | Release.txt 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true, 4 | "tabWidth": 4 5 | } 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | >

由于LLQQNT更新,该插件已过期

2 | 3 | # Sticker++ 4 | 5 | LiteLoaderQQNT插件 - 更多、更好的表情管理😊 6 | 7 | > 需要 [LLAPI](https://github.com/Night-stars-1/LiteLoaderQQNT-Plugin-LLAPI) 支持 8 | 9 | ## 使用 10 | 11 | 依次点击`设置`->`Sticker++`,配置并打开表情存放目录 12 | 直接将`.png .jpg .gif`文件放进去,重启QQ即可 13 | 14 | ## 远程表情 **(实验功能)** 15 | 16 | 通俗的讲,就是将网上的图片作为表情使用 17 | 18 | #### 使用方法 19 | 20 | **请先在设置打开此功能** 21 | 在表情存放目录新建`remotes.txt`,将网址输入进去,多个网址可用换行分隔 22 | 23 | > 先暂且将remotes.txt文件的内容规范称作“`远程描述文件`” 24 | 25 | 网址对应的内容可以是图片或`远程描述文件`规范的文字 26 | 如果是`远程描述文件`规范的文字,将继续获取里面网址的内容 27 | 28 | #### 提示 29 | 30 | - 目前表情面板的远程图标需要在所有表情获取后再显示 31 | - 发送表情时会先将表情下载在本地的`表情文件/tmp/`目录下,一分钟后自动删除 32 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bcmRayCrazy-coder/stickerpp/38ff985eea3ec10e5991151d2901b8bbd4eb62c2/icon.png -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "type": "extension", 4 | "name": "Sticker++", 5 | "slug": "stickerpp", 6 | "description": "更多、更好的表情管理😊", 7 | "version": "0.1.10", 8 | "thumbnail": "./icon.png", 9 | "author": { 10 | "name": "bcmray_crazy", 11 | "link": "https://github.com/bcmRayCrazy-coder" 12 | }, 13 | "repository": { 14 | "repo": "bcmRayCrazy-coder/stickerpp", 15 | "branch": "main", 16 | "use_release": { 17 | "tag": "latest", 18 | "name": "stickerpp.zip" 19 | } 20 | }, 21 | "platform": ["win32", "linux", "darwin"], 22 | "injects": { 23 | "renderer": "./src/renderer/renderer.js", 24 | "main": "./src/main.js", 25 | "preload": "./src/renderer/preload.js" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stickerpp", 3 | "version": "0.1.10", 4 | "description": "更多、更好的表情管理😊", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/bcmRayCrazy-coder/stickerpp.git" 8 | }, 9 | "scripts": { 10 | "lint": "prettier --write ." 11 | }, 12 | "author": "bcmray_crazy", 13 | "license": "ISC", 14 | "bugs": { 15 | "url": "https://github.com/bcmRayCrazy-coder/stickerpp/issues" 16 | }, 17 | "homepage": "https://github.com/bcmRayCrazy-coder/stickerpp#readme", 18 | "dependencies": { 19 | "node-fetch": "^3.3.2" 20 | }, 21 | "devDependencies": { 22 | "electron": "^26.1.0", 23 | "prettier": "^3.0.3" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/config/view.css: -------------------------------------------------------------------------------- 1 | .stckerpp .text-input-wrap { 2 | margin-top: 4px; 3 | margin-bottom: 12px; 4 | } 5 | 6 | .stckerpp .text-input { 7 | padding: 5px 5px; 8 | } 9 | -------------------------------------------------------------------------------- /src/config/view.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

5 | 11 | 庆祝基本功能完成! 15 |

16 | 17 |

常规

18 |
19 |
20 |
21 |

合并本地和远程表情

22 | 将本地和远程表情放在一个栏目中, 24 | 修改后重启生效 26 |
27 |
28 | 29 |
30 |
31 |
32 |
33 |
34 |

启用远程表情

35 | (实验性功能) 在表情存放目录内添加remotes.txt, 37 | 里面写上图片网址, 38 |
网站内容可为图片或另一堆网址, 39 | 多个网址请用换行分割, 修改后重启生效
41 |
42 |
43 | 44 |
45 |
46 |
47 |
48 |
49 |

更新表情 / 表情面板bug修复

50 | 请重启QQ 51 |
52 |
53 |
54 |
55 |
56 |

表情

57 |
58 |
59 |
60 |

表情存放目录

61 | 表情存放的地方,修改后重启生效 64 |
65 |
66 | 73 |
74 |
75 |
76 |
77 |
78 |

打开表情存放目录

79 | 我把表情放哪了😅 80 |
81 | 87 |
88 |
89 |
90 |
91 |

92 | 98 | 101 | 清理表情缓存 103 |

104 | 发送的远程表情会先缓存在表情目录/tmp文件夹下, 106 | 一分钟后自动删除
若在缓存后一分钟内关闭QQ, 107 | 则需要手动清理遗留文件
109 |
110 | 116 |
117 |
118 |
119 |
120 |

联系

121 |
122 |
123 |
124 |

125 | 131 | GitHub 137 |

138 | https://github.com/bcmRayCrazy-coder/stickerpp 141 |
142 | 148 |
149 |
150 |
151 |
152 |
153 | -------------------------------------------------------------------------------- /src/handler/handler.js: -------------------------------------------------------------------------------- 1 | const { ipcMain, shell } = require('electron'); 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | const crypto = require('crypto'); 5 | 6 | const { log } = require('../logger.js'); 7 | const getAllRemoteStickers = require('../remote/remote.js'); 8 | const downloadRemoteStickers = require('../remote/download.js'); 9 | 10 | const stickerFileRegExp = new RegExp(/.+\.(png|jpe?g|gif)/g); 11 | function isValidStickerFile(value) { 12 | return stickerFileRegExp.test(value); 13 | } 14 | 15 | /** 16 | * 递归遍历,获取指定文件夹下面的所有文件路径 17 | * @returns {string[]} 文件 18 | */ 19 | function getAllFiles(filePath) { 20 | let allFilePaths = []; 21 | if (fs.existsSync(filePath)) { 22 | const files = fs.readdirSync(filePath); 23 | for (let i = 0; i < files.length; i++) { 24 | let file = files[i]; 25 | let currentFilePath = filePath + '/' + file; 26 | let stats = fs.lstatSync(currentFilePath); 27 | if (stats.isDirectory()) { 28 | allFilePaths = allFilePaths.concat( 29 | getAllFiles(currentFilePath) 30 | ); 31 | } else { 32 | allFilePaths.push(currentFilePath); 33 | } 34 | } 35 | } else { 36 | console.warn(`指定的目录${filePath}不存在!`); 37 | } 38 | 39 | return allFilePaths; 40 | } 41 | 42 | function setConfig(configPath, content) { 43 | const newConfig = 44 | typeof content == 'string' 45 | ? JSON.stringify(JSON.parse(content), null, 4) 46 | : JSON.stringify(content, null, 4); 47 | fs.writeFileSync(configPath, newConfig, 'utf-8'); 48 | } 49 | 50 | module.exports = function registerHandlers(plugin) { 51 | log('初始化handlers'); 52 | 53 | const pluginDataPath = plugin.path.data; 54 | const configPath = path.join(pluginDataPath, 'config.json'); 55 | var config = JSON.parse(fs.readFileSync(configPath, 'utf-8').toString()); 56 | 57 | const tmpPath = path.join(config.sticker_path, '/tmp/'); 58 | 59 | ipcMain.handle('LiteLoader.stickerpp.getConfig', (event) => { 60 | try { 61 | return config; 62 | } catch (error) { 63 | console.error(error); 64 | return {}; 65 | } 66 | }); 67 | 68 | // 保存设置 69 | ipcMain.handle('LiteLoader.stickerpp.setConfig', (event, content) => { 70 | if (!content) 71 | return console.error('[Sticker++] New config content is', content); 72 | try { 73 | config = 74 | typeof content == 'string' 75 | ? JSON.stringify(JSON.parse(content), null, 4) 76 | : JSON.stringify(content, null, 4); 77 | setConfig(configPath, content); 78 | } catch (error) { 79 | console.error(error); 80 | } 81 | }); 82 | 83 | // 获取本地表情 84 | ipcMain.handle('LiteLoader.stickerpp.getLocalStickers', (event) => { 85 | var paths = getAllFiles(config.sticker_path).filter(isValidStickerFile); 86 | return paths; 87 | }); 88 | 89 | // 获取远程表情 90 | ipcMain.handle('LiteLoader.stickerpp.getRemoteStickers', async (event) => { 91 | const remotePath = path.join(config.sticker_path, 'remotes.txt'); 92 | if (!fs.existsSync(remotePath)) return []; 93 | const stickerUrls = fs.readFileSync(remotePath).toString().split('\n'); 94 | console.log(stickerUrls); 95 | return await getAllRemoteStickers(stickerUrls); 96 | }); 97 | 98 | // 下载远程表情 99 | ipcMain.handle( 100 | 'LiteLoader.stickerpp.downloadRemoteSticker', 101 | async (event, stickerPath) => { 102 | // 16位随机字符串 103 | const localPath = path.join( 104 | tmpPath, 105 | crypto.randomBytes(Math.ceil(32)).toString('hex').slice(0, 16) 106 | ); 107 | await downloadRemoteStickers(stickerPath, localPath); 108 | setTimeout(() => { 109 | // 一分钟后删除缓存 110 | fs.unlinkSync(localPath); 111 | }, 60000); 112 | return localPath; 113 | } 114 | ); 115 | 116 | // 清除缓存 117 | ipcMain.handle('LiteLoader.stickerpp.clearCache', async (event) => { 118 | getAllFiles(tmpPath).forEach(async (filePath) => 119 | fs.unlinkSync(filePath) 120 | ); 121 | }); 122 | 123 | // 操作 124 | ipcMain.handle( 125 | 'LiteLoader.stickerpp.action.openPath', 126 | async (event, path) => await shell.openPath(path) 127 | ); 128 | ipcMain.handle( 129 | 'LiteLoader.stickerpp.action.openExternal', 130 | async (event, url) => await shell.openExternal(url) 131 | ); 132 | ipcMain.handle('LiteLoader.stickerpp.action.showItem', (event, path) => 133 | shell.showItemInFolder(path) 134 | ); 135 | }; 136 | -------------------------------------------------------------------------------- /src/logger.js: -------------------------------------------------------------------------------- 1 | (function (root, factory) { 2 | if (typeof define === 'function' && define.amd) { 3 | define([], factory); 4 | } else if (typeof exports === 'object') { 5 | module.exports = factory(); 6 | } else { 7 | root.logger = factory(); 8 | } 9 | })(globalThis, () => { 10 | return { 11 | log(...args) { 12 | console.log('\u001B[34m[Sticker++]\u001B[39m', args.join(' ')); 13 | }, 14 | }; 15 | }); 16 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | // 运行在 Electron 主进程 下的插件入口 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | 5 | // 默认配置 6 | var defaultConfig = { 7 | sticker_together: false, 8 | enable_remote: false, 9 | sticker_path: '', 10 | }; 11 | 12 | // 加载插件时触发 13 | async function onLoad(plugin) { 14 | const { log } = require('./logger.js'); 15 | const registerHandlers = require('./handler/handler.js'); 16 | 17 | log('初始化配置文件'); 18 | const pluginDataPath = plugin.path.data; 19 | const configPath = path.join(pluginDataPath, 'config.json'); 20 | defaultConfig.sticker_path = path.join(pluginDataPath, 'stickers'); 21 | 22 | // 初始化设置文件 23 | if (!fs.existsSync(pluginDataPath)) 24 | fs.mkdirSync(pluginDataPath, { recursive: true }); 25 | 26 | if (!fs.existsSync(configPath)) 27 | fs.writeFileSync(configPath, JSON.stringify(defaultConfig, null, 4)); 28 | 29 | /** 30 | * @type {defaultConfig} 31 | */ 32 | var config = JSON.parse(fs.readFileSync(configPath, 'utf-8').toString()); 33 | 34 | const tmpPath = path.join(config.sticker_path, '/tmp/'); 35 | 36 | // 初始化表情目录 37 | if (!fs.existsSync(config.sticker_path)) 38 | fs.mkdirSync(config.sticker_path, { recursive: true }); 39 | if (!fs.existsSync(tmpPath)) fs.mkdirSync(tmpPath, { recursive: true }); 40 | 41 | registerHandlers(plugin); 42 | } 43 | 44 | // 创建窗口时触发 45 | function onBrowserWindowCreated(window, plugin) {} 46 | 47 | // 这两个函数都是可选的 48 | module.exports = { 49 | onLoad, 50 | onBrowserWindowCreated, 51 | }; 52 | -------------------------------------------------------------------------------- /src/remote/download.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | 3 | module.exports = async function download(url, localPath) { 4 | const fetch = (await import('node-fetch')).default; 5 | const fetchResponse = await fetch(url); 6 | const buffer = Buffer.from(await fetchResponse.arrayBuffer()); 7 | fs.writeFileSync(localPath, buffer); 8 | }; 9 | -------------------------------------------------------------------------------- /src/remote/remote.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 获取一个远程下的所有表情 3 | * @param {string} url 链接 4 | * @returns {Promise} 5 | */ 6 | async function getRemoteSticker(url) { 7 | const fetch = (await import('node-fetch')).default; 8 | var stickers = []; 9 | try { 10 | const fetchData = await fetch(url, { method: 'head' }); 11 | 12 | if (fetchData.status >= 400) 13 | throw new Error( 14 | '无法获取 ' + url + ' 的表情 - 状态码不在 400 之前', 15 | ); 16 | 17 | const type = fetchData.headers.get('Content-Type').split('/')[0]; 18 | 19 | switch (type) { 20 | case 'image': 21 | stickers.push(fetchData.url); 22 | break; 23 | 24 | case 'text': 25 | const stickerUrls = (await fetchData.text()).split('\n'); 26 | for (let i = 0; i < stickerUrls.length; i++) { 27 | const stickerUrl = stickerUrls[i]; 28 | stickers.push(...(await getRemoteSticker(stickerUrl))); 29 | } 30 | break; 31 | 32 | default: 33 | console.error('错误的表情type', type); 34 | break; 35 | } 36 | } catch (err) { 37 | console.error(err); 38 | } 39 | return stickers; 40 | } 41 | 42 | /** 43 | * 获取所有远程表情 44 | * @param {string[]} url 链接 45 | */ 46 | module.exports = async function getAllRemoteStickers(url) { 47 | var stickers = []; 48 | for (let i = 0; i < url.length; i++) { 49 | const stickerUrl = url[i]; 50 | stickers.push(...(await getRemoteSticker(stickerUrl))); 51 | } 52 | return stickers; 53 | }; 54 | -------------------------------------------------------------------------------- /src/renderer/config/config.js: -------------------------------------------------------------------------------- 1 | const plugin_path = LiteLoader.plugins.stickerpp.path; 2 | await import(`llqqnt://local-file/${plugin_path.plugin}/src/logger.js`); 3 | const { log } = globalThis.logger; 4 | 5 | // 防抖 6 | function debounce(fn, time) { 7 | let timer; 8 | return function (...args) { 9 | timer && clearTimeout(timer); 10 | timer = setTimeout(() => { 11 | fn.apply(this, args); 12 | }, time); 13 | }; 14 | } 15 | 16 | // 添加配置页面 17 | async function addConfigContent(view) { 18 | const pluginPath = LiteLoader.plugins.stickerpp.path.plugin; 19 | const htmlPath = `llqqnt://local-file/${pluginPath}/src/config/view.html`; 20 | const cssPath = `llqqnt://local-file/${pluginPath}/src/config/view.css`; 21 | // 插入设置页 22 | const htmlText = await (await fetch(htmlPath)).text(); 23 | view.insertAdjacentHTML('afterbegin', htmlText); 24 | // 插入设置页样式 25 | const link = document.createElement('link'); 26 | link.rel = 'stylesheet'; 27 | link.href = cssPath; 28 | document.head.appendChild(link); 29 | 30 | const idDarkMode = document.body.getAttribute('q-theme') != 'light'; 31 | view.querySelectorAll('#svg-fill').forEach((e) => { 32 | e.setAttribute('fill', idDarkMode ? '#ffffff' : '#000000'); 33 | }); 34 | 35 | log('Added config view'); 36 | } 37 | 38 | /** 监听配置页面 39 | * @param {Element} view 页面 40 | */ 41 | async function listenConfigContent(view) { 42 | var config = await stickerpp.getConfig(); 43 | 44 | // 防抖更新配置 45 | const updateConfig = debounce((newConfig) => { 46 | stickerpp.setConfig(JSON.stringify(newConfig)); 47 | }, 500); 48 | // 显示表情目录 49 | const showStickerDir = debounce( 50 | () => stickerpp.openPath(config.sticker_path), 51 | 500 52 | ); 53 | // 访问GitHub 54 | const visitGitHub = debounce( 55 | () => 56 | stickerpp.openExternal( 57 | 'https://github.com/bcmRayCrazy-coder/stickerpp' 58 | ), 59 | 500 60 | ); 61 | // 清除缓存 62 | const clearCache = debounce(() => stickerpp.clearCache(), 500); 63 | 64 | /** 65 | * 监听元素 66 | * @param {Element} element 监听的元素 67 | * @param {string} key 配置的键 68 | * @param {(Element)=>any} parseFn 转换函数 69 | * @param {(Element,any)=>void} initFn 初始化元素 70 | */ 71 | function listenChange(key, parseFn, initFn) { 72 | const element = view.querySelector('#' + key); 73 | 74 | initFn(element, config[key]); 75 | element.addEventListener('change', () => { 76 | const newValue = parseFn(element); 77 | config[key] = newValue; 78 | updateConfig(config); 79 | }); 80 | } 81 | 82 | /** 83 | * 监听开关 84 | * @param {string} key 配置的键 85 | * @param {boolean} init 初始值 86 | */ 87 | function listenSwitch(key, init = false) { 88 | const element = view.querySelector('#' + key); 89 | 90 | function update(value = false) { 91 | if (!value) element.classList.remove('is-active'); 92 | else if (!element.classList.contains('is-active')) 93 | element.classList.add('is-active'); 94 | } 95 | 96 | var value = config[key] || init; 97 | update(value); 98 | 99 | element.addEventListener('click', () => { 100 | value = !value; 101 | update(value); 102 | config[key] = value; 103 | updateConfig(config); 104 | }); 105 | } 106 | 107 | /** 108 | * 监听按钮点击 109 | * @param {string} id 按钮id 110 | * @param {()=>void} fn 触发函数 111 | */ 112 | function listenButton(id, fn) { 113 | view.querySelector('#' + id).addEventListener('click', () => fn()); 114 | } 115 | 116 | listenSwitch('sticker_together'); 117 | listenSwitch('enable_remote'); 118 | 119 | listenChange( 120 | 'sticker_path', 121 | (e) => e.value, 122 | (e, defaultValue) => (e.value = defaultValue) 123 | ); 124 | 125 | listenButton('clear_cache', clearCache); 126 | listenButton('show_sticker_dir', showStickerDir); 127 | listenButton('visit_github', visitGitHub); 128 | 129 | log('Listening to config view'); 130 | } 131 | 132 | /** 133 | * 初始化配置界面 134 | * @param {Element} view 配置界面元素 135 | */ 136 | export async function config(view) { 137 | log('设置Config页面'); 138 | await addConfigContent(view); 139 | await listenConfigContent(view); 140 | } 141 | -------------------------------------------------------------------------------- /src/renderer/preload.js: -------------------------------------------------------------------------------- 1 | // Electron 主进程 与 渲染进程 交互的桥梁 2 | const { contextBridge, ipcRenderer } = require('electron'); 3 | 4 | // 在window对象下导出只读对象 5 | contextBridge.exposeInMainWorld('stickerpp', { 6 | // 配置 7 | getConfig: () => ipcRenderer.invoke('LiteLoader.stickerpp.getConfig'), 8 | setConfig: (content) => 9 | ipcRenderer.invoke('LiteLoader.stickerpp.setConfig', content), 10 | 11 | // 表情 12 | getLocalStickers: () => 13 | ipcRenderer.invoke('LiteLoader.stickerpp.getLocalStickers'), 14 | getRemoteStickers: () => 15 | ipcRenderer.invoke('LiteLoader.stickerpp.getRemoteStickers'), 16 | downloadRemoteSticker: (url) => 17 | ipcRenderer.invoke('LiteLoader.stickerpp.downloadRemoteSticker', url), 18 | clearCache: () => 19 | ipcRenderer.invoke('LiteLoader.stickerpp.clearCache'), 20 | 21 | // 操作 22 | openPath: (path) => 23 | ipcRenderer.invoke('LiteLoader.stickerpp.action.openPath', path), 24 | openExternal: (url) => 25 | ipcRenderer.invoke('LiteLoader.stickerpp.action.openExternal', url), 26 | showItem: (path) => 27 | ipcRenderer.invoke('LiteLoader.stickerpp.action.showItem', path), 28 | }); 29 | -------------------------------------------------------------------------------- /src/renderer/renderer.js: -------------------------------------------------------------------------------- 1 | // 运行在 Electron 渲染进程 下的页面脚本 2 | const plugin_path = LiteLoader.plugins.stickerpp.path; 3 | const { config } = await import( 4 | `llqqnt://local-file/${plugin_path.plugin}/src/renderer/config/config.js` 5 | ); 6 | const { initStickerMenu } = await import( 7 | `llqqnt://local-file/${plugin_path.plugin}/src/renderer/sticker/sticker.js` 8 | ); 9 | 10 | // 页面加载完成时触发 11 | function onLoad() { 12 | var getpanelInterval = setInterval(() => { 13 | var panel = document.querySelector( 14 | '#app > div.container > div.tab-container > div > div.aio > div.group-panel.need-token-updated > div.group-chat > div.chat-input-area.no-copy > div.expression-panel > div > div', 15 | ); 16 | if (!panel) return; 17 | 18 | initStickerMenu(panel); 19 | clearInterval(getpanelInterval); 20 | }, 500); 21 | } 22 | 23 | // 打开设置界面时触发 24 | function onConfigView(view) { 25 | config(view); 26 | } 27 | 28 | // 这两个函数都是可选的 29 | export { onLoad, onConfigView }; 30 | -------------------------------------------------------------------------------- /src/renderer/sticker/addMenu.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 显示/隐藏界面 3 | * @param {string} id tab id 4 | * @param {boolean} show 是否显示 5 | * @param {Element} pageWrapper page元素 6 | */ 7 | function setPageShow(id, show, pageWrapper) { 8 | pageWrapper.querySelector('#page-' + id).style.display = show 9 | ? 'block' 10 | : 'none'; 11 | } 12 | 13 | /** 14 | * 添加菜单 15 | * @param {Element} panel 表情面板 16 | * @param {string} title 标题 17 | * @param {string} icon 图标 18 | * @param {Element} page 页面 19 | * @param {string} id tab id 20 | * @returns {HTMLElement} page元素 21 | */ 22 | export function addMenu(panel, title, icon, page, id) { 23 | const iconElement = document.createElement('div'); 24 | iconElement.innerHTML = `${icon}`; 25 | iconElement.classList.add( 26 | 'tabs-container-item', 27 | 'stickerpp-container-item', 28 | ); 29 | iconElement.id = id; 30 | 31 | // Page 32 | const pageElement = document.createElement('div'); 33 | pageElement.classList.add('stickerpp-container'); 34 | pageElement.style.display = 'none'; 35 | pageElement.innerHTML = `
${page}
`; 36 | pageElement.id = 'page-' + id; 37 | 38 | const pageWrapperElement = panel.querySelector('.sticker-panel__pages'); 39 | pageWrapperElement.appendChild(pageElement); 40 | 41 | // Tab 42 | const tabElement = panel.querySelector('div.tabs.sticker-panel__bar > div'); 43 | tabElement.appendChild(iconElement); 44 | 45 | iconElement.addEventListener('click', () => { 46 | // 切换到本tab 47 | panel 48 | .querySelectorAll('.tabs-container-item-active') 49 | .forEach((e) => e.classList.remove('tabs-container-item-active')); 50 | panel 51 | .querySelectorAll('div.sticker-panel__pages > div') 52 | .forEach((e) => (e.style.display = 'none')); 53 | setPageShow(id, true, pageWrapperElement); 54 | }); 55 | 56 | document.querySelectorAll('.tabs-container-item').forEach((e) => 57 | e.addEventListener('click', () => { 58 | if (e.id != id) { 59 | // 切换到其他tab 60 | iconElement.classList.remove('tabs-container-item-active'); 61 | setPageShow(id, false, pageWrapperElement); 62 | } 63 | }), 64 | ); 65 | 66 | setPageShow(id, false, pageWrapperElement); 67 | 68 | // 修复打开插件添加的tab后关闭表情, 再打开无法使用的问题 69 | setInterval(() => { 70 | var shortcutsElement = document.querySelector( 71 | '#app > div.container > div.tab-container > div > div.aio > div.group-panel.need-token-updated > div.group-chat > div.chat-input-area.no-copy > div.chat-func-bar.shortcuts > div:nth-child(1) > div:nth-child(1) > div', 72 | ); 73 | if (!shortcutsElement) return; 74 | shortcutsElement.addEventListener('click', () => { 75 | setPageShow(id, false, pageWrapperElement); 76 | }); 77 | }, 500); 78 | 79 | return pageElement; 80 | } 81 | -------------------------------------------------------------------------------- /src/renderer/sticker/panel.js: -------------------------------------------------------------------------------- 1 | const plugin_path = LiteLoader.plugins.stickerpp.path; 2 | const { addMenu } = await import( 3 | `llqqnt://local-file/${plugin_path.plugin}/src/renderer/sticker/addMenu.js` 4 | ); 5 | 6 | async function sendSticker(stickerPath) { 7 | const peer = await LLAPI.getPeer(); 8 | const elements = [ 9 | { 10 | type: 'image', 11 | file: stickerPath, 12 | asface: true, 13 | }, 14 | ]; 15 | await LLAPI.sendMessage(peer, elements); 16 | } 17 | 18 | /** 19 | * 添加本地表情面板 20 | * @param {Element} panel 表情面板 21 | */ 22 | export async function addLocalStickerPanel(panel) { 23 | /** 24 | * @type {string[]} 25 | */ 26 | const stickers = await stickerpp.getLocalStickers(); 27 | const { sticker_path } = await stickerpp.getConfig(); 28 | 29 | var pageContent = ''; 30 | stickers.forEach((stickerPath) => { 31 | pageContent += ` 32 |
`; 33 | }); 34 | 35 | /** 36 | * @type {HTMLElement} page元素 37 | */ 38 | const page = addMenu( 39 | panel, 40 | '本地表情', 41 | ``, 42 | pageContent, 43 | 'local-stickers' 44 | ); 45 | page.querySelectorAll('#local-sticker-item').forEach((btn) => { 46 | const stickerPath = btn.dataset.src; 47 | btn.addEventListener('click', () => { 48 | sendSticker(stickerPath); 49 | }); 50 | }); 51 | } 52 | 53 | /** 54 | * 添加远程表情面板 55 | * @param {Element} panel 表情面板 56 | */ 57 | export async function addRemoteStickerPanel(panel) { 58 | /** 59 | * @type {string[]} 60 | */ 61 | const stickers = await stickerpp.getRemoteStickers(); 62 | console.log('\n\n', stickers, '\n\n'); 63 | 64 | const { sticker_path } = await stickerpp.getConfig(); 65 | 66 | var pageContent = ''; 67 | stickers.forEach((stickerPath) => { 68 | pageContent += ` 69 |
`; 70 | }); 71 | 72 | /** 73 | * @type {HTMLElement} page元素 74 | */ 75 | const page = addMenu( 76 | panel, 77 | '远程表情', 78 | ``, 79 | pageContent, 80 | 'remote-stickers' 81 | ); 82 | page.querySelectorAll('#remote-sticker-item').forEach((btn) => { 83 | const stickerPath = btn.dataset.src; 84 | btn.addEventListener('click', async () => { 85 | const localPath = 86 | await stickerpp.downloadRemoteSticker(stickerPath); 87 | console.log(localPath); 88 | sendSticker(localPath); 89 | }); 90 | }); 91 | } 92 | -------------------------------------------------------------------------------- /src/renderer/sticker/sticker.js: -------------------------------------------------------------------------------- 1 | const plugin_path = LiteLoader.plugins.stickerpp.path; 2 | const { addLocalStickerPanel, addRemoteStickerPanel } = await import( 3 | `llqqnt://local-file/${plugin_path.plugin}/src/renderer/sticker/panel.js` 4 | ); 5 | 6 | /** 7 | * 初始化表情面板 8 | * @param {Element} panel 表情面板 9 | */ 10 | export async function initStickerMenu(panel) { 11 | const style = document.createElement('link'); 12 | style.rel = 'stylesheet'; 13 | style.href = `llqqnt://local-file/${LiteLoader.plugins.stickerpp.path.plugin}/src/sticker.css`; 14 | document.head.appendChild(style); 15 | 16 | const config = await stickerpp.getConfig(); 17 | 18 | addLocalStickerPanel(panel); 19 | if (config.enable_remote) addRemoteStickerPanel(panel); 20 | } 21 | -------------------------------------------------------------------------------- /src/sticker.css: -------------------------------------------------------------------------------- 1 | .stickerpp-container-item { 2 | align-items: center; 3 | border-radius: 6px; 4 | color: var(--icon_primary); 5 | display: flex; 6 | flex-shrink: 0; 7 | height: 40px; 8 | justify-content: center; 9 | margin-right: 14px; 10 | width: 40px; 11 | } 12 | 13 | .stickerpp-container-item:hover { 14 | background-color: var(--overlay_hover); 15 | color: var(--brand_standard); 16 | } 17 | 18 | .stickerpp-container { 19 | height: 100%; 20 | position: relative; 21 | width: 100%; 22 | } 23 | 24 | .stickerpp-container-list { 25 | align-content: flex-start; 26 | display: flex; 27 | flex-direction: row; 28 | flex-wrap: wrap; 29 | height: 100%; 30 | overflow-x: hidden; 31 | overflow-y: overlay; 32 | padding: 17px 0 0 17px; 33 | width: 100%; 34 | } 35 | 36 | .stickerpp-list-item { 37 | -webkit-app-region: no-drag; 38 | align-items: center; 39 | cursor: pointer; 40 | display: flex; 41 | height: 64px; 42 | justify-content: center; 43 | margin-right: 6px; 44 | width: 64px; 45 | } 46 | 47 | .stickerpp-image { 48 | line-height: 0; 49 | position: relative; 50 | -webkit-user-select: none; 51 | -moz-user-select: none; 52 | user-select: none; 53 | } 54 | 55 | .stickerpp-image .image-content { 56 | object-fit: contain; 57 | object-position: center; 58 | height: 100%; 59 | image-rendering: -webkit-optimize-contrast; 60 | text-indent: 100%; 61 | width: 100%; 62 | } 63 | --------------------------------------------------------------------------------