Hacker News Apps Switcher

Open Hacker News links on the favorite apps

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name                 Hacker News Apps Switcher
// @name:zh-CN           Hacker News 网站切换器
// @namespace            https://www.pipecraft.net/
// @homepage             https://github.com/dev-topics-only/hacker-news-apps-switcher#readme
// @supportURL           https://github.com/dev-topics-only/hacker-news-apps-switcher/issues
// @version              0.0.4
// @description          Open Hacker News links on the favorite apps
// @description:zh-CN    选择其他 HN 网站打开 Hacker News 链接
// @icon                 https://icons.pipecraft.net/favicons/64/news.ycombinator.com/favicon.ico
// @author               Pipecraft
// @license              MIT
// @match                https://*/*
// @match                http://*/*
// @grant                GM_setValue
// @grant                GM_getValue
// @grant                GM_addValueChangeListener
// ==/UserScript==
//
;(() => {
  "use strict"
  var doc = document
  var $ = (element, selectors) =>
    element && typeof element === "object"
      ? element.querySelector(selectors)
      : doc.querySelector(element)
  var $$ = (element, selectors) =>
    element && typeof element === "object"
      ? [...element.querySelectorAll(selectors)]
      : [...doc.querySelectorAll(element)]
  var createElement = (tagName, attributes) => {
    const element = doc.createElement(tagName)
    if (attributes) {
      for (const name in attributes) {
        if (Object.hasOwn(attributes, name)) {
          const value = attributes[name]
          if (name === "textContent") {
            element[name] = value
          } else if (name === "style") {
            setStyle(element, value)
          } else {
            setAttribute(element, name, value)
          }
        }
      }
    }
    return element
  }
  var addEventListener = (element, type, listener, options) => {
    if (!element) {
      return () => 0
    }
    if (typeof type === "object") {
      const removers = []
      for (const type1 in type) {
        if (Object.hasOwn(type, type1)) {
          element.addEventListener(type1, type[type1])
          removers.push(() => element.removeEventListener(type1, type[type1]))
        }
      }
      return () => {
        for (const remover of removers) remover()
      }
    }
    if (typeof type === "string" && typeof listener === "function") {
      element.addEventListener(type, listener, options)
      return () => {
        element.removeEventListener(type, listener, options)
      }
    }
  }
  var getAttribute = (element, name) =>
    element ? element.getAttribute(name) : null
  var setAttribute = (element, name, value) =>
    element ? element.setAttribute(name, value) : void 0
  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", "")
      }
    }
  }
  var toStyleMap = (styleText) => {
    styleText = noStyleSpace(styleText)
    const map = {}
    const keyValues = styleText.split("}")
    for (const keyValue of keyValues) {
      const kv = keyValue.split("{")
      if (kv[0] && kv[1]) {
        map[kv[0]] = kv[1]
      }
    }
    return map
  }
  var noStyleSpace = (text) => text.replace(/\s*([^\w-!])\s*/gm, "$1")
  var createSetStyle = (styleText) => {
    const styleMap = toStyleMap(styleText)
    return (element, value, overwrite) => {
      if (typeof value === "object") {
        setStyle(element, value, overwrite)
      } else if (typeof value === "string") {
        const key = noStyleSpace(value)
        const value2 = styleMap[key]
        setStyle(element, value2 || value, overwrite)
      }
    }
  }
  if (typeof Object.hasOwn !== "function") {
    Object.hasOwn = (instance, prop) =>
      Object.prototype.hasOwnProperty.call(instance, prop)
  }
  var content_default =
    ".hnas_wrapper {  display: inline-block;}.hnas_wrapper > div.hnas_tooltip {  min-width: 250px;  display: none;  position: absolute;  top: 0px;  left: 0px;  box-sizing: border-box;  padding: 10px 15px;  background-color: white;  z-index: 100000;  border-radius: 5px;  -webkit-box-shadow: 0px 10px 39px 10px rgba(62, 66, 66, 0.22);  -moz-box-shadow: 0px 10px 39px 10px rgba(62, 66, 66, 0.22);  box-shadow: 0px 10px 39px 10px rgba(62, 66, 66, 0.22);}.hnas_wrapper > div.hnas_tooltip > div {  display: flex;  flex-direction: column;}.hnas_wrapper > div.hnas_tooltip > div > a {  text-decoration: none;  color: black;  padding: 5px;  border-radius: 5px;  border: none;  font-weight: normal;  font-size: 1rem;  line-height: 1.25rem;}.hnas_wrapper > div.hnas_tooltip > div > a:hover {  text-decoration: underline;  color: black !important;  background-color: #f3f4f6;}"
  var apps = [
    "https://news.ycombinator.com/item?id=1234",
    "https://hn.svelte.dev/item/1234",
    "https://hn-redesign.vercel.app/items/1234",
    "https://insin.github.io/react-hn/#/story/1234",
    "https://lotusreader.netlify.app/item/1234",
    "https://hackernewsmobile.com/#/comments/1234",
    "https://hackerweb.app/#/item/1234",
    "https://hn.premii.com/#/comments/1234",
    "https://whnex.com/items/1234",
    "https://hack.ernews.info/comments-for/1234",
    "https://hacker-news.news/post/1234",
    "Close",
  ]
  var setStyle2 = createSetStyle(content_default)
  var tooltip = null
  function toSiteName(url) {
    return /\/([^/]+)\//.exec(url)[1]
  }
  var handler = (event) => {
    let target = event.target
    const tooltip2 = $(".hnas_tooltip")
    if (tooltip2) {
      while (target !== tooltip2 && target) {
        target = target.parentNode
      }
      if (target === tooltip2) {
        event.preventDefault()
        return
      }
      tooltip2.style.display = "none"
    }
    document.removeEventListener("click", handler)
  }
  function displayTooltip(id, wrapper) {
    if (!tooltip) {
      tooltip = createElement("div")
      setStyle2(tooltip, ".hnas_wrapper > div.hnas_tooltip")
      setAttribute(tooltip, "class", "hnas_tooltip")
      const list = createElement("div")
      setStyle2(list, ".hnas_wrapper > div.hnas_tooltip > div")
      for (const app of apps) {
        const link = createElement("a")
        setStyle2(link, ".hnas_wrapper > div.hnas_tooltip > div > a")
        link.dataset.hnas_link = "1"
        if (app === "Close") {
          link.innerHTML = "Close"
          setStyle2(link, "color: #217dfc; cursor: pointer;")
        } else {
          setAttribute(link, "href", app)
          setAttribute(link, "target", "_blank")
          link.innerHTML = toSiteName(app)
        }
        addEventListener(link, {
          click(event) {
            const tooltip2 = $(".hnas_tooltip")
            if (tooltip2) {
              tooltip2.style.display = "none"
            }
            document.removeEventListener("click", handler)
            if (link.innerHTML === "Close") {
              event.preventDefault()
            }
          },
          mouseover() {
            setStyle2(
              link,
              "text-decoration: underline; background-color: #f3f4f6; color: black !important;"
            )
            if (app === "Close") {
              setStyle2(link, "color: #217dfc; cursor: pointer;")
            }
          },
          mouseout() {
            setStyle2(link, ".hnas_wrapper > div.hnas_tooltip > div > a", true)
            if (app === "Close") {
              setStyle2(link, "color: #217dfc; cursor: pointer;")
            }
          },
        })
        list.append(link)
      }
      tooltip.append(list)
    }
    if (tooltip.style.display === "block" && tooltip.parentNode === wrapper) {
      return
    }
    for (const link of $$(tooltip, "div a")) {
      const href = getAttribute(link, "href")
      if (href) {
        setAttribute(link, "href", href.replace(/\d+/, id))
      }
    }
    const linkElement = wrapper.previousSibling
    const width = linkElement.offsetWidth
    const height = linkElement.offsetHeight
    const top = linkElement.offsetTop
    const left = linkElement.offsetLeft
    wrapper.append(tooltip)
    setStyle2(tooltip, {
      display: "block",
      top: String(top + height) + "px",
      left: String(left) + "px",
      width: String(width) + "px",
    })
    document.removeEventListener("click", handler)
    setTimeout(() => {
      addEventListener(document, "click", handler)
    }, 100)
  }
  function updateLinks() {
    const links = $$(
      'a[href^="https://news.ycombinator.com/item?id="],a[href^="http://news.ycombinator.com/item?id="]'
    )
    for (const link of links) {
      if (link.dataset.hnas_binded || link.dataset.hnas_link) {
        continue
      }
      link.dataset.hnas_binded = "1"
      const wrapper = createElement("span")
      setAttribute(wrapper, "class", "hnas_wrapper")
      link.after(wrapper)
      const id = /id=(\d+)/.exec(getAttribute(link, "href"))[1]
      if (id) {
        addEventListener(link, "click", (event) => {
          event.preventDefault()
          displayTooltip(id, wrapper)
        })
      }
    }
  }
  function main() {
    if (!document.body) {
      return
    }
    setInterval(updateLinks, 1e3)
    updateLinks()
  }
  main()
})()