Niconico Allegation Script

NGボタン右に「通報」「タグ通報」を追加

// ==UserScript==
// @name         Niconico Allegation Script
// @namespace    https://greasyfork.org/users/prozent55
// @version      1.0.0
// @description  NGボタン右に「通報」「タグ通報」を追加
// @match        https://www.nicovideo.jp/*
// @grant        none
// @license      MIT
// ==/UserScript==

(() => {
  'use strict';

  const style = document.createElement('style');
  style.textContent = `
    .nrn-title-ng-button + .tm-rpt-group{
      display:inline-flex; align-items:center; gap:0;
      margin-left:0; padding-left:0; vertical-align:middle; color:#fff;
      --tm-rpt-h: 1em;
    }
    .tm-rpt-button{
      position:relative; display:flex; align-items:center; line-height:1;
      color:inherit; cursor:pointer; user-select:none;
      margin-left:5px; padding-left:5px;
    }
    .tm-rpt-button::before{
      content:""; position:absolute; left:0; top:50%;
      transform:translateY(-50%); height:var(--tm-rpt-h);
      border-left:1px solid currentColor;
    }
    .tm-rpt-button:hover{ text-decoration:underline; }
  `;
  document.head.appendChild(style);

  const qs  = (s, r=document) => r.querySelector(s);

  const getMovieId = (pane, anchorBtn) => {
    const d = anchorBtn?.dataset;
    if (d?.movieId) return d.movieId;
    const any = pane.querySelector('[data-movie-id]');
    if (any?.dataset?.movieId) return any.dataset.movieId;
    const a = pane.closest('*')?.querySelector('a[href*="/watch/"]');
    const m = a?.getAttribute('href')?.match(/sm\d+/);
    return m ? m[0] : null;
  };

  const btn = (label, onClick) => {
    const x = document.createElement('span');
    x.className = 'tm-rpt-button';
    x.textContent = label;
    x.addEventListener('click', e => { e.stopPropagation(); e.preventDefault(); onClick(); });
    return x;
  };

  const syncHeight = (group, titleBtn) => {
    const h = titleBtn?.offsetHeight || group.offsetHeight || 0;
    if (h) group.style.setProperty('--tm-rpt-h', `${h}px`);
  };

  const add = (pane) => {
    if (!pane || pane.dataset.tmRptAdded) return;
    const titleBtn = qs('.nrn-title-ng-button', pane);
    if (!titleBtn) return;

    const group = document.createElement('span');
    group.className = 'tm-rpt-group';

    const report = btn('通報', () => {
      const id = getMovieId(pane, titleBtn);
      if (id) {
        const num = id.replace(/\D/g, '');
        window.open(`https://garage.nicovideo.jp/allegation/${num}`, '_blank', 'noopener');
      }
    });

    const tagReport = btn('タグ通報', () => {
      const id = getMovieId(pane, titleBtn);
      if (id) {
        window.open(`https://www.nicovideo.jp/comment_allegation/${id}`, '_blank', 'noopener');
      }
    });

    group.appendChild(report);
    group.appendChild(tagReport);
    titleBtn.insertAdjacentElement('afterend', group);
    syncHeight(group, titleBtn);
    pane.dataset.tmRptAdded = '1';
  };

  document.querySelectorAll('.nrn-action-pane').forEach(add);

  new MutationObserver(ms => {
    for (const m of ms) {
      for (const n of m.addedNodes) {
        if (n.nodeType !== 1) continue;
        if (n.classList?.contains('nrn-action-pane')) add(n);
        else n.querySelectorAll?.('.nrn-action-pane').forEach(add);
      }
    }
  }).observe(document.body, { childList: true, subtree: true });
})();