Incestflix - UI & Playback Tweaks

Improved site layout, windowed fullscreen support, new video playback options

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

Bạn sẽ cần cài đặt một tiện ích mở rộng như Tampermonkey hoặc Violentmonkey để cài đặt kịch bản này.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(Tôi đã có Trình quản lý tập lệnh người dùng, hãy cài đặt nó!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name Incestflix - UI & Playback Tweaks
// @namespace shobu-san/scripts
// @version 0.0.5
// @description Improved site layout, windowed fullscreen support, new video playback options
// @author shobu-san
// @license MIT
// @grant GM_addStyle
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// @run-at document-start
// @match *://*.incestflix.org/*
// @match *://*.incestflix.com/*
// ==/UserScript==

// Fullscreen CSS for windowed fullscreen
const windowedFullscreenCss = `
    /* Expand video player for windowed fullscreen */
    #incflix-bodywrap {
        max-width: none;
        text-align: center;
    }
    #incflix-player {
        max-height: 100%;
        height: 100%;
    }
    #incflix-videowrap {
        background: #0f0f0f;
        padding: 0.5em 0em;
        margin: 0.5em 0;
    }
    ::-webkit-scrollbar {
        display: none;
    }

    /* Fix recommended videos/tags */
    #photos .img-overflow {
        margin: 1px;
        width: 445px !important;
        height: 250px !important;
    }
    #incflix-indexwrap {
        max-width: none;
        text-align: center;
    }
    #tables {
        text-align: left;
        padding: 0em 0.5em;
        max-width: none;
    }
    #videotags {
        width: 100%;
        margin-top: 0em;
        text-align: left;
    }
`;

// Menu CSS for latest/random/popular video pages
const menuCss = `
    /* Expand search results to fit screen */
    #incflix-bodywrap {
        max-width: none;
        text-align: center;
    }

    /* Fix recommended videos/tags */
    #photos .img-overflow {
        margin: 1px;
        width: 445px !important;
        height: 250px !important;
    }
    #incflix-indexwrap {
        max-width: none;
        text-align: center;
    }
    #tables {
        text-align: left;
        padding: 0em 0.5em;
        max-width: none;
    }
    #videotags {
        width: 100%;
        margin-top: 0em;
        text-align: left;
    }
    #incflix-header{
        position:absolute;
        top:-75px;
    }
    [class=headerlogo]{
        position:absolute;
        top:70px;
        left:0px;
        transform: scale(0.5);
    }
    #tagline{
        display:none!important;
    }
    #videotags {
        width: 100%;
        margin-top: 0em;
        text-align: left;
    }
`;

let styleNode = null; // Used to load CSS rules into the page

let videoPlayer = null; // Global variable to hold the video player element
let pauseTimeout = 200 // Time in milliseconds to wait before a pause event is actioned

// Menu options
const menuOptions = {
    windowedFullscreen: { state: true, label: 'Enable Windowed Fullscreen' },
    fullscreenOnPlay: { state: true, label: 'Fullscreen on Play' },
    fullscreenOnLoad: { state: false, label: 'Fullscreen on Load' },
    autoplayOnLoad: { state: false, label: 'Autoplay on Load' },
    mutedOnLoad: { state: false, label: 'Muted on Load' },
    reloadOnStalled: { state: true, label: 'Auto-reload Stalled Video - BETA' },
    enableLogging: { state: false, label: 'DEBUG: Enable Logging' },
};

// Function for logging if debug mode is enabled
function logDebug(message, ...optionalParams) {
    if (getState('enableLogging')) {
        console.log(`DEBUG: ${message}`, ...optionalParams);
    }
}

/**
 * Get the saved state of an option or the default state if not saved.
 * @param {string} option - The option key.
 * @returns {boolean} - The state of the option.
 */
function getState(option) {
    const savedMenu = GM_getValue('menuOptions', menuOptions);
    return savedMenu.hasOwnProperty(option) ? savedMenu[option].state : menuOptions[option].state;
}

/**
 * Toggle the state of a menu option and update the menu display.
 * @param {string} option - The option key to toggle.
 */
function toggleOption(option) {
    const currentState = getState(option);
    menuOptions[option].state = !currentState;
    GM_setValue('menuOptions', menuOptions);
    displayMenu();
}

/**
 * Build the Tampermonkey menu based on the current state of the options.
 */
function displayMenu() {
    Object.entries(menuOptions).forEach(([key, option]) => {
        const currentState = getState(key);
        GM_registerMenuCommand(`[${currentState ? '✔️' : '❌'}]: ${option.label}`, () => toggleOption(key), { id: key, autoClose: false });
    });
}

/**
 * Injects the provided CSS into the document.
 * @param {string} css - The CSS to be injected.
 */
function injectStyle(css) {
    if (!styleNode) {
        logDebug('Injecting CSS into the document');
        styleNode = document.createElement('style');
        styleNode.textContent = css;
        (document.head || document.documentElement).appendChild(styleNode);
    }
}

/**
 * Removes the injected CSS from the document.
 */
function removeStyle() {
    if (styleNode) {
        logDebug('Removing injected CSS');
        styleNode.remove();
        styleNode = null;
    }
}

// Function to check if the URL contains '/watch'
function applyMenuCss() {
    const url = window.location.href;
    logDebug('Checking if URL contains /watch');
    if (!url.includes('/watch')) {
        logDebug('URL does not contain /watch, applying menu CSS');
        injectStyle(menuCss); // Enable the CSS immediately if URL doesn't contain '/watch'
    }
}

/**
 * Enable fullscreen mode by applying styles and scrolling the video player into view.
 * If windowed fullscreen is disabled, it will trigger the browser's normal fullscreen.
 * @param {HTMLElement} videoPlayer - The video player element.
 */
function enableFullscreen() {
    const windowedFullscreen = getState('windowedFullscreen');
    logDebug(`Enabling fullscreen. Windowed Fullscreen: ${windowedFullscreen}`);

    if (windowedFullscreen) {
        injectStyle(windowedFullscreenCss); // Enable windowed fullscreen
        videoPlayer.scrollIntoView({ behavior: 'instant' });
    } else {
        // Trigger normal fullscreen mode
        if (videoPlayer && videoPlayer.requestFullscreen) {
            logDebug('Requesting browser fullscreen');
            videoPlayer.requestFullscreen();
        }
    }
}

/**
 * Disable fullscreen mode by removing styles or exiting normal fullscreen.
 */
function disableFullscreen() {
    const windowedFullscreen = getState('windowedFullscreen');
    logDebug('Disabling fullscreen');

    removeStyle(); // Disable windowed fullscreen

    if (windowedFullscreen) {
        window.scrollTo(0, 0); // Scroll back to the top of the page
    } else if (document.fullscreenElement) {
        // Exit normal fullscreen if active
        logDebug('Exiting browser fullscreen');
        document.exitFullscreen();
    }
}

// Function to check if the video player exists
function getVideoPlayer() {
    videoPlayer = document.querySelector('#incflix-player');
    logDebug('Checking if video player exists:', videoPlayer ? 'Found' : 'Not Found');
}

async function getStatus(singleVideo) {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve(singleVideo);
        }, pauseTimeout);
    });
}

