I have updated the right click scenes as a fork. If you want to use this simply replace the code in stashRightClickScenes.js with the below.
-
Right clicking and add tag now has the same functionality as using the edit function but is better as you can do it wherever you are on the page. It is a cleaner smaller interface. all current tags show at the top. You can search now to add more. Clear the search with the x and create a new tag. Never will you click on add tag again and you have to search for the box by scrolling aroung the page looking for it. It will always appear next to the card.
-
No need to right click and select add tag just click the tag icon to open the above menu directly
-
You can scrub the scene via the sprites as you add as many tags as you want
-
Scene Card Anchored Popups: The Tag, Performer, and Gallery management popups no longer open blindly under the mouse cursor. They now dynamically anchor directly to the right edge of the clicked scene card to keep your preview clear and unobstructed.
-
Smart Boundary Detection & Auto-Flipping: If there isn’t enough horizontal or vertical space on the right side of the screen, the popup automatically flips to the left side or shifts upward to stay fully visible.
-
Modernized Tabulator Integration: Cleaned up styling for Tabulator tables inside popups using a modern light theme, crisp borders, and refined row selections.
-
Pre-selected & Sorted Items: Existing tag, performer, and gallery links are now automatically pre-selected when you open the popup, and items are sorted with selected ones pinned to the top for faster reviewing.
-
Inline Creation & Duplicate Safety: If you type a new item name and click Create, the script handles the inline mutation. If a duplicate item already exists, the server error is caught and displayed cleanly via a notification toast while preserving your search input so you can instantly select the existing entry.
-
Initial Right-Click Menu Integration: Built the base context menu attached to
.scene-cardelements to handle custom scene actions without having to open the full edit page. -
Tabulator Popup Framework: Introduced the floating, draggable Tabulator-powered interface cards for managing tags, performers, and galleries right from the main grid view.
-
Bulk Updates & ID Syncing: Implemented full GraphQL synchronization (
sceneUpdate) to fetch existing assigned IDs and save bulk changes back to individual scenes cleanly. -
Inline Creation & Search UI: Added the instant search input, clear button (×), and inline Create button functionality that lets you type a new tag, performer, or gallery on the fly and automatically select it upon creation.
-
Refreshes the page when you save the tag but remembers your position. This was the most tricky element. As wanted it to just update the card but still elegant. Perhaps someone can improve this?
// ==UserScript==
// @name Stash Scene Manager Context Menu
// @namespace http://tampermonkey.net/
// @version 1.9.21
// @description Restricted right-click context menu (only on video preview/thumbnail area) and card-wide left-click badge shortcuts for Stash scenes with Tabulator popups & robust scroll restoration
// @match http://localhost:*/*
// @match http://127.0.0.1:*/*
// @grant none
// @run-at document-end
// @require https://unpkg.com/tabulator-tables@5.5.2/dist/js/tabulator.min.js
// @require https://cdn.jsdelivr.net/npm/toastify-js
// ==/UserScript==
(async function() {
'use strict';
// Robust auto-restore scroll position after reload (handles SPA async rendering)
const scrollKey = 'stash_scroll_pos_' + window.location.pathname + window.location.search;
const savedScroll = sessionStorage.getItem(scrollKey);
if (savedScroll !== null) {
sessionStorage.removeItem(scrollKey);
const targetScroll = parseInt(savedScroll, 10);
let attempts = 0;
const restoreScroll = () => {
window.scrollTo(0, targetScroll);
if (window.scrollY !== targetScroll && attempts < 15) {
attempts++;
setTimeout(restoreScroll, 50);
}
};
restoreScroll();
}
let isTabActive = true;
document.addEventListener('visibilitychange', () => {
isTabActive = !document.hidden;
});
if (!document.getElementById('tabulator-external-css')) {
const tabulatorCSS = document.createElement('link');
tabulatorCSS.id = 'tabulator-external-css';
tabulatorCSS.rel = 'stylesheet';
tabulatorCSS.href = 'https://unpkg.com/tabulator-tables@5.5.2/dist/css/tabulator.min.css';
document.head.appendChild(tabulatorCSS);
}
if (!document.getElementById('toastify-external-css')) {
const toastifyCSS = document.createElement('link');
toastifyCSS.id = 'toastify-external-css';
toastifyCSS.rel = 'stylesheet';
toastifyCSS.href = 'https://cdn.jsdelivr.net/npm/toastify-js/src/toastify.min.css';
document.head.appendChild(toastifyCSS);
}
const styleId = 'scenes-manager-modern-styles';
if (!document.getElementById(styleId)) {
const style = document.createElement('style');
style.id = styleId;
style.textContent = `
#scenes-custom-menu {
background: #ffffff;
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 6px;
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.04);
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
font-size: 13px;
min-width: 150px;
animation: menuFadeIn 0.15s ease-out;
}
#scenes-custom-menu a {
display: block;
padding: 8px 12px;
color: #1e293b;
text-decoration: none;
border-radius: 4px;
transition: background 0.15s, color 0.15s;
}
#scenes-custom-menu a:hover {
background: #f1f5f9;
color: #0f172a;
}
@keyframes menuFadeIn {
from { opacity: 0; transform: translateY(-4px); }
to { opacity: 1; transform: translateY(0); }
}
#scenes-popup .tabulator {
background-color: #ffffff !important;
border: 1px solid #e2e8f0 !important;
border-radius: 6px !important;
font-family: system-ui, -apple-system, sans-serif !important;
font-size: 12px !important;
}
#scenes-popup .tabulator .tabulator-header {
background-color: #f8fafc !important;
border-bottom: 1px solid #e2e8f0 !important;
color: #64748b !important;
}
#scenes-popup .tabulator .tabulator-header .tabulator-col {
background-color: transparent !important;
border-right: none !important;
}
#scenes-popup .tabulator .tabulator-header .tabulator-col-title {
color: #475569 !important;
font-weight: 600 !important;
}
#scenes-popup .tabulator .tabulator-row {
background-color: #ffffff !important;
color: #1e293b !important;
border-bottom: 1px solid #f1f5f9 !important;
}
#scenes-popup .tabulator .tabulator-row:hover {
background-color: #f1f5f9 !important;
}
#scenes-popup .tabulator .tabulator-row.tabulator-selected,
#scenes-popup .tabulator .tabulator-row.tabulator-selected:hover {
background-color: #e0e7ff !important;
color: #1e293b !important;
}
#scenes-popup .tabulator .tabulator-placeholder {
color: #94a3b8 !important;
}
`;
document.head.appendChild(style);
}
const fetchGQL = async (query, variables = {}) =>
fetch('/graphql', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query, variables })
}).then(res => res.json());
function showToast(message, type = "success") {
if (typeof Toastify === 'undefined') return;
Toastify({
text: message,
duration: 2000,
gravity: "top",
position: "center",
backgroundColor: type === "success" ? "#10b981" : "#ef4444",
}).showToast();
}
const toastSuccess = (message, debug) => {
showToast(message, 'success');
if (debug) console.log(debug);
};
const toastError = (message, debug) => {
showToast(message, 'error');
console.error(debug);
};
let currentMenu = null;
let currentPopup = null;
function closeMenu() {
if (currentMenu) {
currentMenu.remove();
currentMenu = null;
}
}
function createCustomMenu(clickEvent, sceneId, cardElement) {
const menu = document.createElement('div');
menu.id = 'scenes-custom-menu';
menu.style.position = 'absolute';
menu.style.zIndex = '999999';
const editTagsLink = document.createElement('a');
editTagsLink.href = '#';
editTagsLink.textContent = 'Edit Tags...';
editTagsLink.addEventListener('click', function(e) {
e.preventDefault();
closeMenu();
createTabulatorPopup('Tags', sceneId, fetchTags, fetchExistingTagIds, cardElement);
});
menu.appendChild(editTagsLink);
const editPerformersLink = document.createElement('a');
editPerformersLink.href = '#';
editPerformersLink.textContent = 'Edit Performers...';
editPerformersLink.addEventListener('click', function(e) {
e.preventDefault();
closeMenu();
createTabulatorPopup('Performers', sceneId, fetchPerformers, fetchExistingPerformerIds, cardElement);
});
menu.appendChild(editPerformersLink);
const editGalleriesLink = document.createElement('a');
editGalleriesLink.href = '#';
editGalleriesLink.textContent = 'Edit Galleries...';
editGalleriesLink.addEventListener('click', function(e) {
e.preventDefault();
closeMenu();
createTabulatorPopup('Galleries', sceneId, fetchGalleries, fetchExistingGalleryIds, cardElement);
});
menu.appendChild(editGalleriesLink);
const editSceneLink = document.createElement('a');
editSceneLink.href = '#';
editSceneLink.textContent = 'Edit Scene';
editSceneLink.addEventListener('click', function(e) {
e.preventDefault();
closeMenu();
openEditScenePage(sceneId);
});
menu.appendChild(editSceneLink);
const hr = document.createElement('div');
hr.style.height = '1px';
hr.style.background = '#e2e8f0';
hr.style.margin = '4px 0';
menu.appendChild(hr);
const supportLink = document.createElement('a');
supportLink.href = 'https://www.patreon.com/serechops/membership';
supportLink.textContent = 'Support';
supportLink.style.color = '#d97706';
supportLink.target = '_blank';
supportLink.addEventListener('click', () => { closeMenu(); });
menu.appendChild(supportLink);
document.body.appendChild(menu);
currentMenu = menu;
return menu;
}
function showCustomMenu(event, sceneId, cardElement) {
closeMenu();
if (currentPopup) currentPopup.remove();
const menu = createCustomMenu(event, sceneId, cardElement);
event.preventDefault();
const absX = event.clientX + window.scrollX;
const absY = event.clientY + window.scrollY;
menu.style.visibility = 'hidden';
menu.style.top = `${absY}px`;
menu.style.left = `${absX}px`;
requestAnimationFrame(() => {
const rect = menu.getBoundingClientRect();
let posX = absX;
let posY = absY;
if (event.clientX + rect.width > window.innerWidth) {
posX = (window.innerWidth + window.scrollX) - rect.width - 10;
}
if (event.clientY + rect.height > window.innerHeight) {
posY = (window.innerHeight + window.scrollY) - rect.height - 10;
}
menu.style.top = `${posY}px`;
menu.style.left = `${posX}px`;
menu.style.visibility = 'visible';
});
const handleOutsideClick = (e) => {
if (!menu.contains(e.target)) {
closeMenu();
document.removeEventListener('mousedown', handleOutsideClick);
}
};
document.addEventListener('mousedown', handleOutsideClick);
}
function extractSceneId(cardElement) {
const link = cardElement.querySelector('a[href*="/scenes/"]');
if (link) {
const match = link.href.match(/scenes\/(\d+)/);
if (match) return match[1];
}
return null;
}
async function createTabulatorPopup(type, sceneId, fetchFunction, fetchExistingIdsFunction, cardElement) {
const singularType = type === 'Tags' ? 'Tag' : type === 'Performers' ? 'Performer' : 'Gallery';
const form = document.createElement('form');
form.id = 'scenes-popup';
form.style.position = 'absolute';
form.style.zIndex = '1000000';
form.style.padding = '14px';
form.style.background = '#ffffff';
form.style.border = '1px solid #e2e8f0';
form.style.borderRadius = '10px';
form.style.boxShadow = '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)';
form.style.width = '320px';
form.style.boxSizing = 'border-box';
form.style.fontFamily = 'system-ui, -apple-system, sans-serif';
form.innerHTML = `
<h2 style="margin: 0 0 12px 0; font-size: 13px; font-weight: 600; cursor: move; user-select: none; color: #0f172a; display: flex; align-items: center; justify-content: space-between;">
<span>Edit ${type} for Scene</span>
<span style="font-size: 11px; color: #64748b; font-weight: normal;">Draggable</span>
</h2>
<div style="display: flex; gap: 6px; margin-bottom: 10px; align-items: center;">
<div style="position: relative; flex: 1;">
<input type="text" id="${type.toLowerCase()}-search" placeholder="Search ${type}..." style="width: 100%; padding: 7px 28px 7px 10px; box-sizing: border-box; border-radius: 6px; border: 1px solid #cbd5e1; background: #ffffff; color: #1e293b; font-size: 12px; outline: none; transition: border-color 0.15s;">
<span id="${type.toLowerCase()}-clear" style="position: absolute; right: 8px; top: 50%; transform: translateY(-50%); cursor: pointer; color: #94a3b8; font-size: 16px; line-height: 1; display: none; user-select: none;" title="Clear search">×</span>
</div>
<button type="button" id="${type.toLowerCase()}-create-btn" style="padding: 7px 8px; cursor: pointer; font-size: 12px; font-weight: 500; background: #10b981; color: white; border: none; border-radius: 6px; white-space: nowrap; display: none; transition: background 0.15s;" title="Create new ${singularType}">Create</button>
<button type="button" id="${type.toLowerCase()}-refresh-btn" style="padding: 7px 8px; cursor: pointer; font-size: 12px; font-weight: 500; background: #64748b; color: white; border: none; border-radius: 6px; white-space: nowrap; transition: background 0.15s;" title="Refresh list and sort selections to top">Refresh</button>
</div>
<div id="${type.toLowerCase()}-table" style="margin-bottom: 10px; width: 100%; box-sizing: border-box;"></div>
<div style="display: flex; gap: 8px;">
<button type="button" id="add-${type.toLowerCase()}" style="flex: 1; padding: 7px; cursor: pointer; font-size: 12px; font-weight: 500; background: #6366f1; color: white; border: none; border-radius: 6px; transition: background 0.15s;">Save ${type}</button>
<button type="button" id="cancel" style="flex: 1; padding: 7px; cursor: pointer; font-size: 12px; font-weight: 500; background: #f1f5f9; color: #334155; border: 1px solid #cbd5e1; border-radius: 6px; transition: background 0.15s;">Cancel</button>
</div>
`;
document.body.appendChild(form);
currentPopup = form;
const handlePopupOutsideClick = (e) => {
if (!form.contains(e.target)) {
form.remove();
currentPopup = null;
document.removeEventListener('mousedown', handlePopupOutsideClick);
}
};
setTimeout(() => { document.addEventListener('mousedown', handlePopupOutsideClick); }, 0);
let isDragging = false;
let dragOffsetX, dragOffsetY;
const header = form.querySelector('h2');
header.onmousedown = function(e) {
isDragging = true;
dragOffsetX = e.clientX - form.offsetLeft;
dragOffsetY = e.clientY - form.offsetTop;
document.onmousemove = function(e) {
if (isDragging) {
form.style.left = `${e.clientX - dragOffsetX}px`;
form.style.top = `${e.clientY - dragOffsetY}px`;
}
};
document.onmouseup = function() {
isDragging = false;
document.onmousemove = null;
document.onmouseup = null;
};
};
const tableColumns = [
{ title: "ID", field: "id", width: 55, hozAlign: "center", headerHozAlign: "center" },
{ title: type === 'Galleries' ? 'Title' : 'Name', field: type === 'Galleries' ? 'title' : 'name', widthGrow: 2, minWidth: 130 },
];
if (type === 'Performers') {
tableColumns.push({ title: "Disambiguation", field: "disambiguation", widthGrow: 1, minWidth: 90 });
}
const table = new Tabulator(`#${type.toLowerCase()}-table`, {
layout: "fitColumns",
height: "160px",
placeholder: "No Data Available",
selectable: true,
index: "id",
columns: tableColumns,
});
const existingIds = await fetchExistingIdsFunction(sceneId);
const selectedIds = new Set(existingIds.map(id => String(id)));
let isRestoringSelections = false;
const filterInput = document.getElementById(`${type.toLowerCase()}-search`);
const clearBtn = document.getElementById(`${type.toLowerCase()}-clear`);
const createBtn = form.querySelector(`#${type.toLowerCase()}-create-btn`);
const refreshBtn = form.querySelector(`#${type.toLowerCase()}-refresh-btn`);
const updateControlsVisibility = () => {
const hasValue = filterInput.value.trim().length > 0;
clearBtn.style.display = hasValue ? 'block' : 'none';
createBtn.style.display = hasValue ? 'block' : 'none';
};
table.on("rowSelected", function(row) {
if (!isRestoringSelections) {
const id = row.getData().id;
if (id !== undefined && id !== null) {
selectedIds.add(String(id));
}
if (filterInput.value.trim().length > 0) {
filterInput.value = '';
updateControlsVisibility();
setTimeout(async () => {
await fetchData("");
filterInput.focus();
}, 0);
}
}
});
table.on("rowDeselected", function(row) {
if (!isRestoringSelections) {
const id = row.getData().id;
if (id !== undefined && id !== null) {
selectedIds.delete(String(id));
}
}
});
async function fetchData(query) {
const data = await fetchFunction(query);
data.sort((a, b) => {
const aSelected = selectedIds.has(String(a.id));
const bSelected = selectedIds.has(String(b.id));
if (aSelected && !bSelected) return -1;
if (!aSelected && bSelected) return 1;
const field = type === 'Galleries' ? 'title' : 'name';
return (a[field] || '').localeCompare(b[field] || '');
});
isRestoringSelections = true;
try {
await table.setData(data);
selectedIds.forEach(id => {
const row = table.getRow(id);
if (row) table.selectRow(row);
});
} finally {
isRestoringSelections = false;
}
}
function debounce(func, wait) {
let timeout;
return function(...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait);
};
}
filterInput.addEventListener('input', debounce(e => {
updateControlsVisibility();
fetchData(e.target.value);
}, 300));
clearBtn.addEventListener('click', () => {
filterInput.value = '';
updateControlsVisibility();
fetchData("");
filterInput.focus();
});
refreshBtn.addEventListener('click', async () => {
await fetchData(filterInput.value.trim());
});
createBtn.addEventListener('click', async () => {
const val = filterInput.value.trim();
if (!val) return;
let newId = null;
if (type === 'Tags') newId = await createNewTag(val);
else if (type === 'Performers') newId = await createNewPerformer(val);
else if (type === 'Galleries') newId = await createNewGallery(val);
if (newId) {
selectedIds.add(String(newId));
filterInput.value = '';
updateControlsVisibility();
await fetchData("");
filterInput.focus();
}
});
await fetchData("");
document.getElementById(`add-${type.toLowerCase()}`).addEventListener('click', async function() {
if (!isTabActive) await new Promise(resolve => setTimeout(resolve, 200));
const allSelectedIds = Array.from(selectedIds);
let success = false;
if (type === 'Tags') success = await updateSceneWithTags(sceneId, allSelectedIds);
else if (type === 'Performers') success = await updateSceneWithPerformers(sceneId, allSelectedIds);
else if (type === 'Galleries') success = await updateSceneWithGalleries(sceneId, allSelectedIds);
form.remove();
currentPopup = null;
document.removeEventListener('mousedown', handlePopupOutsideClick);
if (success !== false) {
toastSuccess(`Scene updated! Reloading...`);
sessionStorage.setItem(scrollKey, window.scrollY);
setTimeout(() => { window.location.reload(); }, 400);
}
});
document.getElementById('cancel').addEventListener('click', () => {
form.remove();
currentPopup = null;
document.removeEventListener('mousedown', handlePopupOutsideClick);
});
const cardRect = cardElement.getBoundingClientRect();
let popupX = cardRect.right + window.scrollX + 10;
let popupY = cardRect.top + window.scrollY;
form.style.visibility = 'hidden';
form.style.left = `${popupX}px`;
form.style.top = `${popupY}px`;
requestAnimationFrame(() => {
const formRect = form.getBoundingClientRect();
if (cardRect.right + 10 + formRect.width > window.innerWidth) {
popupX = cardRect.left + window.scrollX - formRect.width - 10;
}
if (cardRect.top + formRect.height > window.innerHeight) {
popupY = (window.innerHeight + window.scrollY) - formRect.height - 10;
}
form.style.left = `${popupX}px`;
form.style.top = `${popupY}px`;
form.style.visibility = 'visible';
filterInput.focus();
});
}
async function fetchTags(query) {
const gqlQuery = `query ($filter: String!) {
findTags(tag_filter: { name: { value: $filter, modifier: INCLUDES } }, filter: { per_page: -1 }) {
tags { id name }
}
}`;
return await fetchGQL(gqlQuery, { filter: query }).then(res => res.data.findTags.tags);
}
async function fetchPerformers(query) {
const gqlQuery = `query ($filter: String!) {
findPerformers(performer_filter: { name: { value: $filter, modifier: INCLUDES } }, filter: { per_page: -1 }) {
performers { id name disambiguation }
}
}`;
return await fetchGQL(gqlQuery, { filter: query }).then(res => res.data.findPerformers.performers);
}
async function fetchGalleries(query) {
const gqlQuery = `query ($filter: String!) {
findGalleries(gallery_filter: { title: { value: $filter, modifier: INCLUDES } }, filter: { per_page: -1 }) {
galleries { id title }
}
}`;
return await fetchGQL(gqlQuery, { filter: query }).then(res => res.data.findGalleries.galleries);
}
async function createNewTag(name) {
const mutation = `mutation ($name: String!) { tagCreate(input: { name: $name }) { id name } }`;
const res = await fetchGQL(mutation, { name });
if (res.errors) { toastError('Failed to create tag', res.errors); return null; }
toastSuccess('Tag created successfully', res.data.tagCreate);
return res.data.tagCreate.id;
}
async function createNewPerformer(name) {
const mutation = `mutation ($name: String!) { performerCreate(input: { name: $name }) { id name } }`;
const res = await fetchGQL(mutation, { name });
if (res.errors) { toastError('Failed to create performer', res.errors); return null; }
toastSuccess('Performer created successfully', res.data.performerCreate);
return res.data.performerCreate.id;
}
async function createNewGallery(title) {
const mutation = `mutation ($title: String!) { galleryCreate(input: { title: $title }) { id title } }`;
const res = await fetchGQL(mutation, { title });
if (res.errors) { toastError('Failed to create gallery', res.errors); return null; }
toastSuccess('Gallery created successfully', res.data.galleryCreate);
return res.data.galleryCreate.id;
}
async function updateSceneWithTags(sceneId, tagIds) {
const mutationQuery = `mutation ($scene_id: ID!, $tag_ids: [ID!]!) { sceneUpdate(input: { id: $scene_id, tag_ids: $tag_ids }) { id } }`;
return await fetchGQL(mutationQuery, { scene_id: sceneId, tag_ids: tagIds })
.then(res => {
if (res.errors) {
toastError('Failed to update the scene with tags', res.errors);
return false;
} else {
return true;
}
}).catch(err => { toastError('Failed to update the scene with tags', err); return false; });
}
async function updateSceneWithPerformers(sceneId, performerIds) {
const gqlQuery = `mutation ($scene_id: ID!, $performer_ids: [ID!]!) { sceneUpdate(input: { id: $scene_id, performer_ids: $performer_ids }) { id } }`;
return await fetchGQL(gqlQuery, { scene_id: sceneId, performer_ids: performerIds })
.then(res => {
if (res.errors) {
toastError('Failed to update the scene with performers', res.errors);
return false;
} else {
return true;
}
}).catch(err => { toastError('Failed to update the scene with performers', err); return false; });
}
async function updateSceneWithGalleries(sceneId, galleryIds) {
const gqlQuery = `mutation ($scene_id: ID!, $gallery_ids: [ID!]!) { sceneUpdate(input: { id: $scene_id, gallery_ids: $gallery_ids }) { id } }`;
return await fetchGQL(gqlQuery, { scene_id: sceneId, gallery_ids: galleryIds })
.then(res => {
if (res.errors) {
toastError('Failed to update the scene with galleries', res.errors);
return false;
} else {
return true;
}
}).catch(err => { toastError('Failed to update the scene with galleries', err); return false; });
}
async function fetchExistingTagIds(sceneId) {
const query = `query ($id: ID!) { findScene(id: $id) { tags { id } } }`;
return await fetchGQL(query, { id: sceneId }).then(res => res.data?.findScene?.tags?.map(tag => tag.id) || []);
}
async function fetchExistingPerformerIds(sceneId) {
const query = `query ($id: ID!) { findScene(id: $id) { performers { id } } }`;
return await fetchGQL(query, { id: sceneId }).then(res => res.data?.findScene?.performers?.map(performer => performer.id) || []);
}
async function fetchExistingGalleryIds(sceneId) {
const query = `query ($id: ID!) { findScene(id: $id) { galleries { id } } }`;
return await fetchGQL(query, { id: sceneId }).then(res => res.data?.findScene?.galleries?.map(gallery => gallery.id) || []);
}
function openEditScenePage(sceneId) {
const editPageUrl = `/scenes/${sceneId}/edit`;
const newWindow = window.open(editPageUrl, '_blank');
if (newWindow) {
newWindow.onload = () => {
setTimeout(() => {
const editTab = newWindow.document.querySelector('[data-rb-event-key="scene-edit-panel"]');
if (editTab) editTab.click();
}, 1000);
};
}
}
// Right-click: Restricted ONLY to the video preview/thumbnail frame area
document.addEventListener('contextmenu', async function(event) {
const sceneCard = event.target.closest('.scene-card');
if (!sceneCard) return;
const previewContainer = event.target.closest('.scene-card-preview, .thumbnail-container, .scene-image, .image-container, .card-image');
if (!previewContainer) return; // Allows the rest of the card to trigger default Chrome right-click
const sceneId = extractSceneId(sceneCard);
if (sceneId) {
showCustomMenu(event, sceneId, sceneCard);
}
});
// Left-click: Badge shortcuts enabled strictly on badge elements
document.addEventListener('click', function(event) {
const sceneCard = event.target.closest('.scene-card');
if (!sceneCard) return;
// Target actual tag/performer/gallery pill or badge elements specifically
const badgeElement = event.target.closest('.tag, .performer-tag, .gallery-tag, .chip, [class*="badge"], [class*="tag"]');
if (!badgeElement) return;
const sceneId = extractSceneId(sceneCard);
if (!sceneId) return;
const svg = badgeElement.querySelector('svg') || event.target.closest('svg');
const iconClass = svg ? svg.getAttribute('class') || '' : '';
const parentHtml = badgeElement.outerHTML;
if (iconClass.includes('fa-tag') || parentHtml.includes('fa-tag') || badgeElement.classList.contains('tag')) {
event.preventDefault();
event.stopPropagation();
closeMenu();
createTabulatorPopup('Tags', sceneId, fetchTags, fetchExistingTagIds, sceneCard);
}
else if (iconClass.includes('fa-user') || parentHtml.includes('fa-user') || badgeElement.classList.contains('performer-tag')) {
event.preventDefault();
event.stopPropagation();
closeMenu();
createTabulatorPopup('Performers', sceneId, fetchPerformers, fetchExistingPerformerIds, sceneCard);
}
else if (iconClass.includes('fa-film') || iconClass.includes('fa-photo-film') || parentHtml.includes('fa-film') || parentHtml.includes('fa-photo-film')) {
event.preventDefault();
event.stopPropagation();
closeMenu();
createTabulatorPopup('Galleries', sceneId, fetchGalleries, fetchExistingGalleryIds, sceneCard);
}
}, true);
})();



