nicovideo-player-expander

ニコニコ動画のプレイヤーを2種類の方法(「シアターモード」または「ブラウザ内最大化」)で拡大します。プレイヤー右下のアイコンでこれらの機能を切り替えられます。それぞれtキー、bキーでも(ブラウザ内最大化解除はescキーでも)切り替えられます。「nicovideo-next-video-canceler」「nicovideo-autoplay-canceler」は別のスクリプトです。

目前為 2025-01-20 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name        nicovideo-player-expander
// @namespace   https://github.com/dnek
// @version     3.4
// @author      dnek
// @description ニコニコ動画のプレイヤーを2種類の方法(「シアターモード」または「ブラウザ内最大化」)で拡大します。プレイヤー右下のアイコンでこれらの機能を切り替えられます。それぞれtキー、bキーでも(ブラウザ内最大化解除はescキーでも)切り替えられます。「nicovideo-next-video-canceler」「nicovideo-autoplay-canceler」は別のスクリプトです。
// @description:ja    ニコニコ動画のプレイヤーを2種類の方法(「シアターモード」または「ブラウザ内最大化」)で拡大します。プレイヤー右下のアイコンでこれらの機能を切り替えられます。それぞれtキー、bキーでも(ブラウザ内最大化解除はescキーでも)切り替えられます。「nicovideo-next-video-canceler」「nicovideo-autoplay-canceler」は別のスクリプトです。
// @homepageURL https://github.com/dnek/nicovideo-player-expander
// @match       https://www.nicovideo.jp/watch/*
// @grant       GM_addStyle
// @grant       GM_getValue
// @grant       GM_setValue
// @license     MIT license
// ==/UserScript==

