Any Hackernews Link

Check if current page has been posted to Hacker News

当前为 2025-01-24 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Any Hackernews Link
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.1.7
  5. // @description Check if current page has been posted to Hacker News
  6. // @author RoCry
  7. // @icon https://news.ycombinator.com/favicon.ico
  8. // @match https://*/*
  9. // @exclude https://news.ycombinator.com/*
  10. // @exclude https://hn.algolia.com/*
  11. // @exclude https://*.google.com/*
  12. // @exclude https://mail.yahoo.com/*
  13. // @exclude https://outlook.com/*
  14. // @exclude https://proton.me/*
  15. // @exclude https://localhost/*
  16. // @exclude https://127.0.0.1/*
  17. // @exclude https://192.168.*.*/*
  18. // @exclude https://10.*.*.*/*
  19. // @exclude https://172.16.*.*/*
  20. // @exclude https://web.whatsapp.com/*
  21. // @exclude https://*.facebook.com/messages/*
  22. // @exclude https://*.twitter.com/messages/*
  23. // @exclude https://*.linkedin.com/messaging/*
  24. // @grant GM_xmlhttpRequest
  25. // @connect hn.algolia.com
  26. // @grant GM_addStyle
  27. // @grant GM_getValue
  28. // @grant GM_setValue
  29. // @require https://update.greasyfork.org/scripts/524693/1525919/Any%20Hackernews%20Link%20Utils.js
  30. // @license MIT
  31. // ==/UserScript==
  32.  
  33. (function() {
  34. 'use strict';
  35.  
  36. /**
  37. * Constants
  38. */
  39. const POSITIONS = {
  40. BOTTOM_LEFT: { bottom: '20px', left: '20px', top: 'auto', right: 'auto' },
  41. BOTTOM_RIGHT: { bottom: '20px', right: '20px', top: 'auto', left: 'auto' },
  42. TOP_LEFT: { top: '20px', left: '20px', bottom: 'auto', right: 'auto' },
  43. TOP_RIGHT: { top: '20px', right: '20px', bottom: 'auto', left: 'auto' }
  44. };
  45.  
  46. /**
  47. * Styles
  48. */
  49. const STYLES = `
  50. @keyframes fadeIn {
  51. 0% { opacity: 0; transform: translateY(10px); }
  52. 100% { opacity: 1; transform: translateY(0); }
  53. }
  54. @keyframes pulse {
  55. 0% { opacity: 1; }
  56. 50% { opacity: 0.6; }
  57. 100% { opacity: 1; }
  58. }
  59. #hn-float {
  60. position: fixed;
  61. bottom: 20px;
  62. left: 20px;
  63. z-index: 9999;
  64. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  65. display: flex;
  66. align-items: center;
  67. gap: 12px;
  68. background: rgba(255, 255, 255, 0.98);
  69. padding: 8px 12px;
  70. border-radius: 12px;
  71. box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1), 0 0 0 1px rgba(0, 0, 0, 0.05);
  72. cursor: move;
  73. user-select: none;
  74. transition: all 0.2s ease;
  75. max-width: 50px;
  76. overflow: hidden;
  77. opacity: 0.95;
  78. height: 40px;
  79. backdrop-filter: blur(8px);
  80. -webkit-backdrop-filter: blur(8px);
  81. animation: fadeIn 0.3s ease forwards;
  82. will-change: transform, max-width, box-shadow;
  83. color: #111827;
  84. display: flex;
  85. align-items: center;
  86. height: 40px;
  87. box-sizing: border-box;
  88. }
  89. #hn-float:hover {
  90. max-width: 600px;
  91. opacity: 1;
  92. transform: translateY(-2px);
  93. box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12), 0 0 0 1px rgba(0, 0, 0, 0.05);
  94. }
  95. #hn-float .hn-icon {
  96. min-width: 24px;
  97. width: 24px;
  98. height: 24px;
  99. background: linear-gradient(135deg, #ff6600, #ff7f33);
  100. color: white;
  101. display: flex;
  102. align-items: center;
  103. justify-content: center;
  104. font-weight: bold;
  105. border-radius: 6px;
  106. flex-shrink: 0;
  107. position: relative;
  108. font-size: 13px;
  109. text-shadow: 0 1px 1px rgba(0, 0, 0, 0.1);
  110. transition: transform 0.2s ease;
  111. line-height: 1;
  112. padding-bottom: 1px;
  113. }
  114. #hn-float:hover .hn-icon {
  115. transform: scale(1.05);
  116. }
  117. #hn-float .hn-icon.not-found {
  118. background: #9ca3af;
  119. }
  120. #hn-float .hn-icon.found {
  121. background: linear-gradient(135deg, #ff6600, #ff7f33);
  122. }
  123. #hn-float .hn-icon.loading {
  124. background: #6b7280;
  125. animation: pulse 1.5s infinite;
  126. }
  127. #hn-float .hn-icon .badge {
  128. position: absolute;
  129. top: -4px;
  130. right: -4px;
  131. background: linear-gradient(135deg, #3b82f6, #2563eb);
  132. color: white;
  133. border-radius: 8px;
  134. min-width: 14px;
  135. height: 14px;
  136. font-size: 10px;
  137. display: flex;
  138. align-items: center;
  139. justify-content: center;
  140. padding: 0 3px;
  141. font-weight: 600;
  142. box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
  143. border: 1.5px solid white;
  144. }
  145. #hn-float .hn-info {
  146. white-space: nowrap;
  147. overflow: hidden;
  148. text-overflow: ellipsis;
  149. line-height: 1.4;
  150. font-size: 13px;
  151. opacity: 0;
  152. transition: opacity 0.2s ease;
  153. width: 0;
  154. flex: 0;
  155. }
  156. #hn-float:hover .hn-info {
  157. opacity: 1;
  158. width: auto;
  159. flex: 1;
  160. }
  161. #hn-float .hn-info a {
  162. color: inherit;
  163. font-weight: 500;
  164. text-decoration: none;
  165. }
  166. #hn-float .hn-info a:hover {
  167. text-decoration: underline;
  168. }
  169. #hn-float .hn-stats {
  170. color: #6b7280;
  171. font-size: 12px;
  172. margin-top: 2px;
  173. }
  174. @media (prefers-color-scheme: dark) {
  175. #hn-float {
  176. background: rgba(17, 24, 39, 0.95);
  177. color: #e5e7eb;
  178. box-shadow: 0 2px 12px rgba(0, 0, 0, 0.2), 0 0 0 1px rgba(255, 255, 255, 0.1);
  179. }
  180. #hn-float:hover {
  181. box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3), 0 0 0 1px rgba(255, 255, 255, 0.1);
  182. }
  183. #hn-float .hn-stats {
  184. color: #9ca3af;
  185. }
  186. #hn-float .hn-icon .badge {
  187. border-color: rgba(17, 24, 39, 0.95);
  188. }
  189. }
  190. `;
  191.  
  192. /**
  193. * UI Component
  194. */
  195. const UI = {
  196. /**
  197. * Create and append the floating element to the page
  198. * @returns {HTMLElement} - The created element
  199. */
  200. createFloatingElement() {
  201. const div = document.createElement('div');
  202. div.id = 'hn-float';
  203. // Create icon element
  204. const iconDiv = document.createElement('div');
  205. iconDiv.className = 'hn-icon loading';
  206. iconDiv.textContent = 'Y';
  207. // Create info element
  208. const infoDiv = document.createElement('div');
  209. infoDiv.className = 'hn-info';
  210. infoDiv.textContent = 'Checking HN...';
  211. // Append children
  212. div.appendChild(iconDiv);
  213. div.appendChild(infoDiv);
  214. document.body.appendChild(div);
  215.  
  216. // Apply saved position
  217. const savedPosition = GM_getValue('hnPosition', 'BOTTOM_LEFT');
  218. this.applyPosition(div, POSITIONS[savedPosition]);
  219.  
  220. // Add drag functionality
  221. this.addDragHandlers(div);
  222.  
  223. return div;
  224. },
  225.  
  226. /**
  227. * Update the floating element with HN data
  228. * @param {Object|null} data - HN post data or null if not found
  229. */
  230. applyPosition(element, position) {
  231. Object.assign(element.style, position);
  232. },
  233.  
  234. getClosestPosition(x, y) {
  235. const viewportWidth = window.innerWidth;
  236. const viewportHeight = window.innerHeight;
  237. const isTop = y < viewportHeight / 2;
  238. const isLeft = x < viewportWidth / 2;
  239.  
  240. if (isTop) {
  241. return isLeft ? 'TOP_LEFT' : 'TOP_RIGHT';
  242. } else {
  243. return isLeft ? 'BOTTOM_LEFT' : 'BOTTOM_RIGHT';
  244. }
  245. },
  246.  
  247. addDragHandlers(element) {
  248. let isDragging = false;
  249. let currentX;
  250. let currentY;
  251. let initialX;
  252. let initialY;
  253.  
  254. element.addEventListener('mousedown', e => {
  255. if (e.target.tagName === 'A') return; // Don't drag when clicking links
  256. isDragging = true;
  257. element.style.transition = 'none';
  258. initialX = e.clientX - element.offsetLeft;
  259. initialY = e.clientY - element.offsetTop;
  260. });
  261.  
  262. document.addEventListener('mousemove', e => {
  263. if (!isDragging) return;
  264.  
  265. e.preventDefault();
  266. currentX = e.clientX - initialX;
  267. currentY = e.clientY - initialY;
  268.  
  269. // Keep the element within viewport bounds
  270. currentX = Math.max(0, Math.min(currentX, window.innerWidth - element.offsetWidth));
  271. currentY = Math.max(0, Math.min(currentY, window.innerHeight - element.offsetHeight));
  272.  
  273. element.style.left = `${currentX}px`;
  274. element.style.top = `${currentY}px`;
  275. element.style.bottom = 'auto';
  276. element.style.right = 'auto';
  277. });
  278.  
  279. document.addEventListener('mouseup', e => {
  280. if (!isDragging) return;
  281. isDragging = false;
  282. element.style.transition = 'all 0.2s ease';
  283.  
  284. const position = this.getClosestPosition(currentX + element.offsetWidth / 2, currentY + element.offsetHeight / 2);
  285. this.applyPosition(element, POSITIONS[position]);
  286. // Save position
  287. GM_setValue('hnPosition', position);
  288. });
  289. },
  290.  
  291. updateFloatingElement(data) {
  292. const iconDiv = document.querySelector('#hn-float .hn-icon');
  293. const infoDiv = document.querySelector('#hn-float .hn-info');
  294. iconDiv.classList.remove('loading');
  295. if (!data) {
  296. iconDiv.classList.add('not-found');
  297. iconDiv.classList.remove('found');
  298. iconDiv.textContent = 'Y';
  299. infoDiv.textContent = 'Not found on HN';
  300. return;
  301. }
  302. iconDiv.classList.remove('not-found');
  303. iconDiv.classList.add('found');
  304. // Clear existing content
  305. iconDiv.textContent = 'Y';
  306. // Make icon clickable
  307. iconDiv.style.cursor = 'pointer';
  308. iconDiv.onclick = (e) => {
  309. e.stopPropagation();
  310. window.open(data.link, '_blank');
  311. };
  312. // Add badge if there are comments
  313. if (data.comments > 0) {
  314. const badge = document.createElement('span');
  315. badge.className = 'badge';
  316. badge.textContent = data.comments > 999 ? '999+' : data.comments.toString();
  317. iconDiv.appendChild(badge);
  318. }
  319. // Clear and rebuild info content
  320. infoDiv.textContent = '';
  321. const titleDiv = document.createElement('div');
  322. const titleLink = document.createElement('a');
  323. titleLink.href = data.link;
  324. titleLink.target = '_blank';
  325. titleLink.textContent = data.title;
  326. titleDiv.appendChild(titleLink);
  327. const statsDiv = document.createElement('div');
  328. statsDiv.className = 'hn-stats';
  329. statsDiv.textContent = `${data.points} points | ${data.comments} comments | ${data.posted}`;
  330. infoDiv.appendChild(titleDiv);
  331. infoDiv.appendChild(statsDiv);
  332. }
  333. };
  334.  
  335. /**
  336. * Initialize the script
  337. */
  338. function init() {
  339. // Skip if we're in an iframe
  340. if (window.top !== window.self) {
  341. console.log('📌 Skipping execution in iframe');
  342. return;
  343. }
  344.  
  345. // Skip if document is hidden (like background tabs or invisible frames)
  346. if (document.hidden) {
  347. console.log('📌 Skipping execution in hidden document');
  348. // Add listener for when the tab becomes visible
  349. document.addEventListener('visibilitychange', function onVisible() {
  350. if (!document.hidden) {
  351. init();
  352. document.removeEventListener('visibilitychange', onVisible);
  353. }
  354. });
  355. return;
  356. }
  357.  
  358. const currentUrl = window.location.href;
  359. // Check if the floating element already exists
  360. if (document.getElementById('hn-float')) {
  361. console.log('📌 HN float already exists, skipping');
  362. return;
  363. }
  364.  
  365. if (URLUtils.shouldIgnoreUrl(currentUrl)) {
  366. console.log('🚫 Ignored URL:', currentUrl);
  367. return;
  368. }
  369.  
  370. // Check if content is primarily English
  371. if (!ContentUtils.isEnglishContent()) {
  372. console.log('🈂️ Non-English content detected, skipping');
  373. return;
  374. }
  375.  
  376. GM_addStyle(STYLES);
  377. const normalizedUrl = URLUtils.normalizeUrl(currentUrl);
  378. console.log('🔗 Normalized URL:', normalizedUrl);
  379. UI.createFloatingElement();
  380. HNApi.checkHackerNews(normalizedUrl, UI.updateFloatingElement);
  381. }
  382.  
  383. // Start the script
  384. init();
  385. })();