// ==UserScript==
// @name linux.do.level
// @namespace https://linux.do/u/io.oi/s/level
// @version 1.4.10
// @author LINUX.DO
// @description Linux.Do 查看用户信任级别以及升级条件,数据来源于 https://connect.linux.do
// @license MIT
// @icon https://linux.do/uploads/default/original/3X/9/d/9dd49731091ce8656e94433a26a3ef36062b3994.png
// @match https://linux.do/*
// @connect connect.linux.do
// @grant GM.xmlHttpRequest
// @grant GM_addStyle
// ==/UserScript==
(e=>{if(typeof GM_addStyle=="function"){GM_addStyle(e);return}const r=document.createElement("style");r.textContent=e,document.head.append(r)})(" .level-window{position:fixed;bottom:0;background:var(--secondary);z-index:999;padding:.5em;color:var(--primary);box-shadow:0 0 4px #00000020;border:1px solid var(--primary-low)}.level-window .title .close{width:24px;height:24px;color:#fff;background:red;display:inline-block;text-align:center;line-height:24px;float:right;cursor:pointer;border-radius:var(--d-button-border-radius);font-size:var(--base-font-size-largest)}.level-window .bg-white{background-color:var(--primary-low);border-radius:var(--d-button-border-radius);padding:.5em;margin-top:.5em}.level-window h1{color:var(--primary);font-size:1.3rem}.level-window h2{font-size:1.25rem}.mb-4 table tr:nth-child(2n){background-color:var(--tertiary-400)}.level-window .text-red-500{color:#ef4444}.level-window .text-green-500{color:#10b981}.level-window .mb-4 table tr td{padding:4px 8px}.language-text{background:var(--primary-very-low);font-family:var(--d-font-family--monospace);font-size:var(--base-font-size-smallest);flex-grow:1;padding:6px}.code-box{display:flex;flex-direction:row;justify-content:space-between}.code-box .copy{padding:.5em 1em;cursor:pointer;-webkit-user-select:none;user-select:none;font-size:var(--base-font-size-smallest);background:var(--secondary)}.connect-button{width:100%;padding:.5em;border-radius:var(--d-button-border-radius)!important;margin-top:.5em!important}.emoji-picker-category-buttons,.emoji-picker-emoji-area{justify-content:center;padding-left:initial}.emoji-picker-category-buttons::-webkit-scrollbar,.emoji-picker-emoji-area::-webkit-scrollbar{width:5px;height:auto;background:var(--primary)}.emoji-picker-category-buttons::-webkit-scrollbar-thumb,.emoji-picker-emoji-area::-webkit-scrollbar-thumb{box-shadow:inset 0 0 5px #0003;background:var(--secondary)}.emoji-picker-category-buttons::-webkit-scrollbar-track,.emoji-picker-emoji-area::-webkit-scrollbar-track{box-shadow:inset 0 0 5px #0003;background:var(--primary-low)}.floor-text{color:var(--tertiary)} ");
(function () {
'use strict';
var __defProp = Object.defineProperty;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
var _GM = /* @__PURE__ */ (() => typeof GM != "undefined" ? GM : void 0)();
async function getLevelFromConnect() {
return await new Promise((resolve, reject) => {
_GM.xmlHttpRequest({
method: "GET",
url: "https://connect.linux.do",
onload: (response) => {
let regx = /<body[^>]*>([\s\S]+?)<\/body>/i;
let contents = regx.exec(response.responseText);
if (contents) {
resolve({
status: true,
content: contents[1],
error: ""
});
}
},
onerror: (e) => {
reject({ status: false, error: e.error, content: "" });
}
});
});
}
function observeDom(selector, onChanged, option) {
let dom = typeof selector === "string" ? document.querySelector(selector) : selector;
if (dom) {
const observer = new MutationObserver(() => {
onChanged(dom);
});
observer.observe(dom, { childList: true });
return observer;
} else {
console.error(`query dom error: [${selector}]`);
return null;
}
}
function isOnTopicPage() {
return window.location.href.includes("https://linux.do/t/topic");
}
const _DomEventBus = class _DomEventBus {
constructor() {
__publicField(this, "listenerMap");
__publicField(this, "observeMap");
this.listenerMap = {};
this.observeMap = {};
}
static getInstance() {
if (!this.instance) {
this.instance = new _DomEventBus();
}
return this.instance;
}
/**
* 监听事件
* @param name 事件名称
* @param listener 事件监听器
* @param dom 如果为 null,使用事件名称查找 dom, 不为空直接使用给定的 dom
*/
add(name, listener, dom = null) {
if (!this.listenerMap[name]) {
this.listenerMap[name] = [];
}
if (this.listenerMap[name].length === 0) {
let observe = dom === null ? observeDom(name, () => {
this.domEmit(name);
}) : observeDom(dom, () => {
this.domEmit(name);
});
if (observe) {
this.observeMap[name] = observe;
}
}
this.listenerMap[name].push(listener);
}
domEmit(event) {
const listeners = this.listenerMap[event];
if (listeners) {
for (const listener of listeners) {
listener();
}
}
}
emit(name) {
this.domEmit(name);
}
clear(name) {
if (!this.listenerMap[name]) {
return;
}
this.listenerMap[name] = [];
}
};
__publicField(_DomEventBus, "instance");
let DomEventBus = _DomEventBus;
function createCodeElement(key) {
var _a;
let realKey = key;
let copied = false;
let root = document.createElement("div");
root.className = "bg-white p-6 rounded-lg mb-4 shadow";
root.innerHTML = `
<h2>DeepLX Api Key</h2>
<div class="code-box">
<span class="hljs language-text">${key.replace(key.substring(12, 21), "**加密**")}</span>
</div>
`;
let copyButton = document.createElement("span");
copyButton.className = "copy";
copyButton.innerHTML = "复制";
copyButton.addEventListener("click", async () => {
if (!copied) {
await navigator.clipboard.writeText(realKey);
copied = true;
copyButton.innerHTML = "已复制";
let timer = setTimeout(() => {
copied = false;
copyButton.innerHTML = "复制";
clearInterval(timer);
}, 2e3);
}
});
(_a = root.querySelector("div.code-box")) == null ? void 0 : _a.appendChild(copyButton);
let connectButton = document.createElement("a");
connectButton.className = "btn btn-primary connect-button";
connectButton.href = "https://connect.linux.do";
connectButton.target = "_blank";
connectButton.innerHTML = "前往 Connect 站";
root.appendChild(connectButton);
return root;
}
function createWindow(title, key, levelTable, onClose) {
let root = document.createElement("div");
root.setAttribute("id", "level-window");
root.className = "level-window";
root.style.right = document.querySelector("div.chat-drawer.is-expanded") ? "430px" : "15px";
root.innerHTML = `
<div class="title">
<span class="close" id="close-button">
<svg class="fa d-icon d-icon-times svg-icon svg-string" xmlns="http://www.w3.org/2000/svg">
<use href="#xmark"></use>
</svg>
</span>
<div id="content" class="content"></div>
</div>`;
let window2 = root.querySelector("div#content");
if (window2) {
window2.appendChild(title);
window2.appendChild(createCodeElement(key));
window2.appendChild(levelTable);
}
let close = root.querySelector("span#close-button");
close == null ? void 0 : close.addEventListener("click", onClose);
DomEventBus.getInstance().add("div.chat-drawer-outlet-container", () => {
let chat = document.querySelector("div.chat-drawer.is-expanded");
root.style.right = chat ? "430px" : "15px";
});
let chatContainer = document.querySelector("div.chat-drawer-outlet-container");
if (chatContainer) {
let observer = new MutationObserver((_) => {
let chat = document.querySelector("div.chat-drawer.is-expanded");
root.style.right = chat ? "430px" : "15px";
});
observer.observe(chatContainer, { childList: true });
}
return root;
}
function getLoadingSvg(size = 60) {
return `<svg xmlns="http://www.w3.org/2000/svg" width="${size}px" height="${size}px" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid" class="lds-ring">
<circle cx="50" cy="50" r="30" stroke="#B3B5B411" stroke-width="10" fill="none"/>
<circle cx="50" cy="50" r="30" stroke="#808281" stroke-width="10" fill="none" transform="rotate(144 50 50)">
<animateTransform attributeName="transform" type="rotate" calcMode="linear" values="0 50 50;360 50 50" keyTimes="0;1" dur="1s" begin="0s" repeatCount="indefinite"/>
<animate attributeName="stroke-dasharray" calcMode="linear" values="18.84955592153876 169.64600329384882;94.2477796076938 94.24777960769377;18.84955592153876 169.64600329384882" keyTimes="0;0.5;1" dur="1" begin="0s" repeatCount="indefinite"/>
</circle>
</svg>`;
}
function showMessageBox(message, title, buttons = [
{
text: "确认",
type: "btn-primary",
onClicked: function() {
}
}
]) {
let root = document.querySelector("div.modal-container");
if (root) {
let box = document.createElement("div");
box.id = "message-box";
box.className = "ember-view modal d-modal discard-draft-modal";
box.setAttribute("data-keyboard", "false");
box.setAttribute("aria-modal", "true");
box.setAttribute("role", "dialog");
box.innerHTML = `
<div class="d-modal__container">
<div class="d-modal__header">${title}</div>
<div class="d-modal__body" tabindex="-1">
<div class="instructions">
${message}
</div>
</div>
<div class="d-modal__footer">
</div>
</div>`;
let backdrop = document.createElement("div");
backdrop.className = "d-modal__backdrop";
root.appendChild(backdrop);
let footer = box.querySelector("div.d-modal__footer");
if (footer) {
for (const button of buttons) {
let btnElement = document.createElement("button");
btnElement.className = "btn btn-text " + button.type;
btnElement.setAttribute("type", "button");
btnElement.innerHTML = `
<span class="d-button-label">
${button.text}
</span>
`;
btnElement.addEventListener("click", () => {
button.onClicked();
box.remove();
backdrop.remove();
});
footer.appendChild(btnElement);
}
root.appendChild(box);
}
}
}
class Level {
constructor() {
__publicField(this, "levelWindow");
__publicField(this, "loading", false);
}
init() {
this.replaceConnectAnchor();
}
loadDomFromString(content) {
let parser = new DOMParser();
return parser.parseFromString(content, "text/html").body;
}
showErrorAndGotoConnect(error) {
showMessageBox(error, "错误", [{
text: "确认",
type: "btn-primary",
onClicked: () => {
}
}, {
text: "前往 Connect 查看",
type: "",
onClicked: () => {
window.open("https://connect.linux.do/", "_blank");
}
}]);
}
getContentsFromDom(dom) {
var _a, _b, _c;
let title = dom.querySelector("h1.text-2xl");
(_a = title == null ? void 0 : title.querySelector('a[href^="/logout/"]')) == null ? void 0 : _a.remove();
let levelTable = (_b = dom.querySelector("div.bg-white.p-6.rounded-lg.mb-4.shadow table")) == null ? void 0 : _b.parentElement;
let key = (_c = dom.querySelector("div.bg-white.p-6.rounded-lg.mb-4.shadow p strong")) == null ? void 0 : _c.innerHTML;
let status = key !== void 0 && levelTable !== null;
return {
status,
key,
title,
content: levelTable,
error: status ? null : "解析 Connect 数据错误。"
};
}
replaceConnectAnchor() {
let connectAnchor = document.querySelector('a[href="https://connect.linux.do"]');
if (connectAnchor) {
connectAnchor.href = "javascript:void(0);";
connectAnchor.addEventListener("click", async () => {
if (!this.loading && this.levelWindow === void 0) {
this.loading = true;
let icon = connectAnchor.querySelector("span.sidebar-section-link-prefix.icon");
if (icon) {
let defaultIcon = icon.innerHTML;
icon.innerHTML = getLoadingSvg();
let result = await getLevelFromConnect();
this.loading = false;
icon.innerHTML = defaultIcon;
if (result.status) {
let dom = this.loadDomFromString(result.content);
let body = this.getContentsFromDom(dom);
if (body.status) {
this.levelWindow = createWindow(body.title, body.key, body.content, () => {
this.close();
});
document.body.appendChild(this.levelWindow);
} else {
this.showErrorAndGotoConnect(body.error);
}
} else {
this.showErrorAndGotoConnect(result.error);
}
}
} else {
this.close();
}
});
return;
}
console.error("replace connect anchor error.");
}
close() {
this.levelWindow.remove();
this.levelWindow = void 0;
}
}
function createFloor(num) {
let button = document.createElement("button");
button.className = "widget-button btn-flat reply create fade-out btn-icon-text";
button.setAttribute("title", `${num}楼`);
button.setAttribute("id", "floor-button");
button.innerHTML = `<span class='d-button-label floor-text'>#${num}</span>`;
return button;
}
class Floor {
constructor() {
__publicField(this, "eventBus");
this.eventBus = DomEventBus.getInstance();
}
observeUrl() {
const changed = () => {
const timer = setInterval(() => {
if (isOnTopicPage()) {
this.eventBus.add("div.post-stream", () => {
if (isOnTopicPage()) {
this.fixFloorDom();
}
});
this.fixFloorDom();
} else {
this.eventBus.clear("div.post-stream");
}
clearInterval(timer);
});
};
this.eventBus.add("div#main-outlet", changed);
if (isOnTopicPage()) {
this.eventBus.emit("div#main-outlet");
}
}
fixFloorDom() {
let timer = setInterval(() => {
var _a;
let floors = Array.from(document.querySelectorAll("div.container.posts section.topic-area div.ember-view div.topic-post"));
for (const floor of floors) {
if (floor.querySelector("button#floor-button")) {
continue;
}
let article = floor.querySelector("article");
if (article) {
let id = (_a = article.getAttribute("id")) == null ? void 0 : _a.replace("post_", "");
let actions = floor.querySelectorAll("article section nav div.actions");
const button = createFloor(id ? id : "??");
if (actions.length > 0) {
const i = actions.length === 2 ? 1 : 0;
actions[i].appendChild(button);
} else {
console.error("query actions error.");
}
}
}
clearInterval(timer);
});
}
init() {
this.observeUrl();
}
}
class Emoji {
constructor() {
__publicField(this, "customs", ["飞书", "小红书", "b站", "贴吧"]);
__publicField(this, "observe", new MutationObserver(() => {
let loadTimes = 0;
let emojiPicker = document.querySelector("div.emoji-picker");
if (emojiPicker) {
let timer = setInterval(() => {
let emojiButtons = emojiPicker.querySelector("div.emoji-picker__sections-nav");
let emojiContainer = emojiPicker.querySelector("div.emoji-picker__sections");
if (emojiButtons && emojiContainer) {
for (const custom of this.customs) {
this.moveElementToFirstBySelector(`div[data-section="${custom}"]`, emojiContainer);
this.moveElementToFirstBySelector(`button[data-section="${custom}"]`, emojiButtons, custom === "贴吧");
}
clearInterval(timer);
}
loadTimes++;
if (loadTimes >= 300) {
console.warn("emoji 加载缓慢,跳过修正,下次打开表情面板即可正常显示。");
clearInterval(timer);
}
});
}
}));
}
moveElementToFirstBySelector(selector, root, click = false) {
let node = root.querySelector(selector);
if (node) {
root.insertBefore(node, root.children[0].nextSibling);
if (click && node instanceof HTMLButtonElement) {
node.click();
}
}
}
init() {
observeDom("div#reply-control", (replay) => {
this.onReplayOpen(replay);
});
}
onReplayOpen(replay) {
if (replay.className.includes("open")) {
let menu = document.querySelector("div#d-menu-portals");
if (menu) {
this.observe.observe(menu, { childList: true });
} else {
console.error("querySelector:div.d-editor");
}
} else {
this.observe.disconnect();
}
}
}
function init() {
window.addEventListener("load", () => {
new Level().init();
new Floor().init();
new Emoji().init();
});
}
init();
})();