Hacker News Apps Switcher

Open Hacker News links on the favorite apps

// ==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()
})()