├── .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 | 庆祝基本功能完成!
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 | 清理表情缓存
103 |
104 |
发送的远程表情会先缓存在表情目录/tmp文件夹下,
106 | 一分钟后自动删除
若在缓存后一分钟内关闭QQ,
107 | 则需要手动清理遗留文件
109 |
110 |
116 |
117 |
118 |
119 |
120 | 联系
121 |
122 |
123 |
124 |
125 | 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 |
--------------------------------------------------------------------------------