Greasy Fork is available in English.
自动@回复 + 引用 + 播放列表 + 全局速度控制 + 自动跳下一条 (增强)
// ==UserScript==
// @name YouTube Mobile 体验增强版
// @namespace yt-mobile-autoreply-ui
// @version 3.8
// @description 自动@回复 + 引用 + 播放列表 + 全局速度控制 + 自动跳下一条 (增强)
// @match https://m.youtube.com/*
// @run-at document-idle
// @grant GM_setValue
// @grant GM_getValue
// @license MIT
// ==/UserScript==
(function () {
'use strict';
const LOG = (...args) => console.log('[YT-PL]', ...args);
function showDebugMsg(msg) {
let box = document.getElementById('yt-debug-msg');
if (!box) {
box = document.createElement('div');
box.id = 'yt-debug-msg';
Object.assign(box.style, {
position: 'fixed',
bottom: '180px',
left: '50%',
transform: 'translateX(-50%)',
background: 'rgba(0,0,0,0.85)',
color: '#fff',
fontSize: '13px',
padding: '8px 16px',
borderRadius: '20px',
zIndex: 999999
});
document.body.appendChild(box);
}
box.textContent = msg;
box.style.opacity = '1';
clearTimeout(box._timer);
box._timer = setTimeout(() => box.style.opacity = '0', 1800);
}
/* ====== 数据 Keys ====== */
const KEY_PLAYLIST = 'yt_mobile_playlist';
const KEY_PLAYED_LIST = 'yt_mobile_played';
let playlist = GM_getValue(KEY_PLAYLIST, []);
let playedList = GM_getValue(KEY_PLAYED_LIST, []);
function markPlayed(id) {
if (id && !playedList.includes(id)) {
playedList.push(id);
GM_setValue(KEY_PLAYED_LIST, playedList);
}
}
function isPlayed(id) {
return playedList.includes(id);
}
/* ====== 按钮组容器 ====== */
function createButtonContainer() {
if (document.getElementById('yt-btn-container')) return;
const container = document.createElement('div');
container.id = 'yt-btn-container';
Object.assign(container.style, {
position: 'fixed',
bottom: '12px',
left: '12px',
display: 'flex',
flexDirection: 'column',
gap: '8px',
zIndex: 999998
});
document.body.appendChild(container);
}
/* ====== 引用开关 ====== */
const KEY_ENABLE_QUOTE = 'enable_quote';
let isQuoteEnabled = GM_getValue(KEY_ENABLE_QUOTE, false);
function createQuoteSwitch() {
if (document.getElementById('yt-quote-switch-btn')) return;
const btn = document.createElement('div');
btn.id = 'yt-quote-switch-btn';
btn.textContent = '❝';
Object.assign(btn.style, {
width: '42px',
height: '42px',
borderRadius: '50%',
backgroundColor: isQuoteEnabled ? '#2ba640' : 'rgba(0,0,0,0.6)',
color: isQuoteEnabled ? '#fff' : '#ccc',
fontSize: '24px',
display: 'flex', alignItems: 'center', justifyContent: 'center',
cursor: 'pointer'
});
btn.title = '引用模式开关';
btn.onclick = () => {
isQuoteEnabled = !isQuoteEnabled;
GM_setValue(KEY_ENABLE_QUOTE, isQuoteEnabled);
btn.style.backgroundColor = isQuoteEnabled ? '#2ba640' : 'rgba(0,0,0,0.6)';
btn.style.color = isQuoteEnabled ? '#fff' : '#ccc';
showDebugMsg(isQuoteEnabled ? '引用: 已开启' : '引用: 已关闭');
};
document.getElementById('yt-btn-container').appendChild(btn);
}
/* ====== 播放列表 ====== */
function savePlaylist() {
GM_setValue(KEY_PLAYLIST, playlist);
LOG('Playlist saved', playlist);
}
function addToPlaylist(item) {
if (playlist.find(v => v.id === item.id)) {
showDebugMsg('⚠ 已在播放列表');
return;
}
playlist.push(item);
savePlaylist();
showDebugMsg('🎵 已加入播放列表');
}
function removeFromPlaylist(id) {
playlist = playlist.filter(v => v.id !== id);
savePlaylist();
renderPlaylistPanel();
}
function createPlaylistButton() {
if (document.getElementById('yt-playlist-btn')) return;
const btn = document.createElement('div');
btn.id = 'yt-playlist-btn';
btn.textContent = '🎵';
Object.assign(btn.style, {
width: '42px',
height: '42px',
borderRadius: '50%',
backgroundColor: '#e91e63',
color: '#fff',
fontSize: '22px',
display: 'flex', alignItems: 'center', justifyContent: 'center',
cursor: 'pointer'
});
btn.title = '播放列表';
btn.onclick = togglePlaylistPanel;
document.getElementById('yt-btn-container').appendChild(btn);
}
function clearPlaylist() {
if (!confirm('确认要清空播放列表吗?此操作不可撤销。')) return;
playlist = [];
playedList = [];
GM_setValue(KEY_PLAYLIST, playlist);
GM_setValue(KEY_PLAYED_LIST, playedList);
renderPlaylistPanel();
showDebugMsg('播放列表已清空');
}
function togglePlaylistPanel() {
const panel = document.getElementById('yt-playlist-panel');
if (panel) panel.remove();
else renderPlaylistPanel();
}
function renderPlaylistPanel() {
const old = document.getElementById('yt-playlist-panel');
if (old) old.remove();
const panel = document.createElement('div');
panel.id = 'yt-playlist-panel';
Object.assign(panel.style, {
position: 'fixed',
bottom: '12px',
left: '72px',
width: '300px',
maxHeight: '60vh',
overflowY: 'auto',
backgroundColor: '#222',
color: '#fff',
padding: '8px',
borderRadius: '8px',
fontSize: '13px',
zIndex: 999999
});
const header = document.createElement('div');
Object.assign(header.style, { display: 'flex', justifyContent: 'space-between', alignItems: 'center' });
const title = document.createElement('div');
title.textContent = `🎶 Playlist (${playlist.length})`;
const clearBtn = document.createElement('button');
clearBtn.textContent = '清空';
clearBtn.style.fontSize = '12px';
clearBtn.onclick = clearPlaylist;
header.appendChild(title);
header.appendChild(clearBtn);
panel.appendChild(header);
playlist.forEach(item => {
const row = document.createElement('div');
Object.assign(row.style, {
display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '6px'
});
const lbl = document.createElement('span');
lbl.textContent = item.title || item.id;
lbl.style.cursor = 'pointer';
lbl.style.color = isPlayed(item.id) ? '#888' : '#fff';
lbl.onclick = () => location.href = item.url;
const ctrl = document.createElement('div');
const playBtn = document.createElement('button');
playBtn.textContent = '▶';
playBtn.onclick = () => location.href = item.url;
const delBtn = document.createElement('button');
delBtn.textContent = '❌';
delBtn.onclick = () => removeFromPlaylist(item.id);
ctrl.appendChild(playBtn);
ctrl.appendChild(delBtn);
row.appendChild(lbl);
row.appendChild(ctrl);
panel.appendChild(row);
});
document.body.appendChild(panel);
}
/* ====== 标题加号 ====== */
function scanVideoEntries() {
document.querySelectorAll('h3.media-item-headline').forEach(headline => {
if (headline.dataset.plBound) return;
try {
const span = headline.querySelector('span[role="text"]');
if (!span) return;
const titleText = span.textContent.trim();
if (!titleText) return;
const btn = document.createElement('span');
btn.textContent = '➕';
Object.assign(btn.style, {
marginRight: '6px',
color: '#0f0',
cursor: 'pointer',
fontSize: '16px',
fontWeight: 'bold'
});
btn.title = '加入播放列表';
btn.onclick = e => {
e.stopPropagation();
e.preventDefault();
let url = null;
const parentA = headline.closest('a[href*="/watch"]');
if (parentA) url = parentA.href;
if (!url) {
showDebugMsg('⚠ 无法提取视频链接');
return;
}
const vid = new URL(url, location.origin).searchParams.get('v');
addToPlaylist({ id: vid, title: titleText, url });
};
span.parentNode.insertBefore(btn, span);
headline.dataset.plBound = '1';
} catch (err) {
LOG('scanVideoEntries err', err);
}
});
}
/* ====== 结束检测 (增强) ====== */
function detectVideoEnd(videoEl) {
if (!videoEl) return;
let triggered = false;
const tryNext = () => {
if (triggered) return;
triggered = true;
playNextInPlaylist();
};
// 进度快到结尾
videoEl.addEventListener('timeupdate', () => {
if (!videoEl.duration) return;
if (videoEl.currentTime >= videoEl.duration - 0.25) tryNext();
});
// 观察 DOM 变化
new MutationObserver(() => {
const nextBtn = document.querySelector(
'.player-controls-middle-core-buttons.center button[aria-label="Next video"]:not([aria-disabled="true"])'
);
if (nextBtn) tryNext();
}).observe(document.body, { subtree: true, childList: true });
}
/* ====== 速度面板 ====== */
const SPEED_OPTIONS = [0.25, 0.5, 1.0, 1.25, 1.5, 1.75, 2.0];
let currentSpeed = GM_getValue('yt_mobile_speed', 1.0);
function createSpeedControlButton() {
if (document.getElementById('yt-speed-btn')) return;
const btn = document.createElement('div');
btn.id = 'yt-speed-btn';
btn.textContent = '⏩';
Object.assign(btn.style, {
width: '42px',
height: '42px',
borderRadius: '50%',
backgroundColor: '#007acc',
color: '#fff',
fontSize: '22px',
display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer'
});
btn.title = `播放速度 (${currentSpeed}x)`;
btn.onclick = toggleSpeedPanel;
document.getElementById('yt-btn-container').appendChild(btn);
}
function toggleSpeedPanel() {
const panel = document.getElementById('yt-speed-panel');
if (panel) panel.remove();
else renderSpeedPanel();
}
function renderSpeedPanel() {
const old = document.getElementById('yt-speed-panel');
if (old) old.remove();
const panel = document.createElement('div');
panel.id = 'yt-speed-panel';
Object.assign(panel.style, {
position: 'fixed',
bottom: '12px',
left: '72px',
backgroundColor: '#333',
color: '#fff',
padding: '8px',
borderRadius: '8px',
fontSize: '13px',
zIndex: 999999
});
SPEED_OPTIONS.forEach(sp => {
const b = document.createElement('button');
b.textContent = `${sp}x`;
b.style.margin = '4px';
b.onclick = () => {
currentSpeed = sp;
GM_setValue('yt_mobile_speed', currentSpeed);
applySpeedToVideo();
showDebugMsg(`播放速度设为 ${currentSpeed}x`);
document.getElementById('yt-speed-btn').title = `播放速度 (${currentSpeed}x)`;
panel.remove();
};
panel.appendChild(b);
});
document.body.appendChild(panel);
}
function applySpeedToVideo() {
const videoEl = document.querySelector('video');
if (videoEl) {
try {
videoEl.playbackRate = currentSpeed;
videoEl.addEventListener('play', () => {
const currentVid = new URL(location.href).searchParams.get('v');
markPlayed(currentVid);
});
detectVideoEnd(videoEl);
} catch {}
}
}
function playNextInPlaylist() {
const currentVid = new URL(location.href).searchParams.get('v');
const idx = playlist.findIndex(v => v.id === currentVid);
const nextItem = playlist[idx + 1];
if (nextItem) location.href = nextItem.url;
}
/* ====== 初始化 ====== */
setInterval(() => {
createButtonContainer();
createQuoteSwitch();
createPlaylistButton();
createSpeedControlButton();
scanVideoEntries();
applySpeedToVideo();
}, 2000);
})();