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