Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions plugins/cjCardTweaks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,8 @@ Adds an additional dimension to the rating banners.
![unnamed_2](https://github.com/user-attachments/assets/f505417d-ed0c-40c4-9c78-647081a41307)

Modify the performer cards to use a traditional profile design

### Stash ID icon
![unnamed_3](https://github.com/user-attachments/assets/181fe3cd-b3e5-437d-8ded-7e48f2c0e446)

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.
148 changes: 146 additions & 2 deletions plugins/cjCardTweaks/cjCardTweaks.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
if (
key === "fileCount" ||
key === "addBannerDimension" ||
key === "performerProfileCards"
key === "performerProfileCards" ||
key === "stashIDIcon"
) {
acc[key] = settings[key];
} else {
Expand All @@ -42,6 +43,8 @@
".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}";
const RATING_BANNER_3D_STYLE =
".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}";
const STASH_ID_ICON_STYLE =
".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}";

/**
* Element to inject custom CSS styles.
Expand All @@ -54,6 +57,8 @@
styleElement.innerHTML += RATING_BANNER_3D_STYLE;
if (SETTINGS.performerProfileCards)
styleElement.innerHTML += PERFORMER_PROFILE_CARD_STYLE;
if (SETTINGS.stashIDIcon)
styleElement.innerHTML += STASH_ID_ICON_STYLE;

function createElementFromHTML(htmlString) {
const div = document.createElement("div");
Expand Down Expand Up @@ -93,7 +98,7 @@
}

/**
* Handles gallery cards to specific paths in Stash.
* Handles gallery cards to specific paths in Stash.
*
* The supported paths are:
* - /galleries
Expand Down Expand Up @@ -207,6 +212,36 @@
cards.forEach((card) => {
maybeAddFileCount(card, stashData, isContentCard);
maybeAddDimensionToBanner(card);
if (cardClass === "performer-card") {
maybeAddStashIDIcon(card, stashData);

// Also set up a MutationObserver to watch for card-popovers being added
if (SETTINGS.stashIDIcon && !card.querySelector(".stash-id-count")) {
const observer = new MutationObserver((mutations) => {
const cardPopovers = card.querySelector(".card-popovers.btn-group") ||
card.querySelector(".card-popovers") ||
card.querySelector('[role="group"].btn-group');
if (cardPopovers && !cardPopovers.querySelector(".stash-id-count")) {
const link = card.querySelector(".thumbnail-section > a");
if (link) {
const id = new URL(link.href).pathname.split("/").pop();
const idNum = parseInt(id, 10);
// Query GraphQL for stash IDs
queryStashIDs(card, id, idNum);
observer.disconnect();
}
}
});

observer.observe(card, {
childList: true,
subtree: true
});

// Disconnect after 5 seconds to avoid memory leaks
setTimeout(() => observer.disconnect(), 5000);
}
}
});
}

Expand Down Expand Up @@ -269,4 +304,113 @@
link.parentElement.appendChild(el);
oldBanner.remove();
}

/**
* Add Stash ID count and icon to performer cards in the card-popovers btn-group
*
* @param {Element} card - Card element from cards list.
* @param {Object} stashData - Data fetched from the GraphQL interceptor. e.g. stash.performers.
*/
function maybeAddStashIDIcon(card, stashData) {
if (!SETTINGS.stashIDIcon) return;

// Verify this function was not run twice on the same card
const existingCount = card.querySelector(".stash-id-count");
if (existingCount) return;

const link = card.querySelector(".thumbnail-section > a");
if (!link) return;

const id = new URL(link.href).pathname.split("/").pop();
const idNum = parseInt(id, 10);

// Query GraphQL for stash IDs
queryStashIDs(card, id, idNum);
}

/**
* Query GraphQL for performer stash IDs
* @param {Element} card - Card element
* @param {string} id - Performer ID as string
* @param {number} idNum - Performer ID as number
*/
async function queryStashIDs(card, id, idNum) {
const query = `
query FindPerformer($id: ID!) {
findPerformer(id: $id) {
id
stash_ids {
endpoint
stash_id
}
}
}
`;

const variables = {
id: idNum
};

try {
const response = await fetch('/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: query,
variables: variables
})
});

const result = await response.json();

if (result.errors) return;

const performer = result.data?.findPerformer;
if (!performer) return;

const stashIDs = performer.stash_ids || [];
const stashIDCount = Array.isArray(stashIDs) ? stashIDs.length : 0;

// Only show if count is greater than 0
if (stashIDCount > 0) {
// Find card-popovers and add button
const cardPopovers = card.querySelector(".card-popovers.btn-group") ||
card.querySelector(".card-popovers") ||
card.querySelector('[role="group"].btn-group');

if (cardPopovers && !cardPopovers.querySelector(".stash-id-count")) {
addStashIDButton(cardPopovers, stashIDCount);
}
}
} catch (error) {
// On error, don't show anything (silent fail)
}
}

/**
* Helper function to add the stash ID button to the card-popovers
*/
function addStashIDButton(cardPopovers, stashIDCount) {
// Check if already added
if (cardPopovers.querySelector(".stash-id-count")) return;

// Box-open icon SVG (StashApp logo style - open box)
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>`;

// Create a wrapper div similar to the tag-count structure
const wrapper = document.createElement("div");

// Create button with count FIRST, then icon (as requested)
const button = createElementFromHTML(
`<button type="button" class="minimal stash-id-count btn btn-primary" title="Has ${stashIDCount} Stash ID${stashIDCount !== 1 ? 's' : ''}">
<span class="stash-id-count-number">${stashIDCount}</span>
<span class="stash-id-icon">${boxIconSVG}</span>
</button>`
);

wrapper.appendChild(button);
cardPopovers.appendChild(wrapper);
}
})();
4 changes: 4 additions & 0 deletions plugins/cjCardTweaks/cjCardTweaks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,7 @@ settings:
displayName: Performer profile cards
description: "Tweaks performer cards to use a traditional profile design."
type: BOOLEAN
stashIDIcon:
displayName: Stash ID icon
description: "Adds a Stash ID icon to the performer cards."
type: BOOLEAN