Greasy Fork

Crunchyroll Auto Skip Intro/Outro, Fullscreen Video & Mouse Volume Control

Automatically clicks the Skip Intro button on Crunchyroll.com when available and makes the video fullscreen

// ==UserScript==
// @name         Crunchyroll Auto Skip Intro/Outro, Fullscreen Video & Mouse Volume Control
// @namespace    https://greasyfork.org/en/users/807108-jeremy-r
// @version      5.1
// @description  Automatically clicks the Skip Intro button on Crunchyroll.com when available and makes the video fullscreen
// @author       JRem
// @match        https://*.crunchyroll.com/watch/*
// @match        https://static.crunchyroll.com/vilos-v2/web/vilos/player.html
// @grant        GM_addStyle
// @grant        GM.xmlHttpRequest
// @license MIT
// ==/UserScript==

////////////////////////
// USER CUSTOMIZATION //
////////////////////////
// 1 = Enabled / 0 = Disabled
const enableFullscreen=1;
const enableSkipIntro=1;
const enableSkipCredits=1;
const enableVolumeControl=1;
// Volume +/- percentage
const volumePercentage = 5; // Default 5, Set the percentage of volume change (+/-)
// Volume Control Disable/Bypass
const holdDisableKey = 'Alt'; // Key to temporarily disable while being pressed (e.g., 'h')
const disableKey = 'Shift'; // Key to completely disable the event listener (e.g., 'd')
// Global user settings for toast appearance and positioning
const toastSettings = {
    toastPositionX: '45', // Default='45' (LeftCenter), Range '0' to '100' on the X axis
    toastPositionY: '0', // Default='0' (Top), Range '0' to '100' on the Y axis
    fontSize: '16px', // Font size for the toast message
    fontFamily: 'Arial, sans-serif', // Font family for the toast message
    backgroundColor: '#333', // Background color for the toast
    textColor: 'white', // Text color for the toast
    padding: '10px', // Padding around the toast text
    margin: '5px', // Margin between toasts
    borderRadius: '5px', // Border radius for rounded corners
    toastDuration: 3500, // Duration to display the toast (in ms)
    fadeDuration: 300, // Duration of the fade-in/out animation (in ms)
};

// Function to show or update toast message
function showToast(message) {
    // Create a toast container if it doesn't already exist
    let toastContainer = document.getElementById('toast-container');
    if (!toastContainer) {
        toastContainer = document.createElement('div');
        toastContainer.id = 'toast-container';
        document.body.appendChild(toastContainer);

        // Add styles for the toast container
        console.log(`transform: translateX(${toastSettings.toastPositionX}%);`);
        console.log(`transform: translateY(${toastSettings.toastPositionY}%);`);
        const style = document.createElement('style');
        style.innerHTML = `
            #toast-container {
                position: fixed;
                z-index: 9999;
                ${toastSettings.position}: 10px;
                ${toastSettings.align}: 50%;
                transform: translateX(${toastSettings.toastPositionX}vw);
                max-width: 90%;
                pointer-events: none; /* Prevent interaction with toasts */
            }
            .toast {
                background-color: ${toastSettings.backgroundColor};
                color: ${toastSettings.textColor};
                padding: ${toastSettings.padding};
                margin: ${toastSettings.margin};
                border-radius: ${toastSettings.borderRadius};
                font-family: ${toastSettings.fontFamily};
                font-size: ${toastSettings.fontSize};
                box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
                opacity: 0;
                transform: translateY(${toastSettings.toastPositionY}vh);
                animation: fadeIn ${toastSettings.fadeDuration / 1000}s forwards, fadeOut 3s forwards ${toastSettings.toastDuration / 1000 - toastSettings.fadeDuration / 1000}s;
            }
            @keyframes fadeIn {
                to {
                    opacity: 1;
                    transform: translateY(0);
                }
            }
            @keyframes fadeOut {
                to {
                    opacity: 0;
                    transform: translateY(20px);
                }
            }
        `;
        document.head.appendChild(style);
    }

    // Function to update toast position (reverts to the original fixed position)
    function updateToastPosition() {
        // Restore the toast position as fixed on the screen
        toastContainer.style.left = ''; // Let it use the default position from toastSettings
        toastContainer.style.top = ''; // Let it use the default position from toastSettings
        toastContainer.style.width = ''; // Use max-width as defined
        toastContainer.style.height = ''; // Default height
    }

    // Update position on fullscreen change
    document.addEventListener('fullscreenchange', updateToastPosition);
    document.addEventListener('webkitfullscreenchange', updateToastPosition); // for Safari
    document.addEventListener('mozfullscreenchange', updateToastPosition); // for Firefox
    document.addEventListener('msfullscreenchange', updateToastPosition); // for IE/Edge

    // Update position initially (restores the default fixed positioning)
    updateToastPosition();

    // Check if there's an existing toast being displayed
    let currentToast = toastContainer.querySelector('.toast');
    if (currentToast) {
        // If a toast is already visible, update its text content
        currentToast.textContent = message;
        // Reset the animation so that the toast updates instantly
        currentToast.style.animation = 'none';
        currentToast.offsetHeight; // Trigger reflow to restart animation
        currentToast.style.animation = `fadeIn ${toastSettings.fadeDuration / 1000}s forwards, fadeOut 3s forwards ${toastSettings.toastDuration / 1000 - toastSettings.fadeDuration / 1000}s`;
    } else {
        // If no toast is visible, create a new toast
        const toast = document.createElement('div');
        toast.classList.add('toast');
        toast.textContent = message;
        toastContainer.appendChild(toast);
    }

    // Remove the toast after the duration is finished
    setTimeout(() => {
        if (currentToast) {
            toastContainer.removeChild(currentToast);
        }
    }, toastSettings.toastDuration);
}


