竹白工具箱 🧰

为竹白(zhubai.love)创作者与阅读者的浏览器扩展、油猴脚本,包括详情页显示文章目录,详情页显示网站目录大纲(TOC),订阅者信息导出,Markdown 编辑器等功能。更多功能欢迎交流。

当前为 2023-04-27 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name                 Zhubai Toolbox 🧰
// @name:zh-CN           竹白工具箱 🧰
// @namespace            https://github.com/utags/zhubai-toolbox
// @homepage             https://github.com/utags/zhubai-toolbox#readme
// @supportURL           https://github.com/utags/zhubai-toolbox/issues
// @version              0.1.0
// @description          Tools for Zhubai creators and readers. 为竹白(zhubai.love)创作者与阅读者的浏览器扩展、油猴脚本,包括详情页显示文章目录,详情页显示网站目录大纲(TOC),订阅者信息导出,Markdown 编辑器等功能。更多功能欢迎交流。
// @description:zh-CN    为竹白(zhubai.love)创作者与阅读者的浏览器扩展、油猴脚本,包括详情页显示文章目录,详情页显示网站目录大纲(TOC),订阅者信息导出,Markdown 编辑器等功能。更多功能欢迎交流。
// @icon                 https://zhubai.love/favicon.png
// @author               Pipecraft
// @license              MIT
// @match                https://*.zhubai.love/*
// @connect              zhubai.love
// @grant                GM_addElement
// ==/UserScript==
//
//// Recent Updates
//// - 0.1.0 2023.04.27
////    - 详情页左侧栏显示文章目录功能
////
;(() => {
  "use strict"
  var doc = document
  var $ = (element, selectors) =>
    element && typeof element === "object"
      ? element.querySelector(selectors)
      : doc.querySelector(element)
  var createElement = (tagName, attributes) =>
    setAttributes(doc.createElement(tagName), attributes)
  var addEventListener = (element, type, listener, options) => {
    if (!element) {
      return
    }
    if (typeof type === "object") {
      for (const type1 in type) {
        if (Object.hasOwn(type, type1)) {
          element.addEventListener(type1, type[type1])
        }
      }
    } else if (typeof type === "string" && typeof listener === "function") {
      element.addEventListener(type, listener, options)
    }
  }
  var setAttribute = (element, name, value) =>
    element ? element.setAttribute(name, value) : void 0
  var setAttributes = (element, attributes) => {
    if (element && attributes) {
      for (const name in attributes) {
        if (Object.hasOwn(attributes, name)) {
          const value = attributes[name]
          if (value === void 0) {
            continue
          }
          if (/^(value|textContent|innerText|innerHTML)$/.test(name)) {
            element[name] = value
          } else if (name === "style") {
            setStyle(element, value, true)
          } else if (/on\w+/.test(name)) {
            const type = name.slice(2)
            addEventListener(element, type, value)
          } else {
            setAttribute(element, name, value)
          }
        }
      }
    }
    return element
  }
  var setStyle = (element, values, overwrite) => {
    if (!element) {
      return
    }
    const style = element.style
    if (typeof values === "string") {
      style.cssText = overwrite ? values : style.cssText + ";" + values
      return
    }
    if (overwrite) {
      style.cssText = ""
    }
    for (const key in values) {
      if (Object.hasOwn(values, key)) {
        style[key] = values[key].replace("!important", "")
      }
    }
  }
  if (typeof Object.hasOwn !== "function") {
    Object.hasOwn = (instance, prop) =>
      Object.prototype.hasOwnProperty.call(instance, prop)
  }
  var addElement = (parentNode, tagName, attributes) => {
    if (typeof parentNode === "string" || typeof tagName === "string") {
      const element = GM_addElement(parentNode, tagName, attributes)
      setAttributes(element, attributes)
      return element
    }
    setAttributes(tagName, attributes)
    parentNode.append(tagName)
    return tagName
  }
  var content_default =
    ".zbtb{width:100%;height:100%;position:fixed;top:72px;left:30px;padding:10px;background-color:#fff;font-weight:600;font-size:16px;color:#060e4b}.zbtb button{font-weight:600;font-size:16px;color:#060e4b}.zbtb button:disabled{opacity:40%}.zbtb textarea{width:90%;height:40%;padding:5px}#left_section{display:none}html[data-zbtb=posts-entry]{--section-width: 300px;margin-left:var(--section-width);margin-right:var(--section-width)}html[data-zbtb=posts-entry] nav{left:0}html[data-zbtb=posts-entry] #left_section{display:block;position:fixed;top:72px;left:0;padding:0 0 72px;height:100%;max-width:var(--section-width);overflow:hidden;box-sizing:border-box}html[data-zbtb=posts-entry] #left_section .container{padding:20px;height:100%;overflow:auto;box-sizing:border-box}html[data-zbtb=posts-entry] #left_section .item{display:block;text-decoration:none;margin-bottom:20px}html[data-zbtb=posts-entry] #left_section .title{margin:0;font-weight:600;font-size:14px;line-height:20px;color:#1a1a1a;word-wrap:break-word;overflow-wrap:break-word}@media(max-width: 1000px){html[data-zbtb=posts-entry]{--section-width: 200px}}"
  var listeners = {}
  var prefix = "extension."
  var getNamespacedKey = (key) => prefix + key
  var getUnnamespacedKey = (key) => key.slice(prefix.length)
  var _getValue = (key) => localStorage.getItem(key)
  var _setValue = (key, value) => {
    const oldValue = localStorage.getItem(key)
    if (oldValue === value) return
    localStorage.setItem(key, value)
    if (listeners[key]) {
      for (const func of listeners[key]) {
        func(getUnnamespacedKey(key), oldValue, value)
      }
    }
  }
  var _addValueChangeListener = (key, func) => {
    listeners[key] = listeners[key] || []
    listeners[key].push(func)
    return () => {
      if (listeners[key] && listeners[key].length > 0) {
        for (let i = listeners[key].length - 1; i >= 0; i--) {
          if (listeners[key][i] === func) {
            listeners[key].splice(i, 1)
          }
        }
      }
    }
  }
  var getValue = (key) => {
    const value = _getValue(getNamespacedKey(key))
    return value && value !== "undefined" ? JSON.parse(value) : void 0
  }
  var setValue = (key, value) => {
    if (value !== void 0)
      _setValue(getNamespacedKey(key), JSON.stringify(value))
  }
  var addValueChangeListener = (key, func) =>
    _addValueChangeListener(getNamespacedKey(key), func)
  var storageKey = "zhubai.posts"
  var nextUrl = ""
  var page = 0
  async function fetchPostList(url) {
    if (!url) {
      return
    }
    page++
    console.log(url, page)
    const response = await fetch(url, {})
    console.log("Fetched page", page)
    if (response.status !== 200) {
      console.warn(response)
      return
    }
    const data = await response.json()
    console.log(data)
    return data
  }
  async function updatePostList(url, updateAll = false) {
    var _a, _b, _c
    const data = await fetchPostList(url)
    if (!data) {
      return
    }
    const newList =
      (_a = data.data) == null
        ? void 0
        : _a.map((post) => ({
            id: post.id,
            title: post.title,
          }))
    nextUrl = (_b = data.pagination) == null ? void 0 : _b.next
    const hasNext = (_c = data.pagination) == null ? void 0 : _c.has_next
    console.log("nextUrl", nextUrl)
    if (newList) {
      const ids = new Set(newList.map((post) => post.id))
      const oldList = (await getValue(storageKey)) || []
      for (const post of oldList) {
        if (!ids.has(post.id)) {
          newList.push(post)
        }
      }
      newList.sort((a, b) => {
        return b.id - a.id
      })
      await setValue(storageKey, newList)
      if (page === 1) {
        await setValue("first_page_updated_at", Date.now())
      }
      if (hasNext === false) {
        await setValue("last_page_updated_at", Date.now())
      }
    }
    if (updateAll && nextUrl) {
      await updatePostList(nextUrl, updateAll)
    }
  }
  async function getAllPostListFromCache() {
    const posts = (await getValue(storageKey)) || []
    console.log(posts)
    return posts
  }
  async function init(token, valueChangeListener) {
    nextUrl = `https://${token}.zhubai.love/api/publications/${token}/posts?publication_id_type=token`
    page = 0
    const now = Date.now()
    const lastPageUpdatedAt = await getValue("last_page_updated_at")
    const firstPageUpdatedAt = await getValue("first_page_updated_at")
    console.log("lastPageUpdatedAt", lastPageUpdatedAt)
    console.log("firstPageUpdatedAt", firstPageUpdatedAt)
    if (
      !lastPageUpdatedAt ||
      now - lastPageUpdatedAt > 10 * 24 * 60 * 60 * 1e3
    ) {
      setTimeout(async () => updatePostList(nextUrl, true), 100)
    } else if (now - firstPageUpdatedAt > 10 * 60 * 1e3) {
      setTimeout(async () => updatePostList(nextUrl, false), 100)
    }
    addValueChangeListener(storageKey, valueChangeListener)
  }
  async function showPostList() {
    const section =
      $("#left_section") ||
      addElement(document.body, "section", {
        id: "left_section",
      })
    const container =
      $("#left_section .container") ||
      addElement(section, "div", {
        class: "container",
      })
    container.innerHTML = ""
    const postList = await getAllPostListFromCache()
    for (const post of postList) {
      const a = addElement(container, "a", {
        class: "item",
        href: "/posts/" + post.id,
      })
      addElement(a, "h2", {
        class: "title",
        textContent: post.title,
      })
    }
  }
  var config = {
    matches: ["https://*.zhubai.love/*"],
    world: "MAIN",
  }
  async function fetchZhubaiSubscriptions(
    page2,
    subscriberEmailSet,
    subscribers
  ) {
    page2 = page2 || 1
    const url = `https://zhubai.love/api/dashboard/subscriptions?limit=20&page=${page2}`
    const response = await fetch(url, {})
    console.log("Fetched page", page2)
    if (response.status !== 200) {
      console.warn(response)
      return
    }
    const data = await response.json()
    for (const subscriber of data.data) {
      subscribers.push(subscriber)
      if (subscriber.subscriber_email) {
        subscriberEmailSet.add(subscriber.subscriber_email)
      }
    }
    const limit = data.pagination.limit
    const total = data.pagination.total_count
    return { limit, total }
  }
  async function main() {
    const subscriberEmailSet = /* @__PURE__ */ new Set()
    const subscribers = []
    let page2 = 1
    let isRunning = false
    let paused = false
    const modal = addElement(document.body, "div", {
      class: "zbtb",
    })
    const toolbar = addElement(modal, "div", {
      style: "display: flex;",
    })
    const pauseButton = addElement(toolbar, "button", {
      textContent: "\u6682\u505C \u23F8\uFE0F",
      async onclick(event) {
        paused = !paused
        event.target.textContent = paused
          ? "\u7EE7\u7EED \u25B6\uFE0F"
          : "\u6682\u505C \u23F8\uFE0F"
        if (!paused) {
          await run()
        }
      },
    })
    const message = addElement(modal, "p", {
      textContent: "\u{1F680} \u6B63\u5728\u5BFC\u51FA\u6570\u636E ...",
    })
    addElement(modal, "p", {
      textContent: "\u{1F4EE} \u90AE\u7BB1\u8BA2\u9605\u5217\u8868",
    })
    const textarea = addElement(modal, "textarea")
    addElement(modal, "p", {
      textContent:
        "\u{1F4D6} \u8BE6\u7EC6\u8BA2\u9605\u7528\u6237\u6570\u636E\uFF0C\u5305\u542B\u90AE\u7BB1\u8BA2\u9605\u548C\u5FAE\u4FE1\u8BA2\u9605",
    })
    const textarea2 = addElement(modal, "textarea")
    const run = async () => {
      if (isRunning) {
        return
      }
      try {
        isRunning = true
        while (isRunning) {
          const result = await fetchZhubaiSubscriptions(
            page2,
            subscriberEmailSet,
            subscribers
          )
          const total = result.total
          const limit = result.limit
          message.textContent = `\u{1F697} \u6B63\u5728\u83B7\u53D6\u6570\u636E: ${page2} / ${Math.round(
            total / 20
          )}`
          if (subscribers.length > 0) {
            textarea.value = JSON.stringify([...subscriberEmailSet], null, 2)
            textarea2.value = JSON.stringify(subscribers, null, 2)
          }
          if (limit * page2 < total) {
            page2++
            if (paused) {
              message.textContent = `\u23F8\uFE0F \u6682\u505C\u4E2D\u3002\u5DF2\u83B7\u53D6\u6570\u636E: ${
                page2 - 1
              } / ${Math.round(total / 20)}`
              isRunning = false
            }
          } else {
            message.textContent = `\u{1F389} \u6570\u636E\u5BFC\u51FA\u5B8C\u6BD5\u3002\u8BF7\u590D\u5236\u4E0B\u9762\u7684\u6570\u636E\uFF0C\u505A\u597D\u5907\u4EFD\u3002`
            isRunning = false
            pauseButton.disabled = true
          }
        }
      } catch (error) {
        console.error(error)
        message.innerHTML = `\u6570\u636E\u5BFC\u51FA\u5931\u8D25\uFF0C\u6709\u95EE\u9898\u8BF7\u5728 <a href="https://github.com/utags/zhubai-toolbox/issues" target="_blank">GitHub</a> \u6216 <a href="https://greasyfork.org/scripts/463934" target="_blank">Greasy Fork</a> \u53CD\u9988\u3002`
      }
    }
    await run()
  }
  async function main2() {
    if (!$("#zbtb_style")) {
      addElement(document.head, "style", {
        id: "zbtb_style",
        textContent: content_default,
      })
    }
    const url = location.href
    if (/^https:\/\/\w+\.zhubai\.love\/posts\//.test(url)) {
      document.documentElement.dataset.zbtb = "posts-entry"
      ;(async () => {
        const token = location.hostname.split(".")[0]
        await init(token, showPostList)
        await showPostList()
      })()
    } else if (url.includes("https://zhubai.love/creator/subscribers")) {
      const intervalId = setInterval(() => {
        const button = document.querySelector("th:nth-of-type(6) button")
        if (button && button.textContent === "\u5BFC\u5165/\u5BFC\u51FA") {
          const newButton = createElement("button", {
            textContent: "\u5BFC\u5165/\u5BFC\u51FA",
            class: "Button_button__2Ce79 Button_defaultButton__1bbUl",
          })
          button.after(newButton)
          button.remove()
          newButton.addEventListener("click", async (event) => {
            await main()
            event.preventDefault()
          })
          clearInterval(intervalId)
        }
      }, 1e3)
    } else {
      document.documentElement.dataset.zbtb = "0"
    }
  }
  main2()
  addEventListener(window, "popstate", main2)
  var _pushState = history.pushState
  history.pushState = function (o, a, u) {
    _pushState.call(history, o, a, u)
    main2()
  }
})()