Web Page Accelerator

Automatically accelerates hyperlinks on web pages to improve loading speed. Integrates the latest instant.page v5.2.0 features with multi-language support (default: English) and removes store link redirection functionality.

  1. // ==UserScript==
  2. // @name Web Page Accelerator
  3. // @namespace instant.page
  4. // @version v1.0.5.2.0
  5. // @author OB_BUFF
  6. // @description Automatically accelerates hyperlinks on web pages to improve loading speed. Integrates the latest instant.page v5.2.0 features with multi-language support (default: English) and removes store link redirection functionality.
  7. // @license GPL-v3
  8. // @require https://registry.npmmirror.com/sweetalert2/10.16.6/files/dist/sweetalert2.min.js
  9. // @resource swalStyle https://registry.npmmirror.com/sweetalert2/10.16.6/files/dist/sweetalert2.min.css
  10. // @match *://*/*
  11. // @noframes
  12. // @run-at document-idle
  13. // @grant GM_openInTab
  14. // @grant GM_setValue
  15. // @grant GM_getValue
  16. // @grant GM_registerMenuCommand
  17. // @grant GM_getResourceText
  18. // @icon data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMjggMTI4Ij48cGF0aCBkPSJNMCA3OWMwLTM1LjQgMjguNS02NCA2My45LTY0LjFzNjQuMSAyOC42IDY0LjEgNjRjMCA5LjQtMi4xIDE4LjQtNS43IDI2LjUtMSAyLjMtMi4zIDQuNi0zLjYgNi43LS40LjYtMSAxLTEuNyAxSDExYy0uNyAwLTEuMy0uNC0xLjctMS0xLjMtMi4yLTIuNS00LjQtMy42LTYuN0MyLjEgOTcuNCAwIDg4LjQgMCA3OXptMjQuNC0zOS43Yy01LjIgNS4xLTkuMiAxMS4xLTEyIDE3LjgtMyA2LjktNC41IDE0LjItNC41IDIxLjhhNTUuODYgNTUuODYgMCAwIDAgNC40IDIxLjhjLjcgMS42IDEuNCAzLjIgMi4yIDQuN2g5OC44Yy44LTEuNSAxLjYtMy4xIDIuMi00LjdhNTUuODYgNTUuODYgMCAwIDAgNC40LTIxLjggNTUuODYgNTUuODYgMCAwIDAtNC40LTIxLjhjLTIuOC02LjctNi45LTEyLjctMTItMTcuOC01LjEtNS4yLTExLjEtOS4yLTE3LjgtMTJhNTUuODYgNTUuODYgMCAwIDAtMjEuOC00LjQgNTUuODYgNTUuODYgMCAwIDAtMjEuOCA0LjRjLTYuNiAyLjgtMTIuNiA2LjgtMTcuNyAxMnoiIGZpbGw9IiM0NDQiLz48cGF0aCBkPSJNMTIuNCA1Ny4xYzIuOC02LjcgNi45LTEyLjcgMTItMTcuOCA1LjEtNS4yIDExLjEtOS4yIDE3LjgtMTJBNTUuODYgNTUuODYgMCAwIDEgNjQgMjIuOWE1NS44NiA1NS44NiAwIDAgMSAyMS44IDQuNGM2LjcgMi44IDEyLjcgNi45IDE3LjggMTIgNS4yIDUuMSA5LjIgMTEuMSAxMiAxNy44YTU1Ljg2IDU1Ljg2IDAgMCAxIDQuNCAyMS44IDU1Ljg2IDU1Ljg2IDAgMCAxLTQuNCAyMS44Yy0uNyAxLjYtMS40IDMuMi0yLjIgNC43SDE0LjZjLS44LTEuNS0xLjYtMy4xLTIuMi00LjdBNTUuODYgNTUuODYgMCAwIDEgOCA3OC45Yy0uMS03LjYgMS40LTE0LjkgNC40LTIxLjh6IiBmaWxsPSIjNjQ5OTUwIi8+PHBhdGggZD0iTTc3LjUgNjAuOUM2OCA4MS4yIDY0LjkgODQuNiA2NC42IDg1Yy0xLjUgMS41LTMuNSAyLjMtNS42IDIuM3MtNC4xLS44LTUuNi0yLjNhNy45MSA3LjkxIDAgMCAxIDAtMTEuMmMuMy0uNCAzLjgtMy40IDI0LjEtMTIuOXptMC04Yy0xLjEgMC0yLjMuMi0zLjQuOEM2My4yIDU4LjggNTEgNjQuOSA0Ny44IDY4LjFjLTYuMiA2LjItNi4yIDE2LjMgMCAyMi41IDMuMSAzLjEgNy4yIDQuNyAxMS4yIDQuN3M4LjEtMS42IDExLjItNC43YzMuMi0zLjIgOS4zLTE1LjQgMTQuNC0yNi4zIDIuNi01LjYtMS43LTExLjQtNy4xLTExLjR6TTYzLjkgMjkuOGMtMjcuMiAwLTQ5LjUgMjIuNi00OS4xIDQ5LjggMCAzLjYuNSA3LjIgMS4zIDEwLjYuNCAxLjggMiAzLjEgMy45IDMuMSAyLjYgMCA0LjQtMi40IDMuOS00LjktLjctMy0xLjEtNi4yLTEuMS05LjNBNDIuMDQgNDIuMDQgMCAwIDEgMjYgNjNjMi01IDUtOS40IDguOC0xMy4yUzQzIDQzLjEgNDcuOSA0MWE0Mi4wNCA0Mi4wNCAwIDAgMSAzMi4yIDBjNC45IDIuMSA5LjMgNS4xIDEzLjEgOC45Qzk3IDUzLjYgOTkuOSA1OCAxMDIgNjNhNDIuMDQgNDIuMDQgMCAwIDEgMy4yIDE2LjFjMCAzLjItLjQgNi4zLTEuMSA5LjMtLjYgMi41IDEuMyA0LjkgMy45IDQuOSAxLjggMCAzLjUtMS4zIDMuOS0zLjEuOC0zLjYgMS4zLTcuMyAxLjMtMTEuMSAwLTI3LjMtMjIuMS00OS4zLTQ5LjMtNDkuM3oiIGZpbGw9IiM0NDQiLz48L3N2Zz4=
  19. // ==/UserScript==
  20.  
  21. /* -------------------------------
  22. Multi-language support (default: English)
  23. You can extend the translations object to support more languages.
  24. ---------------------------------- */
  25. const translations = {
  26. en: {
  27. acceleratedCount: "Accelerated: ",
  28. times: " times",
  29. resetPrompt: "Are you sure you want to reset the acceleration count?",
  30. confirm: "OK",
  31. cancel: "Cancel",
  32. settingsTitle: "Accelerator Settings",
  33. accelerateExternal: "Accelerate external links",
  34. accelerateParams: "Accelerate links with parameters",
  35. openInSameTab: "Open links in the same tab",
  36. animationEffect: "Animation effect",
  37. prefetchDelay: "Prefetch delay (ms)",
  38. excludeURLs: "Exclude the following URLs (one per line)",
  39. excludeKeywords: "Exclude the following keywords (one per line)",
  40. save: "Save",
  41. usageFooter: "Click here to view the usage instructions. This assistant is free and open-source."
  42. }
  43. };
  44. const lang = 'en'; // Default language
  45.  
  46. // -------------------------------
  47. // Utility functions
  48. // -------------------------------
  49. let util = {
  50. getValue(name) {
  51. return GM_getValue(name);
  52. },
  53. setValue(name, value) {
  54. GM_setValue(name, value);
  55. },
  56. // Check if the given string (after removing '-' and '_') contains any of the keywords (case-insensitive)
  57. include(str, arr) {
  58. str = str.replace(/[-_]/ig, '');
  59. for (let i = 0, l = arr.length; i < l; i++) {
  60. let val = arr[i].trim();
  61. if (val !== '' && str.toLowerCase().indexOf(val.toLowerCase()) > -1) {
  62. return true;
  63. }
  64. }
  65. return false;
  66. },
  67. addStyle(id, tag, css) {
  68. tag = tag || 'style';
  69. let doc = document, styleDom = doc.getElementById(id);
  70. if (styleDom) return;
  71. let style = doc.createElement(tag);
  72. style.rel = 'stylesheet';
  73. style.id = id;
  74. tag === 'style' ? style.innerHTML = css : style.href = css;
  75. doc.head.appendChild(style);
  76. },
  77. // Common regex patterns
  78. reg: {
  79. chrome: /^https?:\/\/chrome\.google\.com\/webstore\/.+?\/([a-z]{32})(?=[\/#?]|$)/,
  80. chromeNew: /^https?:\/\/chromewebstore\.google\.com\/.+?\/([a-z]{32})(?=[\/#?]|$)/,
  81. edge: /^https?:\/\/microsoftedge\.microsoft\.com\/addons\/.+?\/([a-z]{32})(?=[\/#?]|$)/,
  82. firefox: /^https?:\/\/(reviewers\.)?(addons\.mozilla\.org|addons(?:-dev)?\.allizom\.org)\/.*?(?:addon|review)\/([^/<>"'?#]+)/,
  83. microsoft: /^https?:\/\/(?:apps|www)\.microsoft\.com\/(?:store|p)\/.+?\/([a-zA-Z\d]{10,})(?=[\/#?]|$)/,
  84. }
  85. };
  86.  
  87. // -------------------------------
  88. // Main logic
  89. // -------------------------------
  90. let main = {
  91. // Initialize configuration values (store values using GM storage)
  92. initValue() {
  93. // Note: The "store link" redirection option has been removed.
  94. let value = [{
  95. name: 'setting_success_times',
  96. value: 0
  97. }, {
  98. name: 'allow_external_links',
  99. value: true
  100. }, {
  101. name: 'allow_query_links',
  102. value: true
  103. }, {
  104. name: 'enable_target_self',
  105. value: false
  106. }, {
  107. name: 'enable_animation',
  108. value: false
  109. }, {
  110. name: 'delay_on_hover',
  111. value: 65
  112. }, {
  113. name: 'exclude_list',
  114. value: ''
  115. }, {
  116. name: 'exclude_keyword',
  117. value: 'login\nlogout\nregister\nsignin\nsignup\nsignout\npay\ncreate\nedit\ndownload\ndel\nreset\nsubmit\ndoubleclick\ngoogleads\nexit'
  118. }];
  119. value.forEach((v) => {
  120. if (util.getValue(v.name) === undefined) {
  121. util.setValue(v.name, v.value);
  122. }
  123. });
  124. },
  125.  
  126. // Register menu commands for the settings panel and reset function
  127. registerMenuCommand() {
  128. GM_registerMenuCommand(
  129. "🚀 " + translations[lang].acceleratedCount + util.getValue('setting_success_times') + translations[lang].times,
  130. () => {
  131. Swal.fire({
  132. showCancelButton: true,
  133. title: translations[lang].resetPrompt,
  134. icon: 'warning',
  135. confirmButtonText: translations[lang].confirm,
  136. cancelButtonText: translations[lang].cancel,
  137. customClass: {
  138. popup: 'instant-popup',
  139. },
  140. }).then((res) => {
  141. if (res.isConfirmed) {
  142. util.setValue('setting_success_times', 0);
  143. history.go(0);
  144. }
  145. });
  146. }
  147. );
  148. let dom = `<div style="font-size: 1em;">
  149. <label class="instant-setting-label">${translations[lang].accelerateExternal}<input type="checkbox" id="S-External" ${util.getValue('allow_external_links') ? 'checked' : ''} class="instant-setting-checkbox"></label>
  150. <label class="instant-setting-label"><span>${translations[lang].accelerateParams} (<a href="https://www.youxiaohou.com/tool/install-instantpage.html#%E9%85%8D%E7%BD%AE%E8%AF%B4%E6%98%8E" target="_blank">Details</a>)</span><input type="checkbox" id="S-Query" ${util.getValue('allow_query_links') ? 'checked' : ''} class="instant-setting-checkbox"></label>
  151. <label class="instant-setting-label">${translations[lang].openInSameTab}<input type="checkbox" id="S-Target" ${util.getValue('enable_target_self') ? 'checked' : ''} class="instant-setting-checkbox"></label>
  152. <label class="instant-setting-label">${translations[lang].animationEffect}<input type="checkbox" id="S-Animate" ${util.getValue('enable_animation') ? 'checked' : ''} class="instant-setting-checkbox"></label>
  153. <label class="instant-setting-label">${translations[lang].prefetchDelay}<input type="number" min="65" id="S-Delay" value="${util.getValue('delay_on_hover')}" class="instant-setting-input"></label>
  154. <label class="instant-setting-label-col">${translations[lang].excludeURLs}<textarea placeholder="One per line, e.g., www.example.com" id="S-Exclude" class="instant-setting-textarea">${util.getValue('exclude_list')}</textarea></label>
  155. <label class="instant-setting-label-col">${translations[lang].excludeKeywords}<textarea placeholder="One per line, e.g., logout" id="S-Exclude-Word" class="instant-setting-textarea">${util.getValue('exclude_keyword')}</textarea></label>
  156. </div>`;
  157. GM_registerMenuCommand(translations[lang].settingsTitle, () => {
  158. Swal.fire({
  159. title: translations[lang].settingsTitle,
  160. html: dom,
  161. showCloseButton: true,
  162. confirmButtonText: translations[lang].save,
  163. footer: `<div style="text-align: center;font-size: 1em;">${translations[lang].usageFooter}</div>`,
  164. customClass: {
  165. popup: 'instant-popup',
  166. },
  167. }).then((res) => {
  168. if (res.isConfirmed) {
  169. history.go(0);
  170. }
  171. });
  172. document.getElementById('S-External').addEventListener('change', (e) => {
  173. util.setValue('allow_external_links', e.currentTarget.checked);
  174. });
  175. document.getElementById('S-Query').addEventListener('change', (e) => {
  176. util.setValue('allow_query_links', e.currentTarget.checked);
  177. });
  178. document.getElementById('S-Target').addEventListener('change', (e) => {
  179. util.setValue('enable_target_self', e.currentTarget.checked);
  180. });
  181. document.getElementById('S-Animate').addEventListener('change', (e) => {
  182. util.setValue('enable_animation', e.currentTarget.checked);
  183. });
  184. document.getElementById('S-Delay').addEventListener('change', (e) => {
  185. util.setValue('delay_on_hover', e.currentTarget.value);
  186. });
  187. document.getElementById('S-Exclude').addEventListener('change', (e) => {
  188. util.setValue('exclude_list', e.currentTarget.value);
  189. });
  190. document.getElementById('S-Exclude-Word').addEventListener('change', (e) => {
  191. util.setValue('exclude_keyword', e.currentTarget.value);
  192. });
  193. });
  194. },
  195.  
  196. // Check if the current host is in the exclude list
  197. inExcludeList() {
  198. let exclude = util.getValue('exclude_list').split('\n').map(s => s.trim());
  199. let host = location.host;
  200. return exclude.includes(host);
  201. },
  202.  
  203. // -------------------------------
  204. // Main prefetch logic integrating instant.page v5.2.0 features
  205. // -------------------------------
  206. instantPage() {
  207. if (window.instantLoaded) return;
  208. window.instantLoaded = true;
  209.  
  210. // Configuration options (some from GM storage, some from data attributes)
  211. const allowQueryString = ('instantAllowQueryString' in document.body.dataset) || util.getValue('allow_query_links');
  212. const allowExternalLinks = ('instantAllowExternalLinks' in document.body.dataset) || util.getValue('allow_external_links');
  213. const _useWhitelist = ('instantWhitelist' in document.body.dataset);
  214. const enableAnimation = util.getValue('enable_animation');
  215. const enableTargetSelf = util.getValue('enable_target_self');
  216. const excludeKeyword = util.getValue('exclude_keyword').split('\n');
  217. let delayOnHover = parseInt(util.getValue('delay_on_hover'));
  218.  
  219. // Internal variables similar to instant.page v5.2.0
  220. let _chromiumMajorVersionInUserAgent = null;
  221. let _speculationRulesType = 'none';
  222. let _delayOnHover = delayOnHover; // in milliseconds
  223. let _lastTouchstartEvent = null;
  224. let _mouseoverTimer = null;
  225. let _preloadedList = new Set();
  226.  
  227. // Browser support check: ensure <link rel="prefetch"> is supported
  228. let supportChecksRelList = document.createElement('link').relList;
  229. if (!(supportChecksRelList && supportChecksRelList.supports && supportChecksRelList.supports('prefetch'))) {
  230. return;
  231. }
  232. const chromium100Check = ('throwIfAborted' in AbortSignal.prototype); // Chromium 100+
  233. const firefox115AndSafari17_0Check = supportChecksRelList.supports('modulepreload'); // Firefox 115+, Safari 17.0+
  234. const safari15_4AndFirefox116Check = (Intl.PluralRules && 'selectRange' in Intl.PluralRules.prototype);
  235. const firefox115AndSafari15_4Check = firefox115AndSafari17_0Check || safari15_4AndFirefox116Check;
  236. const isBrowserSupported = chromium100Check && firefox115AndSafari15_4Check;
  237. if (!isBrowserSupported) return;
  238.  
  239. // If the page sets data-instantVaryAccept (e.g. Shopify), check Chromium version
  240. if (document.body.dataset.instantVaryAccept) {
  241. const chromiumUserAgentIndex = navigator.userAgent.indexOf('Chrome/');
  242. if (chromiumUserAgentIndex > -1) {
  243. _chromiumMajorVersionInUserAgent = parseInt(navigator.userAgent.substring(chromiumUserAgentIndex + 'Chrome/'.length));
  244. }
  245. if (_chromiumMajorVersionInUserAgent && _chromiumMajorVersionInUserAgent < 110) {
  246. return;
  247. }
  248. }
  249.  
  250. // Set speculation rules if supported (<script type="speculationrules">)
  251. if (HTMLScriptElement.supports && HTMLScriptElement.supports('speculationrules')) {
  252. const speculationRulesConfig = document.body.dataset.instantSpecrules;
  253. if (speculationRulesConfig === 'prerender') {
  254. _speculationRulesType = 'prerender';
  255. } else if (speculationRulesConfig !== 'no') {
  256. _speculationRulesType = 'prefetch';
  257. }
  258. }
  259.  
  260. // Determine trigger method based on data-instantIntensity (default is mouseover)
  261. let useMousedown = false;
  262. let useMousedownOnly = false;
  263. let useViewport = false;
  264. const mousedownShortcut = ('instantMousedownShortcut' in document.body.dataset);
  265. if ('instantIntensity' in document.body.dataset) {
  266. const intensity = document.body.dataset.instantIntensity;
  267. if (intensity === 'mousedown' && !mousedownShortcut) {
  268. useMousedown = true;
  269. }
  270. if (intensity === 'mousedown-only' && !mousedownShortcut) {
  271. useMousedown = true;
  272. useMousedownOnly = true;
  273. }
  274. if (intensity === 'viewport' || intensity === 'viewport-all') {
  275. const isOnSmallScreen = document.documentElement.clientWidth * document.documentElement.clientHeight < 450000;
  276. const isConnectionAdequate = !(navigator.connection && (navigator.connection.saveData ||
  277. (navigator.connection.effectiveType && navigator.connection.effectiveType.includes('2g'))));
  278. if (isOnSmallScreen && isConnectionAdequate) {
  279. useViewport = true;
  280. }
  281. if (intensity === 'viewport-all') {
  282. useViewport = true;
  283. }
  284. }
  285. const intensityAsInteger = parseInt(intensity);
  286. if (!isNaN(intensityAsInteger)) {
  287. _delayOnHover = intensityAsInteger;
  288. }
  289. }
  290.  
  291. const eventListenersOptions = {
  292. capture: true,
  293. passive: true,
  294. };
  295.  
  296. // Register event listeners based on trigger method
  297. if (useMousedownOnly) {
  298. document.addEventListener('touchstart', touchstartEmptyListener, eventListenersOptions);
  299. } else {
  300. document.addEventListener('touchstart', touchstartListener, eventListenersOptions);
  301. }
  302. if (!useMousedown) {
  303. document.addEventListener('mouseover', mouseoverListener, eventListenersOptions);
  304. }
  305. if (useMousedown) {
  306. document.addEventListener('mousedown', mousedownListener, eventListenersOptions);
  307. }
  308. if (mousedownShortcut) {
  309. document.addEventListener('mousedown', mousedownShortcutListener, eventListenersOptions);
  310. }
  311.  
  312. // If viewport prefetch is enabled, use IntersectionObserver to preload links in view
  313. if (useViewport) {
  314. const requestIdleCallbackOrFallback = window.requestIdleCallback || function(callback) { callback(); };
  315. requestIdleCallbackOrFallback(function () {
  316. const intersectionObserver = new IntersectionObserver((entries) => {
  317. entries.forEach((entry) => {
  318. if (entry.isIntersecting) {
  319. const anchor = entry.target;
  320. intersectionObserver.unobserve(anchor);
  321. preload(anchor);
  322. }
  323. });
  324. });
  325. document.querySelectorAll('a').forEach((anchor) => {
  326. if (isPreloadable(anchor)) {
  327. intersectionObserver.observe(anchor);
  328. }
  329. });
  330. }, { timeout: 1500 });
  331. }
  332.  
  333. // -------------------------------
  334. // Event handler functions
  335. // -------------------------------
  336. function touchstartListener(event) {
  337. _lastTouchstartEvent = event;
  338. const anchor = event.target.closest('a');
  339. if (!isPreloadable(anchor)) return;
  340. preload(anchor, 'high');
  341. }
  342. function touchstartEmptyListener(event) {
  343. _lastTouchstartEvent = event;
  344. }
  345. function mouseoverListener(event) {
  346. if (isEventLikelyTriggeredByTouch(event)) return;
  347. if (!('closest' in event.target)) return;
  348. const anchor = event.target.closest('a');
  349. if (!isPreloadable(anchor)) return;
  350. anchor.addEventListener('mouseout', mouseoutListener, { passive: true });
  351. _mouseoverTimer = setTimeout(() => {
  352. preload(anchor, 'high');
  353. _mouseoverTimer = null;
  354. }, _delayOnHover);
  355. }
  356. function mousedownListener(event) {
  357. if (isEventLikelyTriggeredByTouch(event)) return;
  358. const anchor = event.target.closest('a');
  359. if (!isPreloadable(anchor)) return;
  360. preload(anchor, 'high');
  361. }
  362. function mouseoutListener(event) {
  363. if (event.relatedTarget && event.target.closest('a') === event.relatedTarget.closest('a')) return;
  364. if (_mouseoverTimer) {
  365. clearTimeout(_mouseoverTimer);
  366. _mouseoverTimer = null;
  367. }
  368. }
  369. function mousedownShortcutListener(event) {
  370. if (isEventLikelyTriggeredByTouch(event)) return;
  371. const anchor = event.target.closest('a');
  372. if (event.which > 1 || event.metaKey || event.ctrlKey) return;
  373. if (!anchor) return;
  374. anchor.addEventListener('click', function (e) {
  375. if (e.detail === 1337) return;
  376. e.preventDefault();
  377. }, { capture: true, passive: false, once: true });
  378. const customEvent = new MouseEvent('click', {
  379. view: window,
  380. bubbles: true,
  381. cancelable: false,
  382. detail: 1337
  383. });
  384. anchor.dispatchEvent(customEvent);
  385. }
  386. // Check if a mouse event is likely triggered by a preceding touch event (avoid duplicate prefetch on touch devices)
  387. function isEventLikelyTriggeredByTouch(event) {
  388. if (!_lastTouchstartEvent || !event) return false;
  389. if (event.target !== _lastTouchstartEvent.target) return false;
  390. const now = event.timeStamp;
  391. const duration = now - _lastTouchstartEvent.timeStamp;
  392. const MAX_DURATION = 2500; // ms
  393. return duration < MAX_DURATION;
  394. }
  395. // Determine whether the link element is preloadable based on various criteria
  396. function isPreloadable(anchor) {
  397. if (!anchor || !anchor.href) return false;
  398. if (_useWhitelist && !('instant' in anchor.dataset)) return false;
  399. if (anchor.origin !== location.origin) {
  400. const allowed = allowExternalLinks || ('instant' in anchor.dataset);
  401. if (!allowed) return false;
  402. }
  403. if (!['http:', 'https:'].includes(anchor.protocol)) return false;
  404. if (anchor.protocol === 'http:' && location.protocol === 'https:') return false;
  405. if (!allowQueryString && anchor.search && !('instant' in anchor.dataset)) return false;
  406. if (anchor.hash && (anchor.pathname + anchor.search === location.pathname + location.search)) return false;
  407. if ('noInstant' in anchor.dataset) return false;
  408. // Exclude links containing any of the specified keywords
  409. if (util.include(anchor.href, excludeKeyword)) return false;
  410. return true;
  411. }
  412. // Perform prefetch if the link has not been prefetched yet.
  413. // Choose between speculation rules (if supported) and <link rel="prefetch">
  414. function preload(anchor, fetchPriority = 'auto') {
  415. const url = anchor.href;
  416. if (_preloadedList.has(url)) return;
  417. if (_speculationRulesType !== 'none') {
  418. preloadUsingSpeculationRules(url);
  419. } else {
  420. preloadUsingLinkElement(url, fetchPriority);
  421. }
  422. _preloadedList.add(url);
  423. if (enableAnimation) {
  424. anchor.classList.add("link-instanted");
  425. }
  426. if (enableTargetSelf) {
  427. anchor.target = '_self';
  428. }
  429. util.setValue('setting_success_times', util.getValue('setting_success_times') + 1);
  430. }
  431. // Prefetch using <script type="speculationrules">
  432. function preloadUsingSpeculationRules(url) {
  433. const script = document.createElement('script');
  434. script.type = 'speculationrules';
  435. script.textContent = JSON.stringify({
  436. [_speculationRulesType]: [{
  437. source: 'list',
  438. urls: [url]
  439. }]
  440. });
  441. document.head.appendChild(script);
  442. }
  443. // Prefetch using <link rel="prefetch">
  444. function preloadUsingLinkElement(url, fetchPriority = 'auto') {
  445. const link = document.createElement('link');
  446. link.rel = 'prefetch';
  447. link.href = url;
  448. link.fetchPriority = fetchPriority;
  449. link.as = 'document';
  450. document.head.appendChild(link);
  451. }
  452. },
  453.  
  454. // Add plugin styles to the page
  455. addPluginStyle() {
  456. let style = `
  457. .instant-popup { font-size: 14px !important; }
  458. .instant-setting-label { display: flex; align-items: center; justify-content: space-between; padding-top: 15px; }
  459. .instant-setting-label-col { display: flex; align-items: flex-start; padding-top: 15px; flex-direction: column; }
  460. .instant-setting-checkbox { width: 16px; height: 16px; }
  461. .instant-setting-textarea { width: 100%; margin: 14px 0 0; height: 60px; resize: none; border: 1px solid #bbb; box-sizing: border-box; padding: 5px 10px; border-radius: 5px; color: #666; line-height: 1.2; }
  462. .instant-setting-input { border: 1px solid #bbb; box-sizing: border-box; padding: 5px 10px; border-radius: 5px; width: 100px; }
  463. @keyframes instantAnminate { from { opacity: 1; } 50% { opacity: 0.4; } to { opacity: 0.9; } }
  464. .link-instanted { animation: instantAnminate 0.6s 1; animation-fill-mode: forwards; }
  465. .link-instanted * { animation: instantAnminate 0.6s 1; animation-fill-mode: forwards; }
  466. `;
  467. if (document.head) {
  468. util.addStyle('swal-pub-style', 'style', GM_getResourceText('swalStyle'));
  469. util.addStyle('instant-style', 'style', style);
  470. }
  471. // Observe changes in the head element and re-add styles if needed
  472. const headObserver = new MutationObserver(() => {
  473. util.addStyle('swal-pub-style', 'style', GM_getResourceText('swalStyle'));
  474. util.addStyle('instant-style', 'style', style);
  475. });
  476. headObserver.observe(document.head, { childList: true, subtree: true });
  477. },
  478.  
  479. // Initialize the script
  480. init() {
  481. this.initValue();
  482. this.addPluginStyle();
  483. this.registerMenuCommand();
  484. if (this.inExcludeList()) return;
  485. this.instantPage();
  486. }
  487. };
  488.  
  489. main.init();