// ==UserScript==
// @name YouTube(油管)添加到稍后观看加强版
// @namespace http://tampermonkey.net/
// @version 1.1
// @description 在后台将 YouTube 视频添加到"稍后观看"列表,按钮贴边半隐藏,可上下拖动,添加设置按钮和自定义快捷键功能
// @author 特比欧炸
// @match https://www.youtube.com/*
// @grant none
// @charset UTF-8
// @license MIT
// ==/UserScript==
(function() {
'use strict';
const i18n = {
en: {
watchLater: 'Watch Later',
settings: 'Settings',
promptShortcut: 'Enter a new shortcut key (current is "{key}")',
invalidShortcut: 'Invalid shortcut, please enter a single character.',
updatedShortcut: 'Shortcut updated to "{key}".'
},
'en-GB': {
watchLater: 'Watch Later',
settings: 'Settings',
promptShortcut: 'Enter a new shortcut key (current is "{key}")',
invalidShortcut: 'Invalid shortcut, please enter a single character.',
updatedShortcut: 'Shortcut updated to "{key}".'
},
zh: {
watchLater: '稍后观看',
settings: '设置',
promptShortcut: '请输入一个新的快捷键(当前快捷键为 "{key}")',
invalidShortcut: '无效的快捷键,请输入单个字符。',
updatedShortcut: '快捷键已更新为 "{key}"'
},
'zh-TW': {
watchLater: '稍後觀看',
settings: '設定',
promptShortcut: '請輸入一個新的快捷鍵(當前快捷鍵為 "{key}")',
invalidShortcut: '無效的快捷鍵,請輸入單個字符。',
updatedShortcut: '快捷鍵已更新為 "{key}"'
},
'zh-HK': {
watchLater: '稍後觀看',
settings: '設定',
promptShortcut: '請輸入一個新的快捷鍵(當前快捷鍵為 "{key}")',
invalidShortcut: '無效的快捷鍵,請輸入單個字符。',
updatedShortcut: '快捷鍵已更新為 "{key}"'
},
th: {
watchLater: 'ดูภายหลัง',
settings: 'การตั้งค่า',
promptShortcut: 'กรุณาป้อนปุ่มลัดใหม่ (ปัจจุบันคือ "{key}")',
invalidShortcut: 'ปุ่มลัดไม่ถูกต้อง โปรดป้อนตัวอักษรตัวเดียว',
updatedShortcut: 'อัปเดตปุ่มลัดเป็น "{key}".'
},
ur: {
watchLater: 'بعد میں دیکھیں',
settings: 'ترتیبات',
promptShortcut: 'نیا شارٹ کٹ کلید درج کریں (موجودہ "{key}" ہے)',
invalidShortcut: 'غلط شارٹ کٹ، براہ کرم ایک حرف درج کریں۔',
updatedShortcut: 'شارٹ کٹ کو "{key}" میں اپ ڈیٹ کیا گیا ہے۔'
},
fa: {
watchLater: 'تماشا بعداً',
settings: 'تنظیمات',
promptShortcut: 'کلید میانبر جدید را وارد کنید (کلید فعلی "{key}" است)',
invalidShortcut: 'کلید میانبر نامعتبر است، لطفاً یک حرف وارد کنید.',
updatedShortcut: 'میانبر به "{key}" به روز شد.'
},
ja: {
watchLater: '後で見る',
settings: '設定',
promptShortcut: '新しいショートカットキーを入力してください(現在のキーは "{key}")',
invalidShortcut: '無効なショートカットです。1文字を入力してください。',
updatedShortcut: 'ショートカットが "{key}" に更新されました。'
},
ko: {
watchLater: '나중에 보기',
settings: '설정',
promptShortcut: '새로운 단축키를 입력하세요 (현재 단축키: "{key}")',
invalidShortcut: '잘못된 단축키입니다. 한 글자를 입력해주세요.',
updatedShortcut: '단축키가 "{key}"로 업데이트되었습니다.'
},
ru: {
watchLater: 'Смотреть позже',
settings: 'Настройки',
promptShortcut: 'Введите новую клавишу быстрого доступа (текущая: "{key}")',
invalidShortcut: 'Недопустимая клавиша быстрого доступа, введите один символ.',
updatedShortcut: 'Клавиша быстрого доступа обновлена на "{key}".'
},
hi: {
watchLater: 'बाद में देखें',
settings: 'सेटिंग्स',
promptShortcut: 'नया शॉर्टकट कुंजी दर्ज करें (वर्तमान "{key}")',
invalidShortcut: 'अमान्य शॉर्टकट, कृपया एक अक्षर दर्ज करें।',
updatedShortcut: 'शॉर्टकट "{key}" पर अपडेट किया गया है।'
},
es: {
watchLater: 'Ver más tarde',
settings: 'Configuraciones',
promptShortcut: 'Ingrese una nueva tecla de acceso rápido (actualmente "{key}")',
invalidShortcut: 'Acceso rápido no válido, ingrese un solo carácter.',
updatedShortcut: 'Tecla de acceso rápido actualizada a "{key}".'
},
pt: {
watchLater: 'Assistir mais tarde',
settings: 'Configurações',
promptShortcut: 'Insira una nova tecla de atalho (atual: "{key}")',
invalidShortcut: 'Tecla de atalho inválida, insira um único caractere.',
updatedShortcut: 'Tecla de atalho atualizada para "{key}".'
}
};
// 获取用户语言,处理不同地区的语言代码
let language = (navigator.language || navigator.userLanguage).toLowerCase();
if (language.includes('-')) {
const [baseLang] = language.split('-');
language = i18n[baseLang] ? baseLang : language;
}
const translations = i18n[language] || i18n['en']; // 默认使用英文
let isCustomButtonClicked = false;
let isCoolingDown = false;
let isDragging = false;
let dragStartTime = 0;
let dragThreshold = 200; // 拖动阈值时间(ms)
let panelOpen = false;
let offsetY = 0;
let watchLaterKey = 'w'; // 默认快捷键是"W"
let playlistKeys = {}; // 存储每个收藏夹对应的快捷键
let isPreloading = false;
let buttonContainer = null; // 存储按钮容器引用
let savedPosition = null; // 保存的位置信息
// 添加CSS来隐藏.opened元素,隐藏弹出窗口
const style = document.createElement('style');
style.textContent = `
.opened {
display: none !important;
}
`;
document.head.appendChild(style);
// 隐藏tp-yt-paper-dialog元素的函数
function hideDialogElements() {
const dialogElements = document.querySelectorAll('tp-yt-paper-dialog.ytd-popup-container.style-scope > .ytd-popup-container.style-scope');
dialogElements.forEach(element => {
element.style.display = 'none'; // 直接设置display为none
});
}
// 显示tp-yt-paper-dialog元素的函数
function showDialogElements() {
const dialogElements = document.querySelectorAll('tp-yt-paper-dialog.ytd-popup-container.style-scope > .ytd-popup-container.style-scope');
dialogElements.forEach(element => {
element.style.display = ''; // 恢复默认显示
});
}
// 保存位置到localStorage
function savePosition(top) {
try {
localStorage.setItem('youtube_button_position', top.toString());
} catch (e) {
console.error('保存位置失败:', e);
}
}
// 从localStorage获取位置
function getSavedPosition() {
try {
const saved = localStorage.getItem('youtube_button_position');
return saved ? parseInt(saved) : null;
} catch (e) {
console.error('获取保存位置失败:', e);
return null;
}
}
// 创建悬浮按钮
function createFloatingButton() {
if (window.location.pathname !== '/watch') return; // 仅在 /watch 页面时创建按钮
// 如果已存在按钮,先移除
if (buttonContainer) {
buttonContainer.remove();
}
const container = document.createElement('div');
container.style.position = 'fixed';
container.style.zIndex = 9999;
container.style.display = 'flex';
container.style.flexDirection = 'column';
container.style.transition = 'right 0.3s';
container.style.right = '0';
// 设置初始位置
const savedTop = getSavedPosition();
if (savedTop !== null) {
container.style.top = `${savedTop}px`;
} else {
container.style.top = '50%';
container.style.transform = 'translateY(-50%)';
}
// 稍后观看按钮
const watchLaterButton = document.createElement('button');
watchLaterButton.innerText = translations.watchLater;
watchLaterButton.style.backgroundColor = '#FF0000';
watchLaterButton.style.color = '#FFFFFF';
watchLaterButton.style.padding = '10px';
watchLaterButton.style.border = 'none';
watchLaterButton.style.borderRadius = '5px';
watchLaterButton.style.cursor = 'pointer';
watchLaterButton.style.boxShadow = '0px 0px 10px rgba(0, 0, 0, 0.5)';
watchLaterButton.style.width = '80px';
watchLaterButton.style.height = '40px';
watchLaterButton.style.position = 'relative';
watchLaterButton.style.right = '-60px'; // 半隐藏
watchLaterButton.style.transition = 'right 0.3s';
watchLaterButton.setAttribute('id', 'customWatchLaterButton');
// 鼠标悬停效果
watchLaterButton.onmouseenter = function() {
if (!isDragging) {
watchLaterButton.style.right = '0';
settingsButton.style.right = '0';
}
};
watchLaterButton.onmouseleave = function() {
if (!isDragging && !panelOpen) {
watchLaterButton.style.right = '-60px';
settingsButton.style.right = '-80px';
}
};
// 设置按钮
const settingsButton = document.createElement('button');
settingsButton.innerText = translations.settings;
settingsButton.style.backgroundColor = '#000000';
settingsButton.style.color = '#FFFFFF';
settingsButton.style.padding = '8px';
settingsButton.style.border = 'none';
settingsButton.style.borderRadius = '5px';
settingsButton.style.cursor = 'pointer';
settingsButton.style.boxShadow = '0px 0px 10px rgba(0, 0, 0, 0.5)';
settingsButton.style.width = '80px';
settingsButton.style.height = '35px';
settingsButton.style.position = 'relative';
settingsButton.style.right = '-80px'; // 完全隐藏
settingsButton.style.transition = 'right 0.3s';
settingsButton.onmouseenter = function() {
if (!isDragging) {
settingsButton.style.right = '0';
watchLaterButton.style.right = '0';
}
};
settingsButton.onmouseleave = function() {
if (!isDragging && !panelOpen) {
settingsButton.style.right = '-80px';
watchLaterButton.style.right = '-60px';
}
};
// 拖动相关变量
let dragStartX = 0;
let dragStartY = 0;
let hasMovedSignificantly = false;
// 拖动逻辑
function startDragging(e) {
dragStartTime = Date.now();
dragStartX = e.clientX;
dragStartY = e.clientY;
hasMovedSignificantly = false;
isDragging = true;
// 移除transform以使用像素坐标
if (container.style.transform) {
const rect = container.getBoundingClientRect();
container.style.top = `${rect.top}px`;
container.style.transform = '';
}
offsetY = e.clientY - container.getBoundingClientRect().top;
document.addEventListener('mousemove', drag);
document.addEventListener('mouseup', stopDragging);
// 防止文字选择
e.preventDefault();
}
function drag(e) {
const deltaX = Math.abs(e.clientX - dragStartX);
const deltaY = Math.abs(e.clientY - dragStartY);
// 如果移动距离超过阈值,标记为显著移动
if (deltaX > 5 || deltaY > 5) {
hasMovedSignificantly = true;
}
if (hasMovedSignificantly) {
const containerHeight = container.offsetHeight;
const windowHeight = window.innerHeight;
let newY = e.clientY - offsetY;
// 限制拖动范围
newY = Math.max(0, Math.min(newY, windowHeight - containerHeight));
container.style.top = `${newY}px`;
// 如果面板打开,也跟着移动
if (panelOpen && settingsPanel) {
settingsPanel.style.top = `${newY}px`;
}
}
}
function stopDragging(e) {
document.removeEventListener('mousemove', drag);
document.removeEventListener('mouseup', stopDragging);
const dragDuration = Date.now() - dragStartTime;
// 如果拖动时间短且移动距离小,视为点击
if (dragDuration < dragThreshold && !hasMovedSignificantly) {
// 不阻止点击事件
isDragging = false;
return;
}
// 保存位置
if (hasMovedSignificantly) {
const rect = container.getBoundingClientRect();
savePosition(rect.top);
}
// 延迟重置拖动状态,防止立即触发点击
setTimeout(() => {
isDragging = false;
}, 50);
}
// 为容器添加拖动事件
container.addEventListener('mousedown', startDragging);
// 稍后观看按钮点击事件
watchLaterButton.addEventListener('click', function(e) {
// 如果正在拖动或刚完成拖动,不处理点击
if (isDragging || hasMovedSignificantly) {
e.preventDefault();
e.stopPropagation();
return;
}
if (isCoolingDown) {
return;
}
isCustomButtonClicked = true;
isCoolingDown = true;
watchLaterButton.style.backgroundColor = '#AAAAAA';
setTimeout(() => {
isCoolingDown = false;
watchLaterButton.style.backgroundColor = '#FF0000';
}, 3500);
const videoId = getVideoId();
if (videoId) {
addWatchLater();
}
});
// 设置按钮点击事件
settingsButton.addEventListener('click', function(e) {
if (isDragging || hasMovedSignificantly) {
e.preventDefault();
e.stopPropagation();
return;
}
toggleSettingsPanel();
});
container.appendChild(watchLaterButton);
container.appendChild(settingsButton);
document.body.appendChild(container);
// 保存容器引用
buttonContainer = container;
// 创建并附加设置面板
const settingsPanel = document.createElement('div');
settingsPanel.id = 'customSettingsPanel';
settingsPanel.style.position = 'fixed';
settingsPanel.style.backgroundColor = '#FFFFFF';
settingsPanel.style.border = '1px solid #888';
settingsPanel.style.borderRadius = '5px';
settingsPanel.style.boxShadow = '0 0 10px rgba(0,0,0,0.5)';
settingsPanel.style.zIndex = 10000;
settingsPanel.style.padding = '15px';
settingsPanel.style.width = '320px';
settingsPanel.style.maxHeight = '400px';
settingsPanel.style.overflowY = 'auto';
settingsPanel.style.display = 'none';
settingsPanel.style.fontSize = '14px';
// 初始位置,与按钮对齐
const rect = container.getBoundingClientRect();
settingsPanel.style.top = `${rect.top}px`;
settingsPanel.style.right = '90px';
// 面板标题和关闭按钮
const panelHeader = document.createElement('div');
panelHeader.style.display = 'flex';
panelHeader.style.justifyContent = 'space-between';
panelHeader.style.alignItems = 'center';
panelHeader.style.marginBottom = '15px';
panelHeader.style.fontWeight = 'bold';
panelHeader.style.fontSize = '16px';
panelHeader.innerText = '快捷键设置';
const closeBtn = document.createElement('button');
closeBtn.innerText = '×';
closeBtn.style.background = 'none';
closeBtn.style.border = 'none';
closeBtn.style.fontSize = '20px';
closeBtn.style.cursor = 'pointer';
closeBtn.style.padding = '0';
closeBtn.style.width = '20px';
closeBtn.style.height = '20px';
closeBtn.onclick = function() {
settingsPanel.style.display = 'none';
panelOpen = false;
if (!isDragging) {
watchLaterButton.style.right = '-60px';
settingsButton.style.right = '-80px';
}
};
panelHeader.appendChild(closeBtn);
settingsPanel.appendChild(panelHeader);
// 稍后观看快捷键设置区域
const watchLaterSection = document.createElement('div');
watchLaterSection.style.marginBottom = '20px';
watchLaterSection.style.padding = '10px';
watchLaterSection.style.border = '1px solid #ddd';
watchLaterSection.style.borderRadius = '5px';
watchLaterSection.style.backgroundColor = '#f9f9f9';
const watchLaterTitle = document.createElement('div');
watchLaterTitle.innerText = '稍后观看快捷键';
watchLaterTitle.style.fontWeight = 'bold';
watchLaterTitle.style.marginBottom = '10px';
watchLaterSection.appendChild(watchLaterTitle);
const watchLaterKeyDiv = document.createElement('div');
watchLaterKeyDiv.style.display = 'flex';
watchLaterKeyDiv.style.alignItems = 'center';
watchLaterKeyDiv.style.gap = '10px';
const watchLaterKeyInput = document.createElement('input');
watchLaterKeyInput.type = 'text';
watchLaterKeyInput.style.width = '150px';
watchLaterKeyInput.style.padding = '5px';
watchLaterKeyInput.style.border = '1px solid #ccc';
watchLaterKeyInput.style.borderRadius = '3px';
watchLaterKeyInput.placeholder = '按下组合键...';
watchLaterKeyInput.readOnly = true;
watchLaterKeyInput.value = formatShortcut(getStoredWatchLaterKey());
const watchLaterResetBtn = document.createElement('button');
watchLaterResetBtn.innerText = '重置';
watchLaterResetBtn.style.padding = '5px 10px';
watchLaterResetBtn.style.border = '1px solid #ccc';
watchLaterResetBtn.style.borderRadius = '3px';
watchLaterResetBtn.style.cursor = 'pointer';
watchLaterResetBtn.onclick = function() {
watchLaterKey = { ctrl: false, alt: false, shift: false, key: 'w' };
watchLaterKeyInput.value = formatShortcut(watchLaterKey);
saveWatchLaterKey();
};
watchLaterKeyDiv.appendChild(watchLaterKeyInput);
watchLaterKeyDiv.appendChild(watchLaterResetBtn);
watchLaterSection.appendChild(watchLaterKeyDiv);
// 为稍后观看快捷键输入框添加键盘事件监听
watchLaterKeyInput.addEventListener('keydown', function(e) {
e.preventDefault();
if (e.key === 'Escape') {
watchLaterKeyInput.blur();
return;
}
if (e.key === 'Tab' || e.key === 'Enter') return;
const newKey = {
ctrl: e.ctrlKey,
alt: e.altKey,
shift: e.shiftKey,
key: e.key.toLowerCase()
};
// 至少需要一个修饰键
if (!newKey.ctrl && !newKey.alt && !newKey.shift) {
alert('请使用组合键(Ctrl/Alt/Shift + 字母/数字)');
return;
}
watchLaterKey = newKey;
watchLaterKeyInput.value = formatShortcut(watchLaterKey);
saveWatchLaterKey();
});
settingsPanel.appendChild(watchLaterSection);
// 收藏夹快捷键设置区域
const playlistSection = document.createElement('div');
playlistSection.style.marginBottom = '15px';
const playlistTitle = document.createElement('div');
playlistTitle.innerText = '收藏夹快捷键';
playlistTitle.style.fontWeight = 'bold';
playlistTitle.style.marginBottom = '10px';
playlistSection.appendChild(playlistTitle);
// 刷新收藏夹按钮
const refreshBtn = document.createElement('button');
refreshBtn.innerText = '刷新收藏夹列表';
refreshBtn.style.padding = '5px 10px';
refreshBtn.style.border = '1px solid #ccc';
refreshBtn.style.borderRadius = '3px';
refreshBtn.style.cursor = 'pointer';
refreshBtn.style.marginBottom = '10px';
refreshBtn.onclick = function() {
updatePlaylistList();
};
playlistSection.appendChild(refreshBtn);
// 列表容器
const panelList = document.createElement('div');
panelList.style.maxHeight = '200px';
panelList.style.overflowY = 'auto';
panelList.style.border = '1px solid #ddd';
panelList.style.borderRadius = '5px';
panelList.style.padding = '5px';
playlistSection.appendChild(panelList);
settingsPanel.appendChild(playlistSection);
// 添加至所选按钮
const addSelectedBtn = document.createElement('button');
addSelectedBtn.innerText = '添加到选中收藏夹';
addSelectedBtn.style.padding = '8px';
addSelectedBtn.style.width = '100%';
addSelectedBtn.style.border = 'none';
addSelectedBtn.style.backgroundColor = '#FF0000';
addSelectedBtn.style.color = '#FFFFFF';
addSelectedBtn.style.borderRadius = '5px';
addSelectedBtn.style.cursor = 'pointer';
addSelectedBtn.onclick = function() {
addToSelectedPlaylists();
};
settingsPanel.appendChild(addSelectedBtn);
document.body.appendChild(settingsPanel);
// 切换设置面板显示隐藏
function toggleSettingsPanel() {
if (settingsPanel.style.display === 'none') {
updatePlaylistList();
settingsPanel.style.display = 'block';
panelOpen = true;
// Position panel near container
const rect = container.getBoundingClientRect();
settingsPanel.style.top = `${rect.top}px`;
settingsPanel.style.right = '90px';
// 保持按钮可见
watchLaterButton.style.right = '0';
settingsButton.style.right = '0';
} else {
settingsPanel.style.display = 'none';
panelOpen = false;
// 隐藏按钮
if (!isDragging) {
watchLaterButton.style.right = '-60px';
settingsButton.style.right = '-80px';
}
}
}
// 更新收藏夹列表
function updatePlaylistList() {
// 清空旧列表
panelList.replaceChildren();
// 获取存储的收藏夹数据
const storedPlaylists = getStoredPlaylists();
// 获取当前页面的收藏夹
const currentPlaylistElems = document.querySelectorAll('#playlists yt-formatted-string[title]');
const currentPlaylists = new Set();
currentPlaylistElems.forEach(elem => {
const name = elem.textContent || elem.getAttribute('title');
if (name && !name.match(/稍后观看|Watch later|历史记录|History/)) {
currentPlaylists.add(name);
}
});
// 合并存储的和当前的收藏夹
const allPlaylists = new Set([...Object.keys(storedPlaylists), ...currentPlaylists]);
// 为每个收藏夹创建列表项
Array.from(allPlaylists).sort().forEach(name => {
const itemDiv = document.createElement('div');
itemDiv.style.display = 'flex';
itemDiv.style.alignItems = 'center';
itemDiv.style.marginBottom = '8px';
itemDiv.style.padding = '5px';
itemDiv.style.border = '1px solid #eee';
itemDiv.style.borderRadius = '3px';
itemDiv.style.backgroundColor = '#fff';
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.style.marginRight = '8px';
itemDiv.appendChild(checkbox);
const label = document.createElement('span');
label.className = 'playlistName';
label.innerText = name;
label.style.flexGrow = '1';
label.style.fontSize = '12px';
// 如果不在当前页面,显示为灰色
if (!currentPlaylists.has(name)) {
label.style.color = '#888';
label.title = '此收藏夹不在当前页面';
}
itemDiv.appendChild(label);
const keyInput = document.createElement('input');
keyInput.type = 'text';
keyInput.className = 'playlistKey';
keyInput.style.width = '100px';
keyInput.style.padding = '2px 5px';
keyInput.style.border = '1px solid #ccc';
keyInput.style.borderRadius = '3px';
keyInput.style.fontSize = '12px';
keyInput.placeholder = '组合键...';
keyInput.readOnly = true;
// 如果已有设置的快捷键,显示
if (storedPlaylists[name]) {
keyInput.value = formatShortcut(storedPlaylists[name]);
}
// 为快捷键输入框添加键盘事件监听
keyInput.addEventListener('keydown', function(e) {
e.preventDefault();
if (e.key === 'Escape') {
keyInput.blur();
return;
}
if (e.key === 'Tab' || e.key === 'Enter') return;
const newKey = {
ctrl: e.ctrlKey,
alt: e.altKey,
shift: e.shiftKey,
key: e.key.toLowerCase()
};
// 至少需要一个修饰键
if (!newKey.ctrl && !newKey.alt && !newKey.shift) {
alert('请使用组合键(Ctrl/Alt/Shift + 字母/数字)');
return;
}
// 检查是否与稍后观看快捷键冲突
if (isShortcutEqual(newKey, watchLaterKey)) {
alert('快捷键与稍后观看快捷键冲突!');
return;
}
// 检查是否与其他收藏夹快捷键冲突
let conflict = false;
for (const [pl, key] of Object.entries(storedPlaylists)) {
if (pl !== name && isShortcutEqual(key, newKey)) {
conflict = true;
break;
}
}
if (conflict) {
alert('快捷键与其他收藏夹冲突!');
return;
}
// 保存快捷键
const playlists = getStoredPlaylists();
playlists[name] = newKey;
savePlaylists(playlists);
keyInput.value = formatShortcut(newKey);
playlistKeys[name] = newKey;
});
// 重置按钮
const resetBtn = document.createElement('button');
resetBtn.innerText = '×';
resetBtn.style.marginLeft = '5px';
resetBtn.style.padding = '2px 5px';
resetBtn.style.border = '1px solid #ccc';
resetBtn.style.borderRadius = '3px';
resetBtn.style.cursor = 'pointer';
resetBtn.style.fontSize = '12px';
resetBtn.onclick = function() {
const playlists = getStoredPlaylists();
delete playlists[name];
delete playlistKeys[name];
savePlaylists(playlists);
keyInput.value = '';
};
itemDiv.appendChild(keyInput);
itemDiv.appendChild(resetBtn);
panelList.appendChild(itemDiv);
});
}
// 将当前视频添加到所选收藏夹
function addToSelectedPlaylists() {
const checkedPlaylists = [];
panelList.querySelectorAll('div').forEach(itemDiv => {
const chk = itemDiv.querySelector('input[type="checkbox"]');
const nameSpan = itemDiv.querySelector('.playlistName');
if (chk.checked && nameSpan) {
checkedPlaylists.push(nameSpan.innerText);
}
});
if (checkedPlaylists.length === 0) {
alert('未选择任何收藏夹');
return;
}
// 关闭面板
settingsPanel.style.display = 'none';
panelOpen = false;
if (!isDragging) {
watchLaterButton.style.right = '-60px';
settingsButton.style.right = '-80px';
}
// 点击保存按钮,预加载对话框
const saveButton = document.querySelector('button[aria-label*="保存"], button[aria-label*="Save"]');
if (saveButton) {
isPreloading = true;
document.body.classList.add('preloading');
saveButton.click();
// 等待对话框加载后点击每个选中的收藏夹
setTimeout(() => {
checkedPlaylists.forEach(name => {
const item = Array.from(document.querySelectorAll('yt-formatted-string')).find(el => el.textContent === name);
if (item) {
item.click();
}
});
finishPreloadAndHideDialog();
}, 500);
}
}
}
// 数据持久化相关函数
function getStoredPlaylists() {
try {
const stored = localStorage.getItem('youtube_playlists_shortcuts');
return stored ? JSON.parse(stored) : {};
} catch (e) {
console.error('获取收藏夹数据失败:', e);
return {};
}
}
function savePlaylists(playlists) {
try {
localStorage.setItem('youtube_playlists_shortcuts', JSON.stringify(playlists));
} catch (e) {
console.error('保存收藏夹数据失败:', e);
}
}
function getStoredWatchLaterKey() {
try {
const stored = localStorage.getItem('youtube_watch_later_key');
return stored ? JSON.parse(stored) : { ctrl: false, alt: false, shift: false, key: 'w' };
} catch (e) {
console.error('获取稍后观看快捷键失败:', e);
return { ctrl: false, alt: false, shift: false, key: 'w' };
}
}
function saveWatchLaterKey() {
try {
localStorage.setItem('youtube_watch_later_key', JSON.stringify(watchLaterKey));
} catch (e) {
console.error('保存稍后观看快捷键失败:', e);
}
}
// 快捷键相关工具函数
function formatShortcut(shortcut) {
if (!shortcut) return '';
const parts = [];
if (shortcut.ctrl) parts.push('Ctrl');
if (shortcut.alt) parts.push('Alt');
if (shortcut.shift) parts.push('Shift');
if (shortcut.key) parts.push(shortcut.key.toUpperCase());
return parts.join(' + ');
}
function isShortcutEqual(key1, key2) {
if (!key1 || !key2) return false;
return key1.ctrl === key2.ctrl &&
key1.alt === key2.alt &&
key1.shift === key2.shift &&
key1.key === key2.key;
}
// 初始化快捷键数据
watchLaterKey = getStoredWatchLaterKey();
playlistKeys = getStoredPlaylists();
// 保留其他逻辑
function getVideoId() {
const urlParams = new URLSearchParams(window.location.search);
return urlParams.get('v');
}
// 添加视频到稍后观看
function addWatchLater() {
const saveButton = document.querySelector('button[aria-label*="保存"], button[aria-label*="Save"]');
if (saveButton) {
isPreloading = true;
document.body.classList.add('preloading');
saveButton.click();
const observer = new MutationObserver(() => {
const watchLaterButton = document.querySelector('yt-formatted-string[title="稍后观看"], yt-formatted-string[title="Watch later"]');
if (watchLaterButton) {
observer.disconnect();
watchLaterButton.click();
console.log('视频已添加到稍后观看');
finishPreloadAndHideDialog();
}
});
observer.observe(document.body, { childList: true, subtree: true });
} else {
console.log('未找到保存按钮');
finishPreloadAndHideDialog();
}
}
// 在预加载完成后移除 'preloading' 类并隐藏对话框
function finishPreloadAndHideDialog() {
isPreloading = false;
document.body.classList.remove('preloading');
hideDialogElements();
}
function setupOriginalSaveButtonListener() {
const observer = new MutationObserver(() => {
const originalSaveButton = document.querySelector('ytd-menu-renderer');
if (originalSaveButton) {
originalSaveButton.addEventListener('click', () => {
isCustomButtonClicked = false;
showDialogElements();
});
observer.disconnect();
}
});
observer.observe(document.body, { childList: true, subtree: true });
}
// 监听键盘事件 - 修改为支持组合键
document.addEventListener('keydown', (event) => {
if (event.target.tagName === 'INPUT' || event.target.tagName === 'TEXTAREA') {
return;
}
const currentKey = {
ctrl: event.ctrlKey,
alt: event.altKey,
shift: event.shiftKey,
key: event.key.toLowerCase()
};
// 检查稍后观看快捷键
if (isShortcutEqual(currentKey, watchLaterKey)) {
event.preventDefault();
const button = document.getElementById('customWatchLaterButton');
if (button) {
button.click();
}
return;
}
// 检查收藏夹快捷键
for (const [playlistName, shortcut] of Object.entries(playlistKeys)) {
if (isShortcutEqual(currentKey, shortcut)) {
event.preventDefault();
const saveButton = document.querySelector('button[aria-label*="保存"], button[aria-label*="Save"]');
if (saveButton) {
isPreloading = true;
document.body.classList.add('preloading');
saveButton.click();
const observer = new MutationObserver(() => {
const item = Array.from(document.querySelectorAll('yt-formatted-string')).find(el => el.textContent === playlistName);
if (item) {
observer.disconnect();
item.click();
console.log(`视频已添加到收藏夹: ${playlistName}`);
finishPreloadAndHideDialog();
}
});
observer.observe(document.body, { childList: true, subtree: true });
}
return;
}
}
});
// 隐藏按钮在主页、迷你播放器和全屏时
function checkVisibility() {
const isHomepage = window.location.href === 'https://www.youtube.com/';
const isMiniPlayer = document.querySelector('.mini-player');
const isFullscreen = document.fullscreenElement !== null;
const button = document.getElementById('customWatchLaterButton');
const settingsButton = document.querySelector('button#customWatchLaterButton + button');
if (button) {
button.style.display = (isHomepage || isMiniPlayer || isFullscreen) ? 'none' : 'block';
settingsButton.style.display = (isHomepage || isMiniPlayer || isFullscreen) ? 'none' : 'block';
}
}
// 页面加载完成后创建按钮
function onPreloadFinish() {
// 延迟创建按钮,等待页面完全加载
setTimeout(() => {
createFloatingButton();
setupOriginalSaveButtonListener();
checkVisibility();
}, 500);
}
// 监听预加载完成事件
const preloadObserver = new MutationObserver(() => {
if (!isPreloading) {
preloadObserver.disconnect();
onPreloadFinish();
}
});
preloadObserver.observe(document.body, { attributes: true, childList: true, subtree: true });
// 监控URL变化
const observer = new MutationObserver(() => {
checkVisibility();
});
observer.observe(document.body, { childList: true, subtree: true });
})();