Greasy Fork is available in English.

「Z-Blog」论坛辅助

针对 Z-Blog 官方论坛的辅助脚本

// ==UserScript==
// @name         「Z-Blog」论坛辅助
// @namespace    https://www.wdssmq.com/
// @version      1.0.5
// @author       沉冰浮水
// @description  针对 Z-Blog 官方论坛的辅助脚本
// @license      MIT
// @link         https://greasyfork.org/zh-CN/scripts/419517
// @null         ----------------------------
// @contributionURL    https://github.com/wdssmq#%E4%BA%8C%E7%BB%B4%E7%A0%81
// @contributionAmount 5.93
// @null         ----------------------------
// @link         https://github.com/wdssmq/userscript
// @link         https://afdian.net/@wdssmq
// @link         https://greasyfork.org/zh-CN/users/6865-wdssmq
// @null         ----------------------------
// @noframes
// @run-at       document-end
// @match        https://bbs.zblogcn.com/*
// @match        https://app.zblogcn.com/zb_system/admin/edit.php*
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// @require      https://cdn.bootcdn.net/ajax/libs/lz-string/1.4.4/lz-string.min.js
// @require      https://cdn.bootcdn.net/ajax/libs/js-yaml/4.1.0/js-yaml.min.js
// @require      https://cdn.jsdelivr.net/npm/showdown@2.1.0/dist/showdown.min.js
// ==/UserScript==

/* eslint-disable */
/* jshint esversion: 6 */

