// ==UserScript==
// @name LinuxDo Custom
// @name:zh-CN LinuxDo自定义 (快速收藏|点击数可视化|大图缩放|小图显示|自定义徽标|去除模糊)
// @description Adds customizable features such as logos, click count visualization, image resize, and quick bookmarking to LinuxDo.
// @description:zh-CN 为 LinuxDo 设置 快速收藏、点击数可视化、图像缩放、小图显示、自定义徽标、去除模糊 等功能。
// @namespace http://tampermonkey.net/
// @version 0.3.5
// @author Yearly
// @match https://linux.do/*
// @icon data:image/svg+xml;base64,PHN2ZyAgdmlld0JveD0iMCAtMiAzNiAzNiIgdmVyc2lvbj0iMS4xIiBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWlkWU1pZCBtZWV0IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIj4KICA8cmVjdCB5PSItMiIgd2lkdGg9IjM2IiBoZWlnaHQ9IjM2IiByeD0iMTUlIiBmaWxsPSIjMDAwIi8+CiAgPHBhdGggZmlsbD0iI2VlZSIgIGQ9Ik0zMy42OCwxNS40SDIyLjIzQTMuNjgsMy42OCwwLDAsMSwxOSw5Ljg5bC40LS42OUg0VjdIMjAuNzFsMS4xNS0ySDRBMiwyLDAsMCwwLDIsN1YyOWEyLDIsMCwwLDAsMiwySDMyYTIsMiwwLDAsMCwyLTJWMTUuMzhaTTE2LjgsMTkuODNsLTEwLDQuNTlWMjEuNzhsNi41MS0zLTYuNTEtM1YxMy4xN2wxMCw0LjU5Wm02LjYsNS41N0gxN1YyM2g2LjRaIiBjbGFzcz0iY2xyLWktc29saWQtLWFsZXJ0ZWQgY2xyLWktc29saWQtcGF0aC0xLS1hbGVydGVkIi8+CiAgPHBhdGggZmlsbD0iI0ZEMSIgZD0iTTI2Ljg1LDEuMTQsMjEuMTMsMTFBMS4yOCwxLjI4LDAsMCwwLDIyLjIzLDEzSDMzLjY4QTEuMjgsMS4yOCwwLDAsMCwzNC43OCwxMUwyOS4wNiwxLjE0QTEuMjgsMS4yOCwwLDAsMCwyNi44NSwxLjE0WiIgY2xhc3M9ImNsci1pLXNvbGlkLS1hbGVydGVkIGNsci1pLXNvbGlkLXBhdGgtMi0tYWxlcnRlZCBjbHItaS1hbGVydCIvPgo8L3N2Zz4K
// @license AGPL-v3.0
// @grant GM_addStyle
// @grant GM_registerMenuCommand
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_deleteValue
// ==/UserScript==
(function() {
var settings = {};
const default_main_icon = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGFyaWEtbGFiZWw9IkxpbnV4IiB2aWV3Qm94PSIwIDAgNTAwIDUwMCIgZmlsbD0iIzMzMyI+PHJlY3Qgd2lkdGg9IjQ1MCIgaGVpZ2h0PSI0NTAiIHJ4PSIxNSUiIGZpbGw9IiNmZmYiLz48cGF0aCBkPSJNMTYxIDE5MGMtNiAxNC00OCA1OC00NCAxMDIgMTYgMTg0IDcyIDYwIDE1NiAxMDYgMCAwIDE1MC04NCAzMC0yMjAtMzQtNDgtNC04Ni0yNi0xMThzLTYwLTM0LTg4LTQgMTIgNzQtMjggMTM0Ii8+PHBhdGggZD0iTTMwOSAyODJzMTgtMzYtMTYtNjJjMzIgMzQgMTIgNjQgMTIgNjRoLTZjLTItNzAtMjAtMzItNDYtMTU2IDMwLTM0LTI4LTY0LTI4LThoLTE4YzItNDgtNDAtMjQtMTYgMTAtMiA3NC00NiAxMDQtNDYgMTU2LTE0LTM2IDEyLTY0IDEyLTY0cy0zNiAzMC0xNCA3NCA2MiAzNCAzNCA1NGM0NCAzMCAxMTIgMTAgMTEwLTU0IDItMTYgNDQtMTAgNDgtNnMtNi04LTI2LThNMTk3IDEyNmMtMTQtNC0xMC0yMi00LTIyczE2IDE0IDQgMjJtMzggMmMtMTAtMTQtMi0yOCA4LTI2czEwIDI2LTggMjYiIGZpbGw9IiNlZWUiLz48ZyBmaWxsPSIjZmMyIiBzdHJva2U9IiMzMzMiIHN0cm9rZS13aWR0aD0iMiI+PHBhdGggZD0ibTE0MyAzMDIgNDIgNjBjMjIgMTQgMTAgNzAtNTAgNDItMzQtMTAtNjItOC02Ni0yNnM4LTIwIDYtMjhjLTgtNDQgMjgtMjIgMzgtNDRzMTAtMzIgMzAtNG0yMjQgMjhjLTgtMTIgMC0zNC0yOC0zMi0xMiAyNC00NiA0OC00OCAwLTIwIDAtNiA0OC0xNCA3MC0xOCA1NCAzNCA1OCA1NiAzMmw1Mi0zNmM0LTYgMTAtMTItMTgtMzRNMTgzIDE0NmMtNi0xMiAyMi0yOCAzMi0yOHMyNCA4IDM4IDEyIDggMTggNCAyMC0yNiAyMC00MiAyMC0yMC0xNi0zMi0yNCIvPjxwYXRoIGQ9Ik0xODMgMTQ0YzE2IDEyIDM0IDIyIDcwLTYiLz48L2c+PHBhdGggZD0iTTIwNSAxMzJjLTQgMCAyLTQgNC0ybTE0IDJjMi0yLTItNC02LTIiLz48L3N2Zz4="
const default_wide_icon = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMTAiIGhlaWdodD0iMTEwIiB2aWV3Ym94PSItNSAtNSAzMTAgMTEwIj48cmVjdCB3aWR0aD0iOTkiIGhlaWdodD0iOTkiIHJ4PSIxMDAlIiBmaWxsPSIjRjFGMUYxIi8+PHRleHQgeD0iMjQiIHk9Ijc2IiBmb250LXNpemU9Ijc3IiBmaWxsPSIjRmMzIiBmb250LXdlaWdodD0iYm9sZCI+TDwvdGV4dD48dGV4dCB4PSI3MCIgeT0iNzUiIGZvbnQtc2l6ZT0iNDYiIGZpbGw9IiM4ODgiIGZvbnQtd2VpZ2h0PSJib2xkIiBsZXR0ZXItc3BhY2luZz0iNSI+SU5VWDwvdGV4dD48dGV4dCB4PSIyMDUiIHk9Ijc3IiBmb250LXNpemU9IjcwIiBmaWxsPSIjRUVFIiBmb250LXdlaWdodD0iNjYwIj5EbzwvdGV4dD48L3N2Zz4=";
const settingsConfig = {
quick_mark : { type: 'checkbox', label: '快速收藏 ', default: true, style:'', info:'在帖子上增加一个⭐用于快速收藏' },
image_view : { type: 'checkbox', label: '增强大图查看 ', default: true, style:'', info:'在点开大图查看时,支持滚轮缩放和鼠标拖动位置' },
cnts_colorful : { type: 'checkbox', label: '点击次数可视化', default: true, style:'', info:'点击次数彩色高亮,数越大,颜色越红' },
spoiler_noblured : { type: 'checkbox', label: '剧透不模糊', default: false, style:'', info:'去除剧透模糊效果,使其直接显示' },
image_mini : { type: 'checkbox', label: '显示小图 ', default: false, style:'', info:'让帖子中的图都变小,在鼠标悬停时恢复原大小' },
image_mini_H : { type: 'number', label: '小图显示高度', default: "50", dependsOn: 'image_mini', style:'' , info:'(单位px,建议设为大于30的数)' },
image_mini_W : { type: 'number', label: '小图显示宽度', default: "50", dependsOn: 'image_mini', style:'' , info:'(单位px,建议设为大于30的数)' },
icon_custom : { type: 'checkbox', label: '自定义LOGO图标', default: false, style:'' , info:'始皇说不建议这样做' },
icon_main : { type: 'text', label: '主图标 url', default: default_main_icon, dependsOn: 'icon_custom', style:'font-size:14px', info:'' },
icon_wide : { type: 'text', label: '宽图标 url', default: default_wide_icon, dependsOn: 'icon_custom', style:'font-size:14px', info:'' },
};
Object.keys(settingsConfig).forEach(key => {
settings[key] = GM_getValue(key, settingsConfig[key].default);
});
GM_registerMenuCommand('Custom Settings', openSettings);
function openSettings() {
const shadow = document.createElement('div');
shadow.style = `position: fixed; top: 0%; left: 0%; z-index:8888; width:100vw; height:100vh; background: #6668;`;
const panel = document.createElement('div');
panel.style = `position: fixed; top: 45%; left: 50%; z-index:9999; transform: translate(-50%, -50%); background: white; padding:15px 25px; border: 1px solid #ccc; color:#000;`;
let html = `
<style type="text/css">
:scope label {color:#666; font-size:16px; display:flex; justify-content:space-between; align-items:center; margin-top:20px;}
:scope label span {color:#6bc; font-size:12px; font-weight: normal; padding:0 8px; margin-right:auto;}
:scope label input[type=text] {width:350px; padding:1px; margin:0 5px;}
:scope label input[type=number] {width:60px; padding:1px; margin:0 5px; text-align:center;}
:scope label input[type=checkbox]:checked {background: pink;}
:scope label input[disabled] {background: #CCC;}
:scope label button {user-select: none; color: #333; padding: 2px 10px; margin-top:10px; border-radius:5px;}
</style>
<h2 style="text-align:center; margin-top:.5rem;">Settings</h2>
`;
Object.keys(settingsConfig).forEach(key => {
const cfg = settingsConfig[key];
const val = settings[key];
const checked = cfg.type === 'checkbox' && val ? 'checked' : '';
const disabled = cfg.dependsOn && !settings[cfg.dependsOn] ? 'disabled' : '';
html += `<label>${cfg.label}<span>${cfg.info}</span><input type="${cfg.type}" id="ujs_set_${key}" value="${val}" ${checked} ${disabled} style="${cfg.style}"></label>`;
});
html += `<label><button id="ld_userjs_apply" style="font-weight: bold; background:#ACE">保存并刷新</button>
<span></span><button id="ld_userjs_save">仅保存</button>
<span></span><button id="ld_userjs_reset">重置</button>
<span></span><button id="ld_userjs_close">取消</button></label>`;
panel.innerHTML = html;
document.body.append(shadow, panel);
Object.keys(settingsConfig).forEach(key => {
if (settingsConfig[key].dependsOn) {
document.getElementById(`ujs_set_${settingsConfig[key].dependsOn}`).addEventListener('change', updateDependencies);
}
});
function updateDependencies() {
Object.keys(settingsConfig).forEach(key => {
if (settingsConfig[key].dependsOn) {
document.getElementById(`ujs_set_${key}`).disabled = !document.getElementById(`ujs_set_${settingsConfig[key].dependsOn}`).checked;
}
});
}
document.querySelector('button#ld_userjs_save').addEventListener('click', () => {
Object.keys(settingsConfig).forEach(key => {
const element = document.getElementById(`ujs_set_${key}`);
settings[key] = element.type === 'checkbox' ? element.checked : element.value;
GM_setValue(key, settings[key]);
});
alert('Settings saved!');
panel.remove();
});
document.querySelector('button#ld_userjs_apply').addEventListener('click', () => {
Object.keys(settingsConfig).forEach(key => {
const element = document.getElementById(`ujs_set_${key}`);
settings[key] = element.type === 'checkbox' ? element.checked : element.value;
GM_setValue(key, settings[key]);
});
window.location.reload();
});
document.querySelector('button#ld_userjs_reset').addEventListener('click', () => {
Object.keys(settingsConfig).forEach(key => {
GM_deleteValue(key);
});
window.location.reload();
});
function setting_hide() {
panel.remove();
shadow.remove();
}
document.querySelector('button#ld_userjs_close').addEventListener('click', () => setting_hide());
shadow.onclick = () => setting_hide();
updateDependencies();
}
// Function 1: Custom Logo
if (settings.icon_custom) {
GM_addStyle(`
#site-logo {
object-fit: scale-down;
object-position: -999vw;
background-size: contain;
background-repeat: no-repeat;
background-image: url('${settings.icon_main}');
opacity: 1;
transition: opacity 0.5s ease;
}
#site-logo.logo-big {
background-image: url('${settings.icon_wide}');
}
#site-logo.logo-mobile {
background-image: url('${settings.icon_wide}');
}
#site-logo:hover {
object-position: unset;
background-image: none;
}
`);
function replaceIcon() {
document.querySelector('link[rel="icon"]').href = settings.icon_main;
}
const observer = new MutationObserver(replaceIcon);
observer.observe(document.head, { childList: true, subtree: true });
replaceIcon();
}
// Function 2: Click Counts Visualization
if (settings.cnts_colorful) {
(function countsColorful() {
const badges = document.querySelectorAll("span.badge.badge-notification.clicks");
let values = Array.from(badges, badge => parseInt(badge.title || badge.textContent));
let maxValue = Math.max(...values);
let minValue = Math.min(...values);
if (maxValue < 100 || (maxValue - minValue < 10)) maxValue = maxValue * 1.5;
badges.forEach(badge => {
if (!badge.style.backgroundColor) {
const number = parseInt(badge.title || badge.textContent);
const hue = 180 - (number / maxValue) * 180;
badge.style.backgroundColor = `hsl(${hue}, 50%, 50%)`;
badge.style.color = "#fff";
const sl = document.createElement('span');
sl.style = `height: 1em; display: inline-block; float: right; background: hsl(${hue}, 50%, 50%); width: ${100 * (number / maxValue)}px;`;
badge.after(sl);
}
});
setTimeout(countsColorful, 1500);
})();
}
// Function 3: Image Resize and Drag
if (settings.image_view) {
let sizePercent = 80;
let isDragging = false;
let startX, startY, initialX, initialY;
function adjustSize(event) {
let contentImg = document.querySelector('section#discourse-lightbox img');
if (contentImg) {
let delta = event.deltaY > 0 ? -10 : 10;
sizePercent += delta;
if (sizePercent > 150) sizePercent = 150;
if (sizePercent < 5) sizePercent = 5;
contentImg.style.width = sizePercent + '%';
contentImg.style.maxWidth = sizePercent + '%';
// contentImg.style.height = sizePercent + '%';
contentImg.style.maxHeight = sizePercent + '150%';
// contentImg.style.objectFit = "contain";
}
}
function startDrag(event) {
let contentImg = document.querySelector('section#discourse-lightbox img');
if (contentImg) {
isDragging = true;
startX = event.clientX;
startY = event.clientY;
initialX = contentImg.offsetLeft;
initialY = contentImg.offsetTop;
event.preventDefault();
}
}
function drag(event) {
if (isDragging) {
let contentImg = document.querySelector('section#discourse-lightbox img');
if (contentImg) {
let dx = event.clientX - startX;
let dy = event.clientY - startY;
contentImg.style.left = (initialX + dx) + 'px';
contentImg.style.top = (initialY + dy) + 'px';
}
}
}
function stopDrag(event) {
isDragging = false;
}
let observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
mutation.addedNodes.forEach(function(node) {
let contentImg = document.querySelector('section#discourse-lightbox img');
if (contentImg) {
document.querySelector('section#discourse-lightbox').onwheel = adjustSize;
contentImg.onmousedown = startDrag;
contentImg.onmouseup = stopDrag;
contentImg.onmousemove = drag;
contentImg.style.cursor = "move";
}
});
});
});
observer.observe(document.body, { childList: true, subtree: true });
}
// Function 4: Quick Bookmark
if (settings.quick_mark) {
const starSvg = `<svg class="svg-icon" aria-hidden="true" style="text-indent: 1px; transform: scale(1); width:18px; height:18px;">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512">
<path d="M259.3 17.8L194 150.2 47.9 171.5c-26.2 3.8-36.7 36.1-17.7 54.6l105.7 103-25 145.5c-4.5 26.3 23.2 46 46.4 33.7L288 439.6l130.7 68.7c23.2 12.2 50.9-7.4 46.4-33.7l-25-145.5 105.7-103c19-18.5 8.5-50.8-17.7-54.6L382 150.2 316.7 17.8c-11.7-23.6-45.6-23.9-57.4 0z"></path></svg></svg> `;
let markMap = new Map();
function handleResponse(xhr, successCallback, errorCallback) {
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
successCallback(xhr);
} else {
errorCallback(xhr);
}
}
};
}
function deleteStarMark(mark_btn, data_id) {
if (markMap.has(data_id)) {
const mark_id = markMap.get(data_id);
var xhr = new XMLHttpRequest();
xhr.open('DELETE', `/bookmarks/${mark_id}`, true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.setRequestHeader('x-requested-with', 'XMLHttpRequest');
xhr.setRequestHeader("x-csrf-token", document.head.querySelector("meta[name=csrf-token]")?.content);
handleResponse(xhr, (xhr) => {
mark_btn.style.color = '#777';
mark_btn.title = "收藏";
mark_btn.onclick = () => addStarMark(mark_btn, data_id);
}, (xhr) => {
alert('删除失败!' + xhr.statusText + "\n" + TryParseJson(xhr.responseText));
});
xhr.send();
}
}
function TryParseJson(str) {
try {
const jsonObj = JSON.parse(str);
return JSON.stringify(jsonObj, null, 1);
} catch (error) {
return str;
}
}
function addStarMark(mark_btn, data_id) {
const xhr = new XMLHttpRequest();
xhr.open('POST', '/bookmarks', true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8');
xhr.setRequestHeader('x-requested-with', 'XMLHttpRequest');
xhr.setRequestHeader('discourse-logged-in', ' true');
xhr.setRequestHeader('discourse-present', ' true');
xhr.setRequestHeader("x-csrf-token", document.head.querySelector("meta[name=csrf-token]")?.content);
const postData = `name=%E6%94%B6%E8%97%8F&auto_delete_preference=3&bookmarkable_id=${data_id}&bookmarkable_type=Post`;
handleResponse(xhr, (xhr) => {
mark_btn.style.color = '#fdd459';
mark_btn.title = "删除收藏";
mark_btn.onclick = () => deleteStarMark(mark_btn, data_id);
}, (xhr) => {
alert('收藏失败!' + xhr.statusText + "\n" + TryParseJson(xhr.responseText));
});
xhr.send(postData);
}
function addMarkBtn() {
let articles = document.querySelectorAll("article[data-post-id]");
if (articles.length <= 0) return;
articles.forEach(article => {
const target = article.querySelector("div.topic-body.clearfix > div.regular.contents > section > nav > div.actions");
if (target && !article.querySelector("div.topic-body.clearfix > div.regular.contents > section > nav > span.star-bookmark")) {
const dataPostId = article.getAttribute('data-post-id');
const starButton = document.createElement('span');
starButton.innerHTML = starSvg;
starButton.className = "star-bookmark";
starButton.style.cursor = 'pointer';
starButton.style.margin = '0px 12px';
if (markMap.has(dataPostId)) {
starButton.style.color = '#fdd459';
starButton.title = "删除收藏";
starButton.onclick = () => deleteStarMark(starButton, dataPostId);
} else {
starButton.style.color = '#777';
starButton.title = "收藏";
starButton.onclick = () => addStarMark(starButton, dataPostId);
}
target.after(starButton);
}
});
}
function getStarMark() {
let articles = document.querySelectorAll("article[data-post-id]");
if (articles.length <= 0) return;
const currentUserElement = document.querySelector('#current-user button');
const currentUsername = currentUserElement ? currentUserElement.getAttribute('href').replace('/u/', '') : null;
const xhr = new XMLHttpRequest();
xhr.open('GET', `/u/${currentUsername}/user-menu-bookmarks`, true);
xhr.setRequestHeader("x-csrf-token", document.head.querySelector("meta[name=csrf-token]")?.content);
handleResponse(xhr, (xhr) => {
var response = JSON.parse(xhr.responseText);
response.bookmarks.forEach(mark => {
markMap.set(mark.bookmarkable_id.toString(), mark.id.toString());
});
addMarkBtn();
}, (xhr) => {
console.error('GET请求失败:', xhr.statusText);
});
xhr.send();
}
let lastUpdateMarkTime = 0;
let lastUpdateButnTime = 0;
function mutationCallback() {
const currentTime = Date.now();
if (currentTime - lastUpdateMarkTime > 9000) {
setTimeout(getStarMark, 500);
lastUpdateMarkTime = currentTime;
}
if (currentTime - lastUpdateButnTime > 1000) {
setTimeout(addMarkBtn, 500);
lastUpdateButnTime = currentTime;
}
}
const mainNode = document.querySelector("#main-outlet");
if (mainNode) {
const observer = new MutationObserver(mutationCallback);
observer.observe(mainNode, { childList: true, subtree: true });
}
getStarMark();
}
// Function 5: mini article image show
if (settings.image_mini) {
let _H = parseInt(settings.image_mini_H);
let _W = parseInt(settings.image_mini_W);
GM_addStyle(`
article div.topic-body div.regular.contents img:not(.thumbnail):not(.ytp-thumbnail-image):not(.emoji) {
transition: max-width 0.5s ease-in-out, max-height 0.5s ease-in-out;
max-width : ${_W}px;
max-height : ${_H}px;
object-fit: contain;
}
article div.topic-body div.regular.contents :hover > img:not(.thumbnail):not(.ytp-thumbnail-image):not(.emoji){
max-width : 100%;
max-height : 100%;
}
`)
}
// Function 6: spoiler-blurred
if (settings.spoiler_noblured) {
GM_addStyle(`
.spoiler-blurred {
filter: drop-shadow(0px 0px 3px #BBB)!important;
}
`)
}
})();