// Fullscreen CSS Edit
if (enableFullscreen == 1) {
var css = '.video-player-wrapper { max-height: calc(100vh - 5.625rem) !important; height: calc(100vh) !important; }';
css += '.erc-header { flex: 0 0 1.55rem !important; }';
css += '.erc-header .header-content { height: 0 !important; }';
GM_addStyle(css);
showToast('Fullscreen enabled'); };

// Volume Control via mouse scroll
// Variable to track whether the volume control event listener is active
let isVolumeControlEnabled = true;
if (enableVolumeControl == 1) {
// Function to simulate the click event
function simulate(element, event) {
    const evt = new MouseEvent(event, { bubbles: true, cancelable: true });
    element.dispatchEvent(evt);
}
    // Get the current video element
    const video = document.querySelector('video');
    // Function to adjust volume
// Function to adjust volume
function adjustVolume(video, percentage) {
    if (!video) {
        console.error("Video element not found.");
        return;
    }

    // Get the current volume (between 0 and 1)
    let currentVolume = video.volume;

    // Calculate the new volume by adjusting it with the percentage
    let volumeChange = currentVolume + (percentage / 100);

    // Ensure the volume is within the valid range of 0 to 1
    if (volumeChange > 1) {
        volumeChange = 1;
    } else if (volumeChange < 0) {
        volumeChange = 0;
    }

    // Apply the new volume to the video
    video.volume = volumeChange;
    var newVol = (video.volume * 100).toFixed(2);
    showToast(`Volume: ${newVol}%`);
}

// Event listener for mouse scroll to adjust volume
let volumeScrollListener = (event) => {
    if (!isVolumeControlEnabled) return; // If volume control is disabled, do nothing

    // Check the direction of the scroll
    if (event.deltaY < 0) {
        // Scroll up (increase volume)
        adjustVolume(video, volumePercentage);
    } else if (event.deltaY > 0) {
        // Scroll down (decrease volume)
        adjustVolume(video, -volumePercentage);
    }
    // Prevent the default action to avoid scrolling the page
    event.preventDefault();
};

// Listen for the mouse wheel event over the video
if (document.getElementById("vilos")) {
    document.getElementById("vilos").addEventListener('wheel', volumeScrollListener);
}

// Listen for keypress events to control volume event listener status
document.addEventListener('keydown', (event) => {
    // Check if the user pressed the key to toggle the event listener on/off
    if (event.key === disableKey) {
        isVolumeControlEnabled = !isVolumeControlEnabled; // Toggle the enable/disable state
        const status = isVolumeControlEnabled ? 'enabled' : 'disabled';
        showToast(`Volume control ${status}`);
    }

    // Check if the user pressed the key to temporarily disable the event listener while pressed
    if (event.key === holdDisableKey) {
        isVolumeControlEnabled = false;
    }
});

// Listen for keyup event to re-enable the temporary disable when the key is released
document.addEventListener('keyup', (event) => {
    // Re-enable volume control when the hold-disable key is released
    if (event.key === holdDisableKey) {
        isVolumeControlEnabled = true;
    }
});

};

// Check for and click Skip Intro
if (enableSkipIntro == 1 || enableSkipCredits == 1) {
setInterval(function () {
// Check for skip intro button
    if (enableSkipIntro ==1 ) {
const skipIntroBtn = document.querySelector('div[data-testid="skipIntroText"]');
if (skipIntroBtn !== null && (skipIntroBtn.textContent.includes("SKIP INTRO"))) {
    simulate(skipIntroBtn, "click");
    console.log('Skip Btn Found');
    showToast('Intro Skipped');
} };

// Check for skip credits button
    if (enableSkipIntro ==1 ) {
const skipCreditsBtn = document.querySelector('div[data-testid="skipIntroText"]');
if (skipCreditsBtn !== null && (skipCreditsBtn.textContent.includes("SKIP CREDITS"))) {
    simulate(skipCreditsBtn, "click");
    console.log('Skip Btn Found');
    showToast('Credits Skipped');
}}
}, 1000)
};