☰

AOF Stash Enhancer

Stash: performer search shortcuts + scene modal player. Free drops: t.me/aofmainhub · Packs & VIP: t.me/aof_chat_farm_2bot

スクリプトをインストールするには、Tampermonkey, Greasemonkey や Violentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、 Tampermonkey や Violentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、Tampermonkey や Violentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、Tampermonkey や Userscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

作者のサイトでサポートを受ける。または、このスクリプトの質問や評価の投稿はこちら、通報はこちらへお寄せください
// ==UserScript==
// @name         AOF Stash Enhancer
// @namespace    https://t.me/aofmainhub
// @version      1.0.1
// @license      MIT
// @author       AOF Network
// @description  Stash: performer search shortcuts + scene modal player. Free drops: t.me/aofmainhub · Packs & VIP: t.me/aof_chat_farm_2bot
// @homepageURL  https://t.me/aofmainhub
// @supportURL   https://t.me/aof_chat_farm_2bot
// @match        http*://*/performers/*
// @match        http://localhost:9999/*
// @match        http://127.0.0.1:9999/*
// @match        http*://*/scenes*
// @grant        none
// @run-at       document-idle
// ==/UserScript==
(function (root) {
  'use strict';

  const STORAGE_PREFIX = 'tbccAofPromoDismissed:';
  const STYLE_ID = 'tbcc-aof-promo-chip-css';

  const DEFAULT_LINKS = [
    { href: 'https://t.me/aofmainhub', label: 't.me/aofmainhub' },
    { href: 'https://t.me/aof_chat_farm_2bot', label: 'Packs & VIP' },
    { href: 'https://t.me/aof_chat_farm_bot', label: 'Support' },
  ];

  function ensureStyles() {
    if (document.getElementById(STYLE_ID)) return;
    const s = document.createElement('style');
    s.id = STYLE_ID;
    s.textContent = `
#tbcc-aof-promo-chip-root{
  position:fixed;inset:0;z-index:2147483638;pointer-events:none;
}
#tbcc-aof-promo-chip-root .tbcc-aof-promo-chip{
  position:fixed;bottom:12px;left:50%;transform:translateX(-50%);z-index:2147483639;
  max-width:min(96vw,520px);padding:8px 14px;border-radius:8px;
  background:rgba(10,10,12,.92);color:#e5e5e5;font:12px/1.45 system-ui,Segoe UI,sans-serif;
  box-shadow:0 4px 20px rgba(0,0,0,.45);text-align:center;pointer-events:auto;
}
#tbcc-aof-promo-chip-root .tbcc-aof-promo-chip a{
  color:#8ec5ff;text-decoration:none;pointer-events:auto;cursor:pointer;
}
#tbcc-aof-promo-chip-root .tbcc-aof-promo-chip a:hover{text-decoration:underline}
#tbcc-aof-promo-chip-root .tbcc-aof-promo-chip button{
  margin-left:8px;background:transparent;border:0;color:inherit;cursor:pointer;opacity:.7;font-size:14px;line-height:1;
}
#tbcc-aof-promo-chip-root .tbcc-aof-promo-chip button:hover{opacity:1}
`;
    (document.head || document.documentElement).appendChild(s);
  }

  function normalizeSiteId(raw) {
    return String(raw || 'default')
      .trim()
      .toLowerCase()
      .replace(/[^\w-]+/g, '-')
      .slice(0, 32) || 'default';
  }

  function isDismissed(siteId) {
    try {
      return localStorage.getItem(STORAGE_PREFIX + siteId) === '1';
    } catch (_) {
      return false;
    }
  }

  function dismiss(siteId) {
    try {
      localStorage.setItem(STORAGE_PREFIX + siteId, '1');
    } catch (_) {}
  }

  function openDirect(href) {
    const url = String(href || '').trim();
    if (!url) return;
    try {
      const opened = window.open(url, '_blank', 'noopener,noreferrer');
      if (!opened) window.location.assign(url);
    } catch (_) {
      try {
        window.location.assign(url);
      } catch (_) {}
    }
  }

  function makeLink(href, label) {
    const a = document.createElement('a');
    a.href = href;
    a.textContent = label;
    a.target = '_blank';
    a.rel = 'noopener noreferrer';
    a.addEventListener('click', (e) => {
      e.preventDefault();
      e.stopPropagation();
      openDirect(href);
    });
    return a;
  }

  function appendText(parent, text) {
    if (text == null || text === '') return;
    parent.appendChild(document.createTextNode(String(text)));
  }

  function mount(opts) {
    if (!document.body) return;
    const siteId = normalizeSiteId(opts && opts.siteId);
    const elId = `tbcc-aof-promo-${siteId}`;
    if (document.getElementById(elId) || isDismissed(siteId)) return;

    ensureStyles();
    let rootEl = document.getElementById('tbcc-aof-promo-chip-root');
    if (!rootEl) {
      rootEl = document.createElement('div');
      rootEl.id = 'tbcc-aof-promo-chip-root';
      document.body.appendChild(rootEl);
    }

    const lead = String((opts && opts.lead) || 'More on AOF').trim();
    let links = Array.isArray(opts && opts.links) ? opts.links.slice() : DEFAULT_LINKS.slice(0, 2);
    const includeLoot = !opts || opts.includeLoot !== false;
    if (includeLoot && links.length <= 2) {
      links = links.concat(DEFAULT_LINKS.slice(2));
    }
    const filtered = links.filter((l) => l && l.href && l.label);

    const el = document.createElement('div');
    el.id = elId;
    el.className = 'tbcc-aof-promo-chip';

    appendText(el, lead);
    filtered.forEach((l, i) => {
      appendText(el, i === 0 ? ' · ' : ' · ');
      el.appendChild(makeLink(String(l.href), String(l.label)));
    });

    const btn = document.createElement('button');
    btn.type = 'button';
    btn.setAttribute('aria-label', 'Dismiss');
    btn.title = 'Dismiss';
    btn.textContent = '×';
    appendText(el, ' ');
    el.appendChild(btn);
    btn.addEventListener('click', (e) => {
      e.stopPropagation();
      dismiss(siteId);
      el.remove();
      if (rootEl && !rootEl.children.length) rootEl.remove();
    });

    rootEl.appendChild(el);
  }

  function unmount(siteId) {
    const id = normalizeSiteId(siteId);
    document.getElementById(`tbcc-aof-promo-${id}`)?.remove();
    const rootEl = document.getElementById('tbcc-aof-promo-chip-root');
    if (rootEl && !rootEl.children.length) rootEl.remove();
  }

  function unmountAll() {
    document.querySelectorAll('[id^="tbcc-aof-promo-"]').forEach((n) => n.remove());
    document.getElementById('tbcc-aof-promo-chip-root')?.remove();
  }

  root.TBCCAofPromoChip = { mount, unmount, unmountAll, makeLink, openDirect };
})(typeof globalThis !== 'undefined' ? globalThis : window);


