// ==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 // @downloadURL https://update.greasyfork.icu/scripts/558134/YouTube%20Mobile%20%E4%BD%93%E9%AA%8C%E5%A2%9E%E5%BC%BA%E7%89%88.user.js // @updateURL https://update.greasyfork.icu/scripts/558134/YouTube%20Mobile%20%E4%BD%93%E9%AA%8C%E5%A2%9E%E5%BC%BA%E7%89%88.meta.js // ==/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); })();