BaiduPanFileList

统计百度盘文件(夹)数量大小

目前為 2025-09-04 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name       BaiduPanFileList
// @namespace  https://greasyfork.org/scripts/5128-baidupanfilelist/code/BaiduPanFileList.user.js
// @version    1.3.3
// @description  统计百度盘文件(夹)数量大小
// @match	*
// @include	*
// @grant GM_xmlhttpRequest
// @grant GM_setClipboard
// @run-at document-end
// @copyright  2014+, [email protected]
// ==/UserScript==

// %Path% = 文件路径
// %FileName% = 文件名
// %Tab% = Tab键
// %FileSize% = 可读文件大小(带单位保留两位小数,如:6.18 MiB)
// %FileSizeInBytes% = 文件大小字节数(为一个非负整数)
(function () {
    'use strict';

    let _BaiduPanFileList_Pattern = "%Path%%Tab%%FileSize%(%FileSizeInBytes% Bytes)";

    let url = document.URL;
    const BTN_WAITING_TEXT = "我只是测试";
    const BTN_RUNNING_TEXT = "处理中...";
    const BASE_URL_API = "https://pan.baidu.com/api/list?channel=chunlei&clienttype=0&web=1&dir=";
    const PREFIX_TO_COUNT = [];
    const SUFFIX_TO_COUNT = [' (JPG).zip', '.zip'];
    
    // 预过滤有效的前缀和后缀,避免重复计算
    const VALID_PREFIXES = PREFIX_TO_COUNT.filter(prefix => prefix && prefix.trim().length > 0);
    const VALID_SUFFIXES = SUFFIX_TO_COUNT.filter(suffix => suffix && suffix.trim().length > 0);

    // 检查是否已存在按钮,避免重复创建
    if (document.getElementById('baidupanfilelist-5128-floating-action-button')) {
        return;
    }

    // 创建按钮元素
    const button = document.createElement('div');
    button.id = 'baidupanfilelist-5128-floating-action-button';
    button.innerHTML = BTN_WAITING_TEXT;

    // 创建提示框
    const tooltip = document.createElement('div');
    tooltip.id = 'floating-button-tooltip';
    tooltip.innerHTML = '📁 点击统计当前文件夹<br/>🔍 Ctrl+点击 统计包含子文件夹<br/>⌨️ 快捷键:Q / Ctrl+Q';

    // 按钮样式
    const buttonStyles = {
        position: 'fixed',
        right: '20px',
        top: '50%',
        transform: 'translateY(-50%)',
        width: '80px',
        height: '36px',
        borderRadius: '18px',
        backgroundColor: '#007bff',
        color: 'white',
        border: 'none',
        cursor: 'pointer',
        display: 'flex',
        alignItems: 'center',
        justifyContent: 'center',
        fontSize: '12px',
        fontWeight: 'bold',
        boxShadow: '0 4px 12px rgba(0, 123, 255, 0.4)',
        zIndex: '10000',
        transition: 'background-color 0.2s ease, box-shadow 0.2s ease',
        userSelect: 'none',
        WebkitUserSelect: 'none',
        MozUserSelect: 'none',
        msUserSelect: 'none'
    };

    // 提示框样式
    const tooltipStyles = {
        position: 'fixed',
        backgroundColor: 'rgba(0, 0, 0, 0.8)',
        color: 'white',
        padding: '8px 12px',
        borderRadius: '6px',
        fontSize: '12px',
        lineHeight: '1.4',
        whiteSpace: 'nowrap',
        zIndex: '10001',
        opacity: '0',
        visibility: 'hidden',
        transition: 'all 0.3s ease',
        pointerEvents: 'none',
        transform: 'translateY(-50%)'
    };

    // 应用样式
    Object.assign(button.style, buttonStyles);
    Object.assign(tooltip.style, tooltipStyles);
    // 按钮状态
    let isProcessing = false;
    // 拖拽相关变量
    let isDragging = false;
    let hasMoved = false;
    let dragStartX, dragStartY;
    let buttonStartX, buttonStartY;
    let dragThreshold = 3; // 降低拖拽阈值,提高响应速度

    // 鼠标按下事件
    button.addEventListener('mousedown', function (e) {
        if (isProcessing) return; // 处理中不允许拖拽

        isDragging = true;
        hasMoved = false;
        dragStartX = e.clientX;
        dragStartY = e.clientY;

        const rect = button.getBoundingClientRect();
        buttonStartX = rect.left;
        buttonStartY = rect.top;

        button.style.cursor = 'grabbing';

        // 拖拽开始时隐藏提示框
        hideTooltip();

        e.preventDefault();
    });

    // 鼠标移动事件 - 优化为更流畅的拖拽
    document.addEventListener('mousemove', function (e) {
        if (!isDragging || isProcessing) return;

        const deltaX = e.clientX - dragStartX;
        const deltaY = e.clientY - dragStartY;

        // 降低拖拽阈值,提高响应速度
        if (Math.abs(deltaX) > dragThreshold || Math.abs(deltaY) > dragThreshold) {
            hasMoved = true;
        }

        const newX = buttonStartX + deltaX;
        const newY = buttonStartY + deltaY;

        // 限制按钮在视窗内
        const maxX = window.innerWidth - button.offsetWidth;
        const maxY = window.innerHeight - button.offsetHeight;

        const constrainedX = Math.max(0, Math.min(newX, maxX));
        const constrainedY = Math.max(0, Math.min(newY, maxY));

        // 使用 translate3d 进行硬件加速,提高性能
        button.style.left = constrainedX + 'px';
        button.style.top = constrainedY + 'px';
        button.style.right = 'auto';
        button.style.transform = 'translate3d(0, 0, 0)';

        e.preventDefault();
    });

    // 鼠标释放事件
    document.addEventListener('mouseup', function (e) {
        if (!isDragging) return;

        isDragging = false;
        button.style.cursor = isProcessing ? 'not-allowed' : 'pointer';

        // 如果没有移动,则触发点击事件
        if (!hasMoved && !isProcessing) {
            handleClick(e);
        }

        // 重置transform
        if (hasMoved) {
            button.style.transform = 'translate3d(0, 0, 0)';
        } else {
            button.style.transform = button.style.left ? 'translate3d(0, 0, 0)' : 'translateY(-50%)';
        }
    });

    // 点击处理函数 - 调用原有的文件统计功能
    async function handleClick(e) {
        if (isProcessing) return; // 防止重复点击

        // 检查是否按住了 Ctrl 键
        const includeSubDir = e && e.ctrlKey;

        try {
            lockButton();
            // 调用原有的文件统计功能
            showInfo(button, includeSubDir);
        } catch (error) {
            alert("❌ 处理失败\n\n💡 提示:直接点击按钮重试即可,无需刷新页面");
            unlockButton();
        }
    }

    // 悬停效果
    button.addEventListener('mouseenter', function () {
        if (!isDragging && !isProcessing) {
            button.style.transform = button.style.transform.includes('translateY') ?
                'translateY(-50%) scale(1.05)' : 'scale(1.05)';

            if (!isProcessing) {
                button.style.boxShadow = '0 6px 16px rgba(0, 123, 255, 0.6)';
            }

            // 显示提示框
            showTooltip();
        }
    });

    button.addEventListener('mouseleave', function () {
        if (!isDragging) {
            button.style.transform = button.style.transform.includes('translateY') ?
                'translateY(-50%)' : (button.style.left ? 'translate3d(0, 0, 0)' : 'none');

            if (!isProcessing) {
                button.style.boxShadow = '0 4px 12px rgba(0, 123, 255, 0.4)';
            }

            // 隐藏提示框
            hideTooltip();
        }
    });

    // 显示提示框
    function showTooltip() {
        const buttonRect = button.getBoundingClientRect();

        // 动态获取提示框实际尺寸
        tooltip.style.visibility = 'hidden';
        tooltip.style.opacity = '1';
        const tooltipRect = tooltip.getBoundingClientRect();
        const tooltipWidth = tooltipRect.width || 160; // 提供默认值
        const tooltipHeight = tooltipRect.height || 50;
        tooltip.style.opacity = '0';
        tooltip.style.visibility = 'hidden';

        // 计算按钮中心点
        const buttonCenterX = buttonRect.left + buttonRect.width / 2;
        const buttonCenterY = buttonRect.top + buttonRect.height / 2;

        // 计算屏幕中心点
        const screenCenterX = window.innerWidth / 2;
        const screenCenterY = window.innerHeight / 2;

        // 默认位置:按钮左侧
        let tooltipX = buttonRect.left - tooltipWidth - 10;
        let tooltipY = buttonCenterY - tooltipHeight / 2;

        // 判断按钮相对于屏幕中心的位置,调整提示框位置
        if (buttonCenterX > screenCenterX) {
            // 按钮在屏幕右侧,提示框显示在左侧
            tooltipX = buttonRect.left - tooltipWidth - 5;
        } else {
            // 按钮在屏幕左侧,提示框显示在右侧
            tooltipX = buttonRect.right + 5;
        }

        if (buttonCenterY > screenCenterY) {
            // 按钮在屏幕下方,提示框显示在上方
            tooltipY = buttonRect.top - tooltipHeight - 5;
        } else {
            // 按钮在屏幕上方,提示框显示在下方
            tooltipY = buttonRect.bottom + 5;
        }

        // 防止提示框超出屏幕边界
        if (tooltipX < 10) {
            tooltipX = 10;
        }
        if (tooltipX + tooltipWidth > window.innerWidth - 10) {
            tooltipX = window.innerWidth - tooltipWidth - 10;
        }
        if (tooltipY < 10) {
            tooltipY = 10;
        }
        if (tooltipY + tooltipHeight > window.innerHeight - 10) {
            tooltipY = window.innerHeight - tooltipHeight - 10;
        }

        // 应用位置
        tooltip.style.left = tooltipX + 'px';
        tooltip.style.top = tooltipY + 'px';
        tooltip.style.right = 'auto';
        tooltip.style.transform = 'none';
        tooltip.style.opacity = '1';
        tooltip.style.visibility = 'visible';
    }

    // 隐藏提示框
    function hideTooltip() {
        tooltip.style.opacity = '0';
        tooltip.style.visibility = 'hidden';
    }

    // 禁用右键菜单,防止 Ctrl+点击时弹出菜单
    button.addEventListener('contextmenu', function (e) {
        e.preventDefault();
        return false;
    });

    // 添加到页面
    document.body.appendChild(button);
    document.body.appendChild(tooltip);

    // 防止页面滚动时按钮位置错乱
    window.addEventListener('scroll', function () {
        if (!button.style.left) {
            // 如果按钮还在初始位置(右侧中间),保持fixed定位
            return;
        }
    });

    // 窗口大小改变时调整按钮位置
    window.addEventListener('resize', function () {
        if (button.style.left) {
            const rect = button.getBoundingClientRect();
            const maxX = window.innerWidth - button.offsetWidth;
            const maxY = window.innerHeight - button.offsetHeight;

            if (rect.left > maxX) {
                button.style.left = maxX + 'px';
            }
            if (rect.top > maxY) {
                button.style.top = maxY + 'px';
            }
        }
    });

    // 键盘快捷键, 确保在按钮添加失败时依旧可用
    document.addEventListener("keydown", function (e) {
        // 检查焦点元素,避免在输入框等元素中触发
        const activeElement = document.activeElement;
        const isInputElement = activeElement && (
            activeElement.tagName === 'INPUT' ||
            activeElement.tagName === 'TEXTAREA' ||
            activeElement.contentEditable === 'true'
        );

        // 如果焦点在输入元素上,不处理快捷键
        if (isInputElement) {
            return;
        }

        // 使用标准的事件对象,无需兼容性处理
        let key = e.key || e.code;

        // 检测 Q 键 (Q 或 q)
        if (key === 'q' || key === 'Q' || key === 'KeyQ') {
            lockButton();
            if (e.ctrlKey) {
                showInfo(button, true);
            } else {
                showInfo(button, false);
            }
            // 阻止默认行为
            e.preventDefault();
        }
    }, false);

    // 处理按钮和快捷键
    function showInfo(button, includeSubDir) {
        if (true) {
            let iconDemo = "📊 统计完成!\n\n";
            iconDemo += "✅ 详细文件列表已复制到剪贴板\n";
            iconDemo += "📁 点击统计当前文件夹\n";
            iconDemo += "🔍 Ctrl+点击 统计包含子文件夹\n";
            iconDemo += "⌨️ 快捷键:Q / Ctrl+Q\n";
            iconDemo += "❌ 处理失败\n";
            iconDemo += "💡 提示:直接点击按钮重试即可\n";
            iconDemo += "⚠️ 网络请求被中断\n";
            iconDemo += "⏰ 请求超时\n";
            iconDemo += "🚫 API响应错误\n";
            iconDemo += "\n包含子目录: " + includeSubDir;
            alert(iconDemo);

            // 等待2秒后解锁按钮
            setTimeout(() => {
                unlockButton();
            }, 2000);
            return;
        }
        // 是否处理错误
        let isGetListHasError = false;
        if (isProcessing) {
            return;
        }

        url = document.URL;
        while (url.includes("%25")) {
            url = url.replace("%25", "%");
        }
        let listUrl = BASE_URL_API;
        let folderAccessTimes = 0;
        let currentDir = "";

        let strAlert = "";
        let numOfAllFiles = 0;
        let numOfAllFolder = 0;
        let prefixCounts = {};
        let suffixCounts = {};
        // 根据预过滤的配置初始化计数器
        VALID_PREFIXES.forEach(prefix => {
            prefixCounts[prefix] = 0;
        });
        VALID_SUFFIXES.forEach(suffix => {
            suffixCounts[suffix] = 0;
        });
        let allFilePath = [];
        let allFileSizeInBytes = 0;
        // 百度api
        // http://pan.baidu.com/api/list?channel=chunlei&clienttype=0&web=1&num=100&page=1&dir=<PATH>&order=time&desc=1&showempty=0&_=1404279060517&bdstoken=9c11ad34c365fb633fc249d71982968f&app_id=250528
        // 测试url
        // http://pan.baidu.com/disk/home#dir/path=<PATH>
        // http://pan.baidu.com/disk/home#from=share_pan_logo&path=<PATH>
        // http://pan.baidu.com/disk/home#key=<KEY>
        // http://pan.baidu.com/disk/home#path=<PATH>
        // http://pan.baidu.com/disk/home
        // http://pan.baidu.com/disk/home#path=<PATH>&key=<KEY>
        if (!url.includes("path=")) {
            listUrl += "%2F";
            currentDir = "/";
            getList(listUrl);
        } else if (url.includes("path=")) {
            let path = url.substring(url.indexOf("path=") + 5);
            if (path.includes("&")) {
                path = path.substring(0, path.indexOf("&"));
            }
            listUrl += path;
            currentDir = decodeURIComponent(path);
            getList(listUrl);
        }

        // 请求数据
        function getList(url) {
            if (isGetListHasError) {
                return;
            }
            try {
                GM_xmlhttpRequest({
                    method: 'GET', synchronous: false, url: url, timeout: 9999,
                    onabort: function () {
                        showError("⚠️ 网络请求被中断\n\n💡 提示:直接点击按钮重试即可");
                    }, onerror: function () {
                        showError("❌ 网络请求失败\n\n💡 提示:请检查网络连接后重试");
                    }, ontimeout: function () {
                        showError("⏰ 请求超时\n\n💡 提示:网络较慢,请稍后重试");
                    }, onload: function (reText) {
                        let JSONObj = {};
                        try {
                            JSONObj = JSON.parse(reText.responseText);
                            if (JSONObj.errno !== 0) {
                                showError("🚫 API响应错误\n\n错误码: " + JSONObj.errno + "\n💡 提示:可能是权限问题");
                                return;
                            }
                        } catch (parseError) {
                            showError("📄 数据解析失败\n\n💡 提示:服务器返回的数据格式异常\n错误详情: " + parseError.message);
                            return;
                        }
                        let size_list = JSONObj.list.length;
                        let curr_item = null;
                        for (let i = 0; i < size_list; i++) {
                            curr_item = JSONObj.list[i];
                            if (curr_item.isdir === 1) {
                                numOfAllFolder++;
                                allFilePath.push(curr_item.path);
                                if (includeSubDir) {
                                    folderAccessTimes++;
                                    getList(BASE_URL_API + encodeURIComponent(curr_item.path));
                                }
                            } else {
                                numOfAllFiles++;
                                // 根据SUFFIX_TO_COUNT和PREFIX_TO_COUNT配置动态计数
                                let currItemServerFilename = curr_item.server_filename;
                                // 前缀统计
                                for (let prefix of VALID_PREFIXES) {
                                    if (currItemServerFilename.startsWith(prefix)) {
                                        prefixCounts[prefix]++;
                                        break; // 匹配到第一个前缀就停止,避免重复计数
                                    }
                                }
                                // 后缀统计
                                for (let suffix of VALID_SUFFIXES) {
                                    if (currItemServerFilename.endsWith(suffix)) {
                                        suffixCounts[suffix]++;
                                        break; // 匹配到第一个后缀就停止,避免重复计数
                                    }
                                }
                                allFileSizeInBytes += curr_item.size;
                                if (typeof _BaiduPanFileList_Pattern === "string") {
                                    allFilePath.push(_BaiduPanFileList_Pattern.replace("%FileName%", currItemServerFilename).replace("%Path%", curr_item.path).replace("%FileSizeInBytes%", curr_item.size).replace("%Tab%", "\t").replace("%FileSize%", getReadableFileSizeString(curr_item.size)));
                                } else {
                                    allFilePath.push(curr_item.path + "\t" + getReadableFileSizeString(curr_item.size) + "(" + curr_item.size + " Bytes)");
                                }
                            }
                        }
                        folderAccessTimes--;
                        if (folderAccessTimes + 1 === 0) {
                            let CTL = "\r\n";
                            let prefixCountsStr = "";
                            let suffixCountsStr = "";

                            // 按预过滤的顺序显示各前缀计数
                            VALID_PREFIXES.forEach(prefix => {
                                prefixCountsStr += prefix + ": " + prefixCounts[prefix] + CTL;
                            });

                            // 按预过滤的顺序显示各后缀计数
                            VALID_SUFFIXES.forEach(suffix => {
                                suffixCountsStr += suffix + ": " + suffixCounts[suffix] + CTL;
                            });

                            strAlert = currentDir + CTL + CTL + "files: " + numOfAllFiles + ", folders: " + numOfAllFolder + CTL + "size: " + getReadableFileSizeString(allFileSizeInBytes) + "  (" + allFileSizeInBytes.toLocaleString() + " Bytes)" + CTL+ prefixCountsStr + suffixCountsStr;
                            GM_setClipboard(strAlert + CTL + CTL + allFilePath.sort().join("\r\n") + "\r\n");
                            alert("📊 统计完成!\n\n" + strAlert.replace(/\r\n/g, "\n") + "\n\n✅ 详细文件列表已复制到剪贴板");
                            // 解锁悬浮按钮
                            unlockButton();
                        }
                    }
                });
            } catch (error) {
                showError("🔧 GM_xmlhttpRequest 调用失败\n\n💡 提示:可能是API权限问题或者返回数据格式变更,请重试\n错误详情: " + error.message);
            }
        }

        // 错误提示
        function showError(info) {
            isGetListHasError = true;
            alert(info);
            unlockButton();
        }

    }

    // 锁定按钮的方法
    function lockButton() {
        // 设置处理状态
        isProcessing = true;
        button.innerHTML = BTN_RUNNING_TEXT;
        button.style.backgroundColor = '#6c757d';
        button.style.cursor = 'not-allowed';
        button.style.boxShadow = '0 4px 12px rgba(108, 117, 125, 0.4)';
    }

    // 解锁按钮的方法
    function unlockButton() {
        isProcessing = false;
        button.innerHTML = BTN_WAITING_TEXT;
        button.style.backgroundColor = '#007bff';
        button.style.cursor = 'pointer';
        button.style.boxShadow = '0 4px 12px rgba(0, 123, 255, 0.4)';
    }

    // 转换可读文件大小
    function getReadableFileSizeString(fileSizeInBytes) {
        let size = fileSizeInBytes; // 使用局部变量,避免修改参数
        let i = 0;
        const byteUnits = [' Bytes', ' KiB', ' MiB', ' GiB', ' TiB', ' PiB', ' EiB', ' ZiB', ' YiB'];
        while (size >= 1024) {
            size = size / 1024;
            i++;
        }
        return size.toFixed(2) + byteUnits[i];
    }
})();