Greasy Fork is available in English.
在视频页面左下角添加播放/设置按钮,全屏时自动隐藏,支持油猴菜单打开设置
当前为
// ==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();
})();