Skip to content

Commit 703c96a

Browse files
authored
[StashRandomButton] Add support for scenes, images, performers, studios, tags, groups and galleries (#566)
1 parent e0cea86 commit 703c96a

File tree

3 files changed

+123
-158
lines changed

3 files changed

+123
-158
lines changed

plugins/StashRandomButton/README.md

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,23 @@
11
# Stash Random Button Plugin
22

3-
https://discourse.stashapp.cc/t/randombutton/1809
3+
[Plugin thread on Discourse](https://discourse.stashapp.cc/t/randombutton/1809)
44

5-
Adds a "Random" button to the image & scenes page to quickly navigate to a random scene.
5+
Adds a "Random" button to the Stash UI, letting you instantly jump to a random scene, image, performer, studio, group, tag, or gallery—including random "internal" navigation (e.g. a random scene inside a studio).
66

77
## Features
8-
- Adds a "Random" button to the Stash UI.
9-
- Selects a random scene via GraphQL query.
8+
9+
- Adds a "Random" button to the Stash UI navigation bar.
10+
- Supports random navigation for:
11+
- **Scenes** (global and within performer, studio, tag, group)
12+
- **Images** (global and within a gallery)
13+
- **Performers** (global)
14+
- **Studios** (global)
15+
- **Groups** (global)
16+
- **Tags** (global)
17+
- **Galleries** (global)
1018
- Lightweight, no external dependencies.
19+
- Uses Stash's GraphQL API.
20+
- Simple, robust, and easy to maintain.
1121

1222
## Installation
1323

@@ -30,11 +40,15 @@ Adds a "Random" button to the image & scenes page to quickly navigate to a rando
3040
- The button should appear on those pages.
3141

3242
## Usage
33-
Click the "Random" button in the navigation bar to jump to a random image or scene depending on the tab.
43+
Click the "Random" button in the navigation bar to jump to a random entity (scene, image, performer, studio, group, tag, or gallery) depending on your current page.
44+
- On internal entity pages (e.g., performer, studio, group, tag, gallery), the button picks a random scene or image from inside that entity.
3445

3546
## Requirements
3647
- Stash version v0.27.2 or higher.
3748

3849
## Development
3950
- Written in JavaScript using the Stash Plugin API.
4051
- Edit `random-button.js` to customize and reload plugins in Stash.
52+
53+
## Changelog
54+
- 2.0.0: Major upgrade! Now supports random navigation for performers, studios, groups, tags, galleries, and images (global and internal).
Lines changed: 101 additions & 150 deletions
Original file line numberDiff line numberDiff line change
@@ -1,182 +1,133 @@
1-
(function () {
1+
(function() {
22
'use strict';
33

4-
function addRandomButton() {
5-
const existingButton = document.querySelector('.random-btn');
6-
if (existingButton) {
7-
const styles = window.getComputedStyle(existingButton);
8-
return true;
9-
}
10-
11-
const navContainer = document.querySelector('.navbar-buttons.flex-row.ml-auto.order-xl-2.navbar-nav');
12-
if (!navContainer) {
13-
return false;
14-
}
4+
function getIdFromPath(regex) {
5+
let m = window.location.pathname.match(regex);
6+
return m ? m[1] : null;
7+
}
158

16-
const randomButtonContainer = document.createElement('div');
17-
randomButtonContainer.className = 'mr-2';
18-
randomButtonContainer.innerHTML = `
19-
<a href="javascript:void(0)">
20-
<button type="button" class="btn btn-primary random-btn" style="display: inline-block !important; visibility: visible !important;">Random</button>
21-
</a>
22-
`;
23-
randomButtonContainer.querySelector('button').addEventListener('click', loadRandomContent);
9+
function getPlural(entity) {
10+
return (entity === "Gallery") ? "Galleries"
11+
: (entity === "Tag") ? "Tags"
12+
: (entity === "Image") ? "Images"
13+
: (entity === "Scene") ? "Scenes"
14+
: (entity === "Performer") ? "Performers"
15+
: (entity === "Studio") ? "Studios"
16+
: (entity === "Group") ? "Groups"
17+
: entity + "s";
18+
}
2419

20+
async function randomGlobal(entity, idField, redirectPrefix, internalFilter) {
21+
const realEntityPlural = getPlural(entity);
22+
let filter = { per_page: 1 };
23+
let variables = { filter };
24+
let filterArg = "";
25+
let filterVar = "";
26+
27+
if (internalFilter) {
28+
filterArg = `, $internal_filter: ${entity}FilterType`;
29+
filterVar = `, ${entity.toLowerCase()}_filter: $internal_filter`;
30+
variables.internal_filter = internalFilter;
31+
}
2532

26-
if (window.location.pathname.match(/^\/(scenes|images)(?:$|\?)/)) {
27-
let refButton = document.querySelector('a[href="/scenes/new"]');
28-
if (window.location.pathname.includes('/images')) {
29-
refButton = document.querySelector('a[href="/stats"]');
30-
}
31-
if (!refButton) {
32-
refButton = navContainer.querySelector('a[href="https://opencollective.com/stashapp"]');
33+
const countQuery = `
34+
query Find${realEntityPlural}($filter: FindFilterType${filterArg}) {
35+
find${realEntityPlural}(filter: $filter${filterVar}) { count }
3336
}
34-
if (refButton) {
35-
refButton.parentElement.insertAdjacentElement('afterend', randomButtonContainer);
36-
} else {
37-
navContainer.appendChild(randomButtonContainer);
37+
`;
38+
let countResp = await fetch('/graphql', {
39+
method: 'POST', headers: { 'Content-Type': 'application/json' },
40+
body: JSON.stringify({ query: countQuery, variables })
41+
});
42+
let countData = await countResp.json();
43+
if (countData.errors) { alert("Error: " + JSON.stringify(countData.errors)); return; }
44+
const totalCount = countData.data[`find${realEntityPlural}`].count;
45+
if (!totalCount) { alert("No results found."); return; }
46+
47+
const randomIndex = Math.floor(Math.random() * totalCount);
48+
let itemVars = { filter: { per_page: 1, page: randomIndex + 1 } };
49+
if (internalFilter) itemVars.internal_filter = internalFilter;
50+
const itemQuery = `
51+
query Find${realEntityPlural}($filter: FindFilterType${filterArg}) {
52+
find${realEntityPlural}(filter: $filter${filterVar}) { ${idField} { id } }
3853
}
39-
return true;
40-
}
54+
`;
55+
let itemResp = await fetch('/graphql', {
56+
method: 'POST', headers: { 'Content-Type': 'application/json' },
57+
body: JSON.stringify({ query: itemQuery, variables: itemVars })
58+
});
59+
let itemData = await itemResp.json();
60+
if (itemData.errors) { alert("Error: " + JSON.stringify(itemData.errors)); return; }
61+
const arr = itemData.data[`find${realEntityPlural}`][idField];
62+
if (!arr || arr.length === 0) { alert("No results found."); return; }
63+
window.location.href = `${redirectPrefix}${arr[0].id}`;
64+
}
4165

42-
if (window.location.pathname.match(/\/(scenes|images)\/\d+/)) {
43-
const refButton = navContainer.querySelector('a[href="https://opencollective.com/stashapp"]');
44-
if (refButton) {
45-
refButton.insertAdjacentElement('afterend', randomButtonContainer);
46-
} else {
47-
const firstLink = navContainer.querySelector('a');
48-
if (firstLink) {
49-
firstLink.parentElement.insertAdjacentElement('afterend', randomButtonContainer);
50-
} else {
51-
navContainer.appendChild(randomButtonContainer);
52-
}
53-
}
54-
return true;
55-
}
66+
async function randomButtonHandler() {
67+
const pathname = window.location.pathname.replace(/\/$/, '');
5668

57-
return false;
58-
}
69+
if (pathname === '/scenes') return randomGlobal('Scene', 'scenes', '/scenes/');
70+
if (pathname === '/performers') return randomGlobal('Performer', 'performers', '/performers/');
71+
if (pathname === '/groups') return randomGlobal('Group', 'groups', '/groups/');
72+
if (pathname === '/studios') return randomGlobal('Studio', 'studios', '/studios/');
73+
if (pathname === '/tags') return randomGlobal('Tag', 'tags', '/tags/');
74+
if (pathname === '/galleries') return randomGlobal('Gallery', 'galleries', '/galleries/');
75+
if (pathname === '/images') return randomGlobal('Image', 'images', '/images/');
5976

60-
function getParentHierarchy(element) {
61-
const hierarchy = [];
62-
let current = element;
63-
while (current && current !== document.body) {
64-
hierarchy.push(current.tagName + (current.className ? '.' + current.className.split(' ').join('.') : ''));
65-
current = current.parentElement;
66-
}
67-
return hierarchy.join(' > ');
68-
}
77+
// --- INTERN ---
78+
let studioId = getIdFromPath(/^\/studios\/(\d+)\/scenes/);
79+
if (studioId) return randomGlobal('Scene', 'scenes', '/scenes/', { studios: { value: [studioId], modifier: "INCLUDES_ALL" } });
6980

70-
async function loadRandomContent() {
71-
try {
72-
const isScenes = window.location.pathname.includes('/scenes');
73-
const isImages = window.location.pathname.includes('/images');
74-
const type = isScenes ? 'scenes' : isImages ? 'images' : 'scenes';
75-
76-
const countQuery = `
77-
query Find${type.charAt(0).toUpperCase() + type.slice(1)}($filter: FindFilterType) {
78-
find${type.charAt(0).toUpperCase() + type.slice(1)}(filter: $filter) {
79-
count
80-
}
81-
}
82-
`;
83-
const countVariables = { filter: { per_page: 1 } };
84-
85-
const countResponse = await fetch('/graphql', {
86-
method: 'POST',
87-
headers: { 'Content-Type': 'application/json' },
88-
body: JSON.stringify({ query: countQuery, variables: countVariables })
89-
});
90-
91-
const countResult = await countResponse.json();
92-
if (countResult.errors) {
93-
return;
94-
}
81+
let groupId = getIdFromPath(/^\/groups\/(\d+)\/scenes/);
82+
if (groupId) return randomGlobal('Scene', 'scenes', '/scenes/', { groups: { value: [groupId], modifier: "INCLUDES_ALL" } });
9583

96-
const totalCount = countResult.data[`find${type.charAt(0).toUpperCase() + type.slice(1)}`].count;
97-
if (totalCount === 0) {
98-
return;
99-
}
84+
let performerId = getIdFromPath(/^\/performers\/(\d+)\/scenes/);
85+
if (performerId) return randomGlobal('Scene', 'scenes', '/scenes/', { performers: { value: [performerId], modifier: "INCLUDES_ALL" } });
10086

101-
const randomIndex = Math.floor(Math.random() * totalCount);
102-
const itemQuery = `
103-
query Find${type.charAt(0).toUpperCase() + type.slice(1)}($filter: FindFilterType) {
104-
find${type.charAt(0).toUpperCase() + type.slice(1)}(filter: $filter) {
105-
${type} {
106-
id
107-
}
108-
}
109-
}
110-
`;
111-
const itemVariables = {
112-
filter: { per_page: 1, page: Math.floor(randomIndex / 1) + 1 }
113-
};
114-
115-
const itemResponse = await fetch('/graphql', {
116-
method: 'POST',
117-
headers: { 'Content-Type': 'application/json' },
118-
body: JSON.stringify({ query: itemQuery, variables: itemVariables })
119-
});
120-
121-
const itemResult = await itemResponse.json();
122-
if (itemResult.errors) {
123-
return;
124-
}
87+
let tagId = getIdFromPath(/^\/tags\/(\d+)\/scenes/);
88+
if (tagId) return randomGlobal('Scene', 'scenes', '/scenes/', { tags: { value: [tagId], modifier: "INCLUDES_ALL" } });
12589

126-
const items = itemResult.data[`find${type.charAt(0).toUpperCase() + type.slice(1)}`][type];
127-
if (items.length === 0) {
128-
return;
129-
}
90+
let galleryId = getIdFromPath(/^\/galleries\/(\d+)/);
91+
if (galleryId) return randomGlobal('Image', 'images', '/images/', { galleries: { value: [galleryId], modifier: "INCLUDES_ALL" } });
13092

131-
const itemId = items[0].id;
132-
window.location.href = `/${type}/${itemId}`;
133-
} catch (error) {
134-
console.error(error);
135-
}
93+
alert('Not supported');
13694
}
13795

138-
window.addEventListener('load', () => {
139-
addRandomButton();
140-
});
96+
function addRandomButton() {
97+
if (document.querySelector('.random-btn')) return;
98+
const navContainer = document.querySelector('.navbar-buttons.flex-row.ml-auto.order-xl-2.navbar-nav');
99+
if (!navContainer) return;
141100

142-
document.addEventListener('click', (event) => {
143-
const target = event.target.closest('a');
144-
if (target && target.href) {
145-
setTimeout(() => {
146-
addRandomButton();
147-
}, 1500);
148-
}
149-
});
101+
const randomButtonContainer = document.createElement('div');
102+
randomButtonContainer.className = 'mr-2';
103+
randomButtonContainer.innerHTML = `
104+
<a href="javascript:void(0)">
105+
<button type="button" class="btn btn-primary random-btn" style="display: inline-block !important; visibility: visible !important;">Random</button>
106+
</a>
107+
`;
108+
randomButtonContainer.querySelector('button').addEventListener('click', randomButtonHandler);
150109

151-
window.addEventListener('popstate', () => {
152-
setTimeout(() => {
153-
addRandomButton();
154-
}, 1500);
155-
});
110+
navContainer.appendChild(randomButtonContainer);
111+
}
156112

157-
window.addEventListener('hashchange', () => {
158-
setTimeout(() => {
159-
addRandomButton();
160-
}, 1500);
113+
window.addEventListener('load', () => addRandomButton());
114+
document.addEventListener('click', (event) => {
115+
const target = event.target.closest('a');
116+
if (target && target.href) setTimeout(() => addRandomButton(), 1500);
161117
});
162-
118+
window.addEventListener('popstate', () => setTimeout(() => addRandomButton(), 1500));
119+
window.addEventListener('hashchange', () => setTimeout(() => addRandomButton(), 1500));
163120
const navContainer = document.querySelector('.navbar-buttons.flex-row.ml-auto.order-xl-2.navbar-nav');
164121
if (navContainer) {
165-
const observer = new MutationObserver((mutations) => {
166-
mutations.forEach(m => {
167-
});
168-
if (!document.querySelector('.random-btn')) {
169-
addRandomButton();
170-
}
122+
const observer = new MutationObserver(() => {
123+
if (!document.querySelector('.random-btn')) addRandomButton();
171124
});
172125
observer.observe(navContainer, { childList: true, subtree: true });
173-
} else {
174126
}
175-
176-
177127
let intervalAttempts = 0;
178128
setInterval(() => {
179129
intervalAttempts++;
180130
addRandomButton();
181131
}, intervalAttempts < 60 ? 500 : 2000);
132+
182133
})();
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
name: RandomButton
2-
description: Adds a button to quickly switch to a random scene or image on both overview and detail pages
3-
version: 1.1.0
2+
description: Adds a button to quickly jump to a random scene, image, performer, studio, group, tag, or gallery, both on overview and internal entity pages.
3+
version: 2.0.0
44
url: https://example.com
55
ui:
66
requires: []
77
javascript:
88
- random_button.js
99
css:
10-
- random_button.css
10+
- random_button.css

0 commit comments

Comments
 (0)