[SFW Switch] Audio handling + Additional plugin support (#679)

* Added additional plugin support, patched additional fields previously left uncensored.

* Update README

Clarified instructions for adding custom selectors to additional_plugins.css.

* SFW Switch now mutes all sound when enabled by default.

* Refactored main css, all UI functions should be accouted for now. Updated plugin yaml description

* targeted stats-elements rather than m-sm-auto to prevent unwanted css stylings. Loosened blur

* Re-added performer card profile pic

* Added audio boolean setting (disabled by default), added additional configuration for hot or not

* adjusted img blur

* Added transition to all unblur

* Major Improvement: Fixed remaining unblurred features, fixed zombie audio bug.

* Readded readme

* Improvement: Added individual wall blur functionality for walls

* Fix double blur for groups, fix unblur in performer tagger

---------

Co-authored-by: DogmaDragon <103123951+DogmaDragon@users.noreply.github.com>
This commit is contained in:
Servbot91
2026-03-20 13:15:07 -04:00
committed by GitHub
parent 74b0986b19
commit 16627510ac
4 changed files with 252 additions and 81 deletions

View File

@@ -12,15 +12,22 @@
/* HotOrNot */
.hon-performer-image,
.hon-performer-card,
.hon-scene-image,
.hon-selection-image,
.hon-image-image-container,
.hon-image-image,
/* O Stats */
.custom-stats-row .stats-element img,
#on-this-day-section [style*="position: relative; height: 400px"]
#on-this-day-section [style*="position: relative; height: 400px"],
/* Sprite Tab */
.sprite-cell
{
filter: blur(30px);
filter: blur(30px);
transition: filter 0.25s ease;
}
/* === LESS BLUR === */
@@ -29,6 +36,7 @@ filter: blur(30px);
.pwr-scene-info,
/* HotOrNot */
.hon-selection-name,
.hon-performer-info.hon-scene-info,
/* O Stats */
@@ -37,7 +45,8 @@ filter: blur(30px);
#on-this-day-section [style*="display: flex"][style*="cursor: pointer"] img + div,
#on-this-day-section > div:last-child
{
filter: blur(2px);
filter: blur(2px);
transition: filter 0.25s ease;
}
/* StashBattle */
@@ -48,10 +57,17 @@ filter: blur(2px);
/* HotOrNot */
.hon-performer-image:hover,
.hon-performer-card:hover,
.hon-scene-image:hover,
.hon-image-image-container:hover,
.hon-image-image:hover,
.hon-performer-info.hon-scene-info:hover,
.hon-selection-card:hover,
.hon-selection-name:hover,
.hon-selection-image:hover,
/* Sprite Tab */
.sprite-cell:hover,
/* O Stats */
.custom-stats-row .stats-element:hover,

View File

@@ -1,134 +1,173 @@
/* [Global changes] Blur NSFW images and unblur on mouse over */
/*Credit: fl0w#9497 */
/* === MORE BLUR === */
/* common */
.thumbnail-container img,
.detail-header-image,
.wall-item-gallery,
/* scene */
.scene-card-preview,
.vjs-poster,
video,
.scene-player-container,
.scene-cover,
.scene-card-preview,
.scrubber-item,
.scene-image,
.scene-card img,
.wall-item-media,
.wall-item.show-title,
/* image */
.image-card-preview,
.image-image,
.gallery-image,
.image-card img,
.image-thumbnail,
.Lightbox-carousel,
.react-photo-gallery--gallery img,
/* group */
.group-card-image,
.group-images,
/* gallery */
.gallery-image,
.gallery-card-image,
table > tbody > tr > td > a > img.w-100,
.gallery-card img,
.gallery-cover img,
.GalleryWallCard.GalleryWallCard-portrait,
.GalleryWallCard.GalleryWallCard-landscape,
/* performer */
.performer-card-image,
img.performer,
.performer-card img,
/* studio */
.studio-card-image,
.studio-card img,
/* tag */
.tag-card-image
.tag-card img
{
filter: blur(30px);
filter: blur(30px);
transition: filter 0.25s ease;
}
/* === LESS BLUR === */
/* common */
.card-section-title,
.detail-item-value.description,
.detail-item-value,
.TruncatedText,
/* scene */
.scene-studio-overlay,
.scene-header > h3,
h3.scene-header,
.studio-logo,
.image-thumbnail,
.TruncatedText.scene-card__description,
.queue-scene-details,
.marker-wall,
/* image */
h3.image-header,
/* performer */
.performer-name,
.card-section,
.name-data,
.aliases-data,
/* gallery */
.gallery-header.no-studio,
.TruncatedText.gallery-card__description,
/* studio */
.studio-name,
.studio-overlay a,
.studio-logo,
.studio-parent-studios,
/* group */
.group-details > div > h2,
/* gallery */
h3.gallery-header,
.TruncatedText.gallery-card__description,
/* studio */
.studio-details .logo,
.studio-details > div > h2,
.studio-card__details,
.studio-parent-studios,
/* image */
h3.image-header,
.Lightbox-carousel:hover,
.TruncatedText.image-card__description,
/* tag */
.logo-container > .logo,
.logo-container > h2,
.TruncatedText.tag-description,
.tag-parent-tags
.tag-item.tag-link.badge.badge-secondary,
.tag-name
{
filter: blur(2px);
}
/* === UNBLUR ON HOVER === */
/* common */
.thumbnail-section:hover *,
.card:hover .card-section-title,
.detail-item-value:hover,
.scene-cover:hover,
.card-section-title:hover,
.TruncatedText.tag-description:hover,
.detail-item-value.description:hover,
.TruncatedText:hover,
/* scene */
.card:hover .scene-studio-overlay,
.video-js:hover .vjs-poster,
video:hover,
.scene-player-container:hover,
.scene-card-preview:hover,
.queue-scene-details:hover,
.scene-card:hover img,
.TruncatedText.scene-card__description:hover,
.scene-player-container:hover,
.scene-header:hover > h3,
div:hover > .scene-header,
.studio-logo:hover,
.scene-cover:hover,
.image-thumbnail:hover,
.scene-card-preview:hover,
.scrubber-item:hover,
.scene-image:hover,
.TruncatedText.scene-card__description:hover,
.wall-item-media:hover,
.marker-wall:hover,
.wall-item.show-title:hover,
/* image */
.image-image:hover,
.detail-header-image:hover,
div:hover > .image-header,
.gallery-image:hover,
.image-card:hover img,
.react-photo-gallery--gallery img:hover,
.image-thumbnail:hover,
.TruncatedText.image-card__description:hover,
.wall-item:hover img,
/* group */
.group-images:hover,
.group-details > div > h2:hover,
.group-card:hover img,
/* gallery */
div:hover > .gallery-header,
table > tbody > tr > td:hover > a > img.w-100,
.gallery-header.no-studio,
.gallery-card:hover img,
.gallery-cover:hover img,
.gallery-image:hover,
.gallery-card-image:hover,
.TruncatedText.gallery-card__description:hover,
.GalleryWallCard.GalleryWallCard-portrait:hover,
.GalleryWallCard.GalleryWallCard-landscape:hover,
/* performer */
img.performer:hover,
.performer-card-image:hover,
.performer-name:hover,
.card-section:hover,
.name-data:hover,
.aliases-data:hover,
.performer-card img:hover,
/* studio */
.studio-details .logo:hover,
.studio-details:hover > div > h2,
.studio-card__details:hover,
.studio-name:hover,
.studio-overlay:hover a,
.studio-card:hover img,
.studio-parent-studios:hover,
.studio-logo:hover,
/* tag */
.logo-container > .logo:hover,
.logo-container:hover > h2,
.TruncatedText.tag-description:hover,
.tag-parent-tags:hover
.tag-card:hover img,
.tag-item.tag-link.badge.badge-secondary:hover,
.tag-name:hover
{
filter: blur(0px);
filter: blur(0);
transition: filter 0.25s ease;
}
/*Credit: fl0w#9497 */

View File

@@ -1,16 +1,48 @@
function sfw_mode() {
let sfw_mediaObserver = null;
let sfw_playListener = null;
let sfw_extraListeners = null;
async function getSfwConfig() {
try {
const response = await fetch('/graphql', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: `{
configuration {
plugins
}
}`
}),
});
const result = await response.json();
const pluginSettings = result.data.configuration.plugins.sfwswitch;
return pluginSettings?.audio_setting === true;
} catch (e) {
console.error("SFW Switch: Could not fetch config", e);
return false;
}
}
async function sfw_mode() {
const stash_css = sfwswitch_findstashcss();
const button = document.getElementById("plugin_sfw");
if (!stash_css) return;
const sfwState = localStorage.getItem("sfw_mode") === "true";
const audioMuteEnabled = await getSfwConfig();
// Apply saved state to the stylesheet
stash_css.disabled = !sfwState;
// Update button color
button.style.color = sfwState ? "#5cff00" : "#f5f8fa";
if (sfwState && audioMuteEnabled) {
sfw_mute_all_media();
} else {
sfw_unmute_all_media();
}
if (button) {
button.style.color = sfwState ? "#5cff00" : "#f5f8fa";
}
}
function sfwswitch_createbutton() {
@@ -45,22 +77,101 @@ function sfwswitch_createbutton() {
setTimeout(() => clearInterval(intervalId), 10000);
}
function sfwswitch_switcher() {
const stash_css = sfwswitch_findstashcss();
if (!stash_css) {
console.error("SFW stylesheet not found.");
return;
// Function to strictly handle the muted state
function sfw_forceMute(media) {
if (!media) return;
media.muted = true;
}
function sfw_mute_all_media() {
// Initial sweep
document.querySelectorAll("audio, video").forEach(sfw_forceMute);
// Global event listener for play, seek, and volume changes
if (!sfw_playListener) {
sfw_playListener = function(e) {
if (e.target.tagName === "VIDEO" || e.target.tagName === "AUDIO") {
sfw_forceMute(e.target);
}
};
document.addEventListener("play", sfw_playListener, true);
document.addEventListener("volumechange", sfw_playListener, true);
document.addEventListener("loadeddata", sfw_playListener, true);
document.addEventListener("seeking", sfw_playListener, true);
}
// Toggle stylesheet
stash_css.disabled = !stash_css.disabled;
// MutationObserver for content loaded via AJAX/Dynamic updates
if (!sfw_mediaObserver) {
sfw_mediaObserver = new MutationObserver(mutations => {
for (const mutation of mutations) {
mutation.addedNodes.forEach(node => {
if (node.tagName === "VIDEO" || node.tagName === "AUDIO") {
sfw_forceMute(node);
} else if (node.querySelectorAll) {
node.querySelectorAll("video, audio").forEach(sfw_forceMute);
}
});
}
});
sfw_mediaObserver.observe(document.body, { childList: true, subtree: true });
}
}
// Save new state to localStorage
localStorage.setItem("sfw_mode", !stash_css.disabled);
function sfw_unmute_all_media() {
// 1. Remove listeners FIRST to prevent them from firing during the unmute loop
if (sfw_playListener) {
document.removeEventListener("play", sfw_playListener, true);
document.removeEventListener("volumechange", sfw_playListener, true);
document.removeEventListener("loadeddata", sfw_playListener, true);
document.removeEventListener("seeking", sfw_playListener, true);
sfw_playListener = null;
}
if (sfw_mediaObserver) {
sfw_mediaObserver.disconnect();
sfw_mediaObserver = null;
}
// 2. Unmute existing media
document.querySelectorAll("audio, video").forEach(media => {
media.muted = false;
// Optional: media.volume = 1.0; // Use if volume was also forced to 0
});
}
async function sfwswitch_switcher() {
const stash_css = sfwswitch_findstashcss();
if (!stash_css) return;
// Toggle the CSS
stash_css.disabled = !stash_css.disabled;
const enabled = !stash_css.disabled;
localStorage.setItem("sfw_mode", enabled);
const audioMuteEnabled = await getSfwConfig();
// Logic Check: If we just disabled SFW, we MUST run unmute immediately
if (enabled && audioMuteEnabled) {
sfw_mute_all_media();
} else {
// This clears observers and sets muted = false
sfw_unmute_all_media();
// CRITICAL: Force a pause/reset on any media that might be stuck in a background buffer
document.querySelectorAll("audio, video").forEach(media => {
if (media.paused && media.muted) {
// If it was supposed to be stopped, make sure it stays stopped
media.muted = false;
}
});
}
const button = document.getElementById("plugin_sfw");
button.style.color = stash_css.disabled ? "#f5f8fa" : "#5cff00";
console.log(`SFW mode ${stash_css.disabled ? "disabled" : "enabled"}`);
if (button) {
button.style.color = enabled ? "#5cff00" : "#f5f8fa";
}
}
function sfwswitch_findstashcss() {

View File

@@ -1,10 +1,15 @@
name: SFW Switch
description: Add a button to blur covers and images.
version: 1.4
version: 1.6
url: https://discourse.stashapp.cc/t/sfw-switch/4658
ui:
javascript:
- sfw.js
css:
- sfw.css
- additional_plugins.css
- additional_plugins.css
settings:
audio_setting:
displayName: Enable Sound Mute
description: By default the plugin does not mute sound. Enabling this feature will have sound sources included when the SFW button is enabled.
type: BOOLEAN