e621 Tag Extract - API solid

Extract tags from e621 images

K instalaci tototo skriptu si budete muset nainstalovat rozšíření jako Tampermonkey, Greasemonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Userscripts.

You will need to install an extension such as Tampermonkey to install this script.

K instalaci tohoto skriptu si budete muset nainstalovat manažer uživatelských skriptů.

(Už mám manažer uživatelských skriptů, nechte mě ho nainstalovat!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Už mám manažer uživatelských stylů, nechte mě ho nainstalovat!)

// ==UserScript==
// @name         e621 Tag Extract - API solid
// @namespace    http://tampermonkey.net/
// @version      2.0
// @description  Extract tags from e621 images
// @author       cemtrex (partly with AI coding assistant)
// @match        https://e621.net/posts/*
// @license      MIT
// @run-at       document-idle
// ==/UserScript==

(function () {
  'use strict';

  const BTN_ID = 'e621TagExtractButton';
  const MODAL_ID = 'e621TagExtractModal';
  const PREF_KEY = 'e621TagExtractPrefs_v2';

  const DEFAULT_PREFS = {
    wordStyle: 'underscores',
    separator: ', ',
    preselectGroups: ['general','species','character','artist','copyright','meta','lore','invalid']
  };

  const unique = (arr) => Array.from(new Set(arr));
  const loadPrefs = () => { try { return Object.assign({}, DEFAULT_PREFS, JSON.parse(localStorage.getItem(PREF_KEY) || '{}')); } catch { return { ...DEFAULT_PREFS }; } };
  const savePrefs = (p) => { try { localStorage.setItem(PREF_KEY, JSON.stringify(p)); } catch {} };

  function copyToClipboard(text) {
    if (navigator.clipboard && window.isSecureContext) {
      return navigator.clipboard.writeText(text).catch(() => fallbackCopy(text));
    }
    return Promise.resolve(fallbackCopy(text));
  }
  function fallbackCopy(text) {
    const ta = document.createElement('textarea');
    ta.value = text;
    ta.style.position = 'fixed';
    ta.style.left = '-9999px';
    ta.setAttribute('readonly', '');
    document.body.appendChild(ta);
    ta.select();
    try { document.execCommand('copy'); } catch {}
    ta.remove();
  }

  async function getTagsFromAPI() {
    const m = location.pathname.match(/\/posts\/(\d+)/);
    if (!m) return { flat: [], grouped: {}, order: [] };
    const postId = m[1];
    try {
      const res = await fetch(`/posts/${postId}.json`, { headers: { 'Accept': 'application/json' }, credentials: 'same-origin', cache: 'no-store' });
      if (!res.ok) return { flat: [], grouped: {}, order: [] };
      const data = await res.json();
      const post = data.post || data;
      const tagGroups = post.tags || {};
      const grouped = Array.isArray(tagGroups) ? { general: tagGroups.map(String) } : Object.fromEntries(Object.entries(tagGroups).map(([k,v])=>[k,(v||[]).map(String)]));
      const preferred = ['artist','species','character','copyright','general','meta','lore','invalid'];
      const order = Object.keys(grouped).sort((a,b) => {
        const ia = preferred.indexOf(a);
        const ib = preferred.indexOf(b);
        const sa = ia === -1 ? 1e9 : ia;
        const sb = ib === -1 ? 1e9 : ib;
        return (sa - sb) || a.localeCompare(b);
      });
      const flat = unique(Object.values(grouped).flat().map(t => t.trim()).filter(Boolean));
      return { flat, grouped, order };
    } catch (e) {
      console.error('[e621 Tag Extract] API error:', e);
      return { flat: [], grouped: {}, order: [] };
    }
  }

  function buildButton() {
    const button = document.createElement('button');
    button.id = BTN_ID;
    button.type = 'button';
    button.textContent = '+';
    button.title = 'Extract, review, and copy tags';
    button.setAttribute('aria-label', 'Extract, review, and copy tags');
    Object.assign(button.style, {
      position: 'fixed', bottom: '20px', right: '20px',
      backgroundColor: '#2563eb', color: '#fff', border: 'none', borderRadius: '50%',
      width: '50px', height: '50px', textAlign: 'center', fontSize: '24px', lineHeight: '50px',
      cursor: 'pointer', boxShadow: '2px 2px 6px rgba(0,0,0,0.3)', zIndex: 2147483647
    });
    return button;
  }

  function makeModal(tagsInfo) {
    const prefs = loadPrefs();

    const overlay = document.createElement('div');
    overlay.id = MODAL_ID;
    Object.assign(overlay.style, { position: 'fixed', inset: '0', background: 'rgba(0,0,0,0.45)', zIndex: 2147483647, display: 'flex', alignItems: 'center', justifyContent: 'center' });

    const modal = document.createElement('div');
    Object.assign(modal.style, { background: '#111827', color: '#e5e7eb', width: 'min(920px, 94vw)', maxHeight: '86vh', overflow: 'hidden', borderRadius: '10px', boxShadow: '0 10px 30px rgba(0,0,0,0.5)', display: 'flex', flexDirection: 'column', border: '1px solid #1f2937' });

    const header = document.createElement('div');
    header.textContent = 'e621 Tag Extract';
    Object.assign(header.style, { padding: '12px 16px', background: '#0b1220', fontWeight: '600', position:'sticky', top:'0', zIndex:'2' });

    const subline = document.createElement('div');
    subline.textContent = 'Check or uncheck the tags below to include or exclude them before copying:';
    Object.assign(subline.style, { padding: '8px 16px', fontSize: '13px', opacity: '0.8', borderBottom:'1px solid #1f2937' });

    const content = document.createElement('div');
    Object.assign(content.style, { display: 'grid', gridTemplateColumns: '300px 1fr', gap: '12px', padding: '12px 16px', overflow: 'hidden' });

    const controls = document.createElement('div');
    Object.assign(controls.style, { position: 'sticky', top: '48px', alignSelf: 'start' });
    controls.innerHTML = `
      <style>
        #${MODAL_ID} label { white-space: normal; word-break: break-word; display: block; line-height: 1.4; }
        #${MODAL_ID} input[type=radio], #${MODAL_ID} input[type=checkbox] { margin-right:4px; }
      </style>
      <div style="margin-bottom:10px">
        <div style="font-weight:600;margin-bottom:6px">Format</div>
        <label><input type="radio" name="wordStyle" value="underscores"> Keep underscores</label>
        <label><input type="radio" name="wordStyle" value="spaces"> Replace underscores with spaces</label>
      </div>
      <div style="margin:10px 0">
        <div style="font-weight:600;margin-bottom:6px">Groups</div>
        ${['general','species','character','artist','copyright','meta','lore','invalid'].map(g=>`
          <label><input type="checkbox" name="grp" value="${g}" checked> ${g}</label>
        `).join('')}
      </div>
    `;

    const rightCol = document.createElement('div');
    Object.assign(rightCol.style, { maxHeight: '72vh', overflowY: 'auto', border: '1px solid #1f2937', borderRadius: '8px', padding: '8px' });

    const footer = document.createElement('div');
    Object.assign(footer.style, { display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: '10px', padding: '12px 16px', background: '#0b1220', borderTop: '1px solid #1f2937', position:'sticky', bottom:'0', zIndex:'2' });
    const cancelBtn = document.createElement('button'); cancelBtn.textContent = 'Cancel';
    const copyBtn = document.createElement('button'); copyBtn.textContent = 'Copy';
    for (const b of [cancelBtn, copyBtn]) Object.assign(b.style, { padding: '8px 12px', borderRadius: '8px', border: '1px solid #374151', background: '#1f2937', color: '#e5e7eb', cursor: 'pointer' });
    Object.assign(copyBtn.style, { background: '#2563eb', borderColor: '#1d4ed8' });

    content.append(controls, rightCol);
    modal.append(header, subline, content, footer);
    footer.append(cancelBtn, copyBtn);
    overlay.appendChild(modal);

    const { grouped, order } = tagsInfo;
    const allItems = [];
    for (const g of order) {
      const tags = grouped[g] || [];
      if (!tags.length) continue;
      const details = document.createElement('details');
      details.open = true;
      details.style.marginBottom = '10px';
      const summary = document.createElement('summary');
      summary.textContent = `${g} (${tags.length})`;
      Object.assign(summary.style, { cursor: 'pointer', fontWeight: '600', padding:'4px 2px' });
      details.appendChild(summary);

      const section = document.createElement('div');
      section.style.paddingLeft = '8px';
      for (const t of tags) {
        const row = document.createElement('label');
        Object.assign(row.style, { display: 'grid', gridTemplateColumns: 'auto 1fr', gap: '8px', alignItems: 'center', padding: '2px 0' });
        row.dataset.group = g; row.dataset.tag = t;
        const cb = document.createElement('input');
        cb.type = 'checkbox';
        cb.checked = true;
        const name = document.createElement('span'); name.textContent = t;
        row.append(cb, name);
        section.appendChild(row);
        allItems.push(row);
      }
      details.appendChild(section);
      rightCol.appendChild(details);
    }

    controls.querySelector(`input[name="wordStyle"][value="${prefs.wordStyle}"]`).checked = true;

    function syncGroupChecks() {
      const allowed = new Set(Array.from(controls.querySelectorAll('input[name="grp"]:checked')).map(i=>i.value));
      for (const r of allItems) {
        const cb = r.querySelector('input[type="checkbox"]');
        if (!cb._touched) cb.checked = allowed.has(r.dataset.group);
      }
    }
    controls.addEventListener('change', (e)=>{
      if (e.target && e.target.name === 'grp') syncGroupChecks();
      savePrefs(currentPrefs());
    });

    function currentPrefs() {
      const wordStyle = controls.querySelector('input[name="wordStyle"]:checked')?.value || 'underscores';
      const separator = DEFAULT_PREFS.separator;
      const preselectGroups = Array.from(controls.querySelectorAll('input[name="grp"]:checked')).map(i=>i.value);
      return { wordStyle, separator, preselectGroups };
    }

    rightCol.addEventListener('change', (e)=>{
      if (e.target && e.target.type === 'checkbox') e.target._touched = true;
    });

    cancelBtn.onclick = () => overlay.remove();
    overlay.addEventListener('click', (e)=>{ if (e.target === overlay) overlay.remove(); });
    document.addEventListener('keydown', function esc(e){ if (e.key==='Escape'){ overlay.remove(); document.removeEventListener('keydown', esc); } });

    copyBtn.onclick = async () => {
      const prefsNow = currentPrefs(); savePrefs(prefsNow);
      let selected = allItems.filter(r=>r.querySelector('input').checked).map(r=>r.dataset.tag);
      if (prefsNow.wordStyle === 'spaces') selected = selected.map(t=>t.replace(/_/g,' '));
      const text = selected.join(prefsNow.separator);
      if (!text) { overlay.remove(); return; }
      await copyToClipboard(text);
      overlay.remove();
      console.log('[e621 Tag Extract] Copied', selected.length, 'tags');
    };

    return overlay;
  }

  async function handleClick() {
    const info = await getTagsFromAPI();
    if (!info.flat.length) {
      const anchors = document.querySelectorAll('a.search-tag, aside a[href^="/posts?tags="]');
      const tags = Array.from(anchors).map(a => (a.textContent || '').trim()).filter(Boolean);
      info.grouped = { general: tags };
      info.order = ['general'];
      info.flat = unique(tags);
    }
    if (!info.flat.length) return;
    const modal = makeModal(info);
    document.body.appendChild(modal);
  }

  function addButtonOnce() {
    if (document.getElementById(BTN_ID)) return;
    const btn = buildButton();
    btn.addEventListener('click', handleClick);
    document.body.appendChild(btn);
  }

  function init() {
    if (document.readyState === 'complete' || document.readyState === 'interactive') { addButtonOnce(); }
    else { document.addEventListener('DOMContentLoaded', addButtonOnce, { once: true }); }
    const mo = new MutationObserver(() => addButtonOnce());
    mo.observe(document.documentElement, { childList: true, subtree: true });
  }

  init();
})();