415
feisay.js
Normal file
415
feisay.js
Normal file
@ -0,0 +1,415 @@
|
|||||||
|
// ==UserScript==
|
||||||
|
// @name 斐说
|
||||||
|
// @namespace http://tampermonkey.net/
|
||||||
|
// @version 1.0
|
||||||
|
// @description 斐说,支持 Ctrl+Enter 快捷键,可配置随机用户、末尾签名,自定义用户列表和情绪状态,并解决8字符困扰
|
||||||
|
// @match https://linux.do/*
|
||||||
|
// @icon https://cdn.linux.do/uploads/default/original/3X/9/d/9dd49731091ce8656e94433a26a3ef36062b3994.png
|
||||||
|
// @grant GM_setValue
|
||||||
|
// @grant GM_getValue
|
||||||
|
// ==/UserScript==
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
const MIN_REQUIRED_CHARS = 8;
|
||||||
|
const ZERO_WIDTH_SPACE = '\u200B';
|
||||||
|
|
||||||
|
// 获取页面的 <meta name="theme-color"> 标签
|
||||||
|
const themeColorMeta = document.querySelector('meta[name="theme-color"]');
|
||||||
|
let themeColor = '#ffffff'; // 默认白色背景
|
||||||
|
let invertedColor = '#000000'; // 默认黑色字体
|
||||||
|
|
||||||
|
if (themeColorMeta) {
|
||||||
|
themeColor = themeColorMeta.getAttribute('content');
|
||||||
|
invertedColor = invertColor(themeColor); // 计算相反颜色
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置样式变量
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.textContent = `
|
||||||
|
:root {
|
||||||
|
--panel-bg: ${themeColor};
|
||||||
|
--panel-text: ${invertedColor};
|
||||||
|
--panel-border: ${invertedColor};
|
||||||
|
--button-bg: ${invertedColor};
|
||||||
|
--button-text: ${themeColor};
|
||||||
|
--button-hover-bg: ${invertColor(invertedColor)};
|
||||||
|
}`;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
|
||||||
|
function invertColor(hex) {
|
||||||
|
// 去掉前面的“#”字符
|
||||||
|
hex = hex.replace('#', '');
|
||||||
|
|
||||||
|
// 如果输入的是3位的hex值,转换为6位的
|
||||||
|
if (hex.length === 3) {
|
||||||
|
hex = hex.split('').map(c => c + c).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算相反的颜色
|
||||||
|
const r = (255 - parseInt(hex.slice(0, 2), 16)).toString(16).padStart(2, '0');
|
||||||
|
const g = (255 - parseInt(hex.slice(2, 4), 16)).toString(16).padStart(2, '0');
|
||||||
|
const b = (255 - parseInt(hex.slice(4, 6), 16)).toString(16).padStart(2, '0');
|
||||||
|
|
||||||
|
return `#${r}${g}${b}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultAnyoneSay = [
|
||||||
|
{ name: '慕思斐', username: 'Musifei', avatar: '/user_avatar/linux.do/musifei/96/8107_2.png', description: '「手持烟火以谋生 心怀诗意以谋爱」', isModerator: true, moderatorCategory: '' },
|
||||||
|
{ name: 'Neo', username: 'Neo', avatar: '/user_avatar/linux.do/neo/96/12_2.png', description: '岂曰无衣?', isModerator: true, moderatorCategory: '' },
|
||||||
|
{ name: 'King-Huiwen-of-Qin', username: 'King-Huiwen-of-Qin', avatar: '/user_avatar/linux.do/king-huiwen-of-qin/96/4672_2.png', description: '🍭 <LINUX DO> 总会会长 🍭', isModerator: true, moderatorCategory: '' },
|
||||||
|
{ name: '神墨网络', username: 'SMNET', avatar: '/user_avatar/linux.do/smnet/96/95935_2.png', description: '🐾 护猫使者 🐾', isModerator: true, moderatorCategory: '' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const defaultEmotionalState = [
|
||||||
|
'容光焕发', '无精打采', '精神抖擞', '奄奄一息', '气宇轩昂',
|
||||||
|
'疲惫不堪', '落落大方', '愁眉苦脸', '满面春风', '心力交瘁',
|
||||||
|
'风度翩翩', '萎靡不振', '温文尔雅', '怏怏不乐', '精神焕发',
|
||||||
|
'人见人爱,花见花开,车见车爆胎'
|
||||||
|
];
|
||||||
|
|
||||||
|
let config = {
|
||||||
|
enableRandomUser: GM_getValue('enableRandomUser', true),
|
||||||
|
enableSignature: GM_getValue('enableSignature', true),
|
||||||
|
enableMinCharCompletion: GM_getValue('enableMinCharCompletion', true),
|
||||||
|
closeConfigAfterSave: GM_getValue('closeConfigAfterSave', true),
|
||||||
|
anyoneSay: updateOldConfig(GM_getValue('anyoneSay', defaultAnyoneSay)),
|
||||||
|
emotionalState: GM_getValue('emotionalState', defaultEmotionalState)
|
||||||
|
};
|
||||||
|
|
||||||
|
const randInt = (start, end) => {
|
||||||
|
return Math.floor(Math.random() * (end - start + 1)) + start;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getEmotionalState = () => {
|
||||||
|
return config.emotionalState[randInt(0, config.emotionalState.length - 1)];
|
||||||
|
};
|
||||||
|
|
||||||
|
function updateOldConfig(oldConfig) {
|
||||||
|
return oldConfig.map(user => ({
|
||||||
|
...user,
|
||||||
|
isModerator: user.isModerator || false,
|
||||||
|
moderatorCategory: user.moderatorCategory || ''
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取预加载数据
|
||||||
|
function getPreloadedData() {
|
||||||
|
const preloadedDataElement = document.querySelector("#data-preloaded");
|
||||||
|
if (!preloadedDataElement) {
|
||||||
|
throw new Error("Preloaded data element not found");
|
||||||
|
}
|
||||||
|
const preloadedData = preloadedDataElement.getAttribute("data-preloaded");
|
||||||
|
return JSON.parse(preloadedData);
|
||||||
|
};
|
||||||
|
// 获取用户名
|
||||||
|
function getUsername() {
|
||||||
|
const preloadedData = getPreloadedData();
|
||||||
|
const preloadedCurrentUserData = JSON.parse(preloadedData.currentUser);
|
||||||
|
return preloadedCurrentUserData.username;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCategoryNames() {
|
||||||
|
const categories = [];
|
||||||
|
|
||||||
|
// 第一种方式
|
||||||
|
const titleWrapper = document.getElementsByClassName('title-wrapper')[0];
|
||||||
|
if (titleWrapper && titleWrapper.children[1]) {
|
||||||
|
for (let child of titleWrapper.children[1].children) {
|
||||||
|
if (child.children[0]) {
|
||||||
|
categories.push(child.children[0].innerText.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 第二种方式
|
||||||
|
const topicCategory = document.getElementsByClassName('topic-category')[0];
|
||||||
|
if (topicCategory) {
|
||||||
|
categories.push(...topicCategory.innerText.split('\n').map(cat => cat.trim()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return categories;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSecretGarden() {
|
||||||
|
const categories = getCategoryNames();
|
||||||
|
return categories.includes("秘密花园");
|
||||||
|
}
|
||||||
|
|
||||||
|
function isCategoryMatch(userCategory, pageCategories) {
|
||||||
|
if (!userCategory) return true; // 如果用户没有指定类别,则认为匹配
|
||||||
|
return pageCategories.some(category => category === userCategory);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRandomUser() {
|
||||||
|
const randomUser = config.anyoneSay[Math.floor(Math.random() * config.anyoneSay.length)];
|
||||||
|
const pageCategories = getCategoryNames();
|
||||||
|
const isModerator = randomUser.isModerator && isCategoryMatch(randomUser.moderatorCategory, pageCategories);
|
||||||
|
|
||||||
|
let userHtml = `<img src="${randomUser.avatar}" alt="" width="36" height="36" class="avatar" /> `;
|
||||||
|
|
||||||
|
// 添加名称,总是带颜色
|
||||||
|
userHtml += isModerator ? `[color=#00aeff]**${randomUser.name}**[/color] ` : `${randomUser.name} `;
|
||||||
|
|
||||||
|
// 如果是版主,添加版主图标
|
||||||
|
if (isModerator) {
|
||||||
|
userHtml += `<img width="11" height="11" src="upload://dYktfGjcK2IRp3Sg7kj7wnG7vpm.png" class=".topic-meta-data .user-status-message img.emoji"> `;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果用户名和名称不同,则添加用户名
|
||||||
|
userHtml += randomUser.name !== randomUser.username ? `${randomUser.username} ` : '';
|
||||||
|
|
||||||
|
// 添加描述和表情
|
||||||
|
userHtml += `[color=#919191]${randomUser.description}[/color] <img width="14" height="14" src="upload://eHs0oDRBVpTk7yaAQJ7zxxDnDrv.png" class=".topic-meta-data .user-status-message img.emoji">\n `;
|
||||||
|
|
||||||
|
return userHtml;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClick(event) {
|
||||||
|
if (event.target && event.target.closest('button.create')) {
|
||||||
|
processTextarea();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleKeydown(event) {
|
||||||
|
if (event.ctrlKey && event.key === 'Enter') {
|
||||||
|
processTextarea();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function processTextarea() {
|
||||||
|
if (!isSecretGarden()) { return; }
|
||||||
|
let textarea = document.querySelector('#reply-control textarea');
|
||||||
|
let text = textarea.value.trim();
|
||||||
|
let originalLength = text.length;
|
||||||
|
|
||||||
|
if (text.length !== 0) {
|
||||||
|
if (config.enableRandomUser && !text.includes("eHs0oDRBVpTk7yaAQJ7zxxDnDrv.png")) {
|
||||||
|
text = getRandomUser() + text;
|
||||||
|
}
|
||||||
|
let signature = `<strong>${getUsername()}</strong>`;
|
||||||
|
if (config.enableSignature && !text.includes(signature)) {
|
||||||
|
text = `${text}\n\n---\n\n<div style="text-align:right">— 来自${getEmotionalState()}的 ${signature}</div>`;
|
||||||
|
}
|
||||||
|
if (config.enableMinCharCompletion && originalLength < MIN_REQUIRED_CHARS) {
|
||||||
|
// 通过添加零宽字符来补足长度到最小要求
|
||||||
|
while (text.length < MIN_REQUIRED_CHARS) {
|
||||||
|
text += ZERO_WIDTH_SPACE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea.value = text;
|
||||||
|
|
||||||
|
// 创建并触发 input 事件
|
||||||
|
const inputEvent = new Event('input', {
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true
|
||||||
|
});
|
||||||
|
// 触发事件
|
||||||
|
textarea.dispatchEvent(inputEvent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createConfigPanel() {
|
||||||
|
const panel = document.createElement('div');
|
||||||
|
panel.id = 'feisay-config-panel';
|
||||||
|
panel.style.position = 'fixed';
|
||||||
|
panel.style.top = '80px';
|
||||||
|
panel.style.right = '360px';
|
||||||
|
panel.style.padding = '20px';
|
||||||
|
panel.style.border = `1px solid var(--panel-border)`;
|
||||||
|
panel.style.borderRadius = '8px';
|
||||||
|
panel.style.zIndex = '10000';
|
||||||
|
panel.style.width = '300px';
|
||||||
|
panel.style.maxHeight = '80%';
|
||||||
|
panel.style.overflowY = 'auto';
|
||||||
|
panel.style.display = 'none'; // 默认隐藏面板
|
||||||
|
panel.style.backgroundColor = 'var(--panel-bg)';
|
||||||
|
panel.style.color = 'var(--panel-text)';
|
||||||
|
panel.style.boxShadow = '0 4px 6px rgba(0, 0, 0, 0.1)';
|
||||||
|
|
||||||
|
const title = document.createElement('h3');
|
||||||
|
title.textContent = '斐说配置';
|
||||||
|
title.style.marginTop = '0';
|
||||||
|
title.style.marginBottom = '15px';
|
||||||
|
panel.appendChild(title);
|
||||||
|
|
||||||
|
const createCheckbox = (id, text, checked) => {
|
||||||
|
const label = document.createElement('label');
|
||||||
|
label.style.display = 'flex';
|
||||||
|
label.style.alignItems = 'center';
|
||||||
|
label.style.marginBottom = '10px';
|
||||||
|
label.style.cursor = 'pointer';
|
||||||
|
|
||||||
|
const checkbox = document.createElement('input');
|
||||||
|
checkbox.type = 'checkbox';
|
||||||
|
checkbox.id = id;
|
||||||
|
checkbox.checked = checked;
|
||||||
|
checkbox.style.marginRight = '10px';
|
||||||
|
checkbox.addEventListener('change', (e) => {
|
||||||
|
config[id] = e.target.checked;
|
||||||
|
GM_setValue(id, config[id]);
|
||||||
|
});
|
||||||
|
|
||||||
|
label.appendChild(checkbox);
|
||||||
|
label.appendChild(document.createTextNode(text));
|
||||||
|
|
||||||
|
return label;
|
||||||
|
};
|
||||||
|
|
||||||
|
panel.appendChild(createCheckbox('enableRandomUser', '启用随机用户头像和名称', config.enableRandomUser));
|
||||||
|
panel.appendChild(createCheckbox('enableSignature', '启用末尾签名', config.enableSignature));
|
||||||
|
panel.appendChild(createCheckbox('enableMinCharCompletion', '启用最小字符数补全', config.enableMinCharCompletion));
|
||||||
|
panel.appendChild(createCheckbox('closeConfigAfterSave', '保存后自动关闭配置页面', config.closeConfigAfterSave));
|
||||||
|
|
||||||
|
const createTextArea = (id, value, placeholder) => {
|
||||||
|
const container = document.createElement('div');
|
||||||
|
container.style.marginBottom = '15px';
|
||||||
|
|
||||||
|
const label = document.createElement('label');
|
||||||
|
label.textContent = placeholder;
|
||||||
|
label.style.display = 'block';
|
||||||
|
label.style.marginBottom = '5px';
|
||||||
|
container.appendChild(label);
|
||||||
|
|
||||||
|
const textarea = document.createElement('textarea');
|
||||||
|
textarea.id = id;
|
||||||
|
textarea.value = JSON.stringify(value, null, 2);
|
||||||
|
textarea.rows = 5;
|
||||||
|
textarea.style.width = '100%';
|
||||||
|
textarea.style.padding = '5px';
|
||||||
|
textarea.style.border = '1px solid var(--panel-border)';
|
||||||
|
textarea.style.borderRadius = '4px';
|
||||||
|
textarea.style.backgroundColor = 'var(--panel-bg)';
|
||||||
|
textarea.style.color = 'var(--panel-text)';
|
||||||
|
container.appendChild(textarea);
|
||||||
|
|
||||||
|
return container;
|
||||||
|
};
|
||||||
|
|
||||||
|
panel.appendChild(createTextArea('anyoneSay', config.anyoneSay, '自定义用户列表:'));
|
||||||
|
panel.appendChild(createTextArea('emotionalState', config.emotionalState, '自定义情绪状态:'));
|
||||||
|
|
||||||
|
const buttonContainer = document.createElement('div');
|
||||||
|
buttonContainer.style.display = 'flex';
|
||||||
|
buttonContainer.style.justifyContent = 'space-between';
|
||||||
|
|
||||||
|
const createButton = (text, onClick, primary = false) => {
|
||||||
|
const button = document.createElement('button');
|
||||||
|
button.textContent = text;
|
||||||
|
button.style.padding = '8px 16px';
|
||||||
|
button.style.border = 'none';
|
||||||
|
button.style.borderRadius = '4px';
|
||||||
|
button.style.cursor = 'pointer';
|
||||||
|
button.style.backgroundColor = primary ? '#0078d7' : '#f0f0f0';
|
||||||
|
button.style.color = primary ? '#ffffff' : '#333333';
|
||||||
|
button.addEventListener('click', onClick);
|
||||||
|
return button;
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveButton = createButton('保存设置', () => {
|
||||||
|
try {
|
||||||
|
const newAnyoneSay = JSON.parse(document.getElementById('anyoneSay').value);
|
||||||
|
config.anyoneSay = updateOldConfig(newAnyoneSay);
|
||||||
|
config.emotionalState = JSON.parse(document.getElementById('emotionalState').value);
|
||||||
|
GM_setValue('anyoneSay', config.anyoneSay);
|
||||||
|
GM_setValue('emotionalState', config.emotionalState);
|
||||||
|
if (config.closeConfigAfterSave) {
|
||||||
|
panel.style.display = 'none';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
alert('保存失败,请检查输入格式是否正确');
|
||||||
|
}
|
||||||
|
}, true);
|
||||||
|
|
||||||
|
const closeButton = createButton('关闭', () => {
|
||||||
|
panel.style.display = 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
buttonContainer.appendChild(saveButton);
|
||||||
|
buttonContainer.appendChild(closeButton);
|
||||||
|
panel.appendChild(buttonContainer);
|
||||||
|
|
||||||
|
document.body.appendChild(panel);
|
||||||
|
return panel;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleConfigPanel() {
|
||||||
|
const panel = document.getElementById('feisay-config-panel') || createConfigPanel();
|
||||||
|
panel.style.display = panel.style.display === 'none' ? 'block' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
function createConfigButton() {
|
||||||
|
const toolbar = document.querySelector('.d-editor-button-bar');
|
||||||
|
if (!toolbar || document.querySelector('.feisay-config')) return;
|
||||||
|
|
||||||
|
const configButton = document.createElement('button');
|
||||||
|
configButton.className = 'btn btn-flat btn-icon no-text user-menu-tab active feisay-config';
|
||||||
|
configButton.title = '斐说配置';
|
||||||
|
configButton.innerHTML = ' <svg class="fa d-icon d-icon-discourse-other-tab svg-icon svg-string" xmlns="http://www.w3.org/2000/svg"><use href="#discourse-other-tab"></use></svg>';
|
||||||
|
configButton.onclick = toggleConfigPanel;
|
||||||
|
|
||||||
|
toolbar.appendChild(configButton);
|
||||||
|
}
|
||||||
|
|
||||||
|
function watchReplyControl() {
|
||||||
|
const replyControl = document.getElementById('reply-control');
|
||||||
|
if (!replyControl) return;
|
||||||
|
|
||||||
|
const observer = new MutationObserver((mutations) => {
|
||||||
|
mutations.forEach((mutation) => {
|
||||||
|
if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
|
||||||
|
if (replyControl.classList.contains('closed')) {
|
||||||
|
const panel = document.getElementById('feisay-config-panel');
|
||||||
|
if (panel) {
|
||||||
|
panel.style.display = 'none';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 当 reply-control 重新打开时,尝试添加配置按钮
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!document.querySelector('.feisay-config')) {
|
||||||
|
createConfigButton();
|
||||||
|
}
|
||||||
|
}, 500); // 给予一些时间让编辑器完全加载
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
observer.observe(replyControl, { attributes: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
function watchForEditor() {
|
||||||
|
const observer = new MutationObserver((mutations) => {
|
||||||
|
mutations.forEach((mutation) => {
|
||||||
|
if (mutation.type === 'childList') {
|
||||||
|
const addedNodes = mutation.addedNodes;
|
||||||
|
for (let node of addedNodes) {
|
||||||
|
if (node.nodeType === Node.ELEMENT_NODE && node.classList.contains('d-editor')) {
|
||||||
|
if (!document.querySelector('.feisay-config')) {
|
||||||
|
createConfigButton();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
observer.observe(document.body, { childList: true, subtree: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
const container = document.getElementById("reply-control");
|
||||||
|
container.addEventListener('click', handleClick, true);
|
||||||
|
document.addEventListener('keydown', handleKeydown, true);
|
||||||
|
if (!document.querySelector('.feisay-config')) {
|
||||||
|
createConfigButton();
|
||||||
|
}
|
||||||
|
watchReplyControl();
|
||||||
|
watchForEditor();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
setTimeout(() => init(), 1000);
|
||||||
|
})();
|
Reference in New Issue
Block a user