// Function to handle fullscreen behavior on video play
function handlePlay() {
    if (!videoPlayer) return;

    const fullscreenOnPlay = getState('fullscreenOnPlay');
    logDebug(`Video started playing. Fullscreen on Play: ${fullscreenOnPlay}`);

    if (fullscreenOnPlay) {
        enableFullscreen(videoPlayer);
    }
}

// Function to handle disabling fullscreen on video pause
function handlePause() {
    if (!videoPlayer) return;

    const fullscreenOnPlay = getState('fullscreenOnPlay');

    getStatus(videoPlayer).then(video => {
        if(video.paused){
            logDebug(`Video paused. Fullscreen on Play: ${fullscreenOnPlay}`);
            if (fullscreenOnPlay) {
                disableFullscreen(); // Disable fullscreen when the video is actually paused
            }
        }else{
            logDebug(`Video is not paused after ${pauseTimeout}ms, skipping pause event.`);
        }})
}

// Function to handle stalled event by reloading the video at the same timestamp
function handleStalled() {
    if (!videoPlayer) return;
    const currentTime = videoPlayer.currentTime; // Capture the current timestamp
    logDebug(`Video stalled at ${currentTime}s. Reloading video source...`);

    const src = videoPlayer.currentSrc; // Capture the current video source
    videoPlayer.src = ''; // Clear the source to force reload
    videoPlayer.load(); // Reset the video element
    videoPlayer.src = src; // Reassign the source
    videoPlayer.currentTime = currentTime; // Seek to the same timestamp
    videoPlayer.play(); // Start playing from the same position

    logDebug('Video reloaded and resumed at the same timestamp.');
}

// Function to handle video settings on page load
function initializeVideoPlayerSettings() {
    logDebug('Initializing video player settings');

    if (!videoPlayer) return;

    // Fullscreen on Load
    if (getState('fullscreenOnLoad')) {
        logDebug('Fullscreen on load is enabled');
        enableFullscreen(videoPlayer);
    }

    // Autoplay on Load
    if (getState('autoplayOnLoad')) {
        logDebug('Autoplay on load is enabled');
        videoPlayer.play();
    }

    // Mute on Load
    if (getState('mutedOnLoad')) {
        logDebug('Muted on load is enabled');
        videoPlayer.muted = true;
    }
}

// Attach event listeners to video player
function attachVideoPlayerListeners() {
    if (!videoPlayer) return;

    logDebug('Attaching event listeners to video player');
    videoPlayer.addEventListener('play', handlePlay, { passive: true });
    videoPlayer.addEventListener('pause', handlePause, { passive: true });
    videoPlayer.addEventListener('stalled', handleStalled, { passive: true });
}

// Initialize the script
function init() {
    getVideoPlayer();
    initializeVideoPlayerSettings();
    attachVideoPlayerListeners();
}

logDebug('Script initialized');
displayMenu();
applyMenuCss();

// Run the initialization after the page is fully loaded
window.addEventListener('load', init, {passive: true});