Greasy Fork

Greasy Fork is available in English.

网页调用MPV播放(视频页面专用版)

在视频页面左下角添加播放/设置按钮,全屏时自动隐藏,支持油猴菜单打开设置

当前为 2026-04-18 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         网页调用MPV播放(视频页面专用版)
// @namespace    http://tampermonkey.net/
// @version      4.8
// @description  在视频页面左下角添加播放/设置按钮,全屏时自动隐藏,支持油猴菜单打开设置
// @author       DeepSeek
// @match        *://*/*
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @run-at       document-end
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    const DEBUG = true;
    function log(...args) {
        if (DEBUG) console.log('[MPV脚本]', ...args);
    }

    // 默认匹配规则(仅B站和YouTube)
    const defaultRules = [
        'https://www.bilibili.com/video/',
        'https://www.youtube.com/watch'
    ];

    let config = {
        mpvProtocol: GM_getValue('mpvProtocol', 'mpv://'),
        customArgs: GM_getValue('customArgs', ''),
        urlRules: GM_getValue('urlRules', defaultRules)
    };

    let currentPlayBtn = null;
    let currentSettingBtn = null;
    let lastUrl = '';
    let observer = null;
    let urlCheckInterval = null;
    let settingWindowOpen = false;

    // 判断当前页面是否匹配任意规则
    function isVideoPage() {
        const currentUrl = window.location.href;
        if (!config.urlRules || config.urlRules.length === 0) return false;

        return config.urlRules.some(rule => {
            if (!rule) return false;
            if (rule.startsWith('/') && rule.endsWith('/')) {
                try {
                    const regex = new RegExp(rule.slice(1, -1));
                    return regex.test(currentUrl);
                } catch(e) {
                    log('无效的正则表达式:', rule);
                    return false;
                }
            }
            return currentUrl.startsWith(rule);
        });
    }

    // 全屏状态变化处理
    function handleFullscreenChange() {
        const isFullscreen = !!(document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement || document.msFullscreenElement);
        if (currentPlayBtn && currentSettingBtn) {
            if (isFullscreen) {
                currentPlayBtn.style.display = 'none';
                currentSettingBtn.style.display = 'none';
                log('全屏模式,按钮已隐藏');
            } else {
                currentPlayBtn.style.display = 'flex';
                currentSettingBtn.style.display = 'flex';
                log('退出全屏,按钮已恢复');
            }
        }
    }

    // 注册全屏事件监听(兼容各浏览器)
    function addFullscreenListener() {
        const events = ['fullscreenchange', 'webkitfullscreenchange', 'mozfullscreenchange', 'MSFullscreenChange'];
        events.forEach(event => {
            document.addEventListener(event, handleFullscreenChange);
        });
    }

    function createPlayButton() {
        const btn = document.createElement('button');
        btn.textContent = '▶';
        btn.title = '用MPV播放当前页面视频';
        btn.style.cssText = `
            position: fixed;
            bottom: 20px;
            left: 20px;
            z-index: 999999;
            width: 48px;
            height: 48px;
            background: #a855f7;
            color: white;
            border: none;
            border-radius: 50%;
            cursor: pointer;
            font-size: 22px;
            display: flex;
            align-items: center;
            justify-content: center;
            box-shadow: 0 2px 8px rgba(0,0,0,0.3);
            transition: all 0.2s ease;
        `;
        btn.addEventListener('mouseenter', () => {
            btn.style.transform = 'scale(1.05)';
            btn.style.background = '#c084fc';
        });
        btn.addEventListener('mouseleave', () => {
            btn.style.transform = 'scale(1)';
            btn.style.background = '#a855f7';
        });
        btn.addEventListener('click', playWithMPV);
        return btn;
    }

    function createSettingButton() {
        const btn = document.createElement('button');
        btn.textContent = '⚙️';
        btn.title = 'MPV播放设置';
        btn.style.cssText = `
            position: fixed;
            bottom: 20px;
            left: 76px;
            z-index: 999999;
            background: transparent;
            border: none;
            cursor: pointer;
            font-size: 28px;
            padding: 6px;
            display: flex;
            align-items: center;
            justify-content: center;
            transition: all 0.2s ease;
            color: #4b5563;
            filter: drop-shadow(0 1px 2px rgba(0,0,0,0.2));
        `;
        btn.addEventListener('mouseenter', () => {
            btn.style.transform = 'scale(1.1)';
            btn.style.color = '#a855f7';
        });
        btn.addEventListener('mouseleave', () => {
            btn.style.transform = 'scale(1)';
            btn.style.color = '#4b5563';
        });
        btn.addEventListener('click', openSetting);
        return btn;
    }

    function removeButtons() {
        if (currentPlayBtn && currentPlayBtn.isConnected) currentPlayBtn.remove();
        if (currentSettingBtn && currentSettingBtn.isConnected) currentSettingBtn.remove();
        currentPlayBtn = null;
        currentSettingBtn = null;
        log('按钮已移除');
    }

    function addButtons() {
        if (!document.body) {
            log('body未就绪,等待...');
            return false;
        }
        if (!isVideoPage()) {
            removeButtons();
            return false;
        }
        if (currentPlayBtn && currentSettingBtn) {
            if (!currentPlayBtn.isConnected || !currentSettingBtn.isConnected) {
                log('按钮存在但不在DOM中,重新添加');
                currentPlayBtn = null;
                currentSettingBtn = null;
            } else {
                return true;
            }
        }
        removeButtons();
        currentPlayBtn = createPlayButton();
        currentSettingBtn = createSettingButton();
        document.body.appendChild(currentPlayBtn);
        document.body.appendChild(currentSettingBtn);
        // 添加后立即检查当前全屏状态,防止全屏时按钮显示
        handleFullscreenChange();
        log('按钮已添加到左下角');
        return true;
    }

    let updateTimer = null;
    function updateUI() {
        if (updateTimer) clearTimeout(updateTimer);
        updateTimer = setTimeout(() => {
            const currentUrl = window.location.href;
            if (currentUrl !== lastUrl) {
                lastUrl = currentUrl;
                log('URL变化:', currentUrl);
            }
            addButtons();
        }, 200);
    }

    function observePageChanges() {
        window.addEventListener('popstate', updateUI);
        window.addEventListener('hashchange', updateUI);
        const originalPushState = history.pushState;
        const originalReplaceState = history.replaceState;
        history.pushState = function() {
            originalPushState.apply(this, arguments);
            updateUI();
        };
        history.replaceState = function() {
            originalReplaceState.apply(this, arguments);
            updateUI();
        };
        if (urlCheckInterval) clearInterval(urlCheckInterval);
        urlCheckInterval = setInterval(() => {
            if (window.location.href !== lastUrl) {
                log('定时器检测到URL变化');
                updateUI();
            }
        }, 1000);
        if (observer) observer.disconnect();
        observer = new MutationObserver(() => {
            if (document.body && isVideoPage() && (!currentPlayBtn || !currentPlayBtn.isConnected)) {
                log('检测到body变化,尝试重新添加按钮');
                addButtons();
            }
        });
        observer.observe(document.documentElement, { childList: true, subtree: true });
        updateUI();
        // 注册全屏事件监听
        addFullscreenListener();
    }

    function playWithMPV() {
        try {
            const pageUrl = window.location.href;
            let callUrl = config.mpvProtocol + pageUrl;
            if (config.customArgs) {
                callUrl += ' ' + config.customArgs;
            }
            window.open(callUrl);
            log('调用MPV:', callUrl);
        } catch(e) {
            alert("调用失败: " + e.message + "\n请先完成协议注册!");
        }
    }

    // 辅助函数:下载文件
    function downloadFile(content, filename) {
        const windowsContent = content.replace(/\r?\n/g, '\r\n');
        const blob = new Blob([windowsContent], { type: 'text/plain' });
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = filename;
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
        URL.revokeObjectURL(url);
    }

    // 全局设置窗口函数
    function openSetting() {
        if (settingWindowOpen) return;
        settingWindowOpen = true;

        let tempProtocol = config.mpvProtocol;
        let tempArgs = config.customArgs;
        let tempRules = [...config.urlRules];

        const mask = document.createElement('div');
        mask.style.cssText = `
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: rgba(0,0,0,0.6);
            z-index: 1000000;
            display: flex;
            align-items: center;
            justify-content: center;
        `;

        const win = document.createElement('div');
        win.style.cssText = `
            background: white;
            padding: 24px;
            border-radius: 20px;
            width: 550px;
            max-width: 90%;
            max-height: 85vh;
            overflow-y: auto;
            box-shadow: 0 20px 35px rgba(0,0,0,0.3);
            font-family: system-ui, -apple-system, sans-serif;
        `;

        // 标题
        const title = document.createElement('h2');
        title.textContent = '⚙️ MPV播放设置';
        title.style.cssText = 'margin-top:0; margin-bottom:16px; font-size:22px; color:#1f2937;';
        win.appendChild(title);

        // 协议前缀
        const protocolDiv = document.createElement('div');
        protocolDiv.style.marginBottom = '20px';
        const protocolLabel = document.createElement('label');
        protocolLabel.textContent = 'MPV自定义协议前缀';
        protocolLabel.style.cssText = 'display:block; margin-bottom:8px; font-weight:500; color:#374151;';
        const protocolInput = document.createElement('input');
        protocolInput.type = 'text';
        protocolInput.value = tempProtocol;
        protocolInput.style.cssText = 'width:100%; padding:10px; border:1px solid #d1d5db; border-radius:10px; font-size:14px;';
        const protocolHint = document.createElement('p');
        protocolHint.textContent = '默认 mpv://,需先注册协议(注册方法见下方)';
        protocolHint.style.cssText = 'font-size:12px; color:#6b7280; margin:6px 0 0;';
        protocolDiv.appendChild(protocolLabel);
        protocolDiv.appendChild(protocolInput);
        protocolDiv.appendChild(protocolHint);
        win.appendChild(protocolDiv);

        // 自定义参数
        const argsDiv = document.createElement('div');
        argsDiv.style.marginBottom = '20px';
        const argsLabel = document.createElement('label');
        argsLabel.textContent = '自定义MPV启动参数';
        argsLabel.style.cssText = 'display:block; margin-bottom:8px; font-weight:500; color:#374151;';
        const argsInput = document.createElement('input');
        argsInput.type = 'text';
        argsInput.value = tempArgs;
        argsInput.style.cssText = 'width:100%; padding:10px; border:1px solid #d1d5db; border-radius:10px; font-size:14px;';
        const argsHint = document.createElement('p');
        argsHint.textContent = '注意:当前VBS协议处理脚本不支持额外参数。如需参数,请修改 mpv_protocol_handler.vbs 或在 mpv.conf 中设置。';
        argsHint.style.cssText = 'font-size:12px; color:#ef4444; margin:6px 0 0;';
        argsDiv.appendChild(argsLabel);
        argsDiv.appendChild(argsInput);
        argsDiv.appendChild(argsHint);
        win.appendChild(argsDiv);

        // URL匹配规则管理
        const rulesDiv = document.createElement('div');
        rulesDiv.style.marginBottom = '20px';
        const rulesLabel = document.createElement('label');
        rulesLabel.textContent = '🌐 视频页面匹配规则(URL前缀或正则)';
        rulesLabel.style.cssText = 'display:block; margin-bottom:8px; font-weight:500; color:#374151;';
        const rulesContainer = document.createElement('div');
        rulesContainer.id = 'rulesContainer';
        rulesContainer.style.cssText = 'background:#f9fafb; border:1px solid #e5e7eb; border-radius:12px; padding:8px 12px; max-height:200px; overflow-y:auto; margin-bottom:12px;';
        const addDiv = document.createElement('div');
        addDiv.style.cssText = 'display:flex; gap:8px; margin-top:4px;';
        const newRuleInput = document.createElement('input');
        newRuleInput.type = 'text';
        newRuleInput.placeholder = '例如: https://www.bilibili.com/video/  或 /\\/watch\\?v=/';
        newRuleInput.style.cssText = 'flex:1; padding:8px 12px; border:1px solid #d1d5db; border-radius:10px; font-size:14px;';
        const addRuleBtn = document.createElement('button');
        addRuleBtn.textContent = '添加';
        addRuleBtn.style.cssText = 'background:#a855f7; color:white; border:none; border-radius:10px; padding:0 16px; cursor:pointer; font-size:14px;';
        addDiv.appendChild(newRuleInput);
        addDiv.appendChild(addRuleBtn);
        const rulesHint = document.createElement('p');
        rulesHint.textContent = '支持URL前缀(字符串开头匹配)或正则表达式(用 / / 包裹)。当前页面URL匹配任意一条即显示按钮。';
        rulesHint.style.cssText = 'font-size:12px; color:#6b7280; margin:8px 0 0;';
        rulesDiv.appendChild(rulesLabel);
        rulesDiv.appendChild(rulesContainer);
        rulesDiv.appendChild(addDiv);
        rulesDiv.appendChild(rulesHint);
        win.appendChild(rulesDiv);

        // 刷新按钮
        const refreshBtn = document.createElement('button');
        refreshBtn.textContent = '🔄 立即刷新按钮显示';
        refreshBtn.style.cssText = 'background:#10b981; color:white; border:none; border-radius:10px; padding:8px 16px; cursor:pointer; font-size:14px; width:100%; margin-bottom:20px;';
        refreshBtn.onclick = () => {
            addButtons();
            alert('已尝试重新添加按钮,请检查左下角。如果仍未出现,请刷新页面重试。');
        };
        win.appendChild(refreshBtn);

        // 底部按钮(取消/保存)
        const btnDiv = document.createElement('div');
        btnDiv.style.cssText = 'display:flex; gap:12px; justify-content:flex-end; margin-bottom:20px;';
        const cancelBtn = document.createElement('button');
        cancelBtn.textContent = '取消';
        cancelBtn.style.cssText = 'padding:8px 18px; border:1px solid #cbd5e1; background:white; border-radius:10px; cursor:pointer; font-size:14px;';
        const saveBtn = document.createElement('button');
        saveBtn.textContent = '保存设置';
        saveBtn.style.cssText = 'padding:8px 18px; background:#2563eb; color:white; border:none; border-radius:10px; cursor:pointer; font-size:14px;';
        btnDiv.appendChild(cancelBtn);
        btnDiv.appendChild(saveBtn);
        win.appendChild(btnDiv);

        // 协议注册说明(带下载按钮)
        const regDiv = document.createElement('div');
        regDiv.style.cssText = 'padding:14px; background:#f8fafc; border-radius:12px; margin-bottom:24px; font-size:13px; border-left:4px solid #a855f7;';

        const regTitle = document.createElement('b');
        regTitle.textContent = '🔧 首次使用注册协议(Windows)';
        regTitle.style.color = '#1e293b';
        regDiv.appendChild(regTitle);

        const step1 = document.createElement('div');
        step1.textContent = '1. 在 MPV 安装目录(与 mpv.exe 相同)下创建两个文件(可点击按钮下载):';
        step1.style.marginTop = '8px';
        regDiv.appendChild(step1);

        // 文件1
        const file1Container = document.createElement('div');
        file1Container.style.cssText = 'display:flex; align-items:center; justify-content:space-between; margin-top:8px; flex-wrap:wrap; gap:8px;';
        const file1Label = document.createElement('span');
        file1Label.textContent = '📄 mpv_protocol_handler.vbs';
        file1Label.style.fontWeight = 'bold';
        const downloadBtn1 = document.createElement('button');
        downloadBtn1.textContent = '📥 下载';
        downloadBtn1.style.cssText = 'background:#3b82f6; color:white; border:none; border-radius:6px; padding:4px 12px; cursor:pointer; font-size:12px;';
        const vbsContent = `' mpv_protocol_handler.vbs
Option Explicit
On Error Resume Next

Dim rawUrl, targetUrl, mpvPath, wshShell
Dim regex, matches, protocol, hostPart, pathPart

If WScript.Arguments.Count = 0 Then WScript.Quit
rawUrl = WScript.Arguments(0)

' 1. 去掉 mpv:// 前缀
If LCase(Left(rawUrl, 6)) = "mpv://" Then
    targetUrl = Mid(rawUrl, 7)
ElseIf LCase(Left(rawUrl, 4)) = "mpv:" Then
    targetUrl = Mid(rawUrl, 5)
Else
    targetUrl = rawUrl
End If

' 2. 所有反斜杠转为正斜杠
targetUrl = Replace(targetUrl, "\\", "/")

' 3. 使用正则表达式匹配并修复 URL
Set regex = New RegExp
regex.IgnoreCase = True
regex.Global = False

regex.Pattern = "^(https?)(?:[:/]+)(.*)$"
If regex.Test(targetUrl) Then
    Set matches = regex.Execute(targetUrl)
    protocol = matches(0).SubMatches(0)
    hostPart = matches(0).SubMatches(1)
    hostPart = LTrim(hostPart, "/")
    targetUrl = LCase(protocol) & "://" & hostPart
Else
    If InStr(targetUrl, ".") > 0 Then
        If Left(targetUrl, 8) = "https://" Then targetUrl = Mid(targetUrl, 9)
        If Left(targetUrl, 7) = "http://"  Then targetUrl = Mid(targetUrl, 8)
        targetUrl = "https://" & LTrim(targetUrl, "/")
    End If
End If

If Left(targetUrl, 8) = "https://" Then
    If Mid(targetUrl, 9, 8) = "https://" Then targetUrl = Mid(targetUrl, 9)
ElseIf Left(targetUrl, 7) = "http://" Then
    If Mid(targetUrl, 8, 7) = "http://" Then targetUrl = Mid(targetUrl, 8)
End If

mpvPath = Left(WScript.ScriptFullName, InStrRev(WScript.ScriptFullName, "\\")) & "mpv.exe"
Set wshShell = CreateObject("WScript.Shell")
wshShell.CurrentDirectory = Left(WScript.ScriptFullName, InStrRev(WScript.ScriptFullName, "\\"))
wshShell.Run """" & mpvPath & """ """ & targetUrl & """", 1, False

Set wshShell = Nothing
Set regex = Nothing`;
        downloadBtn1.onclick = () => downloadFile(vbsContent, 'mpv_protocol_handler.vbs');
        file1Container.appendChild(file1Label);
        file1Container.appendChild(downloadBtn1);
        regDiv.appendChild(file1Container);

        const vbsCode = document.createElement('code');
        vbsCode.textContent = vbsContent;
        vbsCode.style.cssText = 'background:#e2e8f0; display:block; padding:8px; margin:8px 0; border-radius:8px; font-size:11px; white-space:pre-wrap; overflow-x:auto;';
        regDiv.appendChild(vbsCode);

        // 文件2
        const file2Container = document.createElement('div');
        file2Container.style.cssText = 'display:flex; align-items:center; justify-content:space-between; margin-top:8px; flex-wrap:wrap; gap:8px;';
        const file2Label = document.createElement('span');
        file2Label.textContent = '📄 注册mpv.bat';
        file2Label.style.fontWeight = 'bold';
        const downloadBtn2 = document.createElement('button');
        downloadBtn2.textContent = '📥 下载';
        downloadBtn2.style.cssText = 'background:#3b82f6; color:white; border:none; border-radius:6px; padding:4px 12px; cursor:pointer; font-size:12px;';
        const batContent = `@echo off
title MPV Protocol Manager (VBS)
cls

echo ======================================
echo    MPV Protocol Manager (VBS)
echo ======================================
echo 1. Register MPV protocol
echo 2. Unregister MPV protocol
echo ======================================
set /p choice=Enter your choice (1/2):

if "%choice%"=="1" goto register
if "%choice%"=="2" goto unregister

echo Invalid choice! Enter 1 or 2.
pause
exit

:register
echo.
echo Checking required files...
if not exist "%~dp0mpv.exe" (
    echo [ERROR] mpv.exe not found.
    echo Place this batch file in the same folder as mpv.exe.
    pause
    exit /b
)
if not exist "%~dp0mpv_protocol_handler.vbs" (
    echo [ERROR] mpv_protocol_handler.vbs not found.
    pause
    exit /b
)

echo Writing registry entries...
reg add "HKEY_CLASSES_ROOT\\mpv" /ve /d "URL:mpv Protocol" /f >nul
reg add "HKEY_CLASSES_ROOT\\mpv" /v "URL Protocol" /d "" /f >nul
reg add "HKEY_CLASSES_ROOT\\mpv\\shell\\open\\command" /ve /d "wscript.exe \"%~dp0mpv_protocol_handler.vbs\" \"%%1\"" /f >nul

echo.
echo ======================================
echo Registration successful!
echo You can now use mpv://URL in your browser.
echo Example: mpv://https://www.bilibili.com/video/BV1t5v1e9EG4
echo ======================================
goto end

:unregister
echo.
echo Removing MPV protocol registry entries...
reg delete "HKEY_CLASSES_ROOT\\mpv" /f >nul 2>&1
if errorlevel 1 (
    echo Registry entry not found or already removed.
) else (
    echo Unregistration complete.
)
goto end

:end
echo.
echo Press any key to exit...
pause >nul`;
        downloadBtn2.onclick = () => downloadFile(batContent, '注册mpv.bat');
        file2Container.appendChild(file2Label);
        file2Container.appendChild(downloadBtn2);
        regDiv.appendChild(file2Container);

        const batCode = document.createElement('code');
        batCode.textContent = batContent;
        batCode.style.cssText = 'background:#e2e8f0; display:block; padding:8px; margin:8px 0; border-radius:8px; font-size:11px; white-space:pre-wrap; overflow-x:auto;';
        regDiv.appendChild(batCode);

        const step2 = document.createElement('div');
        step2.textContent = '2. 以管理员身份运行 “注册mpv.bat”,输入 1 并回车,完成注册。';
        step2.style.marginTop = '8px';
        regDiv.appendChild(step2);

        const step3 = document.createElement('div');
        step3.textContent = '3. 注册成功后,点击页面左下角的紫色播放按钮即可调用 MPV 播放当前视频。';
        regDiv.appendChild(step3);

        const note = document.createElement('div');
        note.textContent = '⚠️ 注意:自定义参数功能在此VBS方案中无效,如需添加启动参数请修改 mpv_protocol_handler.vbs 或在 mpv.conf 中配置。';
        note.style.marginTop = '8px';
        note.style.color = '#dc2626';
        regDiv.appendChild(note);

        win.appendChild(regDiv);

        mask.appendChild(win);
        document.body.appendChild(mask);

        // 渲染规则列表 - 完全避免 innerHTML
        function renderRules() {
            while (rulesContainer.firstChild) {
                rulesContainer.removeChild(rulesContainer.firstChild);
            }

            if (tempRules.length === 0) {
                const emptyDiv = document.createElement('div');
                emptyDiv.textContent = '暂无匹配规则,请添加(至少一条才能显示按钮)';
                emptyDiv.style.cssText = 'color:#6b7280; text-align:center; padding:12px;';
                rulesContainer.appendChild(emptyDiv);
                return;
            }
            tempRules.forEach((rule, idx) => {
                const itemDiv = document.createElement('div');
                itemDiv.style.cssText = 'display:flex; justify-content:space-between; align-items:center; padding:6px 0; border-bottom:1px solid #e5e7eb;';
                const ruleSpan = document.createElement('span');
                ruleSpan.textContent = rule;
                ruleSpan.style.cssText = 'font-size:13px; color:#1f2937; word-break:break-all; flex:1; margin-right:8px;';
                const delBtn = document.createElement('button');
                delBtn.textContent = '删除';
                delBtn.style.cssText = 'background:#ef4444; color:white; border:none; border-radius:6px; padding:2px 10px; cursor:pointer; font-size:12px;';
                delBtn.onclick = () => {
                    tempRules.splice(idx, 1);
                    renderRules();
                };
                itemDiv.appendChild(ruleSpan);
                itemDiv.appendChild(delBtn);
                rulesContainer.appendChild(itemDiv);
            });
        }
        renderRules();

        addRuleBtn.onclick = () => {
            let newRule = newRuleInput.value.trim();
            if (!newRule) {
                alert('请输入规则');
                return;
            }
            if (tempRules.includes(newRule)) {
                alert('规则已存在');
                return;
            }
            tempRules.push(newRule);
            renderRules();
            newRuleInput.value = '';
        };

        const closeMask = () => {
            mask.remove();
            settingWindowOpen = false;
        };

        cancelBtn.onclick = closeMask;
        saveBtn.onclick = () => {
            const newProtocol = protocolInput.value.trim();
            const newArgs = argsInput.value.trim();
            if (tempRules.length === 0) {
                alert('至少需要一条匹配规则,否则按钮永远不会显示!');
                return;
            }
            GM_setValue('mpvProtocol', newProtocol);
            GM_setValue('customArgs', newArgs);
            GM_setValue('urlRules', tempRules);
            config.mpvProtocol = newProtocol;
            config.customArgs = newArgs;
            config.urlRules = [...tempRules];
            addButtons();
            alert('✅ 设置保存成功!');
            closeMask();
        };
        mask.onclick = (e) => { if (e.target === mask) closeMask(); };
    }

    // 注册油猴菜单命令
    GM_registerMenuCommand('⚙️ MPV播放设置', () => {
        openSetting();
    });

    function init() {
        const oldSiteList = GM_getValue('siteList', null);
        if (oldSiteList && Array.isArray(oldSiteList) && (!GM_getValue('urlRules', null))) {
            log('检测到旧版域名列表,正在迁移为URL前缀规则...');
            const migratedRules = [];
            for (const domain of oldSiteList) {
                if (domain === 'youtube.com') migratedRules.push('https://www.youtube.com/watch');
                else if (domain === 'bilibili.com') migratedRules.push('https://www.bilibili.com/video/');
            }
            if (migratedRules.length === 0) {
                migratedRules.push(...defaultRules);
            }
            GM_setValue('urlRules', migratedRules);
            config.urlRules = migratedRules;
            log('迁移完成,新规则:', migratedRules);
        }

        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', () => {
                observePageChanges();
            });
        } else {
            observePageChanges();
        }
    }

    init();
})();