Redgifs volume slider

Real volume slider for redgifs.com

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

// ==UserScript==
// @name         Redgifs volume slider
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  Real volume slider for redgifs.com
// @match        *://*.redgifs.com/*
// @author       Piipos_
// @license      WTFPL
// @grant        GM_addStyle
// ==/UserScript==

(function() {
  'use strict';

  GM_addStyle(`
        .volume-slider-wrapper {
            position: relative;
            display: inline-flex;
            align-items: center;
            margin-left: 10px;
            height: 40px;
        }
        .volume-slider-container {
            position: absolute;
            right: 100%;
            top: 50%;
            display: flex;
            align-items: center;
            padding-right: 15px;
            transform: translateY(-50%) translateX(10px);
            opacity: 0;
            pointer-events: none;
            transition: opacity 0.3s, transform 0.3s;
            z-index: 5;
        }
        .volume-slider-wrapper:hover .volume-slider-container,
        .volume-slider-container:hover {
            opacity: 1;
            pointer-events: all;
            transform: translateY(-50%) translateX(0);
        }
        .custom-volume-slider {
            -webkit-appearance: none;
            width: 100px;
            height: 4px;
            border-radius: 2px;
            cursor: pointer;
            background: linear-gradient(to right,
#EFEEF0 var(--vol-percent, 75%), rgba(239,238,240,0.3) var(--vol-percent, 75%), rgba(239,238,240,0.3) 100%);

        }
        .custom-volume-slider::-webkit-slider-thumb {
            -webkit-appearance: none;
            width: 12px;
            height: 12px;
            border-radius: 50%;
            background: #EFEEF0;
            cursor: pointer;
            box-shadow: 0 2px 6px rgba(0,0,0,0.5);
        }
        .volume-tooltip {
            font: bold 11px sans-serif;
            color: #fff;
            background: rgba(0,0,0,0.8);
            padding: 4px 6px;
            border-radius: 4px;
            margin-right: 8px;
            min-width: 25px;
            text-align: center;
        }
        .custom-volume-icon {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            display: flex;
            align-items: center;
            justify-content: center;
            pointer-events: none;
            z-index: 1;
        }
        .SoundButton {
            position: relative;
            opacity: 0.01;
        }
        .SoundButton:hover {
            opacity: 0.02;
        }
    `);

  const SVG_MUTED = `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M17 15.0002L19.5 12.5002M19.5 12.5002L22 10.0002M19.5 12.5002L17 10.0002M19.5 12.5002L22 15.0002M8.83667 6.74524C10.0339 4.94944 10.6325 4.05155 11.1114 3.81402C12.0579 3.34467 13.2061 3.69233 13.7332 4.60783C14 5.07115 14 6.15029 14 8.30856V15.6919C14 17.8501 14 18.9293 13.7332 19.3926C13.2061 20.3081 12.0579 20.6558 11.1114 20.1864C10.6325 19.9489 10.0339 19.051 8.83667 17.2552C8.60011 16.9004 8.48183 16.7229 8.33865 16.5808C8.06197 16.3062 7.71106 16.1183 7.32906 16.0405C7.13138 16.0002 6.91814 16.0002 6.49167 16.0002H5.2C4.0799 16.0002 3.51984 16.0002 3.09202 15.7822C2.71569 15.5905 2.40973 15.2845 2.21799 14.9082C2 14.4804 2 13.9203 2 12.8002V11.2002C2 10.0801 2 9.52008 2.21799 9.09226C2.40973 8.71593 2.71569 8.40997 3.09202 8.21823C3.51984 8.00024 4.07989 8.00024 5.2 8.00024H6.49167C6.91814 8.00024 7.13138 8.00024 7.32906 7.95995C7.71106 7.88209 8.06197 7.69429 8.33865 7.41963C8.48183 7.2775 8.60011 7.10008 8.83667 6.74524Z" stroke="#EFEEF0" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg>`;

  const SVG_LOW = `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M8.83667 6.74524C10.0339 4.94944 10.6325 4.05155 11.1114 3.81402C12.0579 3.34467 13.2061 3.69233 13.7332 4.60783C14 5.07115 14 6.15029 14 8.30856V15.6919C14 17.8501 14 18.9293 13.7332 19.3926C13.2061 20.3081 12.0579 20.6558 11.1114 20.1864C10.6325 19.9489 10.0339 19.051 8.83667 17.2552C8.60011 16.9004 8.48183 16.7229 8.33865 16.5808C8.06197 16.3062 7.71106 16.1183 7.32906 16.0405C7.13138 16.0002 6.91814 16.0002 6.49167 16.0002H5.2C4.0799 16.0002 3.51984 16.0002 3.09202 15.7822C2.71569 15.5905 2.40973 15.2845 2.21799 14.9082C2 14.4804 2 13.9203 2 12.8002V11.2002C2 10.0801 2 9.52008 2.21799 9.09226C2.40973 8.71593 2.71569 8.40997 3.09202 8.21823C3.51984 8.00024 4.07989 8.00024 5.2 8.00024H6.49167C6.91814 8.00024 7.13138 8.00024 7.32906 7.95995C7.71106 7.88209 8.06197 7.69429 8.33865 7.41963C8.48183 7.2775 8.60011 7.10008 8.83667 6.74524Z" stroke="#EFEEF0" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path><path d="M15 9.5C16.5 11 16.5 13.5 15 15" stroke="#EFEEF0" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg>`;

  const SVG_MEDIUM = `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M8.83667 6.74524C10.0339 4.94944 10.6325 4.05155 11.1114 3.81402C12.0579 3.34467 13.2061 3.69233 13.7332 4.60783C14 5.07115 14 6.15029 14 8.30856V15.6919C14 17.8501 14 18.9293 13.7332 19.3926C13.2061 20.3081 12.0579 20.6558 11.1114 20.1864C10.6325 19.9489 10.0339 19.051 8.83667 17.2552C8.60011 16.9004 8.48183 16.7229 8.33865 16.5808C8.06197 16.3062 7.71106 16.1183 7.32906 16.0405C7.13138 16.0002 6.91814 16.0002 6.49167 16.0002H5.2C4.0799 16.0002 3.51984 16.0002 3.09202 15.7822C2.71569 15.5905 2.40973 15.2845 2.21799 14.9082C2 14.4804 2 13.9203 2 12.8002V11.2002C2 10.0801 2 9.52008 2.21799 9.09226C2.40973 8.71593 2.71569 8.40997 3.09202 8.21823C3.51984 8.00024 4.07989 8.00024 5.2 8.00024H6.49167C6.91814 8.00024 7.13138 8.00024 7.32906 7.95995C7.71106 7.88209 8.06197 7.69429 8.33865 7.41963C8.48183 7.2775 8.60011 7.10008 8.83667 6.74524Z" stroke="#EFEEF0" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path><path d="M15 9.5C16.5 11 16.5 13.5 15 15" stroke="#EFEEF0" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path><path d="M18 7C21 10 21 14.5 18 17.5" stroke="#EFEEF0" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg>`;

  const SVG_HIGH = `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M8.83667 6.74524C10.0339 4.94944 10.6325 4.05155 11.1114 3.81402C12.0579 3.34467 13.2061 3.69233 13.7332 4.60783C14 5.07115 14 6.15029 14 8.30856V15.6919C14 17.8501 14 18.9293 13.7332 19.3926C13.2061 20.3081 12.0579 20.6558 11.1114 20.1864C10.6325 19.9489 10.0339 19.051 8.83667 17.2552C8.60011 16.9004 8.48183 16.7229 8.33865 16.5808C8.06197 16.3062 7.71106 16.1183 7.32906 16.0405C7.13138 16.0002 6.91814 16.0002 6.49167 16.0002H5.2C4.0799 16.0002 3.51984 16.0002 3.09202 15.7822C2.71569 15.5905 2.40973 15.2845 2.21799 14.9082C2 14.4804 2 13.9203 2 12.8002V11.2002C2 10.0801 2 9.52008 2.21799 9.09226C2.40973 8.71593 2.71569 8.40997 3.09202 8.21823C3.51984 8.00024 4.07989 8.00024 5.2 8.00024H6.49167C6.91814 8.00024 7.13138 8.00024 7.32906 7.95995C7.71106 7.88209 8.06197 7.69429 8.33865 7.41963C8.48183 7.2775 8.60011 7.10008 8.83667 6.74524Z" stroke="#EFEEF0" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path><path d="M15 9.5C16.5 11 16.5 13.5 15 15" stroke="#EFEEF0" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path><path d="M18 7C21 10 21 14.5 18 17.5" stroke="#EFEEF0" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path><path d="M21 4.5C25 8.5 25 15.5 21 19.5" stroke="#EFEEF0" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path></svg>`;

  const getVolumeSVG = (volume) => {
    if (volume === 0) {
      return SVG_MUTED;
    } else if (volume <= 0.33) {
      return SVG_LOW;
    } else if (volume <= 0.66) {
      return SVG_MEDIUM;
    } else {
      return SVG_HIGH;
    }
  };

  const videoVolumeMap = new WeakMap();
  const videoLastVolumeMap = new WeakMap();
  const videoComponentsMap = new WeakMap();

  const updateSliderUI = (slider, tooltip, volume) => {
    const percent = Math.round(volume * 100);
    slider.value = volume;
    slider.style.setProperty('--vol-percent', percent + '%');
    if (tooltip) {
      tooltip.textContent = percent + '%';
    }
  };

  const updateCustomIcon = (customIcon, volume) => {
    if (customIcon) {
      customIcon.innerHTML = getVolumeSVG(volume);
    }
  };

  const toggleMuteIfNeeded = (video, button, newVolume, oldVolume) => {

    if (newVolume === 0 && oldVolume > 0 && !video.muted) {
      if (button && typeof button.click === 'function') {
        button.click();
        return true;
      }
    } else if (newVolume > 0 && oldVolume === 0 && video.muted) {
      if (button && typeof button.click === 'function') {
        button.click();
        return true;
      }
    }
    return false;
  };

  const observer = new MutationObserver(() => {
    document.querySelectorAll('.GifPreview_isActive').forEach(gifPreview => {
      const video = gifPreview.querySelector('video');
      const button = gifPreview.querySelector('.SoundButton');
      if (!video || !button || button.dataset.volumeEnhanced) return;

      button.dataset.volumeEnhanced = 'true';

      const wrapper = document.createElement('div');
      wrapper.className = 'volume-slider-wrapper';

      button.parentNode.insertBefore(wrapper, button);

      wrapper.appendChild(button);

      const customIcon = document.createElement('div');
      customIcon.className = 'custom-volume-icon';

      const sliderContainer = document.createElement('div');
      sliderContainer.className = 'volume-slider-container';

      const tooltip = document.createElement('div');
      tooltip.className = 'volume-tooltip';

      const slider = document.createElement('input');
      slider.type = 'range';
      slider.min = 0;
      slider.max = 1;
      slider.step = 0.01;
      slider.className = 'custom-volume-slider';

      if (!videoVolumeMap.has(video)) {
        const initialVolume = video.muted ? 0 : video.volume;
        videoVolumeMap.set(video, initialVolume);
        if (!video.muted && video.volume > 0) {
          videoLastVolumeMap.set(video, video.volume);
        }
      }

      videoComponentsMap.set(video, {
        slider,
        tooltip,
        button,
        customIcon
      });

      const initialVolume = videoVolumeMap.get(video);
      updateSliderUI(slider, tooltip, initialVolume);
      updateCustomIcon(customIcon, initialVolume);

      let lastVolume = videoVolumeMap.get(video);

      const onSliderInput = (e) => {
        const newVolume = parseFloat(e.target.value);

        if (newVolume > 0) {
          videoLastVolumeMap.set(video, newVolume);
        }

        const wasToggled = toggleMuteIfNeeded(video, button, newVolume, lastVolume);

        videoVolumeMap.set(video, newVolume);

        if (!wasToggled) {
          video.volume = newVolume;
        }

        updateSliderUI(slider, tooltip, newVolume);
        updateCustomIcon(customIcon, newVolume);

        lastVolume = newVolume;
      };

      slider.addEventListener('input', onSliderInput);

      const buttonObserver = new MutationObserver(() => {

        setTimeout(() => {
          const currentVolume = video.muted ? 0 : video.volume;
          videoVolumeMap.set(video, currentVolume);

          updateSliderUI(slider, tooltip, currentVolume);
          updateCustomIcon(customIcon, currentVolume);

          lastVolume = currentVolume;

          if (!video.muted && video.volume > 0) {
            videoLastVolumeMap.set(video, video.volume);
          }
        }, 50);
      });

      buttonObserver.observe(button, {
        childList: true,
        subtree: true
      });

      [slider, sliderContainer].forEach(el => {
        el.addEventListener('click', e => e.stopPropagation());
        el.addEventListener('mousedown', e => e.stopPropagation());
        el.addEventListener('mouseup', e => e.stopPropagation());
      });

      wrapper.appendChild(customIcon);
      sliderContainer.appendChild(tooltip);
      sliderContainer.appendChild(slider);
      wrapper.appendChild(sliderContainer);

      const onVolumeChange = () => {

        if (!video.dataset.userInteracting) {
          const currentVolume = video.muted ? 0 : video.volume;
          videoVolumeMap.set(video, currentVolume);
          updateSliderUI(slider, tooltip, currentVolume);
          updateCustomIcon(customIcon, currentVolume);
          lastVolume = currentVolume;

          if (!video.muted && video.volume > 0) {
            videoLastVolumeMap.set(video, video.volume);
          }
        }
      };

      video.addEventListener('volumechange', onVolumeChange);

      slider.addEventListener('mousedown', () => {
        video.dataset.userInteracting = 'true';
        lastVolume = videoVolumeMap.get(video);
      });

      slider.addEventListener('mouseup', () => {
        setTimeout(() => {
          delete video.dataset.userInteracting;
        }, 100);
      });

      const cleanup = () => {
        video.removeEventListener('volumechange', onVolumeChange);
        buttonObserver.disconnect();
        clearInterval(backupInterval);
        videoComponentsMap.delete(video);

        if (wrapper && wrapper.parentNode) {
          wrapper.parentNode.insertBefore(button, wrapper);
          wrapper.remove();
        }
      };

      const backupInterval = setInterval(() => {
        if (!document.body.contains(video)) {
          cleanup();
          return;
        }

        const targetVolume = videoVolumeMap.get(video);
        if (targetVolume !== undefined && Math.abs(video.volume - targetVolume) > 0.01) {
          video.volume = targetVolume;
        }
      }, 100);
    });
  });

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