Comick Random Comic

Add a random comic button to your Comick reading list

// ==UserScript==
// @name         Comick Random Comic
// @namespace    https://github.com/GooglyBlox
// @version      1.0
// @description  Add a random comic button to your Comick reading list
// @author       GooglyBlox
// @match        https://comick.io/*
// @grant        none
// @license      MIT
// ==/UserScript==

;(function() {
  'use strict'

  ;['pushState', 'replaceState'].forEach(fn => {
    const orig = history[fn]
    history[fn] = function() {
      const ret = orig.apply(this, arguments)
      window.dispatchEvent(new Event('locationchange'))
      return ret
    }
  })
  window.addEventListener('popstate', () => {
    window.dispatchEvent(new Event('locationchange'))
  })

  window.addEventListener('locationchange', onRouteChange)
  document.addEventListener('DOMContentLoaded', onRouteChange)

  async function onRouteChange() {
    if (!/\/user\/[^/]+\/list/.test(location.pathname)) {
      removeButton()
      return
    }

    const filterBtn = await waitForFilterButton()
    removeButton()
    injectRandomButton(filterBtn)
  }

  function waitForFilterButton(timeout = 10000) {
    return new Promise((resolve, reject) => {
      const start = Date.now()
      const tick = () => {
        const btn = findFilterButton()
        if (btn) return resolve(btn)
        if (Date.now() - start > timeout) {
          return reject('Filter button did not appear')
        }
        setTimeout(tick, 300)
      }
      tick()
    })
  }

  function findFilterButton() {
    return Array.from(document.querySelectorAll('button[type="button"]'))
      .find(b => b.textContent.includes('Filter') && b.querySelector('svg')) || null
  }

  function removeButton() {
    const old = document.querySelector('#random-comic-btn')
    if (old) old.remove()
  }

  function injectRandomButton(filterBtn) {
    const btn = document.createElement('button')
    btn.id = 'random-comic-btn'
    btn.type = 'button'
    btn.className = filterBtn.className + ' mr-5'
    btn.innerHTML = getIcon() + 'Random'

    btn.addEventListener('click', async () => {
      btn.disabled = true
      btn.innerHTML = getIcon() + 'Loading...'
      const allComics = await loadAndIndexAllComics()
      goToRandomComic(allComics)
    })

    filterBtn.parentNode.insertBefore(btn, filterBtn)
  }

  function getIcon() {
    return `
      <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="h-4 w-4 mr-2">
          <rect width="12" height="12" x="2" y="10" rx="2" ry="2"/>
          <path d="m17.92 14 3.5-3.5a2.24 2.24 0 0 0 0-3l-5-4.92a2.24 2.24 0 0 0-3 0L10 6"/>
          <path d="M6 18h.01"/>
          <path d="M10 14h.01"/>
          <path d="M15 6h.01"/>
          <path d="M18 9h.01"/>
      </svg>
    `
  }

  function loadAndIndexAllComics() {
    return new Promise(resolve => {
      const allComics = new Map()
      let stableCount = 0
      let lastSize = 0

      window.scrollTo(0, 0)

      const iv = setInterval(() => {
        const currentComics = getCurrentComicLinks()
        currentComics.forEach(comic => {
          allComics.set(comic.href, comic)
        })

        if (allComics.size === lastSize) {
          stableCount++
          if (stableCount >= 2) {
            clearInterval(iv)
            window.scrollTo(0, 0)
            return resolve(Array.from(allComics.values()))
          }
        } else {
          stableCount = 0
          lastSize = allComics.size
        }

        window.scrollBy(0, window.innerHeight * 2)
      }, 50)
    })
  }

  function getCurrentComicLinks() {
    return Array.from(document.querySelectorAll('a[href^="/comic/"]')).filter(a => {
      const parts = a.getAttribute('href').split('/')
      const rect = a.getBoundingClientRect()
      return parts.length === 3
          && parts[2]
          && rect.width > 0
          && rect.height > 0
          && a.textContent.trim()
          && (a.classList.contains('link-effect-no-ring') || !a.querySelector('span'))
    }).map(a => ({
      href: a.href,
      title: a.textContent.trim()
    }))
  }

  function goToRandomComic(allComics) {
    if (!allComics.length) {
      alert('No comics found in your current filtered list!')
      resetButton()
      return
    }

    const randomComic = allComics[Math.floor(Math.random() * allComics.length)]
    location.href = randomComic.href
  }

  function resetButton() {
    const btn = document.querySelector('#random-comic-btn')
    if (btn) {
      btn.disabled = false
      btn.innerHTML = getIcon() + 'Random'
    }
  }
})()