☰

AOF Stash Enhancer

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

Bu betiği kurabilmeniz için Tampermonkey, Greasemonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

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

Bu betiği kurabilmeniz için Tampermonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği kurabilmeniz için Tampermonkey ya da Userscripts gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği indirebilmeniz için ayrıca Tampermonkey gibi bir eklenti kurmanız gerekmektedir.

Bu betiği yüklemek için bir betik yöneticisi eklentisi yüklemeniz gerekecektir.

(Zaten bir betik yöneticim var, hadi yükleyelim!)

Bu stili yüklemek için Stylus gibi bir uzantı yüklemeniz gerekir.

Bu stili yüklemek için Stylus gibi bir uzantı kurmanız gerekir.

Bu stili yükleyebilmek için Stylus gibi bir uzantı yüklemeniz gerekir.

Bu stili yüklemek için bir kullanıcı stili yöneticisi uzantısı yüklemeniz gerekir.

Bu stili yüklemek için bir kullanıcı stili yöneticisi uzantısı kurmanız gerekir.

Bu stili yükleyebilmek için bir kullanıcı stili yöneticisi uzantısı yüklemeniz gerekir.

(Zateb bir user-style yöneticim var, yükleyeyim!)

// ==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 });
})();