Baslimbus Owned Toggle

패시브 페이지에서 '내 소유'만 보기 토글

// ==UserScript==
// @name Baslimbus Owned Toggle
// @match https://baslimbus.info/passive
// @namespace https://dlwlsdn3642.github.io/
// @description  패시브 페이지에서 '내 소유'만 보기 토글
// @version   1.0.0
// @license      MIT
// @run-at document-idle
// @grant none
// @namespace
// ==/UserScript==

(function () {
  const SEASON_KIND = 6;
  const OWNED_LS_KEY = 'selectedIdentities';
  const BTN_LABEL_HTML = '<span class="text-xl text-white">내 소유</span>';
  const BTN_ID = 'owned-support-btn';

  function injectPageScript(js) {
    const s = document.createElement('script');
    s.textContent = js;
    (document.head || document.documentElement).appendChild(s);
    s.remove();
  }

  injectPageScript(`
    (function(){
      const SEASON_KIND=${SEASON_KIND};
      const OWNED_LS_KEY='${OWNED_LS_KEY}';
      const state={on:false,idx:null};
      const norm=s=>(s??'').toString().trim().replace(/\\s+/g,'').normalize('NFC');
      const loadOwned=()=>{try{return new Set(JSON.parse(localStorage.getItem(OWNED_LS_KEY)||'[]').map(Number).filter(Number.isFinite));}catch{return new Set();}};
      async function buildIndex(){
        if(state.idx) return state.idx;
        const map=new Map();
        for(let p=0;p<200;p++){
          const r=await fetch('https://baslimbus.store/dictionary/paginated/identity?size=100&page='+p,{cache:'no-store'});
          if(!r.ok) break;
          const d=await r.json();
          const list=d.list||d;
          for(const it of (list||[])){
            const id=Number(it?.id);
            if(Number.isFinite(id)) map.set(\`\${norm(it?.character)}|\${norm(it?.name)}|\${it?.season??''}\`,id);
          }
          if(d.last===true) break;
        }
        state.idx=map;
        return map;
      }
      function isSkill(input,init){
        try{
          const u=typeof input==='string'?new URL(input,location.origin):new URL(input.url,location.origin);
          const m=(init&&init.method)?String(init.method).toUpperCase():'GET';
          return m==='GET'&&u.pathname.startsWith('/dictionary/paginated/skill/')&&u.pathname.endsWith('/'+SEASON_KIND);
        }catch{return false;}
      }
      const _fetch=window.fetch;
      window.fetch=async function(input,init){
        const res=await _fetch(input,init);
        if(!state.on||!isSkill(input,init)) return res;
        const ct=res.headers.get('content-type')||'';
        if(!ct.includes('application/json')) return res;
        let data; try{data=await res.clone().json();}catch{return res;}
        const list=Array.isArray(data?.list)?data.list:(Array.isArray(data)?data:null);
        if(!Array.isArray(list)||list.length===0) return res;
        const idx=await buildIndex();
        const owned=loadOwned();
        const filtered=list.filter(it=>{
          const key=\`\${norm(it?.sinnerName)}|\${norm(it?.identityName)}|\${it?.season??''}\`;
          const id=idx.get(key);
          return id!=null&&owned.has(id);
        });
        const body=Array.isArray(data)?filtered:{...data,list:filtered};
        return new Response(JSON.stringify(body),{status:res.status,statusText:res.statusText,headers:{'Content-Type':'application/json'}});
      };
      window.addEventListener('message',e=>{
        const m=e?.data; if(!m||m.__ownedSupport!==true) return;
        if(m.type==='toggle'){state.on=!!m.enabled; if(state.on&&!state.idx) buildIndex();}
      });
    })();
  `);

  let btn=null, on=false;

  function setBtn() {
    if (!btn) return;
    btn.classList.remove('bg-primary-400','border','border-primary-100','bg-primary-450');
    if (on) btn.classList.add('bg-primary-400','border','border-primary-100');
    else btn.classList.add('bg-primary-450');
  }

  function mask(el, ms=100){
    if(!el) return ()=>{};
    const wrap=document.createElement('div');
    wrap.style.position='fixed';
    wrap.style.zIndex='2147483647';
    wrap.style.pointerEvents='none';
    const clone=el.cloneNode(true);
    clone.style.transition='none';
    clone.style.animation='none';
    clone.style.margin='0';
    wrap.appendChild(clone);
    document.body.appendChild(wrap);
    let stop=false;
    const place=()=>{if(stop)return;const r=el.getBoundingClientRect();wrap.style.left=r.left+'px';wrap.style.top=r.top+'px';wrap.style.width=r.width+'px';wrap.style.height=r.height+'px';requestAnimationFrame(place);};
    place();
    const off=()=>{if(stop)return;stop=true;wrap.remove();};
    setTimeout(off,ms);
    return off;
  }

  function fire(el){
    ['pointerdown','mousedown','mouseup','click'].forEach(t=>el.dispatchEvent(new MouseEvent(t,{bubbles:true,cancelable:true,view:window})));
  }

  function anchor(){
    const g=document.querySelector('div.grid.grid-cols-3');
    return g?.querySelector('button:nth-of-type(2)')||null;
  }

  function reload(){
    const el=anchor(); if(!el) return;
    mask(el,100); fire(el); setTimeout(()=>fire(el),0);
  }

  function toggle(){
    on=!on; setBtn();
    window.postMessage({__ownedSupport:true,type:'toggle',enabled:on},'*');
    let i=0;(function loop(){if(anchor()){reload();return;} if(++i<10)setTimeout(loop,120);})();
  }

  function gridNode(){
    return document.querySelector('div.bg-primary-500.w-full.rounded.p-4.flex.flex-col.gap-2 div.grid.grid-cols-3')||document.querySelector('div.grid.grid-cols-3');
  }

  function upsert(){
    const g=gridNode(); if(!g) return;
    let exist=document.getElementById(BTN_ID);
    if(exist && exist.parentElement!==g) g.appendChild(exist);
    if(!exist){
      const s=g.querySelector('button, a[role="button"], a[href]');
      exist=document.createElement('button');
      exist.id=BTN_ID;
      exist.type='button';
      exist.className=s?s.className:'inline-flex items-center justify-center rounded-md border px-3 py-1 text-sm font-medium transition bg-primary-450';
      exist.style.whiteSpace='nowrap';
      exist.innerHTML=BTN_LABEL_HTML;
      exist.addEventListener('click',toggle);
      g.appendChild(exist);
    }
    btn=exist; setBtn();
  }

  let scheduled=false;
  const schedule=()=>{ if(scheduled) return; scheduled=true; requestAnimationFrame(()=>{scheduled=false; upsert();}); };

  const mo=new MutationObserver(schedule);
  mo.observe(document.documentElement,{childList:true,subtree:true});

  window.addEventListener('load',schedule);
  document.addEventListener('visibilitychange',schedule,{passive:true});
})();