Scrollable Volume Control with Custom Collapse and Step Control

10/10/24

As of 19.10.2024. See ბოლო ვერსია.

// ==UserScript==
// @name        Scrollable Volume Control with Custom Collapse and Step Control
// @namespace   Tampermonkey Scripts
// @match       https://www.redgifs.com/watch/*
// @grant       GM_registerMenuCommand
// @version     2.8
// @author      Bunglover24
// @license     GNU GPLv3
// @icon        https://icons.duckduckgo.com/ip2/redgifs.com.ico
// @description 10/10/24
// ==/UserScript==

const isFirefox = typeof InstallTrigger !== 'undefined'; // Detect Firefox

// Default step size for volume control, saved in localStorage
let volumeStep = parseFloat(localStorage.getItem('volumeStep')) || 0.01;

// Helper to save square position to localStorage
function savePosition(x, y) {
  localStorage.setItem('volumeControlPos', JSON.stringify({ x, y }));
}

// Helper to load saved square position
function loadPosition() {
  const pos = JSON.parse(localStorage.getItem('volumeControlPos'));
  return pos ? { x: pos.x, y: pos.y } : { x: window.innerWidth / 2, y: window.innerHeight / 2 };
}

// Create the volume control square
function createVolumeControlSquare(video) {
  const controlSquare = document.createElement('div');
  const { x, y } = loadPosition();

  // Style for the square
  Object.assign(controlSquare.style, {
    position: 'fixed',
    left: `${x}px`,
    top: `${y}px`,
    width: '8.5mm', // Size adjusted for 100% text
    height: '8.5mm', // Size adjusted for 100% text
    backgroundColor: 'rgba(23, 23, 23, 0.8)', // Slight transparency for the square
    color: 'white',
    display: 'flex',
    alignItems: 'center',
    justifyContent: 'center',
    borderRadius: '3px', // Slight corner rounding
    cursor: 'pointer',
    zIndex: 1000,
    transition: 'all 0.3s ease',
  });

  let volume = localStorage.getItem('videoVolume') || 0.01; // Default to 1%
  video.volume = volume;

  // Volume text, centered and styled
  const volumeText = document.createElement('div');
  volumeText.textContent = `${Math.round(volume * 100)}%`;
  Object.assign(volumeText.style, {
    pointerEvents: 'none', // Prevent interaction
    fontSize: '12px',
    fontFamily: 'Tahoma, sans-serif',
    color: 'rgba(255, 255, 255, 0.9)', // Slightly less transparent text
    textShadow: '1px 1px 3px rgba(0, 0, 0, 0.5)', // Subtle shadow for contrast
    zIndex: 2, // Ensure it's above other elements
  });
  controlSquare.appendChild(volumeText);

  // Volume control via scroll, adjustable steps
  controlSquare.addEventListener('wheel', (e) => {
    e.preventDefault();
    volume = Math.min(Math.max(parseFloat(volume) + (e.deltaY > 0 ? -volumeStep : volumeStep), 0), 1);
    video.volume = volume;
    localStorage.setItem('videoVolume', volume);
    volumeText.textContent = `${Math.round(volume * 100)}%`;

    if (isFirefox) {
      video.muted = false; // Firefox-specific handling
    }
  });

  // Create the drag bar, adjusted to 3 pixels tall
  const dragBar = document.createElement('div');
  Object.assign(dragBar.style, {
    position: 'absolute',
    top: '0px', // Positioned at the top
    width: '100%',
    height: '3px', // Adjusted height to 3 pixels
    backgroundColor: 'rgba(150, 200, 60, 0.9)', // Dragbar color (#96C83C)
    cursor: 'move',
    zIndex: 1, // Keep it behind the volume text
  });
  controlSquare.appendChild(dragBar);

  // Optimized drag functionality
  let isDragging = false, offsetX = 0, offsetY = 0;
  dragBar.addEventListener('mousedown', (e) => {
    isDragging = true;
    offsetX = e.clientX - controlSquare.getBoundingClientRect().left;
    offsetY = e.clientY - controlSquare.getBoundingClientRect().top;
    document.body.style.userSelect = 'none'; // Disable text selection while dragging
  });

  document.addEventListener('mousemove', (e) => {
    if (isDragging) {
      requestAnimationFrame(() => {
        const x = Math.max(0, Math.min(e.clientX - offsetX, window.innerWidth - controlSquare.offsetWidth));
        const y = Math.max(0, Math.min(e.clientY - offsetY, window.innerHeight - controlSquare.offsetHeight));
        controlSquare.style.left = `${x}px`;
        controlSquare.style.top = `${y}px`;
      });
    }
  });

  document.addEventListener('mouseup', () => {
    if (isDragging) {
      const x = controlSquare.getBoundingClientRect().left;
      const y = controlSquare.getBoundingClientRect().top;
      savePosition(x, y); // Save position on release
      isDragging = false;
      document.body.style.userSelect = ''; // Re-enable text selection
    }
  });

  // Handle collapse/expand on double-click
  let isCollapsed = false;
  controlSquare.addEventListener('dblclick', (e) => {
    if (e.target !== dragBar) {
      if (!isCollapsed) {
        controlSquare.style.width = '5mm'; // Collapsed size, 7mm larger than original
        controlSquare.style.height = '5mm'; // Collapsed size, 7mm larger than original
        controlSquare.style.backgroundColor = 'rgba(150, 200, 60, 0.9)'; // Match dragbar color (#96C83C)
        volumeText.style.display = 'none'; // Hide volume text
        dragBar.style.display = 'none'; // Hide drag bar
      } else {
        controlSquare.style.width = '8.5mm'; // Restore original size
        controlSquare.style.height = '8.5mm'; // Restore original size
        controlSquare.style.backgroundColor = 'rgba(23, 23, 23, 0.8)'; // Restore transparency
        volumeText.style.display = 'block'; // Show volume text
        dragBar.style.display = 'block'; // Show drag bar
      }
      isCollapsed = !isCollapsed;
    }
  });

  // Click to uncollapse
  controlSquare.addEventListener('click', () => {
    if (isCollapsed) {
      controlSquare.style.width = '8.5mm'; // Restore original size
      controlSquare.style.height = '8.5mm'; // Restore original size
      controlSquare.style.backgroundColor = 'rgba(23, 23, 23, 0.8)'; // Restore transparency
      volumeText.style.display = 'block'; // Show volume text
      dragBar.style.display = 'block'; // Show drag bar
      isCollapsed = false;
    }
  });

  // Add the control square to the page
  document.body.appendChild(controlSquare);
}

