Nicon Tag Allegation Autofill

comment_allegation/* で getthumbinfo を参照し、ロック済み×WL一致のタグを本文先頭へ。

// ==UserScript==
// @name         Nicon Tag Allegation Autofill
// @namespace    https://greasyfork.org/users/prozent55
// @version      1.1.1
// @description  comment_allegation/* で getthumbinfo を参照し、ロック済み×WL一致のタグを本文先頭へ。
// @match        https://www.nicovideo.jp/comment_allegation/*
// @run-at       document-idle
// @connect      ext.nicovideo.jp
// @grant        GM.xmlHttpRequest
// @grant        GM_xmlHttpRequest
// @license      MIT
// ==/UserScript==

(() => {
  'use strict';

  const DEFAULT_TARGET='tag', DEFAULT_ITEM='search_interference';
  const DEFAULT_REASON_BODY='動画の内容とは無関係なタグがロックされており、削除できません。\nタグ検索や関連機能に支障をきたすため、荒らし行為にあたり利用規約違反の可能性があると判断しました。';

  const WL_KEY='zr_tag_wl_v1', REASON_KEY='zr_reason_body_v1';
  const WL_DEFAULT=['真夏の夜の淫夢','淫夢実況シリーズ','ひとくち淫夢','淫夢本編リンク','本編改造淫夢','BB先輩シリーズ','ホラー淫夢','タクヤさん'];
  const loadWL=()=>{try{const s=localStorage.getItem(WL_KEY);return s?JSON.parse(s):WL_DEFAULT.slice();}catch{return WL_DEFAULT.slice();}};
  const saveWL=(arr)=>{try{localStorage.setItem(WL_KEY,JSON.stringify(arr||[]));}catch{}};

  const getReason=()=>{try{const s=localStorage.getItem(REASON_KEY);return s??DEFAULT_REASON_BODY;}catch{return DEFAULT_REASON_BODY;}};
  const setReason=(txt)=>{try{(txt==null||txt==='')?localStorage.removeItem(REASON_KEY):localStorage.setItem(REASON_KEY,String(txt));}catch{}};

  const KP='zippy_nico_tag_form_',K_TARGET=KP+'target',K_ITEM=KP+'item',K_TEXT=KP+'text';
  const storage={async get(k,d){try{const v=localStorage.getItem('__'+k);return v==null?d:JSON.parse(v);}catch{return d;}},async set(k,v){try{localStorage.setItem('__'+k,JSON.stringify(v));}catch{}}};

  const norm=s=>(s||'').trim().toLowerCase();
  const qs=(s,r=document)=>r.querySelector(s);
  const qsa=(s,r=document)=>Array.from(r.querySelectorAll(s));
  const fire=(el)=>{if(!el)return;el.dispatchEvent(new Event('input',{bubbles:true}));el.dispatchEvent(new Event('change',{bubbles:true}));};
  function setVal(el,val){const proto=el?.constructor?.prototype||HTMLTextAreaElement.prototype;const d=proto&&Object.getOwnPropertyDescriptor(proto,'value');d?.set?d.set.call(el,val):el.value=val;fire(el);}
  function toast(msg,ms=1200){const n=document.createElement('div');n.textContent=msg;Object.assign(n.style,{position:'fixed',right:'16px',bottom:'110px',zIndex:999999,background:'#00c853',color:'#fff',padding:'8px 10px',borderRadius:'8px',boxShadow:'0 4px 12px rgba(0,0,0,.25)',opacity:'0',transition:'opacity .15s'});document.body.appendChild(n);requestAnimationFrame(()=>n.style.opacity='1');setTimeout(()=>{n.style.opacity='0';setTimeout(()=>n.remove(),180);},ms);}

  function waitForForm(ms=8000){return new Promise(res=>{const pick=()=>{const radios=qsa('input[type="radio"][name="target"]');const select=qs('select[name="select_allegation"]');const ta=qs('textarea[name="inquiry"]#inquiry');return(radios.length&&select&&ta)?{radios,select,ta}:null;};const first=pick();if(first)return res(first);const to=setTimeout(()=>{mo.disconnect();res(null);},ms);const mo=new MutationObserver(()=>{const f=pick();if(f){clearTimeout(to);mo.disconnect();res(f);}});mo.observe(document.body,{childList:true,subtree:true});});}

  function videoIdFromPath(){const m=location.pathname.match(/\/comment_allegation\/([a-z]{2}\d+)/i);return m?m[1]:null;}
  function httpGet(url){return new Promise((resolve,reject)=>{const fn=(typeof GM?.xmlHttpRequest==='function')?GM.xmlHttpRequest:(typeof GM_xmlhttpRequest==='function')?GM_xmlHttpRequest:null;if(!fn)return reject(new Error('GM.xmlHttpRequest not available'));fn({method:'GET',url,onload:r=>resolve(r.responseText),onerror:reject});});}
  async function fetchTags(videoId){if(!videoId)return[];const url=`https://ext.nicovideo.jp/api/getthumbinfo/${encodeURIComponent(videoId)}`;let xml='';try{xml=await httpGet(url);}catch{return[];}let doc;try{doc=new DOMParser().parseFromString(xml,'text/xml');}catch{return[];}const nodes=Array.from(doc.querySelectorAll('thumb > tags > tag, tags > tag'));return nodes.map(t=>({name:(t.textContent||'').trim(),locked:(t.getAttribute('lock')==='1'||t.getAttribute('locked')==='1')})).filter(x=>x.name);}
  function filterWLAllLocked(thumbTags,wl){const src=thumbTags.filter(t=>t.locked).map(t=>t.name);if(!src.length||!wl.length)return[];const S=src.map(norm);const out=[];for(const w of wl){const i=S.indexOf(norm(w));if(i!==-1&&!out.includes(src[i]))out.push(src[i]);}return out;}

  const TAG_LINE_RE=/^【タグの内容】.*(?:\r?\n)?/m;
  function composeWithTagLine(currentText,tags,reason){const body0=(currentText||'').replace(TAG_LINE_RE,'');const tagLine=`【タグの内容】\n${tags.length?tags.join('、'):'(未特定)'}\n`;const body=body0.trim()?body0.replace(/^\r?\n+/,''):`【違反と判断された理由】\n${reason}`;return tagLine+body;}

  // ---- textareaモーダル ----
  function openTextareaModal(title,initial,onSave){
    const overlay=document.createElement('div');
    Object.assign(overlay.style,{position:'fixed',top:0,left:0,right:0,bottom:0,background:'rgba(0,0,0,.4)',zIndex:1000000,display:'flex',alignItems:'center',justifyContent:'center'});
    const box=document.createElement('div');
    Object.assign(box.style,{background:'#fff',padding:'12px',borderRadius:'8px',width:'480px',maxWidth:'90%',display:'flex',flexDirection:'column'});
    const h=document.createElement('div');h.textContent=title;Object.assign(h.style,{marginBottom:'8px',fontWeight:'bold'});box.appendChild(h);
    const ta=document.createElement('textarea');ta.value=initial;Object.assign(ta.style,{flex:'1',minHeight:'160px',font:'13px monospace',marginBottom:'8px'});box.appendChild(ta);
    const row=document.createElement('div');Object.assign(row.style,{textAlign:'right'});
    const ok=document.createElement('button');ok.textContent='保存';ok.onclick=()=>{onSave(ta.value);document.body.removeChild(overlay);};
    const cancel=document.createElement('button');cancel.textContent='キャンセル';cancel.style.marginLeft='6px';cancel.onclick=()=>document.body.removeChild(overlay);
    row.append(ok,cancel);box.appendChild(row);
    overlay.appendChild(box);document.body.appendChild(overlay);
  }

  function panel(nodes,reapply){
    if(document.getElementById('zr-min2-host'))return;
    const host=document.createElement('div');host.id='zr-min2-host';Object.assign(host.style,{position:'fixed',right:'16px',bottom:'16px',zIndex:999999});document.body.appendChild(host);
    const root=host.attachShadow({mode:'open'});
    const wrap=document.createElement('div');wrap.className='zr-min2';
    wrap.innerHTML=`
      <div class="row"><b>自動入力(ローカル⇄既定)</b></div>
      <div class="row">
        <button id="zr-edit-wl" type="button">タグ編集</button>
        <button id="zr-edit-reason" type="button">理由文編集</button>
        <button id="zr-reset" type="button">既定に戻す</button>
      </div>`;
    const style=document.createElement('style');style.textContent=`
      :host{all:initial;}
      .zr-min2{all:initial;display:block;font:12px/1.4 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Noto Sans JP","Hiragino Kaku Gothic ProN",Meiryo,sans-serif;color:#fff;background:#0b1220cc;backdrop-filter:blur(6px);padding:10px 12px;border-radius:10px;box-shadow:0 8px 20px rgba(0,0,0,.35);}
      .row{all:initial;display:block;margin:6px 0;font:inherit;color:inherit;}
      b{all:initial;font:inherit;font-weight:700;color:inherit;}
      button{all:initial;font:inherit;color:#fff;background:#1f6feb;padding:6px 10px;border-radius:6px;cursor:pointer;margin-right:6px;box-shadow:0 1px 2px rgba(0,0,0,.25);}
      button:hover{background:#2b7af3;}button:active{background:#195bd0;}#zr-reset{background:#d93025;}
    `;
    root.append(style,wrap);

    root.getElementById('zr-edit-wl').addEventListener('click',()=>{
      const cur=JSON.stringify(loadWL(),null,2);
      const nxt=prompt('ホワイトリスト(JSON配列):',cur);
      if(!nxt)return;
      try{saveWL(JSON.parse(nxt));reapply({forceDefaultBody:false});toast('タグリストを保存しました');}
      catch{alert('JSONが不正です');}
    });

    root.getElementById('zr-edit-reason').addEventListener('click',()=>{
      openTextareaModal('理由文を編集',getReason(),(val)=>{setReason(val);reapply({forceDefaultBody:true});toast('理由文を保存しました');});
    });

    root.getElementById('zr-reset').addEventListener('click',()=>{
      saveWL(WL_DEFAULT.slice());setReason('');reapply({forceDefaultBody:true});toast('既定に戻しました');
    });
  }

  (async function main(){
    const nodes=await waitForForm();if(!nodes)return;
    const {radios,select,ta}=nodes;
    const savedTarget=await storage.get(K_TARGET,''),savedItem=await storage.get(K_ITEM,''),savedText=await storage.get(K_TEXT,'');
    const target=savedTarget||DEFAULT_TARGET,item=savedItem||DEFAULT_ITEM;
    const r=radios.find(x=>x.value===String(target));if(r){r.checked=true;fire(r);}
    if([...select.options].some(o=>o.value===item)){select.value=item;fire(select);}
    const videoId=videoIdFromPath();const thumbTags=await fetchTags(videoId);
    const applyNow=({forceDefaultBody=false}={})=>{
      const wl=loadWL(),hit=filterWLAllLocked(thumbTags,wl);
      const cur=forceDefaultBody?'':(savedText||ta.value||'');
      const next=composeWithTagLine(cur,hit,getReason());
      setVal(ta,next);
      const check=ta.value||'';const ensured=check.replace(/^【タグの内容】[^\r\n]*\r?\n?/,m=>m.endsWith('\n')?m:(m+'\n'));if(ensured!==check)setVal(ta,ensured);
    };
    applyNow();panel(nodes,applyNow);
  })();
})();