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:
HandyRandyx
2024-06-27 07:54:56 -03:00
committed by DogmaDragon
parent eb09dbd72b
commit fc9b56b502
6 changed files with 1395 additions and 0 deletions

View 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.

View 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%;
}
}

View 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);
};
});
})();

View 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