Restore Right/Middle-Click New Tab Links for manko.fun / solji.kim

Restore right/middle-click new tab function on manko.fun / solji.kim.

// ==UserScript==
// @name         Restore Right/Middle-Click New Tab Links for manko.fun / solji.kim
// @namespace    http://tampermonkey.net/
// @version      1.3.1
// @description  Restore right/middle-click new tab function on manko.fun / solji.kim.
//               Note: This script works properly in Tampermonkey when Developer Mode is enabled.
//
// @author       VanillaMilk
// @license      MIT
// @match        https://manko.fun/*
// @match        https://solji.kim/*
// @run-at       document-end
// @grant        GM_registerMenuCommand
// @grant        GM_setValue
// @grant        GM_getValue
// ==/UserScript==

(async function () {
  'use strict'

  const ID_ATTR='data-id'
  const DEST_BASE='https://solji.kim/movie-info/'
  const KEY_OPEN_BLANK='mf_linkifier_open_blank'
  const KEY_SIDE_NAV='mf_linkifier_side_nav'
  const Rt = {
    en: { popular:"Popular","most popular":"Most Popular","top rated":"Top Rated",top_rated:"Top Rated",new:"New",subtitle:"Subtitle",random:"Random",category:"Category",actor:"Actor",genre:"Genre",country:"Country",usecase:"Download app",censored:"Censored",uncensored:"Uncensored",fc2:"FC2" },
    zh: { popular:"热门作品","most popular":"热门作品","top rated":"最佳评分",top_rated:"最佳评分",new:"最新作品",subtitle:"字幕",random:"随机",category:"类别",actor:"演员",genre:"类型",country:"国家",usecase:"下载应用程序",censored:"马赛克",uncensored:"无马赛克",fc2:"FC2" },
    tw: { popular:"受歡迎的","most popular":"受歡迎的","top rated":"最高評分",top_rated:"最高評分",new:"新的",subtitle:"字幕",random:"隨機的",category:"類別",actor:"演員",genre:"類型",country:"國家",usecase:"下載應用程式",censored:"審查",uncensored:"未經審查",fc2:"FC2" },
    ja: { popular:"人気","most popular":"人気","top rated":"最高評価",top_rated:"最高評価",new:"新作",subtitle:"字幕",random:"ランダム",category:"カテゴリー",actor:"俳優",genre:"ジャンル",country:"国名",usecase:"アプリをダウンロード",censored:"修正",uncensored:"無修正",fc2:"FC2" },
    ko: { popular:"인기작","most popular":"인기작","top rated":"최고평점",top_rated:"최고평점",new:"신작",subtitle:"자막",random:"랜덤",category:"카테고리",actor:"배우",genre:"장르",country:"국가",usecase:"앱 다운로드",censored:"유모",uncensored:"노모",fc2:"FC2" },
    id: { popular:"Populer","most popular":"Populer","top rated":"Peringkat Teratas",top_rated:"Peringkat Teratas",new:"Baru",subtitle:"Subjudul",random:"Acak",category:"Kategori",actor:"Aktor",genre:"Genre",country:"Negara",usecase:"Unduh aplikasi",censored:"Disensor",uncensored:"Tanpa Sensor",fc2:"FC2" },
    ms: { popular:"Popular","most popular":"Popular","top rated":"Tertinggi",top_rated:"Tertinggi",new:"baru",subtitle:"Sari kata",random:"rawak",category:"kategori",actor:"pelakon",genre:"Genre",country:"Negara",usecase:"Muat turun aplikasi",censored:"ditapis",uncensored:"Tidak ditapis",fc2:"FC2" },
    th: { popular:"เป็นที่นิยม","most popular":"เป็นที่นิยม","top rated":"อันดับสูงสุด",top_rated:"อันดับสูงสุด",new:"ใหม่",subtitle:"คำบรรยาย",random:"สุ่ม",category:"หมวดหมู่",actor:"นักแสดงชาย",genre:"ประเภท",country:"ประเทศ",usecase:"ดาวน์โหลดแอป",censored:"เซ็นเซอร์",uncensored:"ไม่เซ็นเซอร์",fc2:"เอฟซี2" },
    vi: { popular:"Phổ biến","most popular":"Phổ biến","top rated":"Đánh giá cao nhất",top_rated:"Đánh giá cao nhất",new:"Mới",subtitle:"Phụ đề",random:"Ngẫu nhiên",category:"Loại",actor:"Diễn viên",genre:"Thể loại",country:"Quốc gia",usecase:"Tải ứng dụng",censored:"Bị kiểm duyệt",uncensored:"Không kiểm duyệt",fc2:"FC2" }
  }

  function englishParamForKey(key){
    if(!key) return null
    if(key==='popular') return Rt.en['most popular'] || 'Most Popular'
    return Rt.en[key] || key
  }
  function cleanText(t){
    return (t||'').replace(/[\p{Extended_Pictographic}\p{Emoji_Presentation}\uFE0F\u200D\u2600-\u27BF]/gu,'').trim()
  }
  function labelToKey(label){
    const t = cleanText(label).toLowerCase()
    for(const map of Object.values(Rt)){
      for(const [k,v] of Object.entries(map)){
        if(String(v).toLowerCase()===t) return k
      }
    }
    if(t==='top rated') return 'top rated'
    if(t==='most popular') return 'most popular'
    return null
  }

  async function gv(k,d='0'){try{return String(await GM_getValue(k,d))==='1'}catch{return false}}
  async function sv(k,v){try{await GM_setValue(k,v?'1':'0')}catch{}}

  let useBlank=await gv(KEY_OPEN_BLANK,'0')
  let sideEnable=await gv(KEY_SIDE_NAV,'0')

  if(typeof GM_registerMenuCommand==='function'){
    GM_registerMenuCommand((useBlank?'☑ ':'☐ ')+'Left-click opens in new tab',async()=>{useBlank=!useBlank;await sv(KEY_OPEN_BLANK,useBlank);location.reload()})
    GM_registerMenuCommand((sideEnable?'☑ ':'☐ ')+'Side buttons: page ±1',async()=>{sideEnable=!sideEnable;await sv(KEY_SIDE_NAV,sideEnable);location.reload()})
  }

(function normalizePageForPagination() {
  try {
    const u = new URL(location.href);
    const path = u.pathname;
    const isEligible = /^\/(movie-list|cate-list|actor-list|home)/.test(path);
    const isExcluded = /^\/(genre|maker|usecase)/.test(path);

    if (isEligible && !isExcluded && !u.searchParams.has('page')) {
      u.searchParams.set('page', '1');
      history.replaceState(null, '', u.toString());
    }
  } catch {}
})();

if (sideEnable) {
  let last = 0;
  const side = (e) => {
    if (e.button !== 3 && e.button !== 4) return;

    const u = new URL(location.href);
    const path = u.pathname;
    const isEligible = /^\/(movie-list|cate-list|actor-list)/.test(path);
    const isExcluded = /^\/(genre|maker|usecase)/.test(path);

    const hasQ = u.searchParams.has('page');
    const hs = u.hash.startsWith('#') ? u.hash.slice(1) : u.hash;
    const sp = new URLSearchParams(hs);
    const hasH = sp.has('page');

    if (isEligible && !isExcluded && !hasQ && !hasH) {
      u.searchParams.set('page','1');
      e.stopImmediatePropagation?.(); e.stopPropagation?.(); e.preventDefault?.();
      location.assign(u.toString());
      return;
    }

    if (!hasQ && !hasH) return;

    const now = Date.now(); if (now - last < 120) return; last = now;
    const delta = (e.button === 3) ? -1 : 1;

    let p = hasQ ? parseInt(u.searchParams.get('page') || '1', 10)
                 : parseInt(sp.get('page') || '1', 10);
    if (!Number.isFinite(p) || p < 1) p = 1;

    let n = p + delta; if (n < 1) n = 1;

    if (hasQ) {
      u.searchParams.set('page', String(n));
    } else {
      sp.set('page', String(n));
      u.hash = '#' + sp.toString();
    }

    e.stopImmediatePropagation?.(); e.stopPropagation?.(); e.preventDefault?.();
    location.assign(u.toString());
  };

  addEventListener('mouseup', side, true);
  addEventListener('pointerup', side, true);
}


  const style=document.createElement('style')
  style.textContent=`
    .tm-wrap{position:relative!important;}
    .tm-ol{position:absolute!important;inset:0!important;width:100%!important;height:100%!important;display:block!important;background:transparent!important;text-decoration:none!important;outline:none!important;pointer-events:auto!important;}
  `
  document.documentElement.appendChild(style)

  function wrap(el, forceNew){
    if(!forceNew){
      const p=el.parentElement
      if(p){const cs=getComputedStyle(p);if(cs.position!=='static')return p}
    }
    const w=document.createElement('span');w.className='tm-wrap';w.style.position='relative'
    el.parentNode?.insertBefore(w,el);w.appendChild(el);return w
  }

  function setOverlay(el,href,forceNew){
    const w=wrap(el,forceNew)
    let a=w.querySelector(':scope > a.tm-ol')
    if(!a){a=document.createElement('a');a.className='tm-ol';a.rel='noopener noreferrer';w.appendChild(a)}
    a.href=href
    a.target=useBlank?'_blank':'_self'
    return a
  }

  function linkifyCard(card){
    if(!(card instanceof Element))return
    if(!card.hasAttribute(ID_ATTR))return
    if(card.__tm_done__)return
    const raw=card.getAttribute(ID_ATTR)||''
    const id=raw.split('?')[0].trim()
    if(!id)return
    setOverlay(card,DEST_BASE+encodeURIComponent(id),true)
    card.__tm_done__=true
  }

  function scanCards(root){
    root.querySelectorAll(`[${ID_ATTR}]`).forEach(linkifyCard)
    linkifyCard(root)
  }
  scanCards(document.body||document.documentElement)

  const moCards=new MutationObserver(muts=>{
    for(const m of muts){
      if(m.type==='childList')m.addedNodes.forEach(n=>n.nodeType===1&&scanCards(n))
      else if(m.type==='attributes'&&m.attributeName===ID_ATTR)linkifyCard(m.target)
    }
  })
  moCards.observe(document.documentElement,{childList:true,subtree:true,attributes:true,attributeFilter:[ID_ATTR]})

  function hrefForTopLabel(txt){
    const key = labelToKey(txt)
    if(!key) return null
    if(key==='category' || key==='actor') return null
    if(key==='genre') return '/genre'
    if(key==='country') return '/maker'
    if(key==='usecase') return '/usecase'
    const cat = englishParamForKey(key) || cleanText(txt)
    return `/movie-list?category=${encodeURIComponent(cat)}`
  }

  function linkifyTopNav(root){
    root.querySelectorAll('nav button').forEach(btn=>{
      if(btn.__tm_top__)return
      const txt=cleanText(btn.textContent||'');if(!txt)return
      const href=hrefForTopLabel(txt)
      if(!href) return
      setOverlay(btn,href,true)
      btn.__tm_top__=true
    })
  }

  const CATEGORY_FIXED_KEYS=new Set(['censored','uncensored','popular','top rated','top_rated','fc2'])
  const ACTOR_FIXED_KEYS =new Set(['censored','uncensored'])

  function resolveDropdownMode(pop){
    let n=pop.previousElementSibling||pop.parentElement?.previousElementSibling
    if(n){
      const k = labelToKey(cleanText(n.textContent||''))
      if(k==='actor') return 'actor'
      if(k==='category') return 'category'
    }
    return 'category'
  }

  function linkifyDropdown(pop){
    const mode=resolveDropdownMode(pop)
    pop.querySelectorAll('button').forEach(btn=>{
      if(btn.__tm_dd__)return
      const label=cleanText(btn.textContent||'');if(!label)return

      const key = labelToKey(label)
      let href
      if(mode==='actor'){
        if(key && ACTOR_FIXED_KEYS.has(key)){
          const cat = englishParamForKey(key) || key
          href = `/actor-list?category=${encodeURIComponent(cat)}&page=1`
        }else{
          href = `/actor-list?category=${encodeURIComponent(label)}&page=1`
        }
      }else{
        if(key && CATEGORY_FIXED_KEYS.has(key)){
          const cat = englishParamForKey(key) || key
          href = `/cate-list?category=${encodeURIComponent(cat)}&page=1`
        }else{
          href = `/cate-list?category=${encodeURIComponent(label)}&page=1`
        }
      }

      setOverlay(btn,href,true)
      btn.__tm_dd__=true
    })
  }

  function fullScan(root){
    linkifyTopNav(root)
    root.querySelectorAll('div.absolute').forEach(div=>{
      if(div.querySelector('button'))linkifyDropdown(div)
    })
  }
  fullScan(document.body||document.documentElement)

  const moMenus=new MutationObserver(muts=>{
    for(const m of muts){
      if(m.type==='childList'){
        m.addedNodes.forEach(n=>{if(n.nodeType===1)fullScan(n)})
      }else if(m.type==='attributes'&&(m.attributeName==='class'||m.attributeName==='style'||m.attributeName==='aria-expanded')){
        if(m.target instanceof Element)fullScan(m.target)
      }
    }
  })
  moMenus.observe(document.documentElement,{childList:true,subtree:true,attributes:true,attributeFilter:['class','style','aria-expanded']})
})()