Greasy Fork

Greasy Fork is available in English.

Firefox 播放器

采用非侵入式UI注入,精准保留原始布局。

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Firefox 播放器
// @namespace    http://tampermonkey.net/
// @version      1.21
// @description  采用非侵入式UI注入,精准保留原始布局。
// @author       Xion.Ai
// @match        *://*/*
// @grant        none
// @run-at       document-start
// ==/UserScript==

(function() {
    'use strict';

    const hideNativeControlsStyle = document.createElement('style');
    hideNativeControlsStyle.id = '__mp_hide_native_controls';
    hideNativeControlsStyle.textContent = `
        video[controls]:not([data-__mp_attached]),
        audio[controls]:not([data-__mp_attached]) {
            background-color: #000; /* Prevent white flash before player loads */
            visibility: hidden !important;
        }
    `;
    (document.head || document.documentElement).appendChild(hideNativeControlsStyle);

    // Re-add the centering logic for local file playback.
    if (window.location.protocol === 'file:') {
        document.addEventListener('DOMContentLoaded', () => {
            if (document.body && document.body.childElementCount === 1 && document.body.firstElementChild && (document.body.firstElementChild.tagName === 'VIDEO' || document.body.firstElementChild.tagName === 'AUDIO')) {
                const styleId = '__mp_center_style';
                if (document.getElementById(styleId)) return;
                const centerCss = `
                    html, body {
                        height: 100%;
                        margin: 0;
                    }
                    body {
                        display: grid;
                        place-items: center;
                        background-color: #000;
                    }
                `;
                const styleNode = document.createElement('style');
                styleNode.id = styleId;
                styleNode.textContent = centerCss;
                document.head.appendChild(styleNode);
            }
        });
    }

    const css = `
/* Wrapper for audio elements - from v1.1 */
.__mp_wrapper {
    position: relative;
    display: inline-block;
    vertical-align: bottom;
}
.__mp_wrapper > audio {
    display: block;
    width: 100%;
}
.__mp_wrapper.__mp_audio_wrapper {
    height: 44px;
    background-color: transparent;
}

/* Base UI for both video and audio */
.__mp_ui {
  position: absolute;
  height: 44px;
  display: flex !important;
  visibility: visible !important;
  align-items: center;
  gap: 8px;
  pointer-events: auto;
  opacity: 0;
  transition: opacity 160ms ease, transform 160ms ease;
  transform: translateY(6px);
  z-index: 2147483647;
}

.__mp_ui.__show {
  opacity: 1 !important;
  transform: translateY(0);
}

/* Common button/progress/time styles */
.__mp_btn {
  width: 36px; height: 36px; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  background: rgba(255,255,255,0.08); backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);
  border: 1px solid rgba(255,255,255,0.06); color: rgba(255,255,255,0.95);
  cursor: pointer; user-select: none; box-shadow: 0 2px 8px rgba(0,0,0,0.25); flex-shrink: 0;
}

.__mp_btn:hover { transform: scale(1.06); transition: transform 120ms; }

.__mp_progress_container {
  position: relative;
  flex: 1 1 auto;
  margin: 0 8px;
  height: 20px;
  display: flex;
  align-items: center;
  cursor: pointer;
}

.__mp_progress {
  position: relative;
  -webkit-appearance: none; appearance: none; height: 4px; border-radius: 999px;
  background: linear-gradient(to right, #fff var(--play-percent, 0%), #aaa var(--play-percent, 0%), #aaa var(--buffer-percent, 0%), rgba(255,255,255,0.18) var(--buffer-percent, 0%));
  outline: none;
  width: 100%;
  margin: 0;
}

.__mp_progress::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none; width: 10px; height: 10px; border-radius: 50%;
  background: transparent; border: none; box-shadow: none;
  cursor: pointer; transition: background .15s ease;
}

.__mp_progress:hover::-webkit-slider-thumb {
  background-color: rgba(255, 255, 255, 0.6) !important;
  background-image: none !important;
  box-shadow: 0 1px 4px rgba(0,0,0,0.4);
}

.__mp_progress::-moz-range-thumb {
  width: 10px; height: 10px; border-radius: 50%;
  background: transparent; border: none; box-shadow: none;
  cursor: pointer; transition: background .15s ease;
}

.__mp_progress:hover::-moz-range-thumb {
  background-color: rgba(255, 255, 255, 0.6) !important;
  background-image: none !important;
  box-shadow: 0 1px 4px rgba(0,0,0,0.4);
}

.__mp_time {
  color: rgba(255,255,255,0.95);
  font-size: 12px;
  font-family: monospace;
  user-select: none;
  flex-shrink: 0;
}

video, audio { -webkit-user-select: none; -moz-user-select: none; user-select: none; }
`;
    const styleNode = document.createElement('style');
    styleNode.textContent = css;
    document.head.appendChild(styleNode);

    const clamp = (v, a, b) => Math.min(b, Math.max(a, v));

    const formatTime = (seconds) => {
        if (isNaN(seconds) || seconds < 0) return '00:00';
        const date = new Date(seconds * 1000);
        const hh = date.getUTCHours();
        const mm = date.getUTCMinutes();
        const ss = date.getUTCSeconds().toString().padStart(2, '0');
        if (hh) {
            return `${hh}:${mm.toString().padStart(2, '0')}:${ss}`;
        }
        return `${mm}:${ss}`;
    };

    function attachUIToMedia(media) {
        if (!media || media.dataset.__mp_attached) return;
        media.dataset.__mp_attached = '1';

        const isAudio = media.tagName === 'AUDIO';

        const ui = document.createElement('div');
        ui.className = '__mp_ui';
        ui.innerHTML = `
            <div class="__mp_btn __mp_play" title="Play/Pause"><svg width="14" height="14" viewBox="0 0 24 24"><path d="M8 5v14l11-7L8 5z" fill="currentColor"/></svg></div>
            <div class="__mp_progress_container">
                <input class="__mp_progress" type="range" min="0" max="100" value="0" step="0.01">
            </div>
            <div class="__mp_time">00:00 / 00:00</div>
            <div class="__mp_btn __mp_mute" title="Mute/Unmute (Scroll to adjust volume)"><svg class="mp_icon_vol_svg" width="14" height="14" viewBox="0 0 24 24"><path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z" fill="currentColor"/></svg></div>
        `;

        let wrapper, originalMargin, resizeObserver;
        let updateUIPosition;


        if (isAudio) {
            // --- AUDIO PATH (v1.1 wrapper logic) ---
            const originalStyle = window.getComputedStyle(media);
            originalMargin = originalStyle.margin;

            wrapper = document.createElement('div');
            wrapper.className = '__mp_wrapper __mp_audio_wrapper';

            wrapper.style.display = originalStyle.display === 'inline' ? 'inline-block' : originalStyle.display;
            if (media.offsetWidth > 0) {
                wrapper.style.width = media.offsetWidth + 'px';
            } else {
                wrapper.style.width = '300px';
            }
            wrapper.style.margin = originalMargin;
            media.style.margin = '0';

            media.parentElement.insertBefore(wrapper, media);
            wrapper.appendChild(media);
            wrapper.appendChild(ui);
            ui.classList.add('__show');
        } else {
            // --- VIDEO PATH (v1.01 absolute positioning logic) ---
            document.body.appendChild(ui);

            updateUIPosition = () => {
                const videoRect = media.getBoundingClientRect();
                ui.style.left = `${videoRect.left + window.scrollX + 8}px`;
                ui.style.top = `${videoRect.top + window.scrollY + videoRect.height - 44 - 8}px`;
                ui.style.width = `${videoRect.width - 16}px`;
            };

            updateUIPosition();
            resizeObserver = new ResizeObserver(updateUIPosition);
            resizeObserver.observe(media);
            window.addEventListener('scroll', updateUIPosition, { passive: true, capture: true });
            window.addEventListener('resize', updateUIPosition, { passive: true });

            let hideTimeout;
            const showUI = () => { clearTimeout(hideTimeout); ui.classList.add('__show'); };
            const hideUI = () => { hideTimeout = setTimeout(() => ui.classList.remove('__show'), 1000); };

            media.addEventListener('mouseenter', showUI);
            ui.addEventListener('mouseenter', showUI);
            media.addEventListener('mouseleave', hideUI);
            ui.addEventListener('mouseleave', hideUI);
            media.addEventListener('mousemove', showUI);
            media.addEventListener('play', () => { showUI(); hideUI(); });
            media.addEventListener('pause', showUI);
        }

        media.controls = false;

        const playBtn = ui.querySelector('.__mp_play');
        const progress = ui.querySelector('.__mp_progress');
        const progressContainer = ui.querySelector('.__mp_progress_container');
        const timeDisplay = ui.querySelector('.__mp_time');

        progressContainer.addEventListener('click', e => {
            const rect = progressContainer.getBoundingClientRect();
            const clickX = e.clientX - rect.left;
            const percentage = clamp(clickX / progressContainer.offsetWidth, 0, 1);
            if (!isNaN(media.duration)) media.currentTime = media.duration * percentage;
        });

        const muteBtn = ui.querySelector('.__mp_mute');
        const volumeIcon = ui.querySelector('.mp_icon_vol_svg');

        const updatePlayIcon = () => {
            const isPaused = media.paused || media.ended;
            playBtn.querySelector('svg path').setAttribute('d', isPaused ? 'M8 5v14l11-7L8 5z' : 'M6 5h4v14H6zM14 5h4v14h-4z');
        };

        const togglePlay = (e) => {
            if (ui.contains(e.target) && e.target !== playBtn && !playBtn.contains(e.target)) return;
            media.paused || media.ended ? media.play() : media.pause();
        };

        (isAudio ? wrapper : media).addEventListener('click', togglePlay);
        playBtn.addEventListener('click', (e) => { e.stopPropagation(); togglePlay(e); });

        if (!isAudio) {
            media.addEventListener('dblclick', e => {
                if (ui.contains(e.target)) return;
                if (!document.fullscreenElement) media.requestFullscreen().catch(err => console.error(`[MP] Fullscreen Error: ${err.message}`));
                else document.exitFullscreen();
            });
        }

        const updateVolumeUI = () => {
            const vol = media.volume, muted = media.muted;
            if (muted || vol === 0) volumeIcon.innerHTML = `<path d="M16.5 12c0-1.77-.77-3.37-2-4.47V16.47c1.23-1.1 2-2.7 2-4.47zM5 9v6h4l5 4V5L9 9H5z" fill="currentColor"/>`;
            else if (vol < 0.5) volumeIcon.innerHTML = `<path d="M5 9v6h4l5 4V5L9 9H5z" fill="currentColor"/>`;
            else volumeIcon.innerHTML = `<path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z" fill="currentColor"/>`;
        };

        muteBtn.addEventListener('click', e => { e.stopPropagation(); media.muted = !media.muted; });
        muteBtn.addEventListener('wheel', e => {
            e.preventDefault(); e.stopPropagation();
            const delta = Math.sign(e.deltaY);
            media.volume = clamp(media.volume - delta * 0.05, 0, 1);
            if (media.volume > 0) media.muted = false;
        }, { passive: false });

        let rafId;
        const tickProgress = () => {
            if (!media.isConnected) { cancelAnimationFrame(rafId); return; }
            if (!isNaN(media.duration)) {
                const val = (media.currentTime / media.duration) * 100;
                progress.value = clamp(val, 0, 100).toString();
                progress.style.setProperty('--play-percent', `${progress.value}%`);
                timeDisplay.textContent = `${formatTime(media.currentTime)} / ${formatTime(media.duration)}`;
            }
            rafId = requestAnimationFrame(tickProgress);
        };

        const updateBufferProgress = () => {
            if (!media.buffered || media.buffered.length === 0 || isNaN(media.duration)) return;
            const bufferEnd = media.buffered.end(media.buffered.length - 1);
            const bufferPercent = (bufferEnd / media.duration) * 100;
            progress.style.setProperty('--buffer-percent', `${clamp(bufferPercent, 0, 100)}%`);
        };

        media.addEventListener('play', updatePlayIcon);
        media.addEventListener('pause', updatePlayIcon);
        media.addEventListener('ended', updatePlayIcon);
        media.addEventListener('volumechange', updateVolumeUI);
        media.addEventListener('progress', updateBufferProgress);

        const onMetadataLoaded = () => {
            updateVolumeUI();
            updateBufferProgress();
            if (!isNaN(media.duration)) timeDisplay.textContent = `${formatTime(media.currentTime)} / ${formatTime(media.duration)}`;
            if (!rafId) rafId = requestAnimationFrame(tickProgress);
        };

        media.addEventListener('loadedmetadata', onMetadataLoaded);
        if (media.readyState >= 1) onMetadataLoaded();

        progress.addEventListener('input', e => { if (!isNaN(media.duration)) media.currentTime = clamp(media.duration * (parseFloat(e.target.value) / 100), 0, media.duration); });

        const cleanup = () => {
            if (rafId) cancelAnimationFrame(rafId);
            if (resizeObserver) resizeObserver.disconnect();
            
            if (isAudio) {
                if (wrapper && wrapper.parentElement) {
                    wrapper.parentElement.insertBefore(media, wrapper);
                    wrapper.remove();
                }
                media.style.margin = originalMargin;
            } else {
                window.removeEventListener('scroll', updateUIPosition, true);
                window.removeEventListener('resize', updateUIPosition);
                if (ui.parentElement) ui.remove();
            }
            
            media.controls = true;
            delete media.dataset.__mp_attached;
            if (attachedMedia.has(media)) attachedMedia.delete(media);
        };

        const intersectionObserver = new IntersectionObserver((entries) => {
            const entry = entries[0];
            ui.style.visibility = entry.isIntersecting ? 'visible' : 'hidden';
        });
        intersectionObserver.observe(media);

        const disconnectObserver = new MutationObserver(() => {
            if (!media.isConnected) {
                cleanup();
                disconnectObserver.disconnect();
                intersectionObserver.disconnect();
            }
        });
        if (media.parentElement) {
            disconnectObserver.observe(media.parentElement, { childList: true, subtree: true });
        }

        updatePlayIcon();
        updateVolumeUI();

        return () => {
            cleanup();
            disconnectObserver.disconnect();
            intersectionObserver.disconnect();
        };
    }

    const attachedMedia = new WeakMap();

    function processMedia(media) {
        if (media.hasAttribute('controls')) {
            if (!attachedMedia.has(media)) {
                try {
                    const cleanup = attachUIToMedia(media);
                    if (cleanup) attachedMedia.set(media, cleanup);
                } catch (e) {
                    console.error('[MP] Error attaching to media:', e);
                }
            }
        } else {
            if (attachedMedia.has(media)) {
                const cleanup = attachedMedia.get(media);
                if (cleanup) cleanup();
                attachedMedia.delete(media);
            }
        }
    }

    function scanAndProcess() {
        document.querySelectorAll('video[controls]:not([data-__mp_attached]), audio[controls]:not([data-__mp_attached])').forEach(processMedia);
    }

    // Initial scan
    scanAndProcess();

    // Global observer for discovering new media
    const globalObserver = new MutationObserver((mutations) => {
        mutations.forEach(mutation => {
            if (mutation.addedNodes) {
                mutation.addedNodes.forEach(node => {
                    if (node.nodeType === 1) {
                        if (node.matches('video[controls], audio[controls]')) {
                            processMedia(node);
                        }
                        node.querySelectorAll('video[controls], audio[controls]').forEach(processMedia);
                    }
                });
            }
        });
    });
    globalObserver.observe(document.documentElement, {
        childList: true,
        subtree: true,
    });

})();