Hacker News 网站切换器

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

目前为 2023-03-29 提交的版本,查看 最新版本

  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.2
  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. ;(() => {
  22. "use strict"
  23. var doc = document
  24. var toCamelCase = function (text) {
  25. return text.replace(/^([A-Z])|[\s-_](\w)/g, function (match, p1, p2) {
  26. if (p2) return p2.toUpperCase()
  27. return p1.toLowerCase()
  28. })
  29. }
  30. var $ = (element, selectors) =>
  31. typeof element === "object"
  32. ? element.querySelector(selectors)
  33. : doc.querySelector(element)
  34. var $$ = (element, selectors) =>
  35. typeof element === "object"
  36. ? [...element.querySelectorAll(selectors)]
  37. : [...doc.querySelectorAll(element)]
  38. var createElement = doc.createElement.bind(doc)
  39. var addEventListener = (element, type, listener) => {
  40. if (typeof type === "object") {
  41. for (const type1 in type) {
  42. if (Object.hasOwn(type, type1)) {
  43. element.addEventListener(type1, type[type1])
  44. }
  45. }
  46. } else if (typeof type === "string" && typeof listener === "function") {
  47. element.addEventListener(type, listener)
  48. }
  49. }
  50. var getAttribute = (element, name) => element.getAttribute(name)
  51. var setAttribute = (element, name, value) => element.setAttribute(name, value)
  52. var setStyle = (element, values, overwrite) => {
  53. const style = element.style
  54. if (overwrite) {
  55. if (typeof values === "string") {
  56. style.cssText = values
  57. return
  58. }
  59. style.cssText = ""
  60. }
  61. if (typeof values === "string") {
  62. values = toStyleKeyValues(values)
  63. }
  64. for (const key in values) {
  65. if (Object.hasOwn(values, key)) {
  66. style[key] = values[key].replace("!important", "")
  67. }
  68. }
  69. }
  70. var toStyleKeyValues = (styleText) => {
  71. const result = {}
  72. const keyValues = styleText.split(/\s*;\s*/)
  73. for (const keyValue of keyValues) {
  74. const kv = keyValue.split(/\s*:\s*/)
  75. const key = toCamelCase(kv[0])
  76. if (key) {
  77. result[key] = kv[1]
  78. }
  79. }
  80. return result
  81. }
  82. var toStyleMap = (styleText) => {
  83. styleText = noStyleSpace(styleText)
  84. const map = {}
  85. const keyValues = styleText.split("}")
  86. for (const keyValue of keyValues) {
  87. const kv = keyValue.split("{")
  88. if (kv[0] && kv[1]) {
  89. map[kv[0]] = kv[1]
  90. }
  91. }
  92. return map
  93. }
  94. var noStyleSpace = (text) => text.replace(/\s*([^\w-!])\s*/gm, "$1")
  95. var createSetStyle = (styleText) => {
  96. const styleMap = toStyleMap(styleText)
  97. return (element, value, overwrite) => {
  98. if (typeof value === "object") {
  99. setStyle(element, value, overwrite)
  100. } else if (typeof value === "string") {
  101. const key = noStyleSpace(value)
  102. const value2 = styleMap[key]
  103. setStyle(element, value2 || value, overwrite)
  104. }
  105. }
  106. }
  107. if (typeof Object.hasOwn !== "function") {
  108. Object.hasOwn = (instance, prop) =>
  109. Object.prototype.hasOwnProperty.call(instance, prop)
  110. }
  111.  
  112. var style_default =
  113. ".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 > ul { list-style: none; padding: 0; margin: 0;}.hnas_wrapper > div.hnas_tooltip > ul > li { display: block;}.hnas_wrapper > div.hnas_tooltip > ul > li > a { text-decoration: none; color: black; padding: 5px; border-radius: 5px; display: flex; font-size: 1rem; line-height: 1.25rem;}.hnas_wrapper > div.hnas_tooltip > ul > li > a:hover { text-decoration: underline; color: black !important; background-color: #f3f4f6;}"
  114.  
  115. var addValueChangeListener = GM_addValueChangeListener
  116.  
  117. var apps = [
  118. "https://news.ycombinator.com/item?id=1234",
  119. "https://hn.svelte.dev/item/1234",
  120. // https://github.com/rocktimsaikia/hackernews-redesign
  121. "https://hn-redesign.vercel.app/items/1234",
  122. "https://insin.github.io/react-hn/#/story/1234",
  123. "https://lotusreader.netlify.app/item/1234",
  124. "https://hackernewsmobile.com/#/comments/1234",
  125. "https://hackerweb.app/#/item/1234",
  126. "https://hn.premii.com/#/comments/1234",
  127. "https://whnex.com/items/1234",
  128. "https://hack.ernews.info/comments-for/1234",
  129. "https://hacker-news.news/post/1234",
  130. "Close",
  131. ]
  132. var setStyle2 = createSetStyle(style_default)
  133. var tooltip = null
  134. function toSiteName(url) {
  135. return /\/([^/]+)\//.exec(url)[1]
  136. }
  137. var handler = (event) => {
  138. let target = event.target
  139. const tooltip2 = $(".hnas_tooltip")
  140. if (tooltip2) {
  141. while (target !== tooltip2 && target) {
  142. target = target.parentNode
  143. }
  144. if (target === tooltip2) {
  145. event.preventDefault()
  146. return
  147. }
  148. tooltip2.style.display = "none"
  149. }
  150. document.removeEventListener("click", handler)
  151. }
  152. function displayTooltip(id, wrapper) {
  153. if (!tooltip) {
  154. tooltip = createElement("div")
  155. setStyle2(tooltip, ".hnas_wrapper > div.hnas_tooltip")
  156. setAttribute(tooltip, "class", "hnas_tooltip")
  157. const ul = createElement("ul")
  158. setStyle2(ul, ".hnas_wrapper > div.hnas_tooltip > ul")
  159. for (const app of apps) {
  160. const li = createElement("li")
  161. setStyle2(li, "display: block;")
  162. const link = createElement("a")
  163. setStyle2(link, ".hnas_wrapper > div.hnas_tooltip > ul > li > a")
  164. link.dataset.hnas_link = "1"
  165. if (app === "Close") {
  166. link.innerHTML = "Close"
  167. setStyle2(link, "color: #217dfc; cursor: pointer;")
  168. } else {
  169. setAttribute(link, "href", app)
  170. setAttribute(link, "target", "_blank")
  171. link.innerHTML = toSiteName(app)
  172. }
  173. addEventListener(link, {
  174. click(event) {
  175. const tooltip2 = $(".hnas_tooltip")
  176. if (tooltip2) {
  177. tooltip2.style.display = "none"
  178. }
  179. document.removeEventListener("click", handler)
  180. if (link.innerHTML === "Close") {
  181. event.preventDefault()
  182. }
  183. },
  184. mouseover() {
  185. setStyle2(
  186. link,
  187. "text-decoration: underline; background-color: #f3f4f6; color: black !important;"
  188. )
  189. if (app === "Close") {
  190. setStyle2(link, "color: #217dfc; cursor: pointer;")
  191. }
  192. },
  193. mouseout() {
  194. setStyle2(
  195. link,
  196. ".hnas_wrapper > div.hnas_tooltip > ul > li > a",
  197. true
  198. )
  199. if (app === "Close") {
  200. setStyle2(link, "color: #217dfc; cursor: pointer;")
  201. }
  202. },
  203. })
  204. li.append(link)
  205. ul.append(li)
  206. }
  207. tooltip.append(ul)
  208. }
  209. if (tooltip.style.display === "block" && tooltip.parentNode === wrapper) {
  210. return
  211. }
  212. for (const link of $$(tooltip, "ul li 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: top + height + "px",
  227. left: left + "px",
  228. width: 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. })()