// ==UserScript== // @name 百度网盘批量选择文件助手(悬浮球版) // @namespace http://tampermonkey.net/ // @version 8.0 // @description 支持手动输入和读取上次选择位置,UI可拖动、可隐藏,带悬浮球 // @author Assistant // @match https://pan.baidu.com/disk/* // @match https://pan.baidu.com/s/* // @match https://pan.baidu.com/share/* // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // ==/UserScript== (function() { 'use strict'; // 全局状态 let isSelecting = false; let autoSelectTimer = null; let targetCount = 500; let batchSize = 100; let isSharePage = false; let lastSelectedIndex = 0; let currentUrlKey = ''; // UI拖动相关变量 let isDragging = false; let isDraggingFloat = false; let dragStartX = 0; let dragStartY = 0; let panelLeft = 0; let panelTop = 0; let floatLeft = 0; let floatTop = 0; let isPanelVisible = true; // 日志面板 let logPanel = null; let logContent = []; let floatBall = null; // 获取当前页面的唯一标识 function getPageKey() { const url = window.location.href; const shareMatch = url.match(/\/s\/([a-zA-Z0-9]+)/); if (shareMatch) { return `share_${shareMatch[1]}`; } const pathMatch = url.match(/\/disk\/(.+)/); if (pathMatch) { return `disk_${pathMatch[1]}`; } return `page_${url.replace(/[^a-zA-Z0-9]/g, '_')}`; } // 保存UI位置 function saveUIPosition(left, top) { GM_setValue('ui_left', left); GM_setValue('ui_top', top); } // 保存悬浮球位置 function saveFloatPosition(left, top) { GM_setValue('float_left', left); GM_setValue('float_top', top); } // 保存UI隐藏状态 function saveUIHidden(hidden) { GM_setValue('ui_hidden', hidden); } // 获取UI位置 function getUIPosition() { const left = GM_getValue('ui_left', null); const top = GM_getValue('ui_top', null); return { left, top }; } // 获取悬浮球位置 function getFloatPosition() { const left = GM_getValue('float_left', null); const top = GM_getValue('float_top', null); return { left, top }; } // 获取UI隐藏状态 function getUIHidden() { return GM_getValue('ui_hidden', false); } // 保存上次选择的位置 function saveLastPosition(index) { const key = `lastPos_${currentUrlKey}`; GM_setValue(key, index); addLog(`💾 已保存选择位置: 第 ${index} 个文件`, 'info'); const positionInput = document.getElementById('position-input'); if (positionInput) { positionInput.value = index; } } // 获取上次选择的位置 function getLastPosition() { const key = `lastPos_${currentUrlKey}`; const pos = GM_getValue(key, 0); return pos; } // 手动设置选择位置 function setManualPosition(position) { const totalFiles = getFileRows().length; if (position < 0) { addLog(`❌ 位置不能为负数`, 'error'); return false; } if (position > totalFiles && totalFiles > 0) { addLog(`⚠ 位置 ${position} 超出文件总数 ${totalFiles},将设置为最后一个文件`, 'warning'); position = totalFiles; } lastSelectedIndex = position; saveLastPosition(lastSelectedIndex); addLog(`📌 手动设置选择位置为: 第 ${lastSelectedIndex} 个文件`, 'success'); refreshSelectedDisplay(); return true; } // 清除保存的位置 function clearLastPosition() { const key = `lastPos_${currentUrlKey}`; GM_deleteValue(key); lastSelectedIndex = 0; const positionInput = document.getElementById('position-input'); if (positionInput) { positionInput.value = ''; } addLog(`🗑 已清除保存的选择位置`, 'info'); refreshSelectedDisplay(); } // 读取保存的位置到输入框 function loadPositionToInput() { const savedPos = getLastPosition(); const positionInput = document.getElementById('position-input'); if (positionInput) { if (savedPos > 0) { positionInput.value = savedPos; addLog(`📖 已读取保存位置: 第 ${savedPos} 个文件`, 'info'); } else { positionInput.value = ''; addLog(`📖 未找到保存的位置`, 'info'); } } return savedPos; } // 添加日志 function addLog(message, type = 'info') { const timestamp = new Date().toLocaleTimeString(); const logEntry = `[${timestamp}] ${message}`; logContent.unshift(logEntry); if (logContent.length > 50) logContent.pop(); if (logPanel) { const logTextarea = logPanel.querySelector('#log-textarea'); if (logTextarea) { logTextarea.value = logContent.join('\n'); logTextarea.scrollTop = 0; } } console.log(logEntry); } // 判断是否为分享页面 function checkIsSharePage() { const url = window.location.href; isSharePage = url.includes('/s/') || url.includes('/share/'); currentUrlKey = getPageKey(); return isSharePage; } // 获取所有文件行 function getFileRows() { if (isSharePage) { return Array.from(document.querySelectorAll('dd.AuPKyz, dd.g-clearfix.AuPKyz')); } else { return Array.from(document.querySelectorAll('li.fufHyA.yfHIsP')); } } // 检查文件行是否已选中 function isRowSelected(row) { if (isSharePage) { return row.classList.contains('JS-item-active'); } else { const checkbox = row.querySelector('.Qxyfvg .NbKJexb'); return checkbox && checkbox.classList.contains('icon-checksmall'); } } // 点击选择/取消选择文件行 function clickRow(row) { if (isSharePage) { const clickable = row.querySelector('.file-name') || row; clickable.click(); return true; } else { const clickArea = row.querySelector('.Qxyfvg'); if (clickArea) { clickArea.click(); return true; } } return false; } // 选择/取消选择文件行 function setRowSelected(row, selected) { const isSelected = isRowSelected(row); if (selected && !isSelected) { return clickRow(row); } else if (!selected && isSelected) { return clickRow(row); } return false; } // 获取当前已选中的文件数量 function getSelectedCount() { if (isSharePage) { const rows = getFileRows(); let count = 0; rows.forEach(row => { if (row.classList.contains('JS-item-active')) { count++; } }); return count; } else { const selectedTextElem = document.querySelector('.MdLxwM'); if (selectedTextElem) { const match = selectedTextElem.innerText.match(/已选中(\d+)个/); if (match) { return parseInt(match[1], 10); } } const fileItems = document.querySelectorAll('li.fufHyA.yfHIsP'); let count = 0; fileItems.forEach(item => { const checkbox = item.querySelector('.Qxyfvg .NbKJexb'); if (checkbox && checkbox.classList.contains('icon-checksmall')) { count++; } }); return count; } } // 从指定位置开始选择指定数量的文件 function selectBatchFilesFromPosition(startIndex, count) { return new Promise((resolve) => { const rows = getFileRows(); if (rows.length === 0) { addLog('未找到文件列表', 'error'); resolve({ selected: 0, lastIndex: startIndex }); return; } let actualStart = startIndex; if (actualStart >= rows.length) { addLog(`⚠ 起始位置 ${startIndex + 1} 超出范围,重置为第1个`, 'warning'); actualStart = 0; } addLog(`共找到 ${rows.length} 个文件,从第 ${actualStart + 1} 个开始选择`); let selected = 0; let skipped = 0; let lastSelectedIdx = actualStart; for (let i = actualStart; i < rows.length && selected < count; i++) { if (!isRowSelected(rows[i])) { setRowSelected(rows[i], true); selected++; lastSelectedIdx = i; if (selected % 5 === 0) { const syncWait = Date.now() + 15; while (Date.now() < syncWait); } } else { skipped++; lastSelectedIdx = i; } } addLog(`本次选择了 ${selected} 个文件(跳过已选 ${skipped} 个),最后位置: ${lastSelectedIdx + 1}`); setTimeout(() => { const finalCount = getSelectedCount(); addLog(`当前总计已选中: ${finalCount} 个`); resolve({ selected: selected, lastIndex: lastSelectedIdx }); }, 500); }); } // 清除所有选中并重置位置记录 function clearAllSelection() { addLog('开始清除所有选中...'); const rows = getFileRows(); let clearedCount = 0; rows.forEach(row => { if (isRowSelected(row)) { setRowSelected(row, false); clearedCount++; if (clearedCount % 10 === 0) { const syncWait = Date.now() + 10; while (Date.now() < syncWait); } } }); clearLastPosition(); setTimeout(() => { const finalCount = getSelectedCount(); addLog(`清除完成,清除了 ${clearedCount} 个,当前已选中 ${finalCount} 个`); refreshSelectedDisplay(); }, 500); } // 重置选择位置 function resetSelectionPosition() { clearLastPosition(); addLog(`🔄 已重置选择位置,下次将从第1个文件开始选择`, 'success'); refreshSelectedDisplay(); } // 自动选择直到达到目标数量 async function autoSelectUntilTarget() { if (!isSelecting) { addLog('自动选择已停止', 'warning'); return; } const rows = getFileRows(); if (rows.length === 0) { addLog('未找到文件列表,请确保页面已加载完成', 'error'); stopAutoSelect(); return; } const currentSelected = getSelectedCount(); updateStatusDisplay(currentSelected, targetCount); if (currentSelected >= targetCount) { addLog(`✓ 已达到目标数量 ${targetCount} 个,自动选择完成!`, 'success'); stopAutoSelect(); return; } let startPos = lastSelectedIndex; if (startPos >= rows.length) { addLog(`⚠ 上次位置 ${startPos + 1} 超出范围,重置为第1个`, 'warning'); startPos = 0; lastSelectedIndex = 0; saveLastPosition(0); } const needSelect = targetCount - currentSelected; const toSelect = Math.min(batchSize, needSelect); addLog(`当前已选 ${currentSelected}/${targetCount},从第 ${startPos + 1} 个开始选择 ${toSelect} 个...`); const result = await selectBatchFilesFromPosition(startPos, toSelect); if (result.selected > 0) { lastSelectedIndex = result.lastIndex + 1; saveLastPosition(lastSelectedIndex); const newSelected = getSelectedCount(); if (newSelected > currentSelected && isSelecting) { autoSelectTimer = setTimeout(() => { autoSelectUntilTarget(); }, 800); } else if (newSelected < targetCount && result.lastIndex >= rows.length - 1) { addLog(`⚠ 已到达文件列表末尾,当前已选 ${newSelected} 个,未达到目标 ${targetCount} 个`, 'warning'); stopAutoSelect(); } else if (isSelecting) { autoSelectTimer = setTimeout(() => { autoSelectUntilTarget(); }, 800); } } else { if (result.lastIndex >= rows.length - 1) { addLog(`⚠ 已到达文件列表末尾,无法继续选择`, 'warning'); stopAutoSelect(); } else { addLog(`⚠ 未选择到新文件,可能已全部选中`, 'warning'); stopAutoSelect(); } } } // 开始自动选择 function startAutoSelect() { if (isSelecting) { addLog('已在运行中', 'warning'); return; } const targetInput = document.getElementById('target-count-input'); if (targetInput) { const val = parseInt(targetInput.value, 10); if (!isNaN(val) && val > 0) { targetCount = val; } } const batchInput = document.getElementById('batch-size-input'); if (batchInput) { const val = parseInt(batchInput.value, 10); if (!isNaN(val) && val > 0 && val <= 500) { batchSize = val; } } const positionInput = document.getElementById('position-input'); if (positionInput && positionInput.value) { const manualPos = parseInt(positionInput.value, 10); if (!isNaN(manualPos) && manualPos >= 0) { const totalFiles = getFileRows().length; if (manualPos <= totalFiles || totalFiles === 0) { lastSelectedIndex = manualPos; saveLastPosition(lastSelectedIndex); addLog(`📌 使用手动设置的位置: 第 ${lastSelectedIndex} 个文件`, 'info'); } else { addLog(`⚠ 手动位置 ${manualPos} 超出文件总数 ${totalFiles},使用保存的位置`, 'warning'); } } } addLog(`========== 开始自动选择 ==========`); addLog(`目标数量: ${targetCount} 个,每批次: ${batchSize} 个`); const currentSelected = getSelectedCount(); addLog(`当前已选中: ${currentSelected} 个`); const totalFiles = getFileRows().length; addLog(`文件夹内共有: ${totalFiles} 个文件`); const startPos = lastSelectedIndex; if (startPos > 0) { addLog(`📌 将从第 ${startPos + 1} 个文件开始选择`, 'info'); } else { addLog(`📌 将从第 1 个文件开始选择`, 'info'); } if (currentSelected >= targetCount) { addLog(`当前已选中 ${currentSelected} 个,已达到目标 ${targetCount},无需选择`, 'success'); return; } if (totalFiles === 0) { addLog(`未检测到文件,请刷新页面重试`, 'error'); return; } isSelecting = true; updateUIState(true); autoSelectUntilTarget(); } // 停止自动选择 function stopAutoSelect() { if (autoSelectTimer) { clearTimeout(autoSelectTimer); autoSelectTimer = null; } isSelecting = false; updateUIState(false); addLog('自动选择已停止,位置已保存,下次可继续', 'warning'); } // 更新UI按钮状态 function updateUIState(isRunning) { const buttons = [ 'start-select-btn', 'stop-select-btn', 'target-count-input', 'batch-size-input', 'position-input', 'set-position-btn', 'load-position-btn', 'clear-select-btn', 'reset-position-btn' ]; buttons.forEach(id => { const el = document.getElementById(id); if (el) { if (id === 'stop-select-btn') { el.disabled = !isRunning; } else if (id === 'start-select-btn') { el.disabled = isRunning; } else { el.disabled = isRunning; } } }); } // 更新状态显示 function updateStatusDisplay(current, target) { const statusSpan = document.getElementById('select-status'); const progressSpan = document.getElementById('select-progress'); const currentSpan = document.getElementById('current-selected'); const positionSpan = document.getElementById('last-position-display'); if (statusSpan) { statusSpan.textContent = isSelecting ? '运行中' : '空闲'; statusSpan.style.backgroundColor = isSelecting ? '#5cb85c' : 'rgba(255,255,255,0.2)'; } if (progressSpan) { progressSpan.textContent = `${current} / ${target}`; } if (currentSpan) { currentSpan.textContent = current; currentSpan.style.color = current >= target ? '#5cb85c' : '#3c8dbc'; } if (positionSpan) { const savedPos = getLastPosition(); if (savedPos > 0) { positionSpan.textContent = `第 ${savedPos} 个`; positionSpan.style.color = '#f0ad4e'; } else { positionSpan.textContent = '未设置'; positionSpan.style.color = '#999'; } } const totalFiles = getFileRows().length; const totalSpan = document.getElementById('total-files'); if (totalSpan) { totalSpan.textContent = totalFiles; } // 更新悬浮球上的数字 if (floatBall) { const countSpan = floatBall.querySelector('.float-count'); if (countSpan) { countSpan.textContent = current; } if (isSelecting) { floatBall.style.backgroundColor = '#5cb85c'; } else { floatBall.style.backgroundColor = '#3c8dbc'; } } } // 刷新当前选中数量显示 function refreshSelectedDisplay() { const count = getSelectedCount(); updateStatusDisplay(count, targetCount); } // 切换面板显示/隐藏 function togglePanel() { const panel = document.getElementById('batch-select-panel-v8'); if (panel) { if (isPanelVisible) { panel.style.display = 'none'; isPanelVisible = false; saveUIHidden(true); if (floatBall) { floatBall.style.display = 'flex'; } addLog('面板已隐藏,可通过悬浮球重新显示', 'info'); } else { panel.style.display = 'block'; isPanelVisible = true; saveUIHidden(false); if (floatBall) { floatBall.style.display = 'none'; } addLog('面板已显示', 'info'); } } } // 显示面板 function showPanel() { const panel = document.getElementById('batch-select-panel-v8'); if (panel && !isPanelVisible) { panel.style.display = 'block'; isPanelVisible = true; saveUIHidden(false); if (floatBall) { floatBall.style.display = 'none'; } addLog('面板已显示', 'info'); } } // 创建悬浮球 function createFloatBall() { if (floatBall) return; floatBall = document.createElement('div'); floatBall.id = 'float-ball'; const { left, top } = getFloatPosition(); let positionStyle = ''; if (left !== null && top !== null) { positionStyle = `left: ${left}px; top: ${top}px; right: auto; bottom: auto;`; } else { positionStyle = `right: 20px; bottom: 80px;`; } floatBall.style.cssText = ` position: fixed; ${positionStyle} width: 56px; height: 56px; background: #3c8dbc; border-radius: 50%; box-shadow: 0 2px 10px rgba(0,0,0,0.3); cursor: move; z-index: 10001; display: ${isPanelVisible ? 'none' : 'flex'}; flex-direction: column; align-items: center; justify-content: center; color: white; font-family: "Microsoft YaHei", Arial, sans-serif; transition: transform 0.2s; `; floatBall.innerHTML = `