Comick Comic Type Column

Adds a column showing comic type (Manga/Manhwa/Manhua) based on country

// ==UserScript==
// @name         Comick Comic Type Column
// @namespace    https://github.com/GooglyBlox
// @version      1.1
// @description  Adds a column showing comic type (Manga/Manhwa/Manhua) based on country
// @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(...args) {
      const ret = orig.apply(this, args)
      window.dispatchEvent(new Event('locationchange'))
      return ret
    }
  })
  window.addEventListener('popstate', () => window.dispatchEvent(new Event('locationchange')))
  window.addEventListener('DOMContentLoaded', onRouteChange)
  window.addEventListener('locationchange', onRouteChange)

  const comicCountryMap = new Map()
  let tableObserver = null

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

    const userId = extractUserIdFromUrl()
    if (!userId) return
    const follows = await fetchUserFollows(userId)
    buildComicCountryMap(follows)

    await waitForSelector('.flex.w-full.items-center.min-w-0', 10_000).catch(() => null)
    teardown()
    processTable()
    setupMutationObserver()
  }

  function teardown() {
    document.querySelectorAll('[data-comic-type-header]').forEach(el => el.remove())
    document.querySelectorAll('[data-comic-type-cell]').forEach(el => el.remove())
    if (tableObserver) {
      tableObserver.disconnect()
      tableObserver = null
    }
  }

  function setupMutationObserver() {
    if (tableObserver) return

    const debounce = (fn, ms=100) => {
      let t
      return (...args) => {
        clearTimeout(t)
        t = setTimeout(() => fn(...args), ms)
      }
    }

    tableObserver = new MutationObserver(debounce(() => {
      processTable()
    }))
    tableObserver.observe(document.body, { childList: true, subtree: true })
  }

  function processTable() {
    const rows = Array.from(document.querySelectorAll('.flex.w-full.items-center.min-w-0'))
    if (!rows.length) return

    const [header, ...dataRows] = rows
    addTypeColumnHeader(header)
    dataRows.forEach(addTypeColumnToRow)
  }

  function addTypeColumnHeader(headerRow) {
    if (headerRow.querySelector('[data-comic-type-header]')) return

    const th = document.createElement('div')
    th.className = 'text-center pl-3 flex items-center h-12 w-20 lg:w-24 font-semibold flex-none text-sm'
    th.setAttribute('data-comic-type-header', '')
    th.innerHTML = '<div class="w-full text-center">Type</div>'

    const ref = headerRow.children[headerRow.children.length - 3]
    headerRow.insertBefore(th, ref)
  }

  function addTypeColumnToRow(row) {
    if (row.querySelector('[data-comic-type-cell]')) return

    const link = row.querySelector('a[href*="/comic/"]')
    if (!link) return

    const slug = link.getAttribute('href').split('/comic/')[1]
    const country = comicCountryMap.get(slug)
    const type = country ? getComicTypeFromCountry(country) : 'Unknown'

    const td = document.createElement('div')
    td.className = 'flex justify-center text-sm md:text-base w-20 lg:w-24 flex-none'
    td.setAttribute('data-comic-type-cell', '')
    td.innerHTML = `<div class="w-full text-center">${type}</div>`

    const ref = row.children[row.children.length - 3]
    row.insertBefore(td, ref)
  }

  function extractUserIdFromUrl() {
    const m = location.pathname.match(/\/user\/([^/]+)\//)
    return m ? m[1] : null
  }

  async function fetchUserFollows(userId) {
    try {
      const res = await fetch(`https://api.comick.io/user/${userId}/follows`)
      if (!res.ok) throw new Error(res.status)
      return await res.json()
    } catch (e) {
      console.error('Failed to fetch follows:', e)
      return []
    }
  }

  function buildComicCountryMap(follows) {
    comicCountryMap.clear()
    follows.forEach(item => {
      const c = item.md_comics
      if (c && c.slug && c.country) comicCountryMap.set(c.slug, c.country)
    })
  }

  function getComicTypeFromCountry(country) {
    switch (country.toLowerCase()) {
      case 'kr': case 'gb': return 'Manhwa'
      case 'cn':             return 'Manhua'
      case 'jp':             return 'Manga'
      default:               return 'Unknown'
    }
  }

  function waitForSelector(selector, timeout = 5000) {
    return new Promise((resolve, reject) => {
      const el = document.querySelector(selector)
      if (el) return resolve(el)

      const obs = new MutationObserver(() => {
        const found = document.querySelector(selector)
        if (found) {
          obs.disconnect()
          resolve(found)
        }
      })
      obs.observe(document.body, { childList: true, subtree: true })

      setTimeout(() => {
        obs.disconnect()
        reject(`Timeout: ${selector}`)
      }, timeout)
    })
  }
})();