(function () {
  'use strict';

  const gm_name = "zbp-xiuno";

  // 初始变量
  const $n = (selector, context = document) => context.querySelector(selector);
  const $ = window.jQuery || unsafeWindow.jQuery;
  const UM = window.UM || unsafeWindow.UM;
  const UE = window.UE || unsafeWindow.UE;
  const curHref = location.href.replace(location.hash, "");
  // localStorage 封装
  const lsObj = {
    setItem: function(key, value) {
      localStorage.setItem(key, JSON.stringify(value));
    },
    getItem: function(key, def = "") {
      const item = localStorage.getItem(key);
      if (item) {
        return JSON.parse(item);
      }
      return def;
    },
  };
  // 预置函数
  const _log = (...args) => console.log(`[${gm_name}]\n`, ...args);
  const _hash = () => location.hash.replace("#", "");
  // Get 封装
  function fnGetRequest(strURL, strData, fnCallback) {
    if (typeof strData === "function") {
      fnCallback = strData;
      strData = "";
    }
    GM_xmlhttpRequest({
      method: "GET",
      data: strData,
      url: strURL,
      onload: function(responseDetail) {
        if (responseDetail.status === 200) {
          fnCallback(responseDetail.responseText, strURL);
        } else {
          console.log(responseDetail);
          alert("请求失败,请检查网络!");
        }
      },
    });
  }
  // formtTime 封装
  function fnFormatTime() {
    const objTime = new Date();
    const strYear = objTime.getFullYear();
    const strMonth = objTime.getMonth() + 1;
    const strDate = objTime.getDate();
    objTime.getHours();
    objTime.getMinutes();
    objTime.getSeconds();
    return (
      [strYear, strMonth, strDate].map(n => n.toString().padStart(2, "0")).join("-") +
      // " " +
      // [strHour, strMinute, strSecond].map((n) => n.toString().padStart(2, "0")).join(":") +
      ""
    ).trim();
  }

  (() => {
    const $body = $n("body");
    const defData = {
      status: 0, // 用于记录状态
      href: curHref,
    };

    // 更新 lsData 中的 status 状态
    const updateStatus = (status) => {
      const lsData = lsObj.getItem("xiuno_login", defData);
      lsData.status = status;
      lsObj.setItem("xiuno_login", lsData);
    };

    // 登录后跳转前登录前的页面
    const goUrl = () => {
      // 读取 localStorage
      const lsData = lsObj.getItem("xiuno_login", defData);
      // 根据记录的状态判断是否跳转
      if (lsData.status === 1) {
        // 更新状态
        updateStatus(2);
        // 跳转前的页面地址
        location.href = lsData.href;
      }
    };

    // 主入口函数,判断是否登录
    const checkLogin = () => {
      // 判断 body 内容是否为空
      if ($body.textContent.trim() !== "") {
        // 有内容,说明已登录,根据记录的地址跳转
        goUrl();
        return;
      }
      // 更新状态及 href 到 localStorage
      updateStatus(1);
      // 跳转登录页
      location.href = "/user-login.html";
    };

    // 延时 3 秒检查
    setTimeout(checkLogin, 3000);
  })();

  /* globals LZString jsyaml*/

  (() => {
    // 定义按钮及提示信息
    const $btnBad = $(" <a class=\"btn btn-primary\">BAD</a>");
    const strTip = "<p>此贴内容或签名不符合论坛规范已作屏蔽处理,请查看置顶贴,以下为原始内容备份。</p>";

    // 绑定点击事件
    $btnBad.css({ color: "#fff" }).click(function () {
      let um = UM.getEditor("message");
      let str = um.getContent();
      if (str.indexOf("#~~") > -1) {
        return;
      }
      let strCode = LZString.compressToBase64(str);
      um.setContent(strTip + `<p>#~~${strCode}~~#</p>`);
      console.log(LZString.decompressFromBase64(strCode));
      // let strDeCode = LZString.decompressFromBase64(strCode);
      // um.setContent(strCode + strDeCode);
    });

    // 放置按钮
    if ($("input[name=update_reason]").length > 0) {
      $("#submit").after($btnBad);
    }

    // 解码
    $("div.message").each(function () {
      let $secP = $(this).find("p:nth-child(2)");
      if ($secP.length == 0) {
        console.log("skip");
        return;
      }
      let str = $secP.html();
      if (str.indexOf("#~~") == -1) {
        return;
      }
      console.log(str);
      str = str.replace(/#~~(.+)~~#/, function (a, b) {
        console.log(arguments);
        let strDeCode = LZString.decompressFromBase64(b);
        console.log(strDeCode);
        return strDeCode;
      });
      $secP.after(str).remove();
    });
  })();

  // _pid.js | 楼层地址
  (() => {
    $("li.media.post").each(function () {
      const $me = $(this);
      const pid = $me.data("pid");
      const $date = $me.find("span.date");
      $date.after(
        `<a class="text-grey ml-2" title="获取当前楼层链接" href="${curHref}#${pid}">「楼层地址」</a>`,
      );
    });
  })();

  /* globals jsyaml*/

  (() => {
    // _log(curHref);
    if (curHref.indexOf("bbs.zblogcn.com") === -1) {
      return;
    }
    _log("devView");
    // CDN 地址替换
    function fnGetCDNUrl(url) {
      const arrMap = [
        ["https://github.com/", "https://cdn.jsdelivr.net/gh/"],
        ["/blob/", "@"],
      ];
      let cdnUrl = url;
      arrMap.forEach((line) => {
        cdnUrl = cdnUrl.replace(line[0], line[1]);
      });
      return cdnUrl;
    }

    // time 2 hour
    function fnTime2Hour(time = null) {
      if (!time) {
        time = new Date();
      }
      // 时间戳
      const timeStamp = time.getTime();
      return Math.floor(timeStamp / 1000 / 60 / 60);
    }

    // 默认配置项
    const defConfig = {
      useCDN: false,
      ymlList: [
        "2023H1",
        "2022H2",
        "2022H1",
        "2021H2",
      ],
      ver: "2023-04-24",
      isNew: true,
    };

    // 配置项读取和首次保存
    const curConfig = GM_getValue("_devConfig", defConfig);
    if (curConfig.isNew || curConfig.ver !== defConfig.ver) {
      curConfig.isNew = false;
      GM_setValue("_devConfig", defConfig);
    }

    // 初始化 ymlList
    function fnInitYML() {
      const useCDN = curConfig.useCDN;
      let ymlList = curConfig.ymlList;
      ymlList = ymlList.map((yml) => {
        let url = `https://raw.githubusercontent.com/wdssmq/ReviewLog/main/data/${yml}.yml`;
        if (useCDN) {
          url = fnGetCDNUrl(url);
        }
        return url;
      });
      return ymlList;
    }

    // 模板函数
    function fnStrtr(
      str,
      obj,
      callback = (str) => {
        return str;
      },
    ) {
      let rltStr = str;
      for (const key in obj) {
        if (Object.hasOwnProperty.call(obj, key)) {
          const value = obj[key];
          const reg = new RegExp(`#${key}#`, "g");
          rltStr = rltStr.replace(reg, value);
        }
      }
      return callback(rltStr);
    }

    // 数据读取封装
    const gobDev = {
      data: {
        lstLogs: [],
        lstCheck: null,
      },
      init: function () {
        this.data = lsObj.getItem("gobDev", this.data);
        _log("gobDev init", this.data);
        this.ymlList = fnInitYML();
      },
      checkUrl: function (url) {
        let rlt = null;
        this.data.lstLogs.forEach((log) => {
          if (log.url.indexOf(url) > -1) {
            _log("checkUrl", url, log.url);
            rlt = log;
          }
        });
        return rlt;
      },
      clear: function () {
        this.data.lstLogs = [];
        lsObj.setItem("gobDev", this.data);
      },
      save: function () {
        lsObj.setItem("gobDev", this.data);
      },
      update: function () {
        const curHour = fnTime2Hour();
        if (this.data.lstCheck === curHour && this.data.lstLogs.length > 0) {
          return;
        }
        this.data.lstLogs = [];
        this.data.lstCheck = curHour;
        this.ajax();
      },
      ajax: function () {
        const self = this;
        this.ymlList.forEach((yml) => {
          fnGetRequest(yml, (responseText, url) => {
            _log("ajax", url);
            const ymlObj = jsyaml.load(responseText, "utf8");
            const curLogs = self.data.lstLogs;
            self.data.lstLogs = curLogs.concat(ymlObj);
            self.save();
          });
        });
      },
    };

    gobDev.init();
    gobDev.update();

    // 缓存清理封装
    const _clearAct = (doClear = false) => {
      const curHash = _hash();
      if (curHash === "clearDone") {
        window.location.href = `${curHref}`;
        // window.location.reload();
      } else if (doClear || curHash === "clear") {
        gobDev.clear();
        window.location.href = `${curHref}#clearDone`;
        setTimeout(() => {
          window.location.reload();
        }, 1000);
      }
    };
    // 默认调用一次用于清后的跳转
    _clearAct();

    // 缓存清理按钮
    const $btnClear = $("<span class=\"small\"><a href=\"javascript:;\" title=\"清理缓存\" class=\"badge badge-warning\">清理缓存</a></span>");
    $btnClear.on("click", function () {
      if (confirm("清理缓存?")) {
        _clearAct(1);
      }
    });

    // 根据 log 数据设置状态徽章
    const _setBadge = (log, $item = null, act = "after") => {
      // console.log("log", log);
      let badgeClass, $badge;
      const status = log?.status || "未记录";
      switch (status) {
        case "通过":
          badgeClass = "badge-success";
          break;
        case "进行中":
          badgeClass = "badge-info";
          break;
        case "拒绝":
          badgeClass = "badge-danger";
          break;
        default:
          badgeClass = "badge-warning";
          break;
      }
      $badge = $(`<span class="badge ${badgeClass}">${status}</span>`);

      if (act === "after") {
        $item.after($badge);
        // $item.after($btnClear);
      } else {
        $item.append($badge);
        $item.append(" ");
        $item.append($btnClear);
      }
    };

    // 标题列表
    const $titleList = $("li.media .subject a");
    $titleList.each(function () {
      const $this = $(this);
      const href = $this.attr("href");
      const title = $this.text();
      if (title.indexOf("申请开发者") === -1) {
        return;
      }
      const log = gobDev.checkUrl(href);
      _setBadge(log, $this);
    });

    // 博文内页
    const $h4 = $(".media-body h4");
    let title = $h4.text().trim();
    if (title.indexOf("申请开发者") === -1) {
      return;
    }
    const log = gobDev.checkUrl(curHref);
    _setBadge(log, $h4, "append");

    _log("curLog", log);

    // 初始化
    $("div.message").each(function () {
      if ($(this).attr("isfirst") == 1) {
        $(this).prepend(
          "<blockquote class=\"blockquote\"><pre class=\"pre-yml\"></pre></blockquote>",
        );
        $(".pre-yml").text("标题格式错误");
      }
    });

    // 标题内容解析
    title = title.replace(/\[|【/g, "「").replace(/\]|】/g, "」");
    const objMatch = title.match(/「([^」]+)」「(theme|plugin)」/);
    _log("objMatch", objMatch);
    if (!objMatch) {
      return;
    }

    // YML 模板
    const tplYML = `
- id: #id#
  type: #type#
  status: #status#
  rating: #rating#
  url: #url#
  git: #git#
  date:
    - #date#
  reviewers:
    - #reviewers#
`;
    // 构建 YML
    const styYML = fnStrtr(
      tplYML,
      {
        id: objMatch[1],
        type: objMatch[2],
        status: log ? log.status : "进行中",
        rating: log ? log.rating : "",
        url: curHref,
        git: log ? log.git : "",
        date: log ? log.date[0] : fnFormatTime(),
        reviewers: log ? log.reviewers.join("\n_4_- ") : "null",
      },
      (str) => {
        str = str.replace(/\n/g, "\\|");
        // str = str.replace(/\s{6}/g, "_2__2_");
        // str = str.replace(/\s{4}/g, "_2_");
        str = str.replace(/_4_/g, "_2__2_");
        str = str.replace(/_2_/g, "  ");
        str = str.replace(/\\\|/g, "\n");
        const objMatch = title.match(/(通过|拒绝)/);
        if (objMatch) {
          str = str.replace(/status: 进行中/, `status: ${objMatch[1]}`);
        }
        return str;
      },
    );
    // 插入 YML
    $(".pre-yml").text(`${styYML}`);
  })();

  // 引入元素插入
  (() => {
    if (typeof UM === "undefined") {
      return;
    }

    // 引用标签插入封装
    function fnBlockQuote() {
      const umObj = UM.getEditor("message");
      if (!umObj.isFocus()) {
        umObj.focus(true);
      }
      const addHTML = "<blockquote class=\"blockquote\"><p><br></p></blockquote><p><br></p>";
      // umObj.execCommand("insertHtml", addHTML);
      umObj.setContent(addHTML, true);
    }

    // 添加引用按钮
    $("head").append("<style>.edui-icon-blockquote:before{content:\"\\f10d\";}");
    (() => {
      const $btn = $.eduibutton({
        icon: "blockquote",
        click: function () {
          fnBlockQuote();
        },
        title: UM.getEditor("message").getLang("labelMap")["blockquote"] || "",
      });
      $(".edui-btn-name-insertcode").after($btn);
    })();

    // 自动排版函数封装
    function fnAutoFormat() {
      const umObj = UM.getEditor("message");
      let strHTML = umObj.getContent();
      strHTML = strHTML.replace(
        /<blockquote>/g,
        "<blockquote class=\"blockquote\">",
      );
      // 第二个参数为 true 表示追加;
      umObj.setContent(strHTML, false);
    }

    // 添加自动排版按钮
    $("head").append("<style>.edui-btn-auto-format:before{content:\"fix\";}");
    (() => {
      const $btn = $.eduibutton({
        icon: "auto-format",
        click: function () {
          fnAutoFormat();
        },
        title: "自动排版",
      });
      $(".edui-btn-name-insertcode").after($btn);
    })();
  })();

  /* global showdown */


  class GM_editor {
    $def;
    defEditor = null;
    htmlContent = "";
    $md;
    mdEditor = null;
    mdContent = "";
    defOption = {
      init($md) { },
      autoSync: false,
      curType: "html",
    };
    option = {};
    constructor(option) {
      this.option = Object.assign({}, this.defOption, option);
      this.init();
      this.option.init(this.$md);
      this.getContent("html").covert2("md").syncContent("md");
    }
    init() {
      const _this = this;
      this.$def = this.option.$defContainer || $(".edui-container");
      this.$md = this.createMdEditor();
      // 编辑器操作对象
      this.defEditor = this.option.defEditor || UM.getEditor("message");
      this.mdEditor = {
        // 内容变化时触发
        addListener(type, fn) {
          if (type === "contentChange") {
            // _log(_this.$md);
            _this.$md.find("#message_md").on("input", fn);
          }
        },
        // 获取内容
        getContent() {
          return _this.$md.find("#message_md").val();
        },
        // 写入内容
        setContent(content) {
          _this.$md.find("#message_md").text(content);
        },
      };
      if (this.option.autoSync) {
        this.defEditor.addListener("contentChange", () => {
          if (_this.option.curType === "md") {
            return;
          }
          this.getContent("html").covert2("md").syncContent("md");
        });
        this.mdEditor.addListener("contentChange", () => {
          if (_this.option.curType === "html") {
            return;
          }
          this.getContent("md").covert2("html").syncContent("html");
        });
      }
    }
    // 读取内容
    getContent(type = "html") {
      if (type === "html") {
        this.htmlContent = this.defEditor.getContent();
      } else if (type === "md") {
        this.mdContent = this.mdEditor.getContent();
      }
      return this;
    }
    // 封装转换函数
    covert2(to = "md") {
      const converter = new showdown.Converter();
      if (to === "md") {
        this.mdContent = converter.makeMarkdown(this.htmlContent);
      } else if (to === "html") {
        this.htmlContent = converter.makeHtml(this.mdContent);
      }
      return this;
    }
    // 封装同步函数
    syncContent(to = "md") {
      if (to === "md") {
        this.mdEditor.setContent(this.mdContent);
      } else if (to === "html") {
        this.defEditor.setContent(this.htmlContent, false);
      }
    }
    // 自动设置 #message_md 的高度
    autoSetHeight() {
      const $mdText = this.$md.find("#message_md");
      // 自动设置高度
      $mdText.height(0);
      $mdText.height($mdText[0].scrollHeight + 4);
      // 判断并绑定 input 事件
      if ($mdText.data("bindInput")) {
        return;
      }
      $mdText.data("bindInput", true);
      $mdText.on("input", () => {
        this.autoSetHeight();
      });
    }
    // 切换编辑器
    switchEditor() {
      this.$def.toggle();
      this.$md.toggle();
      // 根据结果设置 curType
      this.option.curType = this.$def.css("display") === "none" ? "md" : "html";
      // 切换后自动设置高度
      this.autoSetHeight();
    }
    // 创建 markdown 编辑器
    createMdEditor() {
      return $(`
      <div class="mdui-container" style="display: none;">
        <div class="mdui-body">
          <textarea id="message_md" name="message_md" placeholder="markdown" class="mdui-text"></textarea>
        </div>
      </div>
    `);
    }
  }

  GM_addStyle(`
  .is-pulled-right {
    float: right;
  }
  .mdui-container {
    border: 1px solid #d4d4d4;
    padding: 5px 10px;
  }
  .mdui-container:focus-within {
    border: 1px solid #4caf50;
  }
  .mdui-text {
    border: none;
    width: 100%;
    min-height: 300px;
    height: auto;
  }
  .mdui-text:focus,
  .mdui-text:focus-visible {
    outline: none;
    box-shadow: none;
  }
`);

  const mainForBBS = () => {
    const gm_editor = new GM_editor({
      init($md) {
        $(".edui-container").after($md);
      },
      autoSync: true,
    });

    const btnSwitchEditor = `
  <button class="btn btn-primary" type="button" id="btnSwitchEditor">切换编辑器</button>
  `;

    // 判断是否有 name 为 fid 的 select
    if ($("select[name='fid']").length === 0) {
      // quotepid 后追加一行 .form-group
      $("input[name='quotepid']").after("<div class=\"form-group\"><span></span></div>");
    }


    // name 为 quotepid 的 input 下一行追加切换按钮
    $("input[name='quotepid'] + .form-group").addClass("d-flex justify-content-between").append(btnSwitchEditor);

    // 切换编辑器
    $("#btnSwitchEditor").click(() => {
      gm_editor.switchEditor();
    });
  };

  const mainForAPP = () => {
    const gm_editor = new GM_editor({
      init($md) {
        $("#editor_content").after($md);
        // const $mdText = $md.find("#message_md");
        // $mdText.height($("#editor_content").height() - 10);
        $(".mdui-body").css({
          paddingTop: ".3em",
        });
      },
      $defContainer: $("#editor_content"),
      defEditor: UE.getEditor("editor_content"),
      autoSync: true,
    });

    // # cheader 元素内部追加切换按钮
    $("#cheader").append(`
  <span class="is-pulled-right">「<a href="javascript:;" class="btn btn-primary" id="btnSwitchEditor" title="切换编辑器">切换编辑器</a>」</span>
`);


    // 切换编辑器
    $("#btnSwitchEditor").click(() => {
      gm_editor.switchEditor();
    });
  };

  (() => {
    // 判断是否在应用中心编辑页
    if (curHref.indexOf("edit.php") > -1) {
      // _log(UE)
      const editor_api = window.editor_api || unsafeWindow.editor_api;
      editor_api.editor.content.obj.ready(mainForAPP);
    }
    // 判断是否在论坛发帖、回帖页
    if ($("textarea#message").length > 0 && $("li.newpost").length === 0) {
      mainForBBS();
    }
  })();

})();