Skip to content

Commit a3eb5e0

Browse files
Stash-KennyGKennyGDogmaDragon
authored
[CJ's Card Tweaks] Enhancement: Extend to Include Stash Count (#671)
Co-authored-by: KennyG <kennyg@kennyg.com> Co-authored-by: DogmaDragon <103123951+DogmaDragon@users.noreply.github.com>
1 parent ce30de4 commit a3eb5e0

3 files changed

Lines changed: 157 additions & 4 deletions

File tree

plugins/cjCardTweaks/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,8 @@ Adds an additional dimension to the rating banners.
2020
![unnamed_2](https://github.com/user-attachments/assets/f505417d-ed0c-40c4-9c78-647081a41307)
2121

2222
Modify the performer cards to use a traditional profile design
23+
24+
### Stash ID icon
25+
![unnamed_3](https://github.com/user-attachments/assets/181fe3cd-b3e5-437d-8ded-7e48f2c0e446)
26+
27+
Adds a box icon to performer cards that have one or more Stash IDs (GUIDs) attached. The icon appears in the top-left corner of the performer card thumbnail and displays a tooltip showing the count of Stash IDs when hovered. This helps quickly identify performers that are linked to external Stash databases.

plugins/cjCardTweaks/cjCardTweaks.js

Lines changed: 146 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@
2626
if (
2727
key === "fileCount" ||
2828
key === "addBannerDimension" ||
29-
key === "performerProfileCards"
29+
key === "performerProfileCards" ||
30+
key === "stashIDIcon"
3031
) {
3132
acc[key] = settings[key];
3233
} else {
@@ -42,6 +43,8 @@
4243
".performer-card:hover img.performer-card-image{box-shadow: 0 0 0 rgb(0 0 0 / 20%), 0 0 6px rgb(0 0 0 / 90%);transition: box-shadow .5s .5s}@media (min-width: 1691px){.performer-recommendations .card .performer-card-image{height: unset}}button.btn.favorite-button.not-favorite,button.btn.favorite-button.favorite{transition: filter .5s .5s}.performer-card:hover .thumbnail-section button.btn.favorite-button.not-favorite, .performer-card:hover .thumbnail-section button.btn.favorite-button.favorite{filter: drop-shadow(0 0 2px rgba(0, 0, 0, .9))}.performer-card .thumbnail-section button.btn.favorite-button.not-favorite, .performer-card .thumbnail-section button.btn.favorite-button.favorite{top: 10px;filter: drop-shadow(0 2px 2px rgba(0, 0, 0, .9))}.item-list-container .performer-card__age,.recommendation-row .performer-card__age,.item-list-container .performer-card .card-section-title,.recommendation-row .performer-card .card-section-title,.item-list-container .performer-card .thumbnail-section,.recommendation-row .performer-card .thumbnail-section{display: flex;align-content: center;justify-content: center}.item-list-container .performer-card .thumbnail-section a,.recommendation-row .performer-card .thumbnail-section a{display: contents}.item-list-container .performer-card-image,.recommendation-row .performer-card-image{aspect-ratio: 1 / 1;display: flex;object-fit: cover;border: 3px solid var(--plex-yelow);border-radius: 50%;min-width: unset;position: relative;width: 58%;margin: auto;z-index: 1;margin-top: 1.5rem;box-shadow:0 13px 26px rgb(0 0 0 / 20%),0 3px 6px rgb(0 0 0 / 90%);object-position: center;transition: box-shadow .5s .5s}.item-list-container .performer-card hr,.recommendation-row .performer-card hr{width: 90%}.item-list-container .performer-card .fi,.recommendation-row .performer-card .fi{position: absolute;top: 81.5%;left: 69%;border-radius: 50% !important;background-size: cover;margin-left: -1px;height: 1.5rem;width: 1.5rem;z-index: 10;border: solid 2px #252525;box-shadow: unset}.item-list-container .performer-card .card-popovers .btn,.recommendation-row .performer-card .card-popovers .btn{font-size: 0.9rem}";
4344
const RATING_BANNER_3D_STYLE =
4445
".grid-card{overflow:unset}.detail-group .rating-banner-3d,.rating-banner{display:none}.grid-card:hover .rating-banner-3d{opacity:0;transition:opacity .5s}.rating-banner-3d{height:110px;left:-6px;overflow:hidden;position:absolute;top:-6px;width:110px}.rating-banner-3d span{box-shadow:0 5px 4px rgb(0 0 0 / 50%);position:absolute;display:block;width:170px;padding:10px 5px 10px 0;background-color:#ff6a07;color:#fff;font:700 1rem/1 Lato,sans-serif;text-shadow:0 1px 1px rgba(0,0,0,.2);text-transform:uppercase;text-align:center;letter-spacing:1px;right:-20px;top:24px;transform:rotate(-45deg)}.rating-banner-3d::before{top:0;right:0;position:absolute;z-index:-1;content:'';display:block;border:5px solid #a34405;border-top-color:transparent;border-left-color:transparent}.rating-banner-3d::after{bottom:0;left:0;position:absolute;z-index:-1;content:'';display:block;border:5px solid #963e04}";
46+
const STASH_ID_ICON_STYLE =
47+
".stash-id-count{display:inline-flex;align-items:center;flex-direction:row}.stash-id-count-number{display:inline-block;margin-right:0.25rem}.stash-id-icon{display:inline-flex;align-items:center}.stash-id-icon svg{width:0.875rem;height:0.875rem;fill:currentColor;color:#fff}";
4548

4649
/**
4750
* Element to inject custom CSS styles.
@@ -54,6 +57,8 @@
5457
styleElement.innerHTML += RATING_BANNER_3D_STYLE;
5558
if (SETTINGS.performerProfileCards)
5659
styleElement.innerHTML += PERFORMER_PROFILE_CARD_STYLE;
60+
if (SETTINGS.stashIDIcon)
61+
styleElement.innerHTML += STASH_ID_ICON_STYLE;
5762

5863
function createElementFromHTML(htmlString) {
5964
const div = document.createElement("div");
@@ -93,7 +98,7 @@
9398
}
9499

95100
/**
96-
* Handles gallery cards to specific paths in Stash.
101+
* Handles gallery cards to specific paths in Stash.
97102
*
98103
* The supported paths are:
99104
* - /galleries
@@ -207,6 +212,36 @@
207212
cards.forEach((card) => {
208213
maybeAddFileCount(card, stashData, isContentCard);
209214
maybeAddDimensionToBanner(card);
215+
if (cardClass === "performer-card") {
216+
maybeAddStashIDIcon(card, stashData);
217+
218+
// Also set up a MutationObserver to watch for card-popovers being added
219+
if (SETTINGS.stashIDIcon && !card.querySelector(".stash-id-count")) {
220+
const observer = new MutationObserver((mutations) => {
221+
const cardPopovers = card.querySelector(".card-popovers.btn-group") ||
222+
card.querySelector(".card-popovers") ||
223+
card.querySelector('[role="group"].btn-group');
224+
if (cardPopovers && !cardPopovers.querySelector(".stash-id-count")) {
225+
const link = card.querySelector(".thumbnail-section > a");
226+
if (link) {
227+
const id = new URL(link.href).pathname.split("/").pop();
228+
const idNum = parseInt(id, 10);
229+
// Query GraphQL for stash IDs
230+
queryStashIDs(card, id, idNum);
231+
observer.disconnect();
232+
}
233+
}
234+
});
235+
236+
observer.observe(card, {
237+
childList: true,
238+
subtree: true
239+
});
240+
241+
// Disconnect after 5 seconds to avoid memory leaks
242+
setTimeout(() => observer.disconnect(), 5000);
243+
}
244+
}
210245
});
211246
}
212247

@@ -269,4 +304,113 @@
269304
link.parentElement.appendChild(el);
270305
oldBanner.remove();
271306
}
307+
308+
/**
309+
* Add Stash ID count and icon to performer cards in the card-popovers btn-group
310+
*
311+
* @param {Element} card - Card element from cards list.
312+
* @param {Object} stashData - Data fetched from the GraphQL interceptor. e.g. stash.performers.
313+
*/
314+
function maybeAddStashIDIcon(card, stashData) {
315+
if (!SETTINGS.stashIDIcon) return;
316+
317+
// Verify this function was not run twice on the same card
318+
const existingCount = card.querySelector(".stash-id-count");
319+
if (existingCount) return;
320+
321+
const link = card.querySelector(".thumbnail-section > a");
322+
if (!link) return;
323+
324+
const id = new URL(link.href).pathname.split("/").pop();
325+
const idNum = parseInt(id, 10);
326+
327+
// Query GraphQL for stash IDs
328+
queryStashIDs(card, id, idNum);
329+
}
330+
331+
/**
332+
* Query GraphQL for performer stash IDs
333+
* @param {Element} card - Card element
334+
* @param {string} id - Performer ID as string
335+
* @param {number} idNum - Performer ID as number
336+
*/
337+
async function queryStashIDs(card, id, idNum) {
338+
const query = `
339+
query FindPerformer($id: ID!) {
340+
findPerformer(id: $id) {
341+
id
342+
stash_ids {
343+
endpoint
344+
stash_id
345+
}
346+
}
347+
}
348+
`;
349+
350+
const variables = {
351+
id: idNum
352+
};
353+
354+
try {
355+
const response = await fetch('/graphql', {
356+
method: 'POST',
357+
headers: {
358+
'Content-Type': 'application/json',
359+
},
360+
body: JSON.stringify({
361+
query: query,
362+
variables: variables
363+
})
364+
});
365+
366+
const result = await response.json();
367+
368+
if (result.errors) return;
369+
370+
const performer = result.data?.findPerformer;
371+
if (!performer) return;
372+
373+
const stashIDs = performer.stash_ids || [];
374+
const stashIDCount = Array.isArray(stashIDs) ? stashIDs.length : 0;
375+
376+
// Only show if count is greater than 0
377+
if (stashIDCount > 0) {
378+
// Find card-popovers and add button
379+
const cardPopovers = card.querySelector(".card-popovers.btn-group") ||
380+
card.querySelector(".card-popovers") ||
381+
card.querySelector('[role="group"].btn-group');
382+
383+
if (cardPopovers && !cardPopovers.querySelector(".stash-id-count")) {
384+
addStashIDButton(cardPopovers, stashIDCount);
385+
}
386+
}
387+
} catch (error) {
388+
// On error, don't show anything (silent fail)
389+
}
390+
}
391+
392+
/**
393+
* Helper function to add the stash ID button to the card-popovers
394+
*/
395+
function addStashIDButton(cardPopovers, stashIDCount) {
396+
// Check if already added
397+
if (cardPopovers.querySelector(".stash-id-count")) return;
398+
399+
// Box-open icon SVG (StashApp logo style - open box)
400+
const boxIconSVG = `<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="box-open" class="svg-inline--fa fa-box-open" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><path fill="currentColor" d="M58.9 42.1c3-6.1 9.6-9.6 16.3-8.7L320 64 564.8 33.4c6.7-.8 13.3 2.7 16.3 8.7l41.7 83.4c9 17.9-.6 39.6-19.8 45.1L439.6 217.3c-13.9 4-28.8-1.9-36.2-14.3L320 64 236.6 203c-7.4 12.4-22.3 18.3-36.2 14.3L37.1 170.6c-19.3-5.5-28.8-27.2-19.8-45.1L58.9 42.1zM321.1 128l54.9 91.4c14.9 24.8 44.6 36.6 72.5 28.6L576 211.6v167c0 22-15 41.2-36.4 46.6l-204.1 51c-10.2 2.6-20.9 2.6-31 0l-204.1-51C79 419.7 64 400.5 64 378.5v-167L191.6 248c27.8 8 57.6-3.8 72.5-28.6L318.9 128h2.2z"></path></svg>`;
401+
402+
// Create a wrapper div similar to the tag-count structure
403+
const wrapper = document.createElement("div");
404+
405+
// Create button with count FIRST, then icon (as requested)
406+
const button = createElementFromHTML(
407+
`<button type="button" class="minimal stash-id-count btn btn-primary" title="Has ${stashIDCount} Stash ID${stashIDCount !== 1 ? 's' : ''}">
408+
<span class="stash-id-count-number">${stashIDCount}</span>
409+
<span class="stash-id-icon">${boxIconSVG}</span>
410+
</button>`
411+
);
412+
413+
wrapper.appendChild(button);
414+
cardPopovers.appendChild(wrapper);
415+
}
272416
})();

plugins/cjCardTweaks/cjCardTweaks.yml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name: CJ's Card Tweaks.
22
# requires: CommunityScriptsUILibrary
3-
description: Provides various tweaks for the Stash Cards.
4-
version: 1.1
3+
description: Provides various tweaks for the Stash cards.
4+
version: 1.2
55
url: https://discourse.stashapp.cc/t/cjs-card-tweaks/1342
66
ui:
77
requires:
@@ -25,3 +25,7 @@ settings:
2525
displayName: Performer profile cards
2626
description: "Tweaks performer cards to use a traditional profile design."
2727
type: BOOLEAN
28+
stashIDIcon:
29+
displayName: Stash ID icon
30+
description: "Adds a Stash ID icon to the performer cards."
31+
type: BOOLEAN

0 commit comments

Comments
 (0)