Greasy Fork is available in English.

GreasyFork: Check To Search

1/8/2024, 6:31:18 PM

  1. // ==UserScript==
  2. // @name GreasyFork: Check To Search
  3. // @namespace Violentmonkey Scripts
  4. // @match https://greatest.deepsurf.us/*
  5. // @grant none
  6. // @version 0.1.0
  7. // @license MIT
  8. // @description 1/8/2024, 6:31:18 PM
  9. // @run-at document-start
  10. // ==/UserScript==
  11.  
  12.  
  13. function fixURL(url) {
  14. url = url.replace(/(\/\w+\/\d+)\-[%\d\w-]+([?#]|$)/, '$1$2').replace(`${location.origin}/`, '/');
  15. if (!url) return '';
  16. if (url.includes('|')) return '';
  17. return url
  18. }
  19.  
  20.  
  21. const _genericTextChars = {
  22. 1: '\x20\xA0\u2000-\u200A\u202F\u205F\u3000',
  23. 2: '\u200B-\u200D\u2060\uFEFF',
  24. 4: '\u180E\u2800\u3164',
  25. 8: '\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x9F\u2800',
  26. 16: '\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x9F\u2800\t\r\n' // plus tab (\x09), newline (\x0A), \r
  27. }
  28. const _genericTextREs = {
  29. 1: new RegExp(`[${_genericTextChars[1]}]`, 'g'),
  30. 2: new RegExp(`[${_genericTextChars[2]}]`, 'g'),
  31. 4: new RegExp(`[${_genericTextChars[4]}]`, 'g'),
  32. 8: new RegExp(`[${_genericTextChars[8]}]`, 'g'),
  33. 16: new RegExp(`[${_genericTextChars[16]}]`, 'g')
  34. }
  35. function genericText(text, flag) {
  36.  
  37. // https://unicode-explorer.com/articles/space-characters
  38. // https://medium.com/@ray102467/js-regex-3fbfe4d3115
  39.  
  40. if (!text || typeof text !== 'string') return text;
  41.  
  42. // regular space to space
  43. if (flag & 1) text = text.replace(_genericTextREs[1], (flag & (1 << 8)) ? '' : ' '); // 1 | 256
  44.  
  45. // zero-width space to empty
  46. if (flag & 2) text = text.replace(_genericTextREs[2], '');
  47.  
  48. // space chars to space
  49. if (flag & 4) text = text.replace(_genericTextREs[4], (flag & (1 << 8)) ? '' : ' '); // 4 | 1024
  50.  
  51. // improper chars to empty
  52. if (flag & 8) text = text.replace(_genericTextREs[8], '');
  53.  
  54. // improper+ chars to empty
  55. if (flag & 16) text = text.replace(_genericTextREs[16], '');
  56.  
  57. return text;
  58.  
  59. }
  60.  
  61. const cssTextFn = () => `
  62.  
  63. .r41-custom-search-input {
  64. font-size: 16px;
  65. padding: 8px;
  66. border: 1px solid #ccc;
  67. border-radius: 4px;
  68. box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  69. transition: border-color 0.3s ease;
  70. }
  71.  
  72. .r41-custom-search-input:focus {
  73. border-color: #007bff; /* Blue border on focus */
  74. outline: none;
  75. }
  76.  
  77. .r41-loading-spinner {
  78. display: none;
  79. border: 4px solid #f3f3f3; /* Light grey */
  80. border-top: 4px solid #3498db; /* Blue */
  81. border-radius: 50%;
  82. width: 20px;
  83. height: 20px;
  84. animation: r41-spin 2s linear infinite;
  85. margin-left: 4px;
  86. }
  87.  
  88. @keyframes r41-spin {
  89. 0% { transform: rotate(0deg); }
  90. 100% { transform: rotate(360deg); }
  91. }
  92.  
  93. .r41-custom-search-input-header{
  94.  
  95. display: flex;
  96. flex-direction: row;
  97. align-items: center;
  98.  
  99. }
  100.  
  101. .r41-custom-search-input{
  102. opacity: 0;
  103. position:absolute;
  104. z-index:-1;
  105. }
  106. .r41-custom-search-input:focus{
  107. opacity: 1;
  108. position:relative;
  109. z-index:initial;
  110. }
  111. .r41-custom-search-input:focus + .r41-loading-spinner {
  112.  
  113. margin-left: -34px;
  114. }
  115.  
  116.  
  117.  
  118. `;
  119.  
  120. let onloadPromise = Promise.resolve();
  121.  
  122. function addElements() {
  123.  
  124.  
  125. if (!document.querySelector('#gf_390_lz')) {
  126.  
  127. // Load LZ-String library
  128. const script = document.createElement('script');
  129. script.id = 'gf_390_lz';
  130. script.src = 'https://cdnjs.cloudflare.com/ajax/libs/lz-string/1.5.0/lz-string.min.js';
  131. document.head.appendChild(script);
  132.  
  133. onloadPromise = new Promise(resolve => {
  134. script.onload = resolve;
  135. });
  136.  
  137. }
  138.  
  139.  
  140. if (!document.querySelector('#gf_391_style')) {
  141.  
  142. const style = document.createElement('style');
  143. style.id = 'gf_391_style';
  144. style.textContent = cssTextFn();
  145. document.head.appendChild(style);
  146. }
  147.  
  148.  
  149. return onloadPromise;
  150.  
  151. }
  152.  
  153.  
  154. const onReadyPromise = new Promise((resolve) => {
  155.  
  156. document.addEventListener('DOMContentLoaded', resolve)
  157.  
  158.  
  159. });
  160.  
  161. const delayedReady = onReadyPromise.then(() => {
  162.  
  163. if (!location.href.includes('/users/')) return false;
  164. new Promise(resolve => setTimeout(resolve, 200))
  165. });
  166.  
  167.  
  168.  
  169.  
  170. const hookToFn = (w) => {
  171.  
  172.  
  173. let currentPageListHtml = '';
  174. const {sectionId, storagePrefix, listId,elementIdPrefix} = w;
  175.  
  176. delayedReady.then(async (xt) => {
  177.  
  178. if (xt === false) return;
  179.  
  180. const h3 = document.querySelector(`#${sectionId}>header>h3`);
  181. if (h3) {
  182.  
  183. const onloadPromise = addElements();
  184.  
  185. let onload2 = onloadPromise.then((xt) => {
  186. if(xt === false) return;
  187. checkCache();
  188. cacheCurrentPageList();
  189. });
  190.  
  191. h3.addEventListener('click', function () {
  192. clicked = true;
  193. onload2.then(() => {
  194. if ((getSelection() + "") === "" && h3.isConnected === true) {
  195. if (showInput(h3) === false) replaceWithInput(h3);
  196. }
  197. });
  198. });
  199. }
  200.  
  201.  
  202. });
  203.  
  204.  
  205. function checkCache() {
  206.  
  207. let urls = sessionStorage.getItem(`${storagePrefix}urls`) || "";
  208. if (urls.includes(`|${fixURL(location.href)}|`)) return;
  209. for (const url of urls.split('|')) {
  210. if (!url) continue;
  211. sessionStorage.removeItem(`${storagePrefix}${url}`);
  212. }
  213. sessionStorage.removeItem(`${storagePrefix}urls`);
  214.  
  215. }
  216.  
  217. function cacheCurrentPageList() {
  218.  
  219. const listElement = document.querySelector(`ol#${listId}`);
  220. if (listElement) {
  221. currentPageListHtml = listElement.outerHTML;
  222. saveCache(location.href, currentPageListHtml);
  223. }
  224. }
  225.  
  226. let globalChangeCounter = 0;
  227.  
  228.  
  229. function showInput(h3Element) {
  230.  
  231. const input = document.getElementById(`${elementIdPrefix}custom-search-input`)
  232. if (!input) return false;
  233. // input.value = h3Element.textContent;
  234. // input.id = `${elementIdPrefix}custom-search-input`;
  235. // input.classList.add('r41-custom-search-input')
  236. // h3Element.parentNode.classList.add(`${elementIdPrefix}custom-search-input-header`, 'r41-custom-search-input-header');
  237. // h3Element.parentNode.insertBefore(input, h3Element.nextSibling)
  238. // h3Element.parentNode.replaceChild(input, h3Element);
  239. input.select();
  240. // input.addEventListener('input', () => handleInputChange(input));
  241.  
  242. // Add loading spinner (hidden by default)
  243. // const spinner = document.createElement('div');
  244. // spinner.id = 'loading-spinner';
  245. // input.parentNode.insertBefore(spinner, input.nextSibling);
  246. }
  247.  
  248. function replaceWithInput(h3Element) {
  249. const input = document.createElement('input');
  250. // input.value = h3Element.textContent;
  251. input.id = `${elementIdPrefix}custom-search-input`;
  252. input.classList.add('r41-custom-search-input')
  253. h3Element.parentNode.classList.add(`${elementIdPrefix}custom-search-input-header`, 'r41-custom-search-input-header');
  254. h3Element.parentNode.insertBefore(input, h3Element.nextSibling)
  255. // h3Element.parentNode.replaceChild(input, h3Element);
  256. input.select();
  257. input.addEventListener('input', () => handleInputChange(input));
  258.  
  259. // Add loading spinner (hidden by default)
  260. const spinner = document.createElement('div');
  261. spinner.classList.add('r41-loading-spinner')
  262. spinner.id = `${elementIdPrefix}loading-spinner`;
  263. input.parentNode.insertBefore(spinner, input.nextSibling);
  264. }
  265. let lastState = false;
  266. function setLoadingState(isLoading) {
  267. if (lastState === isLoading) return;
  268. const spinner = document.getElementById(`${elementIdPrefix}loading-spinner`);
  269. if (spinner) {
  270. spinner.style.display = isLoading ? 'inline-block' : 'none';
  271. }
  272. lastState = isLoading;
  273. }
  274.  
  275. let busyCache = 0;
  276.  
  277. async function handleInputChange(input) {
  278. const currentChangeCount = ++globalChangeCounter;
  279. if (busyCache === 0) {
  280. setLoadingState(true);
  281. await fetchAndCacheScripts();
  282. await new Promise(resolve => setTimeout(resolve, 140));
  283. } else if (busyCache === 1) {
  284. setLoadingState(true);
  285. await new Promise(resolve => setTimeout(resolve, 600));
  286. } else if (busyCache === 2) {
  287. setLoadingState(true);
  288. await new Promise(resolve => setTimeout(resolve, 140));
  289. }
  290. if (currentChangeCount === globalChangeCounter) {
  291. setLoadingState(true);
  292. const filteredResults = await fetchAndFilterScripts(input.value);
  293. updateCurrentList(filteredResults);
  294. setLoadingState(false);
  295. }
  296. }
  297.  
  298. function saveCache(url, text) {
  299. url = fixURL(url);
  300. if (!url) return;
  301. text = LZString.compress(text || "");
  302. text = text || ""
  303. sessionStorage.setItem(`${storagePrefix}${url}`, text);
  304. sessionStorage.setItem(`${storagePrefix}urls`, (sessionStorage.getItem(`${storagePrefix}urls`) || "") + "|" + url + "|");
  305.  
  306. }
  307. function restoreCache(url) {
  308. url = fixURL(url);
  309. if (!url) return;
  310. let text = LZString.decompress(sessionStorage.getItem(`${storagePrefix}${url}`) || "") || "";
  311. return text;
  312.  
  313. }
  314.  
  315. async function fetchAndCacheScripts() {
  316. if (busyCache > 0) return;
  317. busyCache = 1;
  318. const pages = Array.from(document.querySelectorAll(`#${sectionId} .pagination > a[href]`))
  319. .map(link => link.getAttribute('href'));
  320.  
  321.  
  322. let mMap = new Map();
  323. for (const url of pages) {
  324. mMap.set(fixURL(url), url)
  325.  
  326. }
  327.  
  328.  
  329. for (const [fixedURL, pageURL] of mMap) {
  330. const page = pageURL;
  331. let url = fixedURL;
  332. if (url && !sessionStorage.getItem(`${storagePrefix}${url}`)) {
  333. const response = await fetch(page);
  334. const text = await response.text();
  335. console.log(123, Date.now(), page)
  336. saveCache(page, text);
  337. }
  338. }
  339. busyCache = 2;
  340. }
  341.  
  342.  
  343.  
  344. async function fetchAndFilterScripts(inputValue) {
  345. inputValue = inputValue || '';
  346. inputValue = genericText(inputValue, 1 | 2 | 8);
  347. let pages = Array.from(document.querySelectorAll(`#${sectionId} .pagination > a[href]`))
  348. .map(link => link.getAttribute('href'));
  349.  
  350. let mMap = new Map();
  351. for (const url of pages) {
  352. mMap.set(fixURL(url), url)
  353.  
  354. }
  355.  
  356. const allScripts = [];
  357.  
  358. // Get current page's list from sessionStorage
  359. const currentPageHtml = restoreCache(location.href);
  360. const currentDoc = new DOMParser().parseFromString(currentPageHtml, 'text/html');
  361. filterScripts(currentDoc, inputValue || true, allScripts);
  362. for (const [fixedURL, pageURL] of mMap) {
  363.  
  364. let page = pageURL;
  365. let pageHtml = restoreCache(page);
  366. if (!pageHtml) {
  367. const response = await fetch(page);
  368. const text = await response.text();
  369. console.log(456, Date.now(), page)
  370. saveCache(page, text);
  371. pageHtml = text;
  372. }
  373.  
  374. const doc = new DOMParser().parseFromString(pageHtml, 'text/html');
  375. filterScripts(doc, inputValue || false, allScripts);
  376.  
  377. }
  378.  
  379. return allScripts;
  380. }
  381.  
  382. function filterScripts(doc, inputValue, allScripts) {
  383. if (inputValue === false) return;
  384. const scripts = doc.querySelectorAll(`ol#${listId} li[data-script-id]`);
  385. scripts.forEach(li => {
  386. const html = getContentHTML(li, inputValue);
  387. if (html) allScripts.push(html);
  388. });
  389. }
  390.  
  391. function getContentHTML(li, inputValue) {
  392. const name = genericText(li.querySelector('a.script-link[href]').textContent, 1 | 2 | 8);
  393. const description = genericText(li.querySelector('.script-description').textContent, 1 | 2 | 8);
  394. let text = name + `\n` + description;
  395. if (inputValue === true || text.toLowerCase().includes(inputValue.toLowerCase())) {
  396. return li.outerHTML;
  397. }
  398. }
  399.  
  400. function updateCurrentList(filteredResults) {
  401. const list = document.querySelector(`ol#${listId}`);
  402. if (list) {
  403. list.innerHTML = filteredResults.join('');
  404. }
  405. }
  406.  
  407.  
  408. };
  409.  
  410. hookToFn({
  411. listId: 'user-script-list',
  412. sectionId: 'user-script-list-section',
  413. storagePrefix: 'gF_7H8TV_',
  414. elementIdPrefix: 'gimoa-'
  415. });
  416.  
  417. hookToFn({
  418. listId: 'user-unlisted-script-list',
  419. sectionId: 'user-unlisted-script-list-section',
  420. storagePrefix: 'gF_84IUu_',
  421. elementIdPrefix: 'jexsq-'
  422. });
  423.  
  424. hookToFn({
  425. listId: 'user-library-script-list',
  426. sectionId: 'user-library-list-section',
  427. storagePrefix: 'gF_39rrO_',
  428. elementIdPrefix: 'm01xt-'
  429. });