// ==UserScript==
// @name linux.do.level
// @namespace https://linux.do/u/io.oi/s/level
// @version 1.4.7
// @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 o=document.createElement("style");o.textContent=e,document.head.append(o)})(" .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:4px;font-size:var(--base-font-size-largest)}.level-window .bg-white{background-color:var(--primary-low);border-radius:.5em;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}.code-box{display:flex;flex-direction:row;justify-content:space-between}.code-box .language-text{padding:.6em 1em}.code-box .copy{padding:.6em 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;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)}.modal-container .published-page-content-body td{padding:.5em}.modal-container .d-modal__body::-webkit-scrollbar{width:1em;background:transparent;scrollbar-color:rgba(0,0,0,0) transparent;transition:scrollbar-color .25s ease-in-out;transition-delay:.5s}.modal-container .d-modal__body::-webkit-scrollbar-thumb{background:var(--primary-low)} ");
(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");
}
function loadDomFromString(content) {
let parser = new DOMParser();
return parser.parseFromString(content, "text/html").body;
}
function getHtmlBody(html) {
let regx = /<body[^>]*>([\s\S]+?)<\/body>/i;
let contents = regx.exec(html);
if (contents) {
return loadDomFromString(contents[1]);
}
return null;
}
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;
}
class Modal {
constructor(title, content, maxWidth = null) {
__publicField(this, "element");
__publicField(this, "backdrop");
__publicField(this, "title");
__publicField(this, "container");
__publicField(this, "isShow", false);
this.maxWidth = maxWidth;
this.title = title;
this.element = this.initElement();
this.backdrop = this.createBackdrop();
this.container = this.queryContainer();
this.setContent(content);
}
initElement() {
let root = document.createElement("div");
root.id = "custom-modal";
root.className = "ember-view modal d-modal discard-draft-modal";
root.setAttribute("data-keyboard", "false");
root.setAttribute("aria-modal", "true");
root.setAttribute("role", "dialog");
root.innerHTML = `
<div class="d-modal__container" ${this.maxWidth === null ? "" : `style="max-width:${this.maxWidth}px;"`}>
<div class="d-modal__header">
<div class="d-modal__title">
<h1 id="discourse-modal-title" class="d-modal__title-text">${this.title}</h1>
</div>
<button id="m-close-btn" class="btn no-text btn-icon btn-transparent modal-close" title="关闭" type="button">
<svg class="fa d-icon d-icon-times svg-icon svg-string" xmlns="http://www.w3.org/2000/svg">
<use href="#times"></use>
</svg>
</button>
</div>
<div class="d-modal__body" tabindex="-1">
<div class="instructions">
</div>
</div>
<div class="d-modal__footer">
</div>
</div>`;
const close = root.querySelector("button#m-close-btn");
close == null ? void 0 : close.addEventListener("click", () => this.close());
return root;
}
queryContainer() {
return document.querySelector("div.modal-container");
}
setContent(content) {
const root = this.element.querySelector("div.instructions");
const contentElement = typeof content === "string" ? loadDomFromString(content) : content;
root == null ? void 0 : root.appendChild(contentElement);
}
setFooter(footer) {
var _a;
(_a = this.element.querySelector("div.d-modal__footer")) == null ? void 0 : _a.appendChild(footer);
}
show() {
if (this.container) {
this.container.appendChild(this.element);
this.container.appendChild(this.backdrop);
this.isShow = true;
}
}
close() {
if (this.isShow && this.container) {
this.container.removeChild(this.element);
this.container.removeChild(this.backdrop);
this.isShow = false;
}
}
createBackdrop() {
const back = document.createElement("div");
back.className = "d-modal__backdrop";
return back;
}
}
class MessageBox {
constructor(title, content, buttons = [
{
text: "确认",
type: "btn-primary",
onclick: void 0
}
]) {
__publicField(this, "title", "MessageBox");
__publicField(this, "content");
__publicField(this, "buttons");
this.title = title;
this.content = content;
this.buttons = buttons;
}
show() {
const modal = new Modal(this.title, this.content);
function createButton({ onclick, text, type }) {
let button = document.createElement("button");
button.className = "btn btn-text " + type;
button.setAttribute("type", "button");
button.innerHTML = `
<span class="d-button-label">
${text}
</span>`;
button.addEventListener("click", () => {
if (onclick) {
onclick();
}
modal.close();
});
return button;
}
for (const btn of this.buttons) {
modal.setFooter(createButton(btn));
}
modal.show();
}
}
class Icons {
static getLoading(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>`;
}
}
class Level {
constructor() {
__publicField(this, "levelWindow");
__publicField(this, "loading", false);
this.replaceConnectAnchor();
}
showErrorAndGotoConnect(error) {
const message = new MessageBox("错误", error, [
{
text: "确认",
type: "btn-primary"
},
{
text: "前往 Connect 查看",
type: "",
onclick: () => {
window.open("https://connect.linux.do/", "_blank");
}
}
]);
message.show();
}
getContentsFromDom(dom) {
var _a, _b, _c;
let title = dom.querySelector("h1.text-2xl");
(_a = dom.querySelector("h1.text-2xl a.text-blue-500")) == 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 = Icons.getLoading();
let result = await getLevelFromConnect();
this.loading = false;
icon.innerHTML = defaultIcon;
if (result.status) {
let dom = 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();
this.observeUrl();
}
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, _b;
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 = Array.from(floor.querySelectorAll("article section nav div.actions"));
const button = createFloor(id ?? "??");
(_b = actions.at(-1)) == null ? void 0 : _b.appendChild(button);
}
}
clearInterval(timer);
});
}
}
class Emoji {
constructor() {
__publicField(this, "customs", ["飞书", "小红书", "b站", "贴吧"]);
__publicField(this, "observe", new MutationObserver(() => {
let emojiPicker = document.querySelector("div.emoji-picker.opened");
if (!emojiPicker) {
return;
}
let timer = setInterval(() => {
let emojiButtons = emojiPicker.querySelector("div.emoji-picker-category-buttons");
let emojiContainer = emojiPicker.querySelector("div.emojis-container");
if (emojiButtons && emojiContainer) {
for (const custom of this.customs) {
this.moveElementToFirstBySelector(`button[data-section="custom-${custom}"]`, emojiButtons);
this.moveElementToFirstBySelector(`div[data-section="custom-${custom}"]`, emojiContainer);
}
}
clearInterval(timer);
});
}));
observeDom("div#reply-control", (replay) => {
this.onReplayOpen(replay);
});
}
moveElementToFirstBySelector(selector, root) {
let node = root.querySelector(selector);
if (node) {
root.insertBefore(node, root.children[0].nextSibling);
}
}
onReplayOpen(replay) {
if (replay.className.includes("open")) {
let editor = replay.querySelector("div.d-editor");
if (editor) {
this.observe.observe(editor, { childList: true });
} else {
console.error("querySelector:div.d-editor");
}
} else {
this.observe.disconnect();
}
}
}
class FriendLinks {
constructor() {
__publicField(this, "loading", false);
__publicField(this, "defaultIcon", "");
__publicField(this, "loadingIcon", Icons.getLoading());
__publicField(this, "icon");
__publicField(this, "modal");
this.icon = this.replaceFriendAnchor();
}
setLoading(loading) {
this.loading = loading;
this.icon.innerHTML = this.loading ? this.loadingIcon : this.defaultIcon;
}
replaceFriendAnchor() {
const anchor = document.querySelector('a[href="/pub/friend-links"]');
if (!anchor) {
throw new Error("query friend link error.");
}
anchor.href = "javascript:void(0);";
const icon = anchor.querySelector("span.sidebar-section-link-prefix.icon");
if (!icon) throw new Error("query friend link icon error");
this.defaultIcon = icon.innerHTML;
anchor.addEventListener("click", () => this.handleClick());
return icon;
}
async handleClick() {
var _a, _b;
if (this.loading) return;
this.setLoading(true);
if ((_a = this.modal) == null ? void 0 : _a.isShow) {
this.modal.close();
this.modal = void 0;
this.setLoading(false);
return;
}
try {
const response = await fetch("/pub/friend-links");
if (!response.ok) throw new Error(`fetch friend links error: ${response.statusText}`);
const text = await response.text();
const body = getHtmlBody(text);
if (!body) throw new Error("get html body error");
(_b = body.querySelector("div.published-page-header")) == null ? void 0 : _b.remove();
this.modal = new Modal("友链", body, 900);
this.modal.show();
} catch (error) {
console.error(error);
} finally {
this.setLoading(false);
}
}
}
function init() {
window.addEventListener("load", () => {
new Level();
new FriendLinks();
new Floor();
new Emoji();
});
}
init();
})();