mirror of
https://github.com/stashapp/CommunityScripts.git
synced 2026-02-05 04:45:09 -06:00
merged
This commit is contained in:
commit
d8a5d9f2b2
@ -182,9 +182,6 @@ async def __tag_images(images):
|
||||
async def __tag_scene(scene):
|
||||
async with semaphore:
|
||||
scenePath = scene['files'][0]['path']
|
||||
mutated_path = scenePath
|
||||
for key, value in config.path_mutation.items():
|
||||
mutated_path = mutated_path.replace(key, value)
|
||||
sceneId = scene['id']
|
||||
log.debug("files result:" + str(scene['files'][0]))
|
||||
phash = scene['files'][0].get('fingerprint', None)
|
||||
|
||||
@ -4,8 +4,8 @@ A plugin which shows the metadata of the currently playing Stash scene as your D
|
||||
|
||||
## Setup
|
||||
### Prerequisites to get the plugin working
|
||||
- Download and run [Discord RPC Server](https://github.com/lolamtisch/Discord-RPC-Extension/releases). You **do not** need any browser extensions.
|
||||
- Install [`StashUserscriptLibrary`](https://github.com/stashapp/CommunityScripts/tree/main/plugins/stashUserscriptLibrary) from your Stash plugin menu.
|
||||
- Download and run [Discord Presence Server](https://github.com/NotForMyCV/discord-presence-server/releases). You **do not** need any browser extensions.
|
||||
- Ensure you have CommunityScriptsUILibrary installed in your Stash plugins, if it isn't automatically installed
|
||||
|
||||
#### Why the desktop app?
|
||||
<sub>
|
||||
|
||||
@ -82,59 +82,85 @@
|
||||
|
||||
console.debug("Discord Presence Plugin: loaded config", CONFIG);
|
||||
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
const player = () => document.querySelector("#VideoJsPlayer video");
|
||||
|
||||
let SCENE_ID = null;
|
||||
let INTERVAL_ID = null;
|
||||
let WS_ALIVE = false;
|
||||
/** @type {FlattenedSceneData?} */ let cachedSceneData;
|
||||
|
||||
const doUpdatingPresence = (e) => {
|
||||
clearInterval(INTERVAL_ID);
|
||||
/** @type {WebSocket} */ let ws;
|
||||
const wsAlive = () => ws && ws.readyState === 1;
|
||||
|
||||
const pathname = e.detail.data.location.pathname;
|
||||
const videoListener = (video) => {
|
||||
SCENE_ID = parseInt(location.pathname.split("/")[2]);
|
||||
video.addEventListener("playing", setDiscordActivity);
|
||||
video.addEventListener("play", setDiscordActivity);
|
||||
video.addEventListener("timeupdate", setDiscordActivity);
|
||||
video.addEventListener("seeked", setDiscordActivity);
|
||||
video.addEventListener("ended", clearDiscordActivity);
|
||||
};
|
||||
|
||||
if (!pathname.match(/\/scenes\/\d+/)) {
|
||||
clearDiscordActivity();
|
||||
const unbindVideoListener = (video) => {
|
||||
video.removeEventListener("playing", setDiscordActivity);
|
||||
video.removeEventListener("play", setDiscordActivity);
|
||||
video.removeEventListener("timeupdate", setDiscordActivity);
|
||||
video.removeEventListener("seeked", setDiscordActivity);
|
||||
video.removeEventListener("ended", clearDiscordActivity);
|
||||
};
|
||||
|
||||
// Start ws connection to RPC server and add video listener
|
||||
// Will retry on disconnection/error after 10s
|
||||
async function start() {
|
||||
if (ws && ws.readyState <= 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
SCENE_ID = parseInt(pathname.split("/")[2], 10);
|
||||
// https://github.com/NotForMyCV/discord-presence-server/releases
|
||||
ws = new WebSocket("ws://localhost:6969");
|
||||
|
||||
setDiscordActivity();
|
||||
INTERVAL_ID = setInterval(setDiscordActivity, 5000);
|
||||
};
|
||||
ws.addEventListener("open", () => {
|
||||
csLib.PathElementListener("/scenes/", "video", videoListener);
|
||||
});
|
||||
|
||||
// https://github.com/lolamtisch/Discord-RPC-Extension/releases
|
||||
const ws = new WebSocket("ws://localhost:6969");
|
||||
ws.addEventListener("message", () => (WS_ALIVE = true));
|
||||
ws.addEventListener("open", () =>
|
||||
PluginApi.Event.addEventListener("stash:location", doUpdatingPresence)
|
||||
);
|
||||
ws.addEventListener("close", () => {
|
||||
clearInterval(INTERVAL_ID);
|
||||
PluginApi.Event.removeEventListener("stash:location", doUpdatingPresence);
|
||||
});
|
||||
ws.addEventListener("error", () => {
|
||||
PluginApi.Event.removeEventListener("stash:location", doUpdatingPresence);
|
||||
});
|
||||
window.addEventListener("beforeunload", () => {
|
||||
clearDiscordActivity();
|
||||
});
|
||||
// set timeout for checking liveliness
|
||||
const checkLiveliness = () => {
|
||||
if (!WS_ALIVE) {
|
||||
unbindVideoListener(document.querySelector("#VideoJsPlayer video"));
|
||||
clearInterval(INTERVAL_ID);
|
||||
throw new Error(`Discord Presence Plugin: Discord RPC Extension not running
|
||||
Please consult the README on how to set up the Discord RPC Extension
|
||||
(https://github.com/stashapp/CommunityScripts/tree/main/plugins/discordPresence)`);
|
||||
}
|
||||
};
|
||||
setTimeout(checkLiveliness, 2000);
|
||||
window.addEventListener("beforeunload", () => {
|
||||
clearDiscordActivity();
|
||||
});
|
||||
|
||||
// If failed during video playback, remove the listeners
|
||||
ws.addEventListener("close", async () => {
|
||||
if (player()) {
|
||||
unbindVideoListener(player());
|
||||
}
|
||||
|
||||
await sleep(10000);
|
||||
start();
|
||||
});
|
||||
|
||||
ws.addEventListener("error", async () => {
|
||||
if (player()) {
|
||||
unbindVideoListener(player());
|
||||
}
|
||||
|
||||
console.error(
|
||||
`Discord Presence Plugin: Could not connect to Discord Rich Presence Server.
|
||||
Consult the README on how to setup the Rich Presence Server:
|
||||
https://github.com/stashapp/CommunityScripts/tree/main/plugins/discordPresence`
|
||||
);
|
||||
await sleep(10000);
|
||||
start();
|
||||
});
|
||||
}
|
||||
|
||||
start();
|
||||
|
||||
/** @return {Promise<FlattenedSceneData | null>} */
|
||||
async function getSceneData(sceneId) {
|
||||
if (!sceneId) {
|
||||
return { sceneData: null, duration: 0 };
|
||||
if (!sceneId) return null;
|
||||
|
||||
if (Number(sceneId).toString() === Number(cachedSceneData?.id).toString()) {
|
||||
return cachedSceneData;
|
||||
}
|
||||
|
||||
const reqData = {
|
||||
variables: { id: sceneId },
|
||||
query: SCENE_GQL_QUERY,
|
||||
@ -157,23 +183,29 @@
|
||||
delete sceneData.studio;
|
||||
delete sceneData.files;
|
||||
|
||||
return { ...sceneData, ...newProps };
|
||||
cachedSceneData = { ...sceneData, ...newProps };
|
||||
return cachedSceneData;
|
||||
}
|
||||
|
||||
function clearDiscordActivity() {
|
||||
if (!!SCENE_ID === false || ws.OPEN !== 1) {
|
||||
if (!!SCENE_ID === false || !wsAlive()) {
|
||||
return;
|
||||
}
|
||||
|
||||
SCENE_ID = null;
|
||||
ws.send(JSON.stringify({ action: "disconnect" }));
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
clientId: CONFIG.discordClientId,
|
||||
clearActivity: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
async function setDiscordActivity() {
|
||||
const sceneData = await getSceneData(SCENE_ID);
|
||||
if (!sceneData) return;
|
||||
|
||||
const currentTime = getCurrentVideoTime() ?? 0;
|
||||
const currentTime = player()?.currentTime ?? 0;
|
||||
const endTimestamp =
|
||||
Date.now() + (sceneData.file_duration - currentTime) * 1000;
|
||||
|
||||
@ -197,22 +229,18 @@
|
||||
instance: true,
|
||||
};
|
||||
|
||||
if (!ws.OPEN) {
|
||||
if (!wsAlive()) {
|
||||
return;
|
||||
}
|
||||
|
||||
ws.send(
|
||||
JSON.stringify({
|
||||
clientId: CONFIG.discordClientId,
|
||||
extId: "stash-discord-rpc-plugin",
|
||||
presence: body,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const getCurrentVideoTime = () =>
|
||||
document.querySelector("#VideoJsPlayer video")?.currentTime;
|
||||
|
||||
/**
|
||||
* Performs string replacement on templated config vars with scene data
|
||||
* @param {string} templateStr
|
||||
@ -222,21 +250,4 @@
|
||||
const pattern = /{\s*(\w+?)\s*}/g;
|
||||
return templateStr.replace(pattern, (_, token) => sceneData[token] ?? "");
|
||||
}
|
||||
|
||||
// add listener for video events
|
||||
const videoListener = (video) => {
|
||||
SCENE_ID = parseInt(location.pathname.split("/")[2]);
|
||||
video.addEventListener("playing", setDiscordActivity);
|
||||
video.addEventListener("play", setDiscordActivity);
|
||||
video.addEventListener("seeked", setDiscordActivity);
|
||||
// end on video end
|
||||
video.addEventListener("ended", clearDiscordActivity);
|
||||
};
|
||||
const unbindVideoListener = (video) => {
|
||||
video.removeEventListener("playing", setDiscordActivity);
|
||||
video.removeEventListener("play", setDiscordActivity);
|
||||
video.removeEventListener("seeked", setDiscordActivity);
|
||||
video.removeEventListener("ended", clearDiscordActivity);
|
||||
};
|
||||
csLib.PathElementListener("/scenes/", "video", videoListener);
|
||||
})();
|
||||
|
||||
@ -2,7 +2,7 @@ name: Discord Presence
|
||||
description: Sets currently playing scene data as your Discord status. See README for prerequisites and config options (blue hyperlink next to enable/disable button)
|
||||
url: https://github.com/stashapp/CommunityScripts/tree/main/plugins/discordPresence
|
||||
# requires: CommunityScriptsUILibrary
|
||||
version: 1.0
|
||||
version: 1.1
|
||||
settings:
|
||||
discordClientId:
|
||||
displayName: Custom Discord application ID
|
||||
|
||||
@ -1,19 +1,154 @@
|
||||
# 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.
|
||||
Hot Cards is a Stash CommunityScript plugin designed to enhance your visual experience by applying custom styling to card elements based on a Tag ID or a Rating Threshold. This plugin is perfect for highlighting certain performers or scenes and making sure you don't forget them!
|
||||
|
||||
## Features
|
||||
|
||||
- Adds custom CSS to card elements that match a specified tag ID or rating threshold.
|
||||
- Custom styling 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.
|
||||
- Specify Hot Cards to be tag-based or rating-based for each card type, as desired.
|
||||
- Customizable Hot Cards.
|
||||
|
||||
## 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.
|
||||
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.
|
||||
After installation, you can configure the plugin to suit your needs. Set a desired Tag ID or Rating Threshold and enable Hot Cards for the card types you want. Customize the appearance of Hot Cards for each type of card (scene, image, movie, gallery, performer, studio) using the format provided or leave the fields empty to apply the default style.
|
||||
|
||||
### Configure the field format:
|
||||
|
||||
_[criterion]\_[value]\_[style]\_[gradient-opts]\_[hover-opts]\_[card-opts]_
|
||||
|
||||
**Important**: If you have previously installed the plugin, after updating to `1.1.0`, be sure to update your settings from the old boolean format to the new string format. Refresh the page for the changes to take effect.
|
||||
|
||||
| Parameter | Description | Details |
|
||||
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `<criterion>` | Defines the basis for applying styles. Use `t` for tag-based criteria, `r` for rating-based criteria, or `d` to disable. | If left empty, it will default to the global **Tag ID** or **Rating Threshold** configuration. If both options are enabled and unspecified, the Tag ID will be used by default. |
|
||||
| `<value>` | Specifies the exact value for the Tag ID or Rating Threshold to be used. |
|
||||
| `<style>` | Defines the styling options as a comma-separated list of colors or presets. Options include: a fixed color (e.g., #5ff2a2), a Style Preset (e.g., hot), or a gradient (e.g., #ef1313,#3bd612,... with hex color codes or color names). | Defaults to **default** (basic style preset)<br><br>Style Presets available: **default**, **hot**, **gold**. |
|
||||
| `<gradient_opts>` | Specifies gradient options as a comma-separated list: `<type>,<angle>,<animation>`.</br></br> Example: **linear,35deg,4s alternate infinite** for a linear gradient with a 35-degree angle and a 4-second alternating infinite animation. | `<type>` Defaults to **linear**</br></br>Refer to [Using CSS gradients](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_images/Using_CSS_gradients) to see all types you can use.</br></br>`<angle>` Defaults to **0deg**</br></br>`<animation>` Defaults to **none**</br></br>Note that you can only configure the animation properties of the element. See [Using CSS animations](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_animations/Using_CSS_animations) for additional info. |
|
||||
| `<hover_opts>` | Specifies hover options as a comma-separated list: `<color>,<animation>`.</br></br>Example: **#ff0000,2s ease infinite** for a hover effect with a color of #ff0000 and a 2-second ease infinite animation. | `<color>` Defaults to **transparent**</br></br>`<animation>` Defaults to **none**</br></br>Similar to the gradient animation, you can only configure the animation properties of the element. |
|
||||
| `<card_opts>` | Specifies the general options for the card as a comma-separated list: `<fill>,<opacity>`. | `fill` Defaults to **true**<br>_Indicates whether the card should be filled._</br></br>Tip: You can set this to **false** to color only the border of the card.</br></br>`opacity` Defaults to **80**<br>_Represents the opacity for the card background. Range from 0 to 100._ |
|
||||
|
||||
<br />
|
||||
|
||||
**Note**: _It is recommended to refresh the page once you are done configuring for the changes to take effect and the previous style to be overwritten._
|
||||
|
||||
## Examples
|
||||
|
||||
**Style Preset**:
|
||||
|
||||
`t_123_gold`
|
||||
|
||||
| Segment | Value | Meaning |
|
||||
| ------------- | ----- | ------------------ |
|
||||
| criterion | t | Tag-based |
|
||||
| value | 123 | Use 123 as Tag ID |
|
||||
| style | gold | Use Gold preset |
|
||||
| gradient-opts | | No gradient |
|
||||
| hover-opts | | No hover effect |
|
||||
| card-opts | | Use default values |
|
||||
|
||||
---
|
||||
|
||||
**Modify an existing Style Preset**:
|
||||
|
||||
`__hot_,,none_pink,none`
|
||||
|
||||
| Segment | Value | Meaning |
|
||||
| ------------- | --------- | ------------------------------------------ |
|
||||
| criterion | | Use tag or rating as configured |
|
||||
| value | | Use global tag or rating value |
|
||||
| style | hot | Use Hot preset |
|
||||
| gradient-opts | ,,none | No gradient animation |
|
||||
| hover-opts | pink,none | Set hover effect color, no hover animation |
|
||||
| card-opts | | Use default values |
|
||||
|
||||
---
|
||||
|
||||
**Fixed Color**
|
||||
|
||||
`r__#2673b8`
|
||||
|
||||
| Segment | Value | Meaning |
|
||||
| ------------- | ------- | ----------------------- |
|
||||
| criterion | r | Rating-based |
|
||||
| value | | Use global rating value |
|
||||
| style | #2673b8 | Set fixed color |
|
||||
| gradient-opts | | No gradient |
|
||||
| hover-opts | | No hover effect |
|
||||
| card-opts | | Use default values |
|
||||
|
||||
---
|
||||
|
||||
**Fixed Color border-only**
|
||||
|
||||
`r_4_white___false`
|
||||
|
||||
| Segment | Value | Meaning |
|
||||
| ------------- | ----- | ------------------------- |
|
||||
| criterion | r | Rating-based |
|
||||
| value | 4 | Use 4 as Rating Threshold |
|
||||
| style | white | Set fixed color |
|
||||
| gradient-opts | | No gradient |
|
||||
| hover-opts | | No hover effect |
|
||||
| card-opts | false | No fill |
|
||||
|
||||
---
|
||||
|
||||
**Fixed Color with hover effect**
|
||||
|
||||
`__#5ff2a2__#5ff1a1`
|
||||
|
||||
| Segment | Value | Meaning |
|
||||
| ------------- | ------- | ------------------------------- |
|
||||
| criterion | | Use tag or rating as configured |
|
||||
| value | | Use global tag or rating value |
|
||||
| style | #5ff2a2 | Set fixed color |
|
||||
| gradient-opts | | No gradient |
|
||||
| hover-opts | #5ff1a1 | Set hover color |
|
||||
| card-opts | | Use default values |
|
||||
|
||||
---
|
||||
|
||||
**Gradient with hover effect**
|
||||
|
||||
`_67_pink,red,yellow,green,red,blue_,30deg,5s ease infinite_red,1s ease-in-out infinite`
|
||||
|
||||
| Segment | Value | Meaning |
|
||||
| ------------- | ------------------------------ | ----------------------------------------- |
|
||||
| criterion | | Use tag or rating as configured |
|
||||
| value | 67 | Use 67 as Tag ID or Rating Threshold |
|
||||
| style | pink,red,yellow,green,red,blue | Use gradient |
|
||||
| gradient-opts | ,30deg,5s ease infinite | Specify angle, and animate gradient |
|
||||
| hover-opts | red,1s ease-in-out infinite | Set hover effect color, and animate hover |
|
||||
| card-opts | ,100 | Use max opacity |
|
||||
|
||||
**Note**: _You can also skip inner values, notice the first comma in **gradient-opts**. The type is not provided, so linear gradient will be used by default._
|
||||
|
||||
## Style Presets
|
||||
|
||||
These presets provide predefined styles for quick and easy customization.
|
||||
|
||||
### Default
|
||||
|
||||

|
||||
|
||||
You can specify '\_\_default' for the card type you want the **default** preset to be applied and it will use the configured Tag ID or Rating Threshold. You can also leave the field empty and the default style will be applied anyway.
|
||||
|
||||
### Hot
|
||||
|
||||

|
||||
|
||||
You can specify '\_\_hot' for the card type you want the **hot** preset to be applied and it will use the configured Tag ID or Rating Threshold.
|
||||
|
||||
### Gold
|
||||
|
||||

|
||||
|
||||
You can specify '\_\_gold' for the card type you want the **gold** preset to be applied and it will use the configured Tag ID or Rating Threshold.
|
||||
|
||||
BIN
plugins/hotCards/assets/default.png
Normal file
BIN
plugins/hotCards/assets/default.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 148 KiB |
BIN
plugins/hotCards/assets/gold.png
Normal file
BIN
plugins/hotCards/assets/gold.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 124 KiB |
BIN
plugins/hotCards/assets/hot.png
Normal file
BIN
plugins/hotCards/assets/hot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 149 KiB |
@ -1,69 +1,56 @@
|
||||
.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%)
|
||||
);
|
||||
.hot-card > .hot-border {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
background-size: 300% 300%;
|
||||
background-position: 0 50%;
|
||||
border-radius: calc(2 * var(--border-width));
|
||||
animation: moveGradient 4s alternate infinite;
|
||||
.hot-card::after {
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.hot-border:hover {
|
||||
box-shadow:
|
||||
0px 0px 1em var(--hover-color),
|
||||
0px 0px 2em var(--hover-color);
|
||||
animation: none !important;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
box-shadow: 0 0 0 0 rgba(255, 36, 9, 0);
|
||||
}
|
||||
70% {
|
||||
box-shadow:
|
||||
0px 0px 1em var(--hover-color),
|
||||
0px 0px 2em var(--hover-color);
|
||||
}
|
||||
100% {
|
||||
box-shadow: 0 0 0 0 rgba(255, 36, 9, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes moveGradient {
|
||||
@keyframes move {
|
||||
0% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.hot-card {
|
||||
animation: none;
|
||||
}
|
||||
.hot-card:hover {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,63 +2,129 @@
|
||||
"use strict";
|
||||
|
||||
const userSettings = await csLib.getConfiguration("hotCards", {});
|
||||
const TAG_ID = userSettings?.tagId;
|
||||
const RATING_THRESHOLD = parseInt(userSettings?.threshold ?? 0);
|
||||
const CARDS = {
|
||||
gallery: {
|
||||
class: "gallery-card",
|
||||
data: stash.galleries,
|
||||
enabled: userSettings?.galleries,
|
||||
const SEPARATOR = "_";
|
||||
const INNER_SEPARATOR = ",";
|
||||
const DEFAULTS = {
|
||||
criterion: "",
|
||||
value: "",
|
||||
style: "default",
|
||||
gradient_opts: {
|
||||
type: "linear",
|
||||
angle: "0deg",
|
||||
animation: "",
|
||||
},
|
||||
image: {
|
||||
class: "image-card",
|
||||
data: stash.images,
|
||||
enabled: userSettings?.images,
|
||||
hover_opts: {
|
||||
color: "transparent",
|
||||
animation: "",
|
||||
},
|
||||
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,
|
||||
card_opts: {
|
||||
fill: true,
|
||||
opacity: 80,
|
||||
},
|
||||
};
|
||||
const CRITERIA = { tag: "t", rating: "r", disabled: "d" };
|
||||
const SETTINGS = parseSettings(userSettings ?? "");
|
||||
const TAG_ID = SETTINGS.tagId;
|
||||
const RATING_THRESHOLD = parseInt(SETTINGS.threshold ?? 0);
|
||||
const CARD_KEYS = {
|
||||
galleries: "gallery",
|
||||
images: "image",
|
||||
movies: "movie",
|
||||
performers: "performer",
|
||||
scenes: "scene",
|
||||
studios: "studio",
|
||||
};
|
||||
|
||||
const CARDS = Object.entries(CARD_KEYS).reduce((acc, [plural, singular]) => {
|
||||
acc[singular] = {
|
||||
class: `${singular}-card`,
|
||||
config: SETTINGS[plural] || "",
|
||||
data: stash[plural],
|
||||
enabled: SETTINGS[plural]?.criterion !== CRITERIA.disabled,
|
||||
};
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
/**
|
||||
* Custom CSS style presets for hot cards.
|
||||
*/
|
||||
const STYLES = {
|
||||
default: getDefaultStylePreset(),
|
||||
hot: getHotStylePreset(),
|
||||
gold: getGoldStylePreset(),
|
||||
};
|
||||
|
||||
/**
|
||||
* Element to inject custom CSS styles.
|
||||
*/
|
||||
const styleElement = document.createElement("style");
|
||||
document.head.appendChild(styleElement);
|
||||
|
||||
const isTagBased = TAG_ID?.length;
|
||||
const isRatingBased = RATING_THRESHOLD !== 0;
|
||||
const isTagOrRatingBased = isTagBased || isRatingBased;
|
||||
const isStarsRatingSystem = RATING_THRESHOLD <= 5;
|
||||
|
||||
let backupCards = [];
|
||||
let hotCards = [];
|
||||
|
||||
function parseSettings(settings) {
|
||||
return Object.keys(settings).reduce((acc, key) => {
|
||||
if (key === "threshold" || key === "tagId" || key === "home") {
|
||||
acc[key] = settings[key];
|
||||
} else {
|
||||
acc[key] = parseCustomFormat(settings[key]);
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
function parseCustomFormat(custom) {
|
||||
const segments = custom.toString().split(SEPARATOR);
|
||||
|
||||
return {
|
||||
criterion: segments[0] || DEFAULTS.criterion,
|
||||
value: segments[1] || DEFAULTS.value,
|
||||
style: segments[2] || DEFAULTS.style,
|
||||
gradient_opts: parseSegment(segments[3], DEFAULTS.gradient_opts, [
|
||||
"type",
|
||||
"angle",
|
||||
"animation",
|
||||
]),
|
||||
hover_opts: parseSegment(segments[4], DEFAULTS.hover_opts, [
|
||||
"color",
|
||||
"animation",
|
||||
]),
|
||||
card_opts: parseSegment(segments[5], DEFAULTS.card_opts, [
|
||||
"fill",
|
||||
"opacity",
|
||||
]),
|
||||
};
|
||||
}
|
||||
|
||||
function parseSegment(segment, defaults, keys) {
|
||||
const values = segment ? segment.split(INNER_SEPARATOR) : [];
|
||||
return keys.reduce((acc, key, index) => {
|
||||
acc[key] = values[index] || defaults[key];
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
// Mapping of configuration keys to functions
|
||||
const hotCardsHandlers = {
|
||||
home: handleHomeHotCards,
|
||||
galleries: handleGalleriesHotCards,
|
||||
images: handleImagesHotCards,
|
||||
movies: handleMoviesHotCards,
|
||||
performers: handlePerformersHotCards,
|
||||
scenes: handleScenesHotCards,
|
||||
studios: handleStudiosHotCards,
|
||||
gallery: handleGalleriesHotCards,
|
||||
image: handleImagesHotCards,
|
||||
movie: handleMoviesHotCards,
|
||||
performer: handlePerformersHotCards,
|
||||
scene: handleScenesHotCards,
|
||||
studio: 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)
|
||||
) {
|
||||
// Handle home hot cards separately
|
||||
if (SETTINGS.home && isTagOrRatingBased) handleHomeHotCards();
|
||||
|
||||
for (const [key, card] of Object.entries(CARDS)) {
|
||||
if (card.enabled && hotCardsHandlers[key] && isTagOrRatingBased) {
|
||||
hotCardsHandlers[key]();
|
||||
}
|
||||
}
|
||||
@ -184,30 +250,41 @@
|
||||
|
||||
function handleHotCards(card, isHome = false) {
|
||||
waitForClass(card.class, () => {
|
||||
createAndInsertHotCards(card.data, card.class, isHome);
|
||||
createAndInsertHotCards(card.data, card.class, card.config, isHome);
|
||||
setHotCardStyling(card);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* On the home page, multiple GraphQL requests are intercepted,
|
||||
* each corresponding to a premade filter / user saved filter.
|
||||
*
|
||||
* 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.
|
||||
* This function is called for each card type enabled to add hot elements.
|
||||
*
|
||||
* 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 {Object} config - User settings for the current card type.
|
||||
* @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.
|
||||
function createAndInsertHotCards(stashData, cardClass, config, isHome) {
|
||||
const isCriterionTag = config.criterion === CRITERIA.tag;
|
||||
const isCriterionRating = config.criterion === CRITERIA.rating;
|
||||
const isCriterionEmpty = config.criterion.length === 0;
|
||||
const valueNotSet = config.value.length === 0;
|
||||
const isCriterionTagOrEmpty =
|
||||
isTagBased && (isCriterionTag || isCriterionEmpty);
|
||||
const isCriterionRatingOrEmpty =
|
||||
isRatingBased && (isCriterionRating || isCriterionEmpty);
|
||||
|
||||
/**
|
||||
* To avoid DOM exceptions, it runs if `hotCards` is empty and we are not in the home page
|
||||
* or if we are in the home page.
|
||||
*/
|
||||
if (hotCards.length === 0 || isHome) {
|
||||
const cards = document.querySelectorAll(`.${cardClass}`);
|
||||
|
||||
@ -216,25 +293,36 @@
|
||||
const id = new URL(link.href).pathname.split("/").pop();
|
||||
const data = stashData[id];
|
||||
|
||||
if (isTagBased) {
|
||||
if (isCriterionTagOrEmpty) {
|
||||
if (data?.tags?.length) {
|
||||
// If the tag ID for this card type is not set, use the default tag ID.
|
||||
const tagId = valueNotSet ? TAG_ID : config.value;
|
||||
data.tags.forEach((tag) => {
|
||||
if (tag.id === TAG_ID) createHotElementAndAttachToDOM(card);
|
||||
if (tag.id === tagId)
|
||||
createHotElementAndAttachToDOM(card, cardClass, isHome);
|
||||
});
|
||||
}
|
||||
} else if (isRatingBased && data?.rating100 !== null) {
|
||||
if (data.rating100 >= RATING_THRESHOLD)
|
||||
createHotElementAndAttachToDOM(card);
|
||||
} else if (isCriterionRatingOrEmpty && data?.rating100) {
|
||||
const rating = isStarsRatingSystem
|
||||
? data.rating100 / 20
|
||||
: data.rating100;
|
||||
// If the rating threshold for this card type is not set, use the default threshold.
|
||||
const ratingThreshold = valueNotSet ? RATING_THRESHOLD : config.value;
|
||||
if (rating >= ratingThreshold)
|
||||
createHotElementAndAttachToDOM(card, cardClass, isHome);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function createHotElementAndAttachToDOM(cardElement) {
|
||||
const hotElement = createElementFromHTML(`<div class="hot-border">`);
|
||||
function createHotElementAndAttachToDOM(cardElement, cardClass, isHome) {
|
||||
const hotElement = createElementFromHTML(
|
||||
`<div class="hot-card hot-${cardClass}">`
|
||||
);
|
||||
if (isHome) hotElement.style.height = "100%";
|
||||
|
||||
backupCards.push(cardElement);
|
||||
cardElement.classList.add("hot-card");
|
||||
cardElement.classList.add("hot-border");
|
||||
cardElement.before(hotElement);
|
||||
hotElement.append(cardElement);
|
||||
hotCards.push(hotElement);
|
||||
@ -246,6 +334,262 @@
|
||||
return div.firstChild;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the style of the hot card based on the user's configuration.
|
||||
*/
|
||||
function setHotCardStyling(card) {
|
||||
const { style, gradient_opts, hover_opts, card_opts } = card.config;
|
||||
const colors = style.split(INNER_SEPARATOR).map((color) => color.trim());
|
||||
|
||||
const pseudoElementStyle =
|
||||
colors.length === 1
|
||||
? applySingleColorStyle(
|
||||
card,
|
||||
colors[0],
|
||||
gradient_opts,
|
||||
hover_opts,
|
||||
card_opts
|
||||
)
|
||||
: applyCustomGradientStyle(
|
||||
card,
|
||||
colors,
|
||||
gradient_opts,
|
||||
hover_opts,
|
||||
card_opts
|
||||
);
|
||||
|
||||
styleElement.innerHTML += pseudoElementStyle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a single color style, which can be a style preset or a fixed color.
|
||||
*/
|
||||
function applySingleColorStyle(
|
||||
card,
|
||||
color,
|
||||
gradient_opts,
|
||||
hover_opts,
|
||||
card_opts
|
||||
) {
|
||||
return STYLES[color]
|
||||
? applyPresetStyle(
|
||||
card,
|
||||
STYLES[color],
|
||||
gradient_opts,
|
||||
hover_opts,
|
||||
card_opts
|
||||
)
|
||||
: applyFixedColorStyle(card, color, hover_opts, card_opts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a style preset.
|
||||
*/
|
||||
function applyPresetStyle(
|
||||
card,
|
||||
preset,
|
||||
gradient_opts,
|
||||
hover_opts,
|
||||
card_opts
|
||||
) {
|
||||
const { gradient, hover } = preset;
|
||||
const { angle, animation } = gradient_opts;
|
||||
const { color: hoverColor, animation: hoverAnimation } = hover_opts;
|
||||
|
||||
// Update gradient options with preset defaults if not provided
|
||||
const updatedGradientOpts = {
|
||||
type: gradient.type,
|
||||
angle: angle !== DEFAULTS.gradient_opts.angle ? angle : gradient.angle,
|
||||
animation: animation || gradient.animation,
|
||||
};
|
||||
|
||||
// Update hover options with preset defaults if not provided
|
||||
const updatedHoverOpts = {
|
||||
color:
|
||||
hoverColor !== DEFAULTS.hover_opts.color ? hoverColor : hover.color,
|
||||
animation: hoverAnimation || hover.animation,
|
||||
};
|
||||
|
||||
return applyCustomGradientStyle(
|
||||
card,
|
||||
gradient.colors,
|
||||
updatedGradientOpts,
|
||||
updatedHoverOpts,
|
||||
card_opts
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a fixed color style.
|
||||
*/
|
||||
function applyFixedColorStyle(card, color, hover_opts, card_opts) {
|
||||
setHoverStyleProperties(card, hover_opts.color, hover_opts.animation);
|
||||
return getHotCardPseudoElementString(card, card_opts, color);
|
||||
}
|
||||
|
||||
/**
|
||||
* If there are more than one color, it's a custom gradient.
|
||||
*/
|
||||
function applyCustomGradientStyle(
|
||||
card,
|
||||
colors,
|
||||
gradient_opts,
|
||||
hover_opts,
|
||||
card_opts
|
||||
) {
|
||||
const { type, angle, animation } = gradient_opts;
|
||||
const gradient = getGradient(type, angle, colors);
|
||||
setHoverStyleProperties(card, hover_opts.color, hover_opts.animation);
|
||||
return getHotCardPseudoElementString(card, card_opts, gradient, animation);
|
||||
}
|
||||
|
||||
function setHoverStyleProperties(card, color, animation) {
|
||||
const animationStr = animation ? `pulse ${animation}` : "";
|
||||
document
|
||||
.querySelectorAll(`.hot-${card.class} > .hot-border`)
|
||||
.forEach((hotBorderCard) => {
|
||||
hotBorderCard.style.setProperty("--hover-color", color);
|
||||
hotBorderCard.style.animation = animationStr;
|
||||
});
|
||||
}
|
||||
|
||||
function getGradient(type, positionAngle = "", colors) {
|
||||
const positionAngleStr = positionAngle ? `${positionAngle},` : "";
|
||||
return `${type}-gradient(${positionAngleStr} ${colors.join(", ")})`;
|
||||
}
|
||||
|
||||
function getHotCardPseudoElementString(
|
||||
card,
|
||||
card_opts,
|
||||
background,
|
||||
gradientAnimation = "",
|
||||
filter = ""
|
||||
) {
|
||||
const opacity = getBackgroundOpacity(card_opts.opacity);
|
||||
const fill = /true/i.test(card_opts.fill);
|
||||
const gradientAnimationStr = gradientAnimation
|
||||
? `animation: move ${gradientAnimation};`
|
||||
: "";
|
||||
const fillStr = fill ? `background-color: rgba(0, 0, 0, ${opacity});` : "";
|
||||
const filterStr = filter ? `filter: ${filter};` : "";
|
||||
const hotCardClass = `.hot-${card.class}`;
|
||||
|
||||
return `${hotCardClass}::before,
|
||||
${hotCardClass}::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: calc(0.8 * var(--border-width));
|
||||
left: calc(0.8 * var(--border-width));
|
||||
width: calc(100% + var(--border-width) * -1.5);
|
||||
height: calc(100% + var(--border-width) * -1.5);
|
||||
border-radius: calc(2 * var(--border-width));
|
||||
background: ${background};
|
||||
background-size: 300% 300%;
|
||||
background-position: 0 50%;
|
||||
${gradientAnimationStr}
|
||||
}
|
||||
${hotCardClass} > .hot-border {
|
||||
${fillStr}
|
||||
}
|
||||
${hotCardClass}::after {
|
||||
${filterStr}
|
||||
}`;
|
||||
}
|
||||
|
||||
function getBackgroundOpacity(opacity) {
|
||||
return parseFloat((1 - opacity / 100).toFixed(1));
|
||||
}
|
||||
|
||||
function createCardStyle(
|
||||
hoverColor,
|
||||
hoverAnimation,
|
||||
gradientType,
|
||||
gradientAngle,
|
||||
gradientColors,
|
||||
gradientAnimation,
|
||||
filter
|
||||
) {
|
||||
return {
|
||||
hover: {
|
||||
color: hoverColor,
|
||||
animation: hoverAnimation,
|
||||
},
|
||||
gradient: {
|
||||
type: gradientType,
|
||||
angle: gradientAngle,
|
||||
colors: gradientColors,
|
||||
animation: gradientAnimation,
|
||||
generated: getGradient(gradientType, gradientAngle, gradientColors),
|
||||
},
|
||||
filter,
|
||||
};
|
||||
}
|
||||
|
||||
function getDefaultStylePreset() {
|
||||
return createCardStyle(
|
||||
"#ff2409",
|
||||
"3s ease-in-out infinite",
|
||||
"linear",
|
||||
"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%)",
|
||||
],
|
||||
"4s alternate infinite"
|
||||
);
|
||||
}
|
||||
|
||||
function getHotStylePreset() {
|
||||
return createCardStyle(
|
||||
"#a41111",
|
||||
"4s ease-in-out infinite",
|
||||
"radial",
|
||||
"",
|
||||
[
|
||||
"hsl(351.7, 86.5%, 62.4%)",
|
||||
"hsl(351.7, 86.4%, 46.1%)",
|
||||
"hsl(357, 86.6%, 49.6%)",
|
||||
"hsl(343.3, 73.1%, 39.4%)",
|
||||
"hsl(0, 84.9%, 36.5%)",
|
||||
"hsl(354.4, 72.9%, 40.6%)",
|
||||
"hsl(348.8, 92.9%, 44.1%)",
|
||||
"hsl(345, 80%, 49%)",
|
||||
"hsl(354.5, 83.1%, 46.5%)",
|
||||
"hsl(357, 86.6%, 49.6%)",
|
||||
"hsl(328.2, 73.9%, 22.5%)",
|
||||
"hsl(345, 81%, 49.4%)",
|
||||
"hsl(0, 70%, 31.4%)",
|
||||
],
|
||||
"20s linear infinite"
|
||||
); // 'blur(2.0rem)'
|
||||
}
|
||||
|
||||
function getGoldStylePreset() {
|
||||
return createCardStyle(
|
||||
"#d4af37",
|
||||
"6s ease-in-out infinite",
|
||||
"linear",
|
||||
"45deg",
|
||||
[
|
||||
"hsl(19.9, 62.7%, 52.7%)",
|
||||
"hsl(45, 90.4%, 40.8%)",
|
||||
"hsl(40.2, 56.5%, 37.8%)",
|
||||
"hsl(42.1, 96.5%, 55.1%)",
|
||||
"hsl(30.4, 100%, 27.1%)",
|
||||
"hsl(30.8, 49.4%, 45.7%)",
|
||||
"hsl(20, 85%, 60%)",
|
||||
"hsl(14.9, 75.8%, 32.4%)",
|
||||
],
|
||||
"8s ease-in-out infinite"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Since it was necessary to insert a div before the card for
|
||||
* the border design to be visible (otherwise the overflow:hidden; property of
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
name: Hot Cards
|
||||
description: Adds custom css to card elements that match a tag id or a rating threshold.
|
||||
version: 1.0.0
|
||||
description: Adds custom styling to card elements that match a tag ID or a rating threshold.
|
||||
version: 1.1.2
|
||||
url: https://github.com/stashapp/CommunityScripts/tree/main/plugins/hotCards
|
||||
# requires: CommunityScriptsUILibrary
|
||||
ui:
|
||||
@ -17,37 +17,37 @@ ui:
|
||||
settings:
|
||||
tagId:
|
||||
displayName: Tag ID
|
||||
description: The tag ID to match against. Leave blank to disable tag-based hot cards.
|
||||
description: Tag ID to match against. Leave blank to disable tag-based hot cards.
|
||||
type: STRING
|
||||
threshold:
|
||||
displayName: Rating Threshold
|
||||
description: The rating threshold (0-100) to match against. Set to 0 to disable rating-based hot cards.
|
||||
description: Rating threshold (0-5). Use 6-100 for tenths. Set to 0 to disable rating-based hot cards.
|
||||
type: NUMBER
|
||||
home:
|
||||
displayName: Enable on home
|
||||
description: Enable hot cards on the home page.
|
||||
displayName: Enable for the homepage
|
||||
description: Enable Hot Cards on the home page.
|
||||
type: BOOLEAN
|
||||
scenes:
|
||||
displayName: Enable on scenes
|
||||
description: Enable hot cards on scene cards.
|
||||
type: BOOLEAN
|
||||
displayName: Enable for scenes
|
||||
description: "Empty to enable, 'd' to disable. Customize: [criterion]_[value]_[style]_[grad-opts]_[hover-opts]_[card-opts]. See docs."
|
||||
type: STRING
|
||||
images:
|
||||
displayName: Enable on images
|
||||
description: Enable hot cards on image cards.
|
||||
type: BOOLEAN
|
||||
displayName: Enable for images
|
||||
description: "Empty to enable, 'd' to disable. Customize: [criterion]_[value]_[style]_[grad-opts]_[hover-opts]_[card-opts]. See docs."
|
||||
type: STRING
|
||||
movies:
|
||||
displayName: Enable on movies
|
||||
description: Enable hot cards on movie cards.
|
||||
type: BOOLEAN
|
||||
displayName: Enable for movies
|
||||
description: "Empty to enable, 'd' to disable. Customize: [criterion]_[value]_[style]_[grad-opts]_[hover-opts]_[card-opts]. See docs."
|
||||
type: STRING
|
||||
galleries:
|
||||
displayName: Enable on galleries
|
||||
description: Enable hot cards on gallery cards.
|
||||
type: BOOLEAN
|
||||
displayName: Enable for galleries
|
||||
description: "Empty to enable, 'd' to disable. Customize: [criterion]_[value]_[style]_[grad-opts]_[hover-opts]_[card-opts]. See docs."
|
||||
type: STRING
|
||||
performers:
|
||||
displayName: Enable on performers
|
||||
description: Enable hot cards on performer cards.
|
||||
type: BOOLEAN
|
||||
displayName: Enable for performers
|
||||
description: "Empty to enable, 'd' to disable. Customize: [criterion]_[value]_[style]_[grad-opts]_[hover-opts]_[card-opts]. See docs."
|
||||
type: STRING
|
||||
studios:
|
||||
displayName: Enable on studios
|
||||
description: Enable hot cards on studio cards.
|
||||
type: BOOLEAN
|
||||
displayName: Enable for studios
|
||||
description: "Empty to enable, 'd' to disable. Customize: [criterion]_[value]_[style]_[grad-opts]_[hover-opts]_[card-opts]. See docs."
|
||||
type: STRING
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user