/* ---- feature: performer-search ---- */
(function __tbccFeature_performer_search() {
'use strict';
if (window.__TBCC_FEATURE_performer_search__) return;
window.__TBCC_FEATURE_performer_search__ = true;
(function () {
    'use strict';

    /* ---------- SimpCity Prefill Helper ---------- */
    if (
        location.hostname.includes('simpcity.cr') &&
        location.pathname.startsWith('/search') &&
        new URLSearchParams(location.search).has('sus_auto')
    ) {
        window.addEventListener('DOMContentLoaded', () => {
            const params = new URLSearchParams(location.search);
            const qParam = params.get('q') || '';
            if (!qParam) return;

            const term = decodeURIComponent(qParam.replace(/\+/g, ' '));
            let attempts = 0;
            const tryFill = () => {
                const input = document.querySelector('input[name="q"]');
                if (input) {
                    input.value = term;
                    input.focus();
                } else if (attempts < 20) {
                    attempts++;
                    setTimeout(tryFill, 250);
                }
            };
            tryFill();
        });
        return;
    }

    /* ---------- Settings ---------- */
    const LS_KEY = 'stash-universal-search:settings';
    const DEFAULT_SETTINGS = {
        openInNewTabs: true,
        enabledSites: ['empornium', 'bunkr_albums', 'bunkr_pics', 'simpcity'],
    };
    const loadSettings = () => {
        try {
            return Object.assign({}, DEFAULT_SETTINGS, JSON.parse(localStorage.getItem(LS_KEY) || '{}'));
        } catch { return { ...DEFAULT_SETTINGS }; }
    };
    const saveSettings = (s) => localStorage.setItem(LS_KEY, JSON.stringify(s));
    let SETTINGS = loadSettings();

    /* ---------- Site Definitions ---------- */
    const SITES = {
        empornium: {
            label: 'Empornium',
            tooltip: 'Empornium: taglist uses firstname.lastname (lowercase); mononyms plain.',
            buildUrls: (name) => {
                const parts = name.trim().split(/\s+/);
                const tag =
                      parts.length === 1
                ? parts[0].toLowerCase()
                : `${parts[0].toLowerCase()}.${parts.pop().toLowerCase()}`;
                return [`https://www.empornium.sx/torrents.php?&taglist=${encodeURIComponent(tag)}`];
            },
        },

        /*
        bunkr_archive: {
            label: 'Bunkr Explorer',
            tooltip: 'Bunkr Explorer: spaces become + (literal, not encoded).',
            buildUrls: (name) => {
                const q = name.trim().replace(/\s+/g, '+');
                return [`https://bunkr-explorer.pages.dev/search?q=${q}`];
            },
        },
        */

        bunkr_albums: {
            label: 'Bunkr Albums',
            tooltip: 'balbums.st: spaces become +.',
            buildUrls: (name) => {
                const q = name.trim().replace(/\s+/g, '+');
                return [`https://balbums.st/?search=${q}&mode=broad&per=20&sort=latest`];
            },
        },

        bunkr_pics: {
            label: 'Bunkr Pics',
            tooltip: 'bunkrr.pics: spaces become +.',
            buildUrls: (name) => {
                const q = name.trim().replace(/\s+/g, '+');
                return [`https://bunkrr.pics/search?q=${q}`];
            },
        },

        simpcity: {
            label: 'SimpCity',
            tooltip: 'SimpCity: opens search page (title-only), prefilled and focused. MUST HIT ENTER OR CLICK SEARCH.',
            buildUrls: (name) => [
                `https://simpcity.cr/search/?t=post&c[title_only]=1&q=${encodeURIComponent(name)}&sus_auto=1`,
            ],
        },

        // --- Coomer with OnlyFans/Fansly username extraction ---
        coomer: {
            label: 'Coomer',
            tooltip: 'Coomer: prefers OnlyFans/Fansly handle if performer URLs contain one.',
            buildUrls: (name, _aliases, _inc, _cap, performer) => {
                const extractHandle = (urls) => {
                    if (!urls) return null;
                    const all = Array.isArray(urls) ? urls.join('\n') : String(urls);
                    const m = all.match(/(?:onlyfans\.com|fansly\.com)\/([A-Za-z0-9._-]+)/i);
                    return m ? m[1] : null;
                };

                const handle = performer?.urls ? extractHandle(performer.urls) : null;
                let term;

                if (handle) {
                    term = handle;
                    console.log(`[SUS] Using ${handle} from performer URLs for Coomer search`);
                } else {
                    term = name.trim().replace(/\s+/g, '');
                }

                return [`https://coomer.st/artists?q=${encodeURIComponent(term)}`];
            },
        },
    };

    /* ---------- GraphQL ---------- */
    const gql = async (query, variables) => {
        const res = await fetch('/graphql', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ query, variables }),
            credentials: 'same-origin',
        });
        const j = await res.json();
        if (j.errors) throw new Error(JSON.stringify(j.errors));
        return j.data;
    };

    const fetchPerformer = async (id) => {
        const q = `
      query FindPerformer($id: ID!) {
        findPerformer(id: $id) {
          id
          name
          alias_list
          urls
        }
      }`;
      const d = await gql(q, { id });
      return d?.findPerformer || null;
  };

    /* ---------- Helpers ---------- */
    const isPerf = () => /\/performers\/\d+/.test(location.pathname);
    const getPerfId = () => location.pathname.match(/\/performers\/(\d+)/)?.[1];

    const addStyles = () => {
        if (document.getElementById('sus-styles')) return;
        const css = `
      .sus-group{display:flex;flex-wrap:wrap;gap:6px;margin-top:6px}
      .sus-btn{border:1px solid #4b5563;background:#1f2937;color:#e5e7eb;
        padding:6px 10px;border-radius:6px;font-size:12px;cursor:pointer}
      .sus-btn:hover{filter:brightness(1.15)}
      #sus-panel{position:fixed;top:20px;right:20px;z-index:99999;
        background:#111827;color:#e5e7eb;border:1px solid #6b7280;
        border-radius:8px;padding:12px;max-width:360px;}
      #sus-panel input[type="checkbox"]{accent-color:#93c5fd;}
      #sus-panel label{display:block;margin:4px 0;}
      #sus-panel button{margin-top:6px;}
    `;
      const s = document.createElement('style');
      s.id = 'sus-styles';
      s.textContent = css;
      document.head.appendChild(s);
  };

    const openUrls = (urls) => {
        urls.forEach((u, i) =>
                     setTimeout(() => window.open(u, SETTINGS.openInNewTabs ? '_blank' : '_self'), i * 150)
                    );
    };

    /* ---------- Settings Panel ---------- */
    function showSettingsPanel() {
        const existing = document.getElementById('sus-panel');
        if (existing) existing.remove();

        const p = document.createElement('div');
        p.id = 'sus-panel';
        const sites = Object.keys(SITES);
        p.innerHTML = `
      <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:6px;">
        <b>Universal Search Settings</b>
        <span style="cursor:pointer;" id="sus-close">✕</span>
      </div>
      <label><input type="checkbox" id="sus-tabs" ${SETTINGS.openInNewTabs ? 'checked' : ''}>
        Open results in new tabs</label>
      <hr>
      <b>Enabled sites:</b>
      ${sites.map((k) =>
                  `<label><input type="checkbox" data-site="${k}" ${SETTINGS.enabledSites.includes(k) ? 'checked' : ''}>
         ${SITES[k].label}</label>`).join('')}
      <div style="text-align:right;">
        <button id="sus-save" class="sus-btn">Save</button>
      </div>
    `;
      document.body.appendChild(p);

      p.querySelector('#sus-close').onclick = () => p.remove();
      p.querySelector('#sus-save').onclick = () => {
          SETTINGS.openInNewTabs = p.querySelector('#sus-tabs').checked;
          SETTINGS.enabledSites = Array.from(p.querySelectorAll('input[data-site]:checked'))
              .map((i) => i.getAttribute('data-site'));
          saveSettings(SETTINGS);
          p.remove();
          alert('Settings saved. Refresh performer page to apply.');
      };
  }

    /* ---------- Build Buttons ---------- */
    const buildButtons = (perf) => {
        const anchor = Array.from(document.querySelectorAll('button'))
        .find((b) => /Edit/i.test(b.textContent || ''))?.parentElement;
        if (!anchor) return;

        const group = document.createElement('div');
        group.className = 'sus-group';

        SETTINGS.enabledSites.forEach((k) => {
            const site = SITES[k];
            if (!site) return;
            const btn = document.createElement('button');
            btn.className = 'sus-btn';
            btn.textContent = site.label;
            btn.title = site.tooltip;
            btn.onclick = () => {
                const urls = site.buildUrls(perf.name, perf.alias_list, false, 0, perf);
                openUrls(urls);
            };
            group.appendChild(btn);
        });

        const gear = document.createElement('button');
        gear.className = 'sus-btn';
        gear.textContent = '⚙️ Settings';
        gear.title = 'Configure Universal Search';
        gear.onclick = showSettingsPanel;
        group.appendChild(gear);

        anchor.parentElement.insertBefore(group, anchor.nextSibling);
    };

    /* ---------- SPA / Hard Refresh Fallback ---------- */
    let lastPath = location.pathname;
    let isInitializing = false;

    async function init() {
        if (isInitializing) return;
        isInitializing = true;

        if (!isPerf()) { isInitializing = false; return; }
        const id = getPerfId();
        if (!id) { isInitializing = false; return; }

        try {
            const perf = await fetchPerformer(id);
            if (perf) {
                if (!document.querySelector('.sus-group')) {
                    addStyles();
                    buildButtons(perf);
                }
            }
        } catch (e) {
            console.error('[SUS] GraphQL fail', e);
        }

        isInitializing = false;

        // If the page is still a performer page but buttons weren't inserted
        // (DOM may not have been fully ready during the GraphQL fetch), retry.
        if (isPerf() && !document.querySelector('.sus-group')) {
            setTimeout(waitForPerformerPage, 500);
        }
    }

    function waitForPerformerPage() {
        const editButton = Array.from(document.querySelectorAll('button')).find(
            (b) => /Edit/i.test(b.textContent || '')
        );
        if (editButton) {
            init();
        } else {
            setTimeout(waitForPerformerPage, 500);
        }
    }

    waitForPerformerPage();

    const observer = new MutationObserver(() => {
        if (location.pathname !== lastPath) {
            lastPath = location.pathname;
            document.querySelectorAll('.sus-group').forEach((e) => e.remove());
            waitForPerformerPage();
        }
    });
    observer.observe(document, { childList: true, subtree: true });
})();
})();