// Wait for video element to load using MutationObserver
let observer = new MutationObserver((mutations) => {
  for (let mutation of mutations) {
    if (mutation.addedNodes.length) {
      let element = document.querySelector('.GifPreview_isActive');
      if (element) {
        const video = document.querySelector('video');
        if (video) {
          createVolumeControlSquare(video);
          observer.disconnect(); // Stop observing once square is added
          return;
        }
      }
    }
  }
});

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

// Register Tampermonkey menu commands

// Reset button to reset volume to 1%
GM_registerMenuCommand('Reset Volume to 1%', () => {
  const videos = document.querySelectorAll('video');
  videos.forEach((video) => {
    video.volume = 0.01;
    localStorage.setItem('videoVolume', 0.01);
  });
  alert('Volume has been reset to 1%.');
});

// Button to set volume step size
GM_registerMenuCommand('Set Volume Step Size', () => {
  let step = prompt(`Current step size is ${volumeStep * 100}%. Enter new step size as a percentage:`, volumeStep * 100);
  if (step !== null) {
    step = parseFloat(step) / 100;
    if (!isNaN(step) && step > 0 && step <= 1) {
      volumeStep = step;
      localStorage.setItem('volumeStep', volumeStep);
      alert(`Volume step size set to ${volumeStep * 100}%`);
    } else {
      alert('Invalid input. Please enter a number between 0.01 and 100.');
    }
  }
});