Hacker News 网站切换器

选择其他 HN 网站打开 Hacker News 链接

  1. // ==UserScript==
  2. // @name Hacker News Apps Switcher
  3. // @name:zh-CN Hacker News 网站切换器
  4. // @namespace https://www.pipecraft.net/
  5. // @homepage https://github.com/dev-topics-only/hacker-news-apps-switcher#readme
  6. // @supportURL https://github.com/dev-topics-only/hacker-news-apps-switcher/issues
  7. // @version 0.0.4
  8. // @description Open Hacker News links on the favorite apps
  9. // @description:zh-CN 选择其他 HN 网站打开 Hacker News 链接
  10. // @icon https://icons.pipecraft.net/favicons/64/news.ycombinator.com/favicon.ico
  11. // @author Pipecraft
  12. // @license MIT
  13. // @match https://*/*
  14. // @match http://*/*
  15. // @grant GM_setValue
  16. // @grant GM_getValue
  17. // @grant GM_addValueChangeListener
  18. // ==/UserScript==
  19. //
  20. ;(() => {
  21. "use strict"
  22. var doc = document
  23. var $ = (element, selectors) =>
  24. element && typeof element === "object"
  25. ? element.querySelector(selectors)
  26. : doc.querySelector(element)
  27. var $$ = (element, selectors) =>
  28. element && typeof element === "object"
  29. ? [...element.querySelectorAll(selectors)]
  30. : [...doc.querySelectorAll(element)]
  31. var createElement = (tagName, attributes) => {
  32. const element = doc.createElement(tagName)
  33. if (attributes) {
  34. for (const name in attributes) {
  35. if (Object.hasOwn(attributes, name)) {
  36. const value = attributes[name]
  37. if (name === "textContent") {
  38. element[name] = value
  39. } else if (name === "style") {
  40. setStyle(element, value)
  41. } else {
  42. setAttribute(element, name, value)
  43. }
  44. }
  45. }
  46. }
  47. return element
  48. }
  49. var addEventListener = (element, type, listener, options) => {
  50. if (!element) {
  51. return () => 0
  52. }
  53. if (typeof type === "object") {
  54. const removers = []
  55. for (const type1 in type) {
  56. if (Object.hasOwn(type, type1)) {
  57. element.addEventListener(type1, type[type1])
  58. removers.push(() => element.removeEventListener(type1, type[type1]))
  59. }
  60. }
  61. return () => {
  62. for (const remover of removers) remover()
  63. }
  64. }
  65. if (typeof type === "string" && typeof listener === "function") {
  66. element.addEventListener(type, listener, options)
  67. return () => {
  68. element.removeEventListener(type, listener, options)
  69. }
  70. }
  71. }
  72. var getAttribute = (element, name) =>
  73. element ? element.getAttribute(name) : null
  74. var setAttribute = (element, name, value) =>
  75. element ? element.setAttribute(name, value) : void 0
  76. var setStyle = (element, values, overwrite) => {
  77. if (!element) {
  78. return
  79. }
  80. const style = element.style
  81. if (typeof values === "string") {
  82. style.cssText = overwrite ? values : style.cssText + ";" + values
  83. return
  84. }
  85. if (overwrite) {
  86. style.cssText = ""
  87. }
  88. for (const key in values) {
  89. if (Object.hasOwn(values, key)) {
  90. style[key] = values[key].replace("!important", "")
  91. }
  92. }
  93. }
  94. var toStyleMap = (styleText) => {
  95. styleText = noStyleSpace(styleText)
  96. const map = {}
  97. const keyValues = styleText.split("}")
  98. for (const keyValue of keyValues) {
  99. const kv = keyValue.split("{")
  100. if (kv[0] && kv[1]) {
  101. map[kv[0]] = kv[1]
  102. }
  103. }
  104. return map
  105. }
  106. var noStyleSpace = (text) => text.replace(/\s*([^\w-!])\s*/gm, "$1")
  107. var createSetStyle = (styleText) => {
  108. const styleMap = toStyleMap(styleText)
  109. return (element, value, overwrite) => {
  110. if (typeof value === "object") {
  111. setStyle(element, value, overwrite)
  112. } else if (typeof value === "string") {
  113. const key = noStyleSpace(value)
  114. const value2 = styleMap[key]
  115. setStyle(element, value2 || value, overwrite)
  116. }
  117. }
  118. }
  119. if (typeof Object.hasOwn !== "function") {
  120. Object.hasOwn = (instance, prop) =>
  121. Object.prototype.hasOwnProperty.call(instance, prop)
  122. }
  123. var content_default =
  124. ".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;}"
  125. var apps = [
  126. "https://news.ycombinator.com/item?id=1234",
  127. "https://hn.svelte.dev/item/1234",
  128. "https://hn-redesign.vercel.app/items/1234",
  129. "https://insin.github.io/react-hn/#/story/1234",
  130. "https://lotusreader.netlify.app/item/1234",
  131. "https://hackernewsmobile.com/#/comments/1234",
  132. "https://hackerweb.app/#/item/1234",
  133. "https://hn.premii.com/#/comments/1234",
  134. "https://whnex.com/items/1234",
  135. "https://hack.ernews.info/comments-for/1234",
  136. "https://hacker-news.news/post/1234",
  137. "Close",
  138. ]
  139. var setStyle2 = createSetStyle(content_default)
  140. var tooltip = null
  141. function toSiteName(url) {
  142. return /\/([^/]+)\//.exec(url)[1]
  143. }
  144. var handler = (event) => {
  145. let target = event.target
  146. const tooltip2 = $(".hnas_tooltip")
  147. if (tooltip2) {
  148. while (target !== tooltip2 && target) {
  149. target = target.parentNode
  150. }
  151. if (target === tooltip2) {
  152. event.preventDefault()
  153. return
  154. }
  155. tooltip2.style.display = "none"
  156. }
  157. document.removeEventListener("click", handler)
  158. }
  159. function displayTooltip(id, wrapper) {
  160. if (!tooltip) {
  161. tooltip = createElement("div")
  162. setStyle2(tooltip, ".hnas_wrapper > div.hnas_tooltip")
  163. setAttribute(tooltip, "class", "hnas_tooltip")
  164. const list = createElement("div")
  165. setStyle2(list, ".hnas_wrapper > div.hnas_tooltip > div")
  166. for (const app of apps) {
  167. const link = createElement("a")
  168. setStyle2(link, ".hnas_wrapper > div.hnas_tooltip > div > a")
  169. link.dataset.hnas_link = "1"
  170. if (app === "Close") {
  171. link.innerHTML = "Close"
  172. setStyle2(link, "color: #217dfc; cursor: pointer;")
  173. } else {
  174. setAttribute(link, "href", app)
  175. setAttribute(link, "target", "_blank")
  176. link.innerHTML = toSiteName(app)
  177. }
  178. addEventListener(link, {
  179. click(event) {
  180. const tooltip2 = $(".hnas_tooltip")
  181. if (tooltip2) {
  182. tooltip2.style.display = "none"
  183. }
  184. document.removeEventListener("click", handler)
  185. if (link.innerHTML === "Close") {
  186. event.preventDefault()
  187. }
  188. },
  189. mouseover() {
  190. setStyle2(
  191. link,
  192. "text-decoration: underline; background-color: #f3f4f6; color: black !important;"
  193. )
  194. if (app === "Close") {
  195. setStyle2(link, "color: #217dfc; cursor: pointer;")
  196. }
  197. },
  198. mouseout() {
  199. setStyle2(link, ".hnas_wrapper > div.hnas_tooltip > div > a", true)
  200. if (app === "Close") {
  201. setStyle2(link, "color: #217dfc; cursor: pointer;")
  202. }
  203. },
  204. })
  205. list.append(link)
  206. }
  207. tooltip.append(list)
  208. }
  209. if (tooltip.style.display === "block" && tooltip.parentNode === wrapper) {
  210. return
  211. }
  212. for (const link of $$(tooltip, "div a")) {
  213. const href = getAttribute(link, "href")
  214. if (href) {
  215. setAttribute(link, "href", href.replace(/\d+/, id))
  216. }
  217. }
  218. const linkElement = wrapper.previousSibling
  219. const width = linkElement.offsetWidth
  220. const height = linkElement.offsetHeight
  221. const top = linkElement.offsetTop
  222. const left = linkElement.offsetLeft
  223. wrapper.append(tooltip)
  224. setStyle2(tooltip, {
  225. display: "block",
  226. top: String(top + height) + "px",
  227. left: String(left) + "px",
  228. width: String(width) + "px",
  229. })
  230. document.removeEventListener("click", handler)
  231. setTimeout(() => {
  232. addEventListener(document, "click", handler)
  233. }, 100)
  234. }
  235. function updateLinks() {
  236. const links = $$(
  237. 'a[href^="https://news.ycombinator.com/item?id="],a[href^="http://news.ycombinator.com/item?id="]'
  238. )
  239. for (const link of links) {
  240. if (link.dataset.hnas_binded || link.dataset.hnas_link) {
  241. continue
  242. }
  243. link.dataset.hnas_binded = "1"
  244. const wrapper = createElement("span")
  245. setAttribute(wrapper, "class", "hnas_wrapper")
  246. link.after(wrapper)
  247. const id = /id=(\d+)/.exec(getAttribute(link, "href"))[1]
  248. if (id) {
  249. addEventListener(link, "click", (event) => {
  250. event.preventDefault()
  251. displayTooltip(id, wrapper)
  252. })
  253. }
  254. }
  255. }
  256. function main() {
  257. if (!document.body) {
  258. return
  259. }
  260. setInterval(updateLinks, 1e3)
  261. updateLinks()
  262. }
  263. main()
  264. })()