/* ---- feature: scene-modal-player ---- */
(function __tbccFeature_scene_modal_player() {
'use strict';
if (window.__TBCC_FEATURE_scene_modal_player__) return;
window.__TBCC_FEATURE_scene_modal_player__ = true;
(function () {
  'use strict';

  const MODAL_ID = 'tbcc-stash-scene-modal';
  const STYLE_ID = 'tbcc-stash-scene-modal-css';
  const LS_KEY = 'tbcc-stash-scene-modal:enabled';
  const enabled = () => {
    try {
      return localStorage.getItem(LS_KEY) !== '0';
    } catch (_) {
      return true;
    }
  };

  function sceneIdFromHref(href) {
    const m = String(href || '').match(/\/(scenes|images|groups)\/(\d+)/);
    return m ? { kind: m[1], id: m[2] } : null;
  }

  function isListContext() {
    const p = location.pathname;
    if (/^\/(scenes|images|groups)\/\d+/.test(p)) return false;
    return /^\/(scenes|images|groups)(\/|$)/.test(p) || p === '/' || p === '/scenes';
  }

  function ensureStyles() {
    if (document.getElementById(STYLE_ID)) return;
    const s = document.createElement('style');
    s.id = STYLE_ID;
    s.textContent = `
#${MODAL_ID}{
  position:fixed;inset:0;z-index:2147483600;display:none;align-items:center;justify-content:center;
  background:rgba(0,0,0,.72);backdrop-filter:blur(4px);
}
#${MODAL_ID}.open{display:flex}
#${MODAL_ID} .tbcc-stash-modal-shell{
  width:min(96vw,1280px);height:min(92vh,900px);background:#202b33;border-radius:10px;
  box-shadow:0 16px 48px rgba(0,0,0,.55);display:flex;flex-direction:column;overflow:hidden;
  border:1px solid rgba(255,255,255,.08);
}
#${MODAL_ID} .tbcc-stash-modal-bar{
  display:flex;align-items:center;gap:10px;padding:8px 12px;background:#1a2329;border-bottom:1px solid rgba(255,255,255,.06);
}
#${MODAL_ID} .tbcc-stash-modal-title{flex:1;font:600 13px/1.4 system-ui,Segoe UI,sans-serif;color:#e5e7eb;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
#${MODAL_ID} .tbcc-stash-modal-bar button{
  border:1px solid rgba(255,255,255,.12);background:#2a3942;color:#e5e7eb;border-radius:6px;padding:6px 10px;font:12px system-ui;cursor:pointer;
}
#${MODAL_ID} .tbcc-stash-modal-bar button:hover{background:#33424c}
#${MODAL_ID} iframe{flex:1;border:0;width:100%;background:#0f1419}
`;
    document.head.appendChild(s);
  }

  function ensureModal() {
    ensureStyles();
    let root = document.getElementById(MODAL_ID);
    if (root) return root;
    root = document.createElement('div');
    root.id = MODAL_ID;
    root.innerHTML =
      '<div class="tbcc-stash-modal-shell" role="dialog" aria-modal="true">' +
      '<div class="tbcc-stash-modal-bar">' +
      '<span class="tbcc-stash-modal-title">Scene</span>' +
      '<button type="button" data-act="tab">Open in tab</button>' +
      '<button type="button" data-act="close">Close</button>' +
      '</div>' +
      '<iframe title="Stash scene" sandbox="allow-same-origin allow-scripts allow-forms allow-popups"></iframe>' +
      '</div>';
    document.body.appendChild(root);
    root.addEventListener('click', (e) => {
      if (e.target === root) closeModal();
    });
    root.querySelector('[data-act="close"]').addEventListener('click', closeModal);
    root.querySelector('[data-act="tab"]').addEventListener('click', () => {
      const href = root.dataset.href;
      if (href) window.open(href, '_blank', 'noopener,noreferrer');
    });
    document.addEventListener('keydown', (e) => {
      if (e.key === 'Escape' && root.classList.contains('open')) closeModal();
    });
    return root;
  }

  function closeModal() {
    const root = document.getElementById(MODAL_ID);
    if (!root) return;
    root.classList.remove('open');
    const iframe = root.querySelector('iframe');
    if (iframe) iframe.src = 'about:blank';
  }

  function openModal(kind, id, label) {
    const root = ensureModal();
    const href = '/' + kind + '/' + id;
    root.dataset.href = href;
    root.querySelector('.tbcc-stash-modal-title').textContent = label || href;
    root.querySelector('iframe').src = href;
    root.classList.add('open');
  }

  document.addEventListener(
    'click',
    (e) => {
      if (!enabled() || !isListContext()) return;
      if (e.defaultPrevented || e.button !== 0 || e.ctrlKey || e.metaKey || e.shiftKey || e.altKey) return;
      const anchor = e.target.closest('a[href*="/scenes/"], a[href*="/images/"], a[href*="/groups/"]');
      if (!anchor) return;
      const parsed = sceneIdFromHref(anchor.getAttribute('href'));
      if (!parsed) return;
      e.preventDefault();
      e.stopPropagation();
      const card = anchor.closest('.card, .grid-card, [class*="card"]');
      const label =
        (card && (card.querySelector('.card-section-title, h5, .truncated') || {}).textContent) ||
        anchor.getAttribute('title') ||
        anchor.textContent ||
        parsed.kind + ' ' + parsed.id;
      openModal(parsed.kind, parsed.id, String(label).trim().slice(0, 120));
    },
    true
  );
})();
})();

/* TBCC stash_enhancer — boot */
(function () {
  'use strict';
  function mountPromo() {
    try {
      globalThis.TBCCAofPromoChip?.mount?.({ siteId: 'stash', lead: 'AOF network' });
    } catch (_) {}
  }
  if (document.body) mountPromo();
  else document.addEventListener('DOMContentLoaded', mountPromo, { once: true });
})();