GreasyFork: Check To Search

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

// ==UserScript==
// @name        GreasyFork: Check To Search
// @namespace   Violentmonkey Scripts
// @match       https://greasyfork.org/*
// @grant       none
// @version     0.1.0
// @license     MIT
// @description 1/8/2024, 6:31:18 PM
// @run-at document-start
// ==/UserScript==


function fixURL(url) {
    url = url.replace(/(\/\w+\/\d+)\-[%\d\w-]+([?#]|$)/, '$1$2').replace(`${location.origin}/`, '/');
    if (!url) return '';
    if (url.includes('|')) return '';
    return url
  }


  const _genericTextChars = {
    1: '\x20\xA0\u2000-\u200A\u202F\u205F\u3000',
    2: '\u200B-\u200D\u2060\uFEFF',
    4: '\u180E\u2800\u3164',
    8: '\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x9F\u2800',
    16: '\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x9F\u2800\t\r\n' // plus tab (\x09), newline (\x0A), \r
  }
  const _genericTextREs = {
    1: new RegExp(`[${_genericTextChars[1]}]`, 'g'),
    2: new RegExp(`[${_genericTextChars[2]}]`, 'g'),
    4: new RegExp(`[${_genericTextChars[4]}]`, 'g'),
    8: new RegExp(`[${_genericTextChars[8]}]`, 'g'),
    16: new RegExp(`[${_genericTextChars[16]}]`, 'g')
  }
  function genericText(text, flag) {

    // https://unicode-explorer.com/articles/space-characters
    // https://medium.com/@ray102467/js-regex-3fbfe4d3115

    if (!text || typeof text !== 'string') return text;

    // regular space to space
    if (flag & 1) text = text.replace(_genericTextREs[1], (flag & (1 << 8)) ? '' : ' '); // 1 | 256

    // zero-width space to empty
    if (flag & 2) text = text.replace(_genericTextREs[2], '');

    // space chars to space
    if (flag & 4) text = text.replace(_genericTextREs[4], (flag & (1 << 8)) ? '' : ' '); // 4 | 1024

    // improper chars to empty
    if (flag & 8) text = text.replace(_genericTextREs[8], '');

    // improper+ chars to empty
    if (flag & 16) text = text.replace(_genericTextREs[16], '');

    return text;

  }

  const cssTextFn = () => `

  .r41-custom-search-input {
  font-size: 16px;
  padding: 8px;
  border: 1px solid #ccc;
  border-radius: 4px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  transition: border-color 0.3s ease;
  }

  .r41-custom-search-input:focus {
  border-color: #007bff; /* Blue border on focus */
  outline: none;
  }

  .r41-loading-spinner {
  display: none;
  border: 4px solid #f3f3f3; /* Light grey */
  border-top: 4px solid #3498db; /* Blue */
  border-radius: 50%;
  width: 20px;
  height: 20px;
  animation: r41-spin 2s linear infinite;
  margin-left: 4px;
  }

  @keyframes r41-spin {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
  }

  .r41-custom-search-input-header{

  display: flex;
  flex-direction: row;
  align-items: center;

  }

  .r41-custom-search-input{
  opacity: 0;
  position:absolute;
  z-index:-1;
  }
  .r41-custom-search-input:focus{
  opacity: 1;
  position:relative;
  z-index:initial;
  }
  .r41-custom-search-input:focus + .r41-loading-spinner {

  margin-left: -34px;
  }



  `;

  let onloadPromise = Promise.resolve();

  function addElements() {


    if (!document.querySelector('#gf_390_lz')) {

      // Load LZ-String library
      const script = document.createElement('script');
      script.id = 'gf_390_lz';
      script.src = 'https://cdnjs.cloudflare.com/ajax/libs/lz-string/1.5.0/lz-string.min.js';
      document.head.appendChild(script);

      onloadPromise = new Promise(resolve => {
        script.onload = resolve;
      });

    }


    if (!document.querySelector('#gf_391_style')) {

      const style = document.createElement('style');
      style.id = 'gf_391_style';
      style.textContent = cssTextFn();
      document.head.appendChild(style);
    }


    return onloadPromise;

  }


  const onReadyPromise = new Promise((resolve) => {

    document.addEventListener('DOMContentLoaded', resolve)


  });

  const delayedReady = onReadyPromise.then(() => {

    if (!location.href.includes('/users/')) return false;
    new Promise(resolve => setTimeout(resolve, 200))
  });




  const hookToFn = (w) => {


    let currentPageListHtml = '';
    const {sectionId, storagePrefix, listId,elementIdPrefix} = w;

    delayedReady.then(async (xt) => {

      if (xt === false) return;

      const h3 = document.querySelector(`#${sectionId}>header>h3`);
      if (h3) {

        const onloadPromise = addElements();

        let onload2 = onloadPromise.then((xt) => {
          if(xt === false) return;
          checkCache();
          cacheCurrentPageList();
        });

        h3.addEventListener('click', function () {
          clicked = true;
          onload2.then(() => {
            if ((getSelection() + "") === "" && h3.isConnected === true) {
              if (showInput(h3) === false) replaceWithInput(h3);
            }
          });
        });
      }


    });


    function checkCache() {

      let urls = sessionStorage.getItem(`${storagePrefix}urls`) || "";
      if (urls.includes(`|${fixURL(location.href)}|`)) return;
      for (const url of urls.split('|')) {
        if (!url) continue;
        sessionStorage.removeItem(`${storagePrefix}${url}`);
      }
      sessionStorage.removeItem(`${storagePrefix}urls`);

    }

    function cacheCurrentPageList() {

      const listElement = document.querySelector(`ol#${listId}`);
      if (listElement) {
        currentPageListHtml = listElement.outerHTML;
        saveCache(location.href, currentPageListHtml);
      }
    }

    let globalChangeCounter = 0;


    function showInput(h3Element) {

      const input = document.getElementById(`${elementIdPrefix}custom-search-input`)
      if (!input) return false;
      // input.value = h3Element.textContent;
      // input.id = `${elementIdPrefix}custom-search-input`;
      // input.classList.add('r41-custom-search-input')
      // h3Element.parentNode.classList.add(`${elementIdPrefix}custom-search-input-header`, 'r41-custom-search-input-header');
      // h3Element.parentNode.insertBefore(input, h3Element.nextSibling)
      // h3Element.parentNode.replaceChild(input, h3Element);
      input.select();
      // input.addEventListener('input', () => handleInputChange(input));

      // Add loading spinner (hidden by default)
      // const spinner = document.createElement('div');
      // spinner.id = 'loading-spinner';
      // input.parentNode.insertBefore(spinner, input.nextSibling);
    }

    function replaceWithInput(h3Element) {
      const input = document.createElement('input');
      // input.value = h3Element.textContent;
      input.id = `${elementIdPrefix}custom-search-input`;
      input.classList.add('r41-custom-search-input')
      h3Element.parentNode.classList.add(`${elementIdPrefix}custom-search-input-header`, 'r41-custom-search-input-header');
      h3Element.parentNode.insertBefore(input, h3Element.nextSibling)
      // h3Element.parentNode.replaceChild(input, h3Element);
      input.select();
      input.addEventListener('input', () => handleInputChange(input));

      // Add loading spinner (hidden by default)
      const spinner = document.createElement('div');
      spinner.classList.add('r41-loading-spinner')
      spinner.id = `${elementIdPrefix}loading-spinner`;
      input.parentNode.insertBefore(spinner, input.nextSibling);
    }
    let lastState = false;
    function setLoadingState(isLoading) {
      if (lastState === isLoading) return;
      const spinner = document.getElementById(`${elementIdPrefix}loading-spinner`);
      if (spinner) {
        spinner.style.display = isLoading ? 'inline-block' : 'none';
      }
      lastState = isLoading;
    }

  let busyCache = 0;

    async function handleInputChange(input) {
      const currentChangeCount = ++globalChangeCounter;
      if (busyCache === 0) {
        setLoadingState(true);
        await fetchAndCacheScripts();
        await new Promise(resolve => setTimeout(resolve, 140));
      } else if (busyCache === 1) {
        setLoadingState(true);
        await new Promise(resolve => setTimeout(resolve, 600));
      } else if (busyCache === 2) {
        setLoadingState(true);
        await new Promise(resolve => setTimeout(resolve, 140));
      }
      if (currentChangeCount === globalChangeCounter) {
        setLoadingState(true);
        const filteredResults = await fetchAndFilterScripts(input.value);
        updateCurrentList(filteredResults);
        setLoadingState(false);
      }
    }

    function saveCache(url, text) {
      url = fixURL(url);
      if (!url) return;
      text = LZString.compress(text || "");
      text = text || ""
      sessionStorage.setItem(`${storagePrefix}${url}`, text);
      sessionStorage.setItem(`${storagePrefix}urls`, (sessionStorage.getItem(`${storagePrefix}urls`) || "") + "|" + url + "|");

    }
    function restoreCache(url) {
      url = fixURL(url);
      if (!url) return;
      let text = LZString.decompress(sessionStorage.getItem(`${storagePrefix}${url}`) || "") || "";
      return text;

    }

    async function fetchAndCacheScripts() {
      if (busyCache > 0) return;
      busyCache = 1;
      const pages = Array.from(document.querySelectorAll(`#${sectionId} .pagination > a[href]`))
        .map(link => link.getAttribute('href'));


      let mMap = new Map();
      for (const url of pages) {
        mMap.set(fixURL(url), url)

      }


      for (const [fixedURL, pageURL] of mMap) {
        const page = pageURL;
        let url = fixedURL;
        if (url && !sessionStorage.getItem(`${storagePrefix}${url}`)) {
          const response = await fetch(page);
          const text = await response.text();
          console.log(123, Date.now(), page)
          saveCache(page, text);
        }
      }
      busyCache = 2;
    }



    async function fetchAndFilterScripts(inputValue) {
      inputValue = inputValue || '';
      inputValue = genericText(inputValue, 1 | 2 | 8);
      let pages = Array.from(document.querySelectorAll(`#${sectionId} .pagination > a[href]`))
        .map(link => link.getAttribute('href'));

      let mMap = new Map();
      for (const url of pages) {
        mMap.set(fixURL(url), url)

      }

      const allScripts = [];

      // Get current page's list from sessionStorage
      const currentPageHtml = restoreCache(location.href);
      const currentDoc = new DOMParser().parseFromString(currentPageHtml, 'text/html');
      filterScripts(currentDoc, inputValue || true, allScripts);
      for (const [fixedURL, pageURL] of mMap) {

        let page = pageURL;
        let pageHtml = restoreCache(page);
        if (!pageHtml) {
          const response = await fetch(page);
          const text = await response.text();
          console.log(456, Date.now(), page)
          saveCache(page, text);
          pageHtml = text;
        }

        const doc = new DOMParser().parseFromString(pageHtml, 'text/html');
        filterScripts(doc, inputValue || false, allScripts);

      }

      return allScripts;
    }

    function filterScripts(doc, inputValue, allScripts) {
      if (inputValue === false) return;
      const scripts = doc.querySelectorAll(`ol#${listId} li[data-script-id]`);
      scripts.forEach(li => {
        const html = getContentHTML(li, inputValue);
        if (html) allScripts.push(html);
      });
    }

    function getContentHTML(li, inputValue) {
      const name = genericText(li.querySelector('a.script-link[href]').textContent, 1 | 2 | 8);
      const description = genericText(li.querySelector('.script-description').textContent, 1 | 2 | 8);
      let text = name + `\n` + description;
      if (inputValue === true || text.toLowerCase().includes(inputValue.toLowerCase())) {
        return li.outerHTML;
      }
    }

    function updateCurrentList(filteredResults) {
      const list = document.querySelector(`ol#${listId}`);
      if (list) {
        list.innerHTML = filteredResults.join('');
      }
    }


  };

  hookToFn({
    listId: 'user-script-list',
    sectionId: 'user-script-list-section',
    storagePrefix: 'gF_7H8TV_',
    elementIdPrefix: 'gimoa-'
  });

  hookToFn({
    listId: 'user-unlisted-script-list',
    sectionId: 'user-unlisted-script-list-section',
    storagePrefix: 'gF_84IUu_',
    elementIdPrefix: 'jexsq-'
  });

  hookToFn({
    listId: 'user-library-script-list',
    sectionId: 'user-library-list-section',
    storagePrefix: 'gF_39rrO_',
    elementIdPrefix: 'm01xt-'
  });