(async () => {
    'use strict';

    const THEATER_MODE_BUTTON_CONTAINER_ID = 'npebTheaterModeButtonContainer';
    const BROWSER_FULL_BUTTON_CONTAINER_ID = 'npebBrowserFullButtonContainer';

    let _isTheaterMode = await GM_getValue('isTheaterMode', false);
    const getIsTheaterMode = () => _isTheaterMode;
    const setIsTheaterMode = (newValue) => {
        _isTheaterMode = newValue;
        GM_setValue('isTheaterMode', newValue);
    };

    const theaterModeStyleEl = GM_addStyle(`
#root > div.min-w_\\[max-content\\] {
    min-width: auto;
}
div[aria-label="nicovideo-content"] > section.grid-template-areas_\\[_\\"player_sidebar\\"_\\"meta_sidebar\\"_\\"bottom_sidebar\\"_\\"\\._sidebar\\"_\\] {
    --watch-player-expandable-width-by-browser: calc(
        100vw
        - var(--watch-layout-gap-width) * 2
        - var(--scrollbar-width)
    );
    --watch-player-expandable-height-by-browser: calc(
        100vh
        - var(--common-header-height)
        - var(--web-header-height)
        - var(--watch-controller-height)
        - var(--watch-actionbar-height)
        - var(--watch-player-bottom-margin-height)
    );
    --watch-player-expandable-width: calc(
        min(
            var(--watch-player-expandable-width-by-browser),
            var(--watch-player-expandable-height-by-browser) * (16 / 9)
        )
    );
    grid-template-columns: var(--watch-player-expandable-width);
    grid-template-areas: "player" "meta" "bottom" "sidebar";
}
div.grid-area_\\[sidebar\\] > div[data-nvpc-part="floating"] > section {
    position: fixed;
    top: calc(var(--sizes-common-header-in-view-height) + var(--sizes-web-header-height) + var(--spacing-x3));
    width: var(--sizes-watch-sidebar-width);
}
div:has(> div > button[aria-label="コメント投稿ボタン"]) {
    min-width: auto !important;
}
@media (max-width: 720px), (max-height: 720px) {
    div:has(> button[aria-label="設定"]) {
        gap: 0;
    }
}
@media (max-width: 650px), (max-height: 680px) {
    div.d_flex:has(> button[aria-label$=" 秒戻る"]) {
        gap: 0;
        > div.d_flex {
            flex-flow: column;
            > span.mx_x0_5 {
                display: none;
            }
        }
    }
}
@media (max-width: 580px), (max-height: 640px) {
    svg.fill_icon\\.watchControllerBase {
        padding-inline: 4px;
    }
}
`);
    theaterModeStyleEl.disabled = getIsTheaterMode();

    let isBrowserFull = false;

    const browserFullStyleEl = GM_addStyle(`
:not(
    :has([data-styling-id=":r2:"]),
    [data-styling-id=":r2:"], [data-styling-id=":r2:"] *,
    :has([data-nvpc-scope="watch-floating-panel"]),
    [data-nvpc-scope="watch-floating-panel"],
    [data-nvpc-scope="watch-floating-panel"] *,
    :is([data-scope="toast"]), :is([data-scope="toast"]) *,
    :is(body > [data-part="positioner"]), :is(body > [data-part="positioner"]) *,
    :is(body > [data-part="backdrop"]), :is(body > [data-part="backdrop"]) *,
    body
) {
    display: none;
}
body {
    overflow-y: hidden !important;
}
#root > div.min-w_\\[max-content\\] {
    min-width: auto;
}
div[aria-label="nicovideo-content"] {
    padding-block: 0;
}
div.grid-area_\\[player\\] div.bdr_m {
    border-radius: 0;
}
div[aria-label="nicovideo-content"] > section.grid-template-areas_\\[_\\"player_sidebar\\"_\\"meta_sidebar\\"_\\"bottom_sidebar\\"_\\"\\._sidebar\\"_\\] {
    grid-template-columns: 100vw;
    grid-template-areas: "player" "meta" "bottom" "sidebar";
    padding-inline: 0;
    padding-bottom: 0;
    row-gap: 0;
}
div[data-styling-id=":r3:"] {
    aspect-ratio: auto;
    height: calc(
        100vh
        - var(--watch-controller-height)
        - var(--watch-player-actionbar-gap-height)
        - var(--watch-actionbar-height)
    );
}
div.grid-area_\\[sidebar\\] > div[data-nvpc-part="floating"] > section {
    position: fixed;
    top: var(--spacing-x3);
    width: var(--sizes-watch-sidebar-width);
}
div:has(> div > button[aria-label="コメント投稿ボタン"]) {
    min-width: auto !important;
}
@media (max-width: 640px) {
    div.d_flex:has(> button[aria-label$=" 秒戻る"]) {
        gap: 0;
        > div.d_flex {
            flex-flow: column;
            > span.mx_x0_5 {
                display: none;
            }
        }
    }
}
@media (max-width: 550px) {
    div:has(> button[aria-label="設定"]) {
        gap: 0;
    }
}
#${THEATER_MODE_BUTTON_CONTAINER_ID}, button[aria-label="全画面表示する"] {
    display: none;
}
`);
    browserFullStyleEl.disabled = true;

    const baseStyleEl = GM_addStyle(`
.npeb_container {
    display: grid;
    > .npeb_tooltip_container {
        position: relative;
        > .npeb_tooltip {
            visibility: hidden;
            min-width: max-content;
            background-color: var(--colors-tooltip-background);
            color: var(--colors-tooltip-text-on-background);
            font-size: var(--font-sizes-s);
            border-radius: var(--radii-s);
            padding: var(--spacing-base);
            position: absolute;
            top: 0;
            left: 50%;
            transform: translate(-50%, -135%);
            opacity: 0;
            transition: opacity .2s .2s;
        }
    }
}
.npeb_container:is(:hover,[data-hover]) .npeb_tooltip {
    visibility: visible;
    opacity: 1;
}
.npeb_icon {
    stroke: var(--colors-icon-watch-controller-base);
}
.npeb_icon:is(:hover,[data-hover]) {
    stroke: var(--colors-icon-watch-controller-hover);
}
`);
    baseStyleEl.disabled = true;

    const videoFullscreenStyleEl = GM_addStyle(`
.npeb_container {
    display: none;
}
`);

    let refreshButtonsAndStyles = () => { };
    let switchTheaterMode = () => { };
    let switchBrowserFull = () => { };

    const initButtons = () => {
        const fullScreenButtonEl = document.querySelector('button[aria-label="全画面表示する"]');
        if (fullScreenButtonEl === null) {
            return;
        }

        const initButton = (containerId, svgElToOn, svgElToOff, captionToOn, captionToOff) => {
            const buttonContainerEl = document.createElement('div');
            buttonContainerEl.setAttribute('id', containerId);
            buttonContainerEl.classList.add('npeb_container');

            const tooltipContainerEl = document.createElement('div');
            tooltipContainerEl.classList.add('npeb_tooltip_container');

            const tooltipEl = document.createElement('span');
            tooltipEl.classList.add('npeb_tooltip');
            tooltipContainerEl.appendChild(tooltipEl);

            buttonContainerEl.appendChild(tooltipContainerEl);

            const buttonEl = document.createElement('button');
            buttonEl.classList.add('cursor_pointer');
            buttonEl.setAttribute('tabindex', '0');
            buttonEl.setAttribute('type', 'button');
            buttonContainerEl.appendChild(buttonEl);

            fullScreenButtonEl.before(buttonContainerEl);

            const setButtonIsOn = (isOn) => {
                while (buttonEl.firstChild) {
                    buttonEl.removeChild(buttonEl.firstChild);
                }
                buttonEl.appendChild(isOn ? svgElToOff : svgElToOn);
                buttonEl.setAttribute('aria-label', isOn ? captionToOff : captionToOn);
                tooltipEl.innerText = isOn ? captionToOff : captionToOn;
            };

            return [buttonEl, setButtonIsOn];
        };

        const SVG_NS = 'http://www.w3.org/2000/svg';

        const createButtonSvgEl = () => {
            const svgEl = document.createElementNS(SVG_NS, 'svg');
            svgEl.setAttribute('width', '24');
            svgEl.setAttribute('height', '24');
            svgEl.setAttribute('viewBox', '0 0 24 24');
            svgEl.classList.add('w_auto', 'h_x5', 'p_base', 'fill_icon.watchControllerBase', 'hover:fill_icon.watchControllerHover', 'npeb_icon');
            return svgEl;
        };

        const createTheaterModeRectEl = (x, y, width, height) => {
            const rectEl = document.createElementNS(SVG_NS, 'rect');
            rectEl.setAttribute('x', x);
            rectEl.setAttribute('y', y);
            rectEl.setAttribute('width', width);
            rectEl.setAttribute('height', height);
            rectEl.setAttribute('stroke-width', '2');
            rectEl.setAttribute('stroke-linejoin', 'round');
            return rectEl;
        }

        const createbrowserFullRectEl = () => {
            const rectEl = document.createElementNS(SVG_NS, 'rect');
            rectEl.setAttribute('x', '1.5');
            rectEl.setAttribute('y', '2.5');
            rectEl.setAttribute('width', '21');
            rectEl.setAttribute('height', '19');
            rectEl.setAttribute('fill', 'transparent');
            rectEl.setAttribute('stroke-width', '2');
            rectEl.setAttribute('stroke-linejoin', 'round');
            return rectEl;
        };

        const createPolylineEl = (points) => {
            const polylineEl = document.createElementNS(SVG_NS, 'polyline');
            polylineEl.setAttribute('points', points);
            polylineEl.setAttribute('stroke-width', '2');
            polylineEl.setAttribute('stroke-linecap', 'round');
            polylineEl.setAttribute('stroke-linejoin', 'round');
            return polylineEl;
        };

        const theaterModeToOnSvgEl = createButtonSvgEl();
        theaterModeToOnSvgEl.appendChild(createTheaterModeRectEl('4', '4', '16', '9'));
        theaterModeToOnSvgEl.appendChild(createTheaterModeRectEl('4', '18', '16', '2'));

        const theaterModeToOffSvgEl = createButtonSvgEl();
        theaterModeToOffSvgEl.appendChild(createTheaterModeRectEl('4', '4', '9', '16'));
        theaterModeToOffSvgEl.appendChild(createTheaterModeRectEl('18', '4', '2', '16'));

        const [theaterModeButtonEl, setTheaterModeButtonIsOn] = initButton(
            THEATER_MODE_BUTTON_CONTAINER_ID,
            theaterModeToOnSvgEl,
            theaterModeToOffSvgEl,
            'シアターモードにする(t)',
            'シアターモード解除(t)'
        );

        const browserFullToOnSvgEl = createButtonSvgEl();
        browserFullToOnSvgEl.appendChild(createbrowserFullRectEl());
        browserFullToOnSvgEl.appendChild(createPolylineEl('6,12 9,9'));
        browserFullToOnSvgEl.appendChild(createPolylineEl('6,12 9,15'));
        browserFullToOnSvgEl.appendChild(createPolylineEl('18,12 15,9'));
        browserFullToOnSvgEl.appendChild(createPolylineEl('18,12 15,15'));

        const browserFullToOffSvgEl = createButtonSvgEl();
        browserFullToOffSvgEl.appendChild(createbrowserFullRectEl());
        browserFullToOffSvgEl.appendChild(createPolylineEl('10,12 7,9'));
        browserFullToOffSvgEl.appendChild(createPolylineEl('10,12 7,15'));
        browserFullToOffSvgEl.appendChild(createPolylineEl('14,12 17,9'));
        browserFullToOffSvgEl.appendChild(createPolylineEl('14,12 17,15'));

        const [browserFullButtonEl, setbrowserFullButtonIsOn] = initButton(
            BROWSER_FULL_BUTTON_CONTAINER_ID,
            browserFullToOnSvgEl,
            browserFullToOffSvgEl,
            'ブラウザ内で最大化する(b)',
            'ブラウザ内最大化解除(b)'
        );

        refreshButtonsAndStyles = async () => {
            const isVideoFullscreen = document.fullscreenElement !== null;
            baseStyleEl.disabled = isVideoFullscreen;
            videoFullscreenStyleEl.disabled = !isVideoFullscreen;
            const isTheaterModeRequired = !isBrowserFull && getIsTheaterMode();
            theaterModeStyleEl.disabled = isVideoFullscreen || !isTheaterModeRequired;
            browserFullStyleEl.disabled = isVideoFullscreen || !isBrowserFull;
            setTheaterModeButtonIsOn(isTheaterModeRequired);
            setbrowserFullButtonIsOn(isBrowserFull);
        };

        refreshButtonsAndStyles();

        switchTheaterMode = async () => {
            setIsTheaterMode(!getIsTheaterMode());
            refreshButtonsAndStyles();
        };

        theaterModeButtonEl.addEventListener('click', switchTheaterMode);

        switchBrowserFull = () => {
            isBrowserFull = !isBrowserFull;
            refreshButtonsAndStyles();
        };

        browserFullButtonEl.addEventListener('click', switchBrowserFull);

        console.log('nicovideo-player-expander buttons are added.')
    };

    document.addEventListener('fullscreenchange', () => {
        refreshButtonsAndStyles();
    });

    document.addEventListener('keydown', (e) => {
        if (document.fullscreenElement !== null) {
            return;
        }

        if (e.target && ['input', 'textarea'].includes(e.target.tagName.toLowerCase())) {
            return;
        }
        if (e.isComposing || e.altKey || e.ctrlKey || e.metaKey || e.shiftKey || e.repeat) {
            return;
        }

        if (e.code === 'KeyT' && !isBrowserFull) {
            switchTheaterMode();
        }

        if (e.code === 'KeyB' || (e.code === 'Escape' && isBrowserFull)) {
            switchBrowserFull();
        }
    });

    setInterval(() => {
        if (document.getElementById(THEATER_MODE_BUTTON_CONTAINER_ID) === null) {
            initButtons();
        }
    }, 100);
})();