mirror of
https://github.com/stashapp/CommunityScripts.git
synced 2026-05-04 15:54:50 -05:00
Add Hot Cards plugin (#346)
* Add Hot Cards plugin * Format code using prettier Add Pornhub inspired theme (#347) Format Update theme order Update theme order
This commit is contained in:
19
plugins/hotCards/README.md
Normal file
19
plugins/hotCards/README.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# Hot Cards
|
||||
|
||||
Hot Cards is a Stash CommunityScript plugin that offers a visual aid by applying custom CSS to card elements based on a tag ID or a rating threshold. You can use this plugin to remind yourself of certain performers, scenes, studios, movies, images or galleries.
|
||||
|
||||
## Features
|
||||
|
||||
- Adds custom CSS to card elements that match a specified tag ID or rating threshold.
|
||||
- Enable or disable Hot Cards on various sections like home, scenes, images, movies, galleries, performers, and studios.
|
||||
|
||||
## Installation
|
||||
|
||||
1. Go to Settings > Plugins.
|
||||
2. Under Available Plugins expand the Community (stable) option.
|
||||
3. Search for Hot Cards.
|
||||
4. Select the plugin and click Install.
|
||||
|
||||
## Usage
|
||||
|
||||
Once installed, you can configure the plugin. Set a desire tag ID or a rating threshold and enable the sections where you want the hot cards to be displayed.
|
||||
69
plugins/hotCards/hotCards.css
Normal file
69
plugins/hotCards/hotCards.css
Normal file
@@ -0,0 +1,69 @@
|
||||
.hot-border > .card {
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.hot-card {
|
||||
animation: pulse 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@-webkit-keyframes pulse {
|
||||
0% {
|
||||
-webkit-box-shadow: 0 0 0 0 rgba(255, 36, 9, 0);
|
||||
}
|
||||
70% {
|
||||
-webkit-box-shadow:
|
||||
0px 0px 1em #ff2409,
|
||||
0px 0px 2em #ff2409;
|
||||
}
|
||||
100% {
|
||||
-webkit-box-shadow: 0 0 0 0 rgba(255, 36, 9, 0);
|
||||
}
|
||||
}
|
||||
|
||||
.hot-card:hover {
|
||||
box-shadow:
|
||||
0px 0px 1em #ff2409,
|
||||
0px 0px 2em #ff2409;
|
||||
animation: none;
|
||||
animate: glow;
|
||||
}
|
||||
|
||||
.hot-border {
|
||||
--border-width: 0.2rem;
|
||||
|
||||
display: flex;
|
||||
position: relative;
|
||||
border-radius: var(--border-width);
|
||||
|
||||
&::after {
|
||||
position: absolute;
|
||||
content: "";
|
||||
top: calc(0.8 * var(--border-width));
|
||||
left: calc(0.8 * var(--border-width));
|
||||
z-index: -1;
|
||||
width: calc(100% + var(--border-width) * -1.5);
|
||||
height: calc(100% + var(--border-width) * -1.5);
|
||||
background: linear-gradient(
|
||||
60deg,
|
||||
hsl(224, 85%, 66%),
|
||||
hsl(269, 85%, 66%),
|
||||
hsl(314, 85%, 66%),
|
||||
hsl(359, 85%, 66%),
|
||||
hsl(44, 85%, 66%),
|
||||
hsl(357.2, 87.7%, 52.4%),
|
||||
hsl(301, 70.2%, 50%),
|
||||
hsl(179, 85%, 66%)
|
||||
);
|
||||
|
||||
background-size: 300% 300%;
|
||||
background-position: 0 50%;
|
||||
border-radius: calc(2 * var(--border-width));
|
||||
animation: moveGradient 4s alternate infinite;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes moveGradient {
|
||||
50% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
}
|
||||
280
plugins/hotCards/hotCards.js
Normal file
280
plugins/hotCards/hotCards.js
Normal file
@@ -0,0 +1,280 @@
|
||||
(async () => {
|
||||
"use strict";
|
||||
|
||||
const userSettings = await csLib.getConfiguration("hotCards", {});
|
||||
const TAG_ID = userSettings.tagId;
|
||||
const RATING_THRESHOLD = userSettings.threshold;
|
||||
const CARDS = {
|
||||
gallery: {
|
||||
class: "gallery-card",
|
||||
data: stash.galleries,
|
||||
enabled: userSettings.galleries,
|
||||
},
|
||||
image: {
|
||||
class: "image-card",
|
||||
data: stash.images,
|
||||
enabled: userSettings.images,
|
||||
},
|
||||
movie: {
|
||||
class: "movie-card",
|
||||
data: stash.movies,
|
||||
enabled: userSettings.movies,
|
||||
},
|
||||
performer: {
|
||||
class: "performer-card",
|
||||
data: stash.performers,
|
||||
enabled: userSettings.performers,
|
||||
},
|
||||
scene: {
|
||||
class: "scene-card",
|
||||
data: stash.scenes,
|
||||
enabled: userSettings.scenes,
|
||||
},
|
||||
studio: {
|
||||
class: "studio-card",
|
||||
data: stash.studios,
|
||||
enabled: userSettings.studios,
|
||||
},
|
||||
};
|
||||
const isTagBased = TAG_ID?.length !== 0;
|
||||
const isRatingBased =
|
||||
RATING_THRESHOLD && !["0", 0].includes(RATING_THRESHOLD);
|
||||
let backupCards = [];
|
||||
let hotCards = [];
|
||||
|
||||
// Mapping of configuration keys to functions
|
||||
const hotCardsHandlers = {
|
||||
home: handleHomeHotCards,
|
||||
galleries: handleGalleriesHotCards,
|
||||
images: handleImagesHotCards,
|
||||
movies: handleMoviesHotCards,
|
||||
performers: handlePerformersHotCards,
|
||||
scenes: handleScenesHotCards,
|
||||
studios: handleStudiosHotCards,
|
||||
};
|
||||
|
||||
// Iterate over the corresponding config to call the appropriate functions
|
||||
for (const [key, value] of Object.entries(userSettings)) {
|
||||
if (
|
||||
value === true &&
|
||||
hotCardsHandlers[key] &&
|
||||
(isTagBased || isRatingBased)
|
||||
) {
|
||||
hotCardsHandlers[key]();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add hot cards to the home page.
|
||||
*
|
||||
* Sets up a path listener for the home page, and after a delay,
|
||||
* processes each enabled card type to add hot elements.
|
||||
*/
|
||||
function handleHomeHotCards() {
|
||||
const pattern = /^\/$/;
|
||||
registerPathChangeListener(pattern, () => {
|
||||
setTimeout(() => {
|
||||
for (const [_, card] of Object.entries(CARDS))
|
||||
if (card.enabled) handleHotCards(card, true);
|
||||
}, 3000);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds gallery hot cards to specific paths in the application.
|
||||
*
|
||||
* The supported paths are:
|
||||
* - /galleries
|
||||
* - /performers/{id}/galleries
|
||||
* - /studios/{id}/galleries
|
||||
* - /tags/{id}/galleries
|
||||
* - /scenes/{id}
|
||||
*/
|
||||
function handleGalleriesHotCards() {
|
||||
const pattern =
|
||||
/^\/(galleries$|performers\/\d+\/galleries$|studios\/\d+\/galleries$|tags\/\d+\/galleries$|scenes\/\d+$)/;
|
||||
addHotCards(pattern, CARDS.gallery);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds image hot cards to specific paths in the application.
|
||||
*
|
||||
* The supported paths are:
|
||||
* - /images
|
||||
* - /performers/{id}/images
|
||||
* - /studios/{id}/images
|
||||
* - /tags/{id}/images
|
||||
* - /galleries/{id}
|
||||
*/
|
||||
function handleImagesHotCards() {
|
||||
const pattern =
|
||||
/^\/(images$|performers\/\d+\/images$|studios\/\d+\/images$|tags\/\d+\/images$|galleries\/\d+$)/;
|
||||
addHotCards(pattern, CARDS.image);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds movie hot cards to specific paths in the application.
|
||||
*
|
||||
* The supported paths are:
|
||||
* - /movies
|
||||
* - /performers/{id}/movies
|
||||
* - /studios/{id}/movies
|
||||
* - /tags/{id}/movies
|
||||
* - /scenes/{id}
|
||||
*/
|
||||
function handleMoviesHotCards() {
|
||||
const pattern =
|
||||
/^\/(movies$|performers\/\d+\/movies$|studios\/\d+\/movies$|tags\/\d+\/movies$|scenes\/\d+$)/;
|
||||
addHotCards(pattern, CARDS.movie);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds performer hot cards to specific paths in the application.
|
||||
*
|
||||
* The supported paths are:
|
||||
* - /performers
|
||||
* - /performers/{id}/appearswith
|
||||
* - /studios/{id}/performers
|
||||
* - /tags/{id}/performers
|
||||
* - /scenes/{id}
|
||||
* - /galleries/{id}
|
||||
* - /images/{id}
|
||||
*/
|
||||
function handlePerformersHotCards() {
|
||||
const pattern =
|
||||
/^\/(performers(\/\d+\/appearswith)?|studios\/\d+\/performers|tags\/\d+\/performers|scenes\/\d+|galleries\/\d+|images\/\d+)$/;
|
||||
addHotCards(pattern, CARDS.performer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds scene hot cards to specific paths in the application.
|
||||
*
|
||||
* The supported paths are:
|
||||
* - /scenes
|
||||
* - /performers/{id}/scenes
|
||||
* - /studios/{id}/scenes
|
||||
* - /tags/{id}/scenes
|
||||
* - /movies/{id}
|
||||
* - /galleries/{id}
|
||||
*/
|
||||
function handleScenesHotCards() {
|
||||
const pattern =
|
||||
/^\/(scenes$|performers\/\d+\/scenes$|studios\/\d+\/scenes$|tags\/\d+\/scenes$|movies\/\d+$|galleries\/\d+$)/;
|
||||
addHotCards(pattern, CARDS.scene);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds studio hot cards to specific paths in the application.
|
||||
*
|
||||
* The supported paths are:
|
||||
* - /studios
|
||||
* - /studios/{id}/childstudios
|
||||
* - /tags/{id}/studios
|
||||
*/
|
||||
function handleStudiosHotCards() {
|
||||
const pattern = /^\/studios(\/\d+\/childstudios)?$|^\/tags\/\d+\/studios$/;
|
||||
addHotCards(pattern, CARDS.studio);
|
||||
}
|
||||
|
||||
function addHotCards(pattern, card) {
|
||||
registerPathChangeListener(pattern, () => {
|
||||
handleHotCards(card);
|
||||
});
|
||||
}
|
||||
|
||||
function handleHotCards(card, isHome = false) {
|
||||
waitForClass(card.class, () => {
|
||||
createAndInsertHotCards(card.data, card.class, isHome);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps cards in "hot" elements based on specific conditions (tag or rating).
|
||||
*
|
||||
* On the home page, this function may be triggered by multiple intercepted GraphQL requests,
|
||||
* each corresponding to a user-customized carousel.
|
||||
*
|
||||
* The user is able to customize the home page as desired and add
|
||||
* several carousels of the same type of resource with different filters saved.
|
||||
* As a result, several graphql request are intercepted and this function runs
|
||||
* as many times as the user configured carousels.
|
||||
*
|
||||
* The first time it runs, the hotCards array is populated,
|
||||
* so we need an additional flag to differentiate that we are on the home page.
|
||||
*
|
||||
* @param {Object} stashData - Data fetched from the GraphQL interceptor. e.g. stash.performers.
|
||||
* @param {string} cardClass - CSS class used to identify cards in the DOM. e.g. 'performer-card'.
|
||||
* @param {boolean} isHome - Flag indicating if the current page is the homepage.
|
||||
*/
|
||||
function createAndInsertHotCards(stashData, cardClass, isHome) {
|
||||
// To avoid DOM exceptions, it only runs when `hotCards` is empty and we are not in the home page.
|
||||
if (hotCards.length === 0 || isHome) {
|
||||
const cards = document.querySelectorAll(`.${cardClass}`);
|
||||
|
||||
cards.forEach((card) => {
|
||||
const link = card.querySelector(".thumbnail-section > a");
|
||||
const url = link.href;
|
||||
const id = url.split("/").pop().split("?").shift();
|
||||
const data = stashData[id];
|
||||
|
||||
if (isTagBased) {
|
||||
if (data?.tags?.length) {
|
||||
data.tags.forEach((tag) => {
|
||||
if (tag.id === TAG_ID) createHotElementAndAttachToDOM(card);
|
||||
});
|
||||
}
|
||||
} else if (isRatingBased && data?.rating100 !== null) {
|
||||
if (data.rating100 >= RATING_THRESHOLD)
|
||||
createHotElementAndAttachToDOM(card);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function createHotElementAndAttachToDOM(cardElement) {
|
||||
const hotElement = createElementFromHTML(`<div class="hot-border">`);
|
||||
|
||||
backupCards.push(cardElement);
|
||||
cardElement.classList.add("hot-card");
|
||||
cardElement.before(hotElement);
|
||||
hotElement.append(cardElement);
|
||||
hotCards.push(hotElement);
|
||||
}
|
||||
|
||||
function createElementFromHTML(htmlString) {
|
||||
const div = document.createElement("div");
|
||||
div.innerHTML = htmlString.trim();
|
||||
return div.firstChild;
|
||||
}
|
||||
|
||||
/**
|
||||
* Since it was necessary to insert a div before the card for
|
||||
* the border design to be visible (otherwise the overflow:hidden; property of
|
||||
* the .card class does not allow it to be seen), this also brought up another problem:
|
||||
*
|
||||
* "DOMException: Node.removeChild: The node to be removed is not a child of this node".
|
||||
*
|
||||
* Because of how the internal content of some divs are updated when navigating.
|
||||
*
|
||||
* This restores the card back to the original DOM structure to prevent that.
|
||||
* -
|
||||
*/
|
||||
function restoreCards() {
|
||||
backupCards.forEach((backupCard, i) => {
|
||||
if (hotCards[i] && hotCards[i].parentNode) {
|
||||
hotCards[i].before(backupCard);
|
||||
hotCards[i].remove();
|
||||
}
|
||||
});
|
||||
backupCards.length = 0;
|
||||
hotCards.length = 0;
|
||||
}
|
||||
|
||||
["pushState", "replaceState"].forEach((method) => {
|
||||
const original = history[method];
|
||||
history[method] = function () {
|
||||
restoreCards();
|
||||
return original.apply(this, arguments);
|
||||
};
|
||||
});
|
||||
})();
|
||||
50
plugins/hotCards/hotCards.yml
Normal file
50
plugins/hotCards/hotCards.yml
Normal file
@@ -0,0 +1,50 @@
|
||||
name: Hot Cards
|
||||
description: Adds custom css to card elements that match a tag id or a rating threshold.
|
||||
version: 1.0.0
|
||||
url: https://github.com/stashapp/CommunityScripts/tree/main/plugins/hotCards
|
||||
ui:
|
||||
javascript:
|
||||
- hotCards.js
|
||||
- https://cdn.jsdelivr.net/gh/HandyRandyx/stash-plugins@main/utils/fetchInterceptor.js
|
||||
- https://cdn.jsdelivr.net/gh/HandyRandyx/stash-plugins@main/utils/stashHandler.js
|
||||
- https://cdn.jsdelivr.net/gh/HandyRandyx/stash-plugins@main/utils/registerPathChangeListener.js
|
||||
- https://cdn.jsdelivr.net/gh/HandyRandyx/stash-plugins@main/utils/waitForClass.js
|
||||
css:
|
||||
- hotCards.css
|
||||
settings:
|
||||
tagId:
|
||||
displayName: Tag ID
|
||||
description: The tag ID to match against. Leave blank to disable tag-based hot cards.
|
||||
type: STRING
|
||||
threshold:
|
||||
displayName: Rating Threshold
|
||||
description: The rating threshold to match against. Set to 0 to disable rating-based hot cards.
|
||||
type: NUMBER
|
||||
home:
|
||||
displayName: Enable on home
|
||||
description: Enable hot cards on the home page.
|
||||
type: BOOLEAN
|
||||
scenes:
|
||||
displayName: Enable on scenes
|
||||
description: Enable hot cards on scene cards.
|
||||
type: BOOLEAN
|
||||
images:
|
||||
displayName: Enable on images
|
||||
description: Enable hot cards on image cards.
|
||||
type: BOOLEAN
|
||||
movies:
|
||||
displayName: Enable on movies
|
||||
description: Enable hot cards on movie cards.
|
||||
type: BOOLEAN
|
||||
galleries:
|
||||
displayName: Enable on galleries
|
||||
description: Enable hot cards on gallery cards.
|
||||
type: BOOLEAN
|
||||
performers:
|
||||
displayName: Enable on performers
|
||||
description: Enable hot cards on performer cards.
|
||||
type: BOOLEAN
|
||||
studios:
|
||||
displayName: Enable on studios
|
||||
description: Enable hot cards on studio cards.
|
||||
type: BOOLEAN
|
||||
Reference in New Issue
Block a user