linux.do.level

Linux.Do 查看用户信任级别以及升级条件,数据来源于 https://connect.linux.do

// ==UserScript==
// @name         linux.do.level
// @namespace    https://linux.do/u/io.oi/s/level
// @version      1.5.0
// @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)}.metaverse{border-right:none!important}.metaverse .md-table table table{width:100%}.metaverse .md-table table td{padding:var(--space-2)}.metaverse-show-all{padding:.5em!important} ");

(function () {
  'use strict';

  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 && contents.length > 1) {
            resolve({
              status: true,
              content: contents[1],
              error: ""
            });
          } else {
            resolve({
              status: false,
              content: "",
              error: "解析 Connect 数据错误。"
            });
          }
        },
        onerror: (e) => {
          reject({ status: false, error: e.error, content: "" });
        }
      });
    });
  }
  async function getMetaverse() {
    return await new Promise((resolve, reject) => {
      _GM.xmlHttpRequest({
        method: "GET",
        url: "https://linux.do/pub/resources",
        onload: (response) => {
          const dom = new DOMParser().parseFromString(response.responseText, "text/html");
          const div = dom.querySelector("div.published-page-content-body");
          if (div) {
            resolve({
              status: true,
              content: div,
              error: ""
            });
          } else {
            resolve({
              status: false,
              content: null,
              error: "解析数据错误。"
            });
          }
        },
        onerror: (e) => {
          reject({ status: false, error: e.error, content: "" });
        }
      });
    });
  }
  function createUI(context, onClose) {
    const plane = `
          <div class="panel-body">
            <div class="panel-body-contents">
              <div id="quick-access-all-notifications" class="quick-access-panel metaverse">
                ${context.innerHTML}
              </div>
            </div>
          </div>`;
    const root = document.createElement("div");
    root.className = "user-menu-dropdown-wrapper";
    root.setAttribute("tabindex", "0");
    const body = document.createElement("div");
    body.className = "user-menu revamped menu-panel show-avatars drop-down";
    body.setAttribute("data-tab-id", "all-notifications");
    body.setAttribute("data-max-width", "320");
    body.style.padding = "1em";
    body.innerHTML = plane;
    root.appendChild(body);
    const closeButton = document.createElement("div");
    closeButton.className = "panel-body-bottom btn no-text btn-default show-all metaverse-show-all";
    closeButton.innerHTML = "关闭";
    closeButton.addEventListener("click", () => {
      onClose();
    });
    body.appendChild(closeButton);
    return root;
  }
  function createDivLink(content, title = "社区子系统和元宇宙") {
    const div = document.createElement("div");
    div.className = "btn no-text icon btn-flat";
    div.title = title;
    div.style.cursor = "pointer";
    div.innerHTML = content;
    return div;
  }
  function getLoadingSvg(size = 60) {
    return `<svg class="fa d-icon d-icon-envelope svg-icon svg-string" xmlns="http://www.w3.org/2000/svg" width="${size}px" height="${size}px" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid">
              <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 Base {
    showInfo(title, content, onClose) {
      showMessageBox(content, title, [{
        text: "确认",
        type: "btn-primary",
        onClicked: () => {
          if (onClose) {
            onClose();
          }
        }
      }]);
    }
    showError(title, content, onClose) {
      showMessageBox(content, title, [{
        text: "确认",
        type: "btn-danger",
        onClicked: () => {
          if (onClose) {
            onClose();
          }
        }
      }]);
    }
  }
  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");
  }
  class DomEventBus {
    static instance;
    listenerMap;
    observeMap;
    constructor() {
      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] = [];
    }
  }
  function createCodeElement(key) {
    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);
      }
    });
    root.querySelector("div.code-box")?.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?.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 Metaverse extends Base {
    metaverseWindow = void 0;
    connectWindow = void 0;
    init() {
      this.relaceMetaverseAnchor();
    }
    relaceMetaverseAnchor() {
      let metaverseAnchor = document.querySelector('a[href="https://linux.do/pub/resources"]');
      if (metaverseAnchor) {
        const divLink = createDivLink(metaverseAnchor.innerHTML);
        divLink.addEventListener("click", async (event) => {
          await this.onMetaverseLinkClicked(divLink, event);
        });
        metaverseAnchor.parentElement?.appendChild(divLink);
        metaverseAnchor.remove();
      }
    }
    async onMetaverseLinkClicked(divLink, event) {
      if (event.ctrlKey) {
        await this.onConnectLinkClicked();
      } else {
        if (this.metaverseWindow) {
          this.closeMetaverseWindow();
          return;
        }
        const originalHtml = divLink.innerHTML;
        divLink.innerHTML = getLoadingSvg(100);
        const metaverse = await getMetaverse();
        divLink.innerHTML = originalHtml;
        if (metaverse.status) {
          if (this.connectWindow) {
            this.closeConnectWindow();
          }
          this.metaverseWindow = createUI(metaverse.content, () => {
            this.closeMetaverseWindow();
          });
          const connect = this.metaverseWindow.querySelector('a[href="https://connect.linux.do"]');
          if (connect) {
            connect.parentElement?.appendChild(this.createConnectDivLink(connect));
            connect.remove();
          }
          const header = document.querySelector("div.panel[role=navigation]");
          if (header) {
            header.appendChild(this.metaverseWindow);
            const t = setInterval(() => {
              this.metaverseWindow.focus();
              clearInterval(t);
            }, 0);
          } else {
            console.error("query div.panel[role=navigation] error");
          }
        } else {
          this.showError("错误", metaverse.error);
          console.error(metaverse.error);
        }
      }
    }
    createConnectDivLink(connect) {
      const connectDivLink = createDivLink(connect.innerHTML, "Linux do connect");
      connectDivLink.className = "";
      connectDivLink.style.padding = "0";
      connectDivLink.style.color = "var(--d-link-color)";
      connectDivLink.style.textDecoration = "none";
      connectDivLink.style.cursor = "pointer";
      connectDivLink.addEventListener("click", async () => {
        await this.onConnectLinkClicked();
      });
      return connectDivLink;
    }
    async onConnectLinkClicked() {
      const result = await getLevelFromConnect();
      if (result.status) {
        const connect = this.loadDomFromString(result.content);
        this.createConnectWindow(connect);
        this.closeMetaverseWindow();
      } else {
        showMessageBox(result.error, "错误", [{
          text: "确认",
          type: "btn-primary",
          onClicked: () => {
          }
        }, {
          text: "前往 Connect 查看",
          type: "",
          onClicked: () => {
            window.open("https://connect.linux.do/", "_blank");
          }
        }]);
      }
    }
    loadDomFromString(content) {
      let parser = new DOMParser();
      return parser.parseFromString(content, "text/html").body;
    }
    getContentsFromDom(dom) {
      let title = dom.querySelector("h1.text-2xl");
      title?.querySelector('a[href^="/logout/"]')?.remove();
      let levelTable = dom.querySelector("div.bg-white.p-6.rounded-lg.mb-4.shadow table")?.parentElement;
      let key = dom.querySelector("div.bg-white.p-6.rounded-lg.mb-4.shadow p strong")?.innerHTML;
      let status = key !== void 0 && levelTable !== null;
      return {
        status,
        key,
        title,
        content: levelTable,
        error: status ? null : "解析 Connect 数据错误。"
      };
    }
    createConnectWindow(content) {
      if (this.connectWindow) {
        this.closeConnectWindow();
      } else {
        const connect = this.getContentsFromDom(content);
        if (connect.status) {
          this.connectWindow = createWindow(connect.title, connect.key, connect.content, () => {
            this.closeConnectWindow();
          });
          document.body.appendChild(this.connectWindow);
        } else {
          this.showError("错误", connect.error);
        }
      }
    }
    closeConnectWindow() {
      if (this.connectWindow) {
        this.connectWindow.remove();
        this.connectWindow = void 0;
      }
    }
    closeMetaverseWindow() {
      if (this.metaverseWindow) {
        this.metaverseWindow.remove();
        this.metaverseWindow = void 0;
      }
    }
  }
  function initMetaverse() {
    const metaverse = new Metaverse();
    metaverse.init();
  }
  class Emoji {
    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();
        }
      }
    }
    customs = ["飞书", "小红书", "b站", "贴吧"];
    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);
          }
        });
      }
    });
    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 initEmoji() {
    new Emoji().init();
  }
  function createFloor(num) {
    let button = document.createElement("button");
    button.className = "btn no-text btn-icon btn-flat";
    button.setAttribute("title", `${num}楼`);
    button.setAttribute("id", "floor-button");
    button.innerHTML = `<span class='d-button-label floor-text'>#${num}</span>`;
    return button;
  }
  class Floor {
    eventBus;
    constructor() {
      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(() => {
        let floors = Array.from(document.querySelectorAll("div.container.posts div.posts-wrapper div.post-stream div.topic-post"));
        for (const floor of floors) {
          if (floor.querySelector("button#floor-button")) {
            continue;
          }
          let article = floor.querySelector("article");
          if (article) {
            let id = article.getAttribute("id")?.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);
      }, 500);
    }
    init() {
      this.observeUrl();
    }
  }
  function initFloor() {
    new Floor().init();
  }
  function init() {
    window.addEventListener("load", (event) => {
      if (event.target === document && !event.bubbles) {
        initFloor();
        initEmoji();
        initMetaverse();
      }
    });
  }
  init();

})();