BaiduPanFileList

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

// ==UserScript==
// @name       BaiduPanFileList
// @namespace  https://greasyfork.org/zh-CN/scripts/5128-baidupanfilelist
// @version    2.0.014
// @description  统计百度盘文件(夹)数量大小
// @match	https://pan.baidu.com*
// @include	https://pan.baidu.com*
// @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';

    // 配置指定前缀和后缀数量统计
    const PREFIX_TO_COUNT = ['', ''];
    const SUFFIX_TO_COUNT = ['', ''];

    const RANDOM_BUTTON_COLOR = true;

    const BAIDU_PAN_FILE_LIST_PATTERN = "%Path%%Tab%%FileSize%(%FileSizeInBytes% Bytes)";

    const BUTTON_BACKGROUND_COLOR = [
        '#007BFF', '#0ABAB5', '#50C878',
        '#FF7F50', '#D4A017', '#7B1FA2',
        '#FF69B4', '#228B22', '#948DD6',
        '#FF8C00', '#C71585', '#EF4444'
    ];

    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 VALID_PREFIXES = PREFIX_TO_COUNT.filter(prefix => prefix && prefix.trim().length > 0).map(prefix => prefix.toLowerCase());
    const VALID_SUFFIXES = SUFFIX_TO_COUNT.filter(suffix => suffix && suffix.trim().length > 0).map(suffix => suffix.toLowerCase());

    // 按钮颜色 - 从预设颜色中随机选择
    const buttonColorHex = RANDOM_BUTTON_COLOR ? BUTTON_BACKGROUND_COLOR[Math.floor(Math.random() * BUTTON_BACKGROUND_COLOR.length)] : BUTTON_BACKGROUND_COLOR[0];

    // 将十六进制颜色转换为RGB
    function hexToRgb(hex) {
        const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
        return result ? {
            r: parseInt(result[1], 16),
            g: parseInt(result[2], 16),
            b: parseInt(result[3], 16)
        } : null;
    }

    const buttonColorRgb = hexToRgb(buttonColorHex);
    const buttonColorRgba = `rgba(${buttonColorRgb.r}, ${buttonColorRgb.g}, ${buttonColorRgb.b}, 0.4)`;
    const buttonColorRgbaHover = `rgba(${buttonColorRgb.r}, ${buttonColorRgb.g}, ${buttonColorRgb.b}, 0.6)`;

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

    // 检查是否在顶级窗口中,如果不是则退出(避免在iframe中重复创建)
    if (window !== window.top) {
        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: 'auto',
        minWidth: '80px',
        height: '36px',
        borderRadius: '18px',
        backgroundColor: buttonColorHex,
        color: 'white',
        border: 'none',
        cursor: 'pointer',
        display: 'flex',
        alignItems: 'center',
        justifyContent: 'center',
        padding: '0 12px',
        fontSize: '12px',
        fontWeight: 'bold',
        boxShadow: `0 4px 12px ${buttonColorRgba}`,
        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 {
            // 调用原有的文件统计功能
            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 ${buttonColorRgbaHover}`;
            }

            // 显示提示框
            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 ${buttonColorRgba}`;
            }

            // 隐藏提示框
            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 () {
        const rect = button.getBoundingClientRect();
        const maxX = window.innerWidth - button.offsetWidth - 20; // 保持20px边距
        const maxY = window.innerHeight - button.offsetHeight;

        // 如果按钮被挤出右边界,调整到安全位置
        if (rect.right > window.innerWidth - 20) {
            if (button.style.left) {
                // 拖拽后的按钮
                button.style.left = Math.max(20, maxX) + 'px';
            } else {
                // 初始位置的按钮,切换到left定位
                button.style.right = 'auto';
                button.style.left = Math.max(20, maxX) + 'px';
            }
        }

        // 垂直位置保护
        if (rect.bottom > window.innerHeight) {
            button.style.top = Math.max(20, 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') {
            if (e.ctrlKey) {
                showInfo(button, true);
            } else {
                showInfo(button, false);
            }
            // 阻止默认行为
            e.preventDefault();
        }
    }, false);

    // 处理按钮和快捷键
    function showInfo(button, includeSubDir) {
        // 是否处理错误
        let isGetListHasError = false;
        if (isProcessing) {
            return;
        }
        lockButton();

        // 记录开始时间
        const startTime = Date.now();

        let url = document.URL;
        while (url.includes("%25")) {
            url = url.replace("%25", "%");
        }
        let listUrl = BASE_URL_API;
        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);
        }

        let currNumOfAccessFolder = 1;
        // 请求数据
        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) {
                                    currNumOfAccessFolder++;
                                    getList(BASE_URL_API + encodeURIComponent(curr_item.path));
                                }
                            } else {
                                numOfAllFiles++;
                                setButtonText(BTN_RUNNING_TEXT + "(" + numOfAllFiles + ")");
                                // 根据SUFFIX_TO_COUNT和PREFIX_TO_COUNT配置动态计数
                                let currItemServerFilename = curr_item.server_filename;
                                // 前缀统计
                                for (let prefix of VALID_PREFIXES) {
                                    if (currItemServerFilename.toLowerCase().startsWith(prefix)) {
                                        prefixCounts[prefix]++;
                                        break; // 匹配到第一个前缀就停止,避免重复计数
                                    }
                                }
                                // 后缀统计
                                for (let suffix of VALID_SUFFIXES) {
                                    if (currItemServerFilename.toLowerCase().endsWith(suffix)) {
                                        suffixCounts[suffix]++;
                                        break; // 匹配到第一个后缀就停止,避免重复计数
                                    }
                                }
                                allFileSizeInBytes += curr_item.size;
                                if (typeof BAIDU_PAN_FILE_LIST_PATTERN === "string") {
                                    allFilePath.push(BAIDU_PAN_FILE_LIST_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)");
                                }
                            }
                        }
                        currNumOfAccessFolder--;
                        if (currNumOfAccessFolder === 0) {
                            const 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 + "文件夹数量: " + numOfAllFolder + ", 文件数量: " + numOfAllFiles + CTL + "大小: " + getReadableFileSizeString(allFileSizeInBytes) + "  (" + allFileSizeInBytes.toLocaleString() + " Bytes)" + CTL + prefixCountsStr + suffixCountsStr;
                            GM_setClipboard(strAlert + CTL + CTL + allFilePath.sort().join("\r\n") + "\r\n");
                            // 计算耗时
                            let durationSecondsStr = ((Date.now() - startTime) / 1000).toFixed(2);
                            window.setTimeout(() => {
                                alert("📊 统计完成" + (includeSubDir ? "(含子文件夹)" : "(仅当前文件夹)") + "!耗时 " + durationSecondsStr + " 秒\n\n" + strAlert.replace(/\r\n/g, "\n") + "\n\n✅ 详细文件列表已复制到剪贴板");
                                // 解锁悬浮按钮
                                unlockButton();
                            }, 0);
                        }
                    }
                });
            } catch (error) {
                showError("🔧 GM_xmlhttpRequest 调用失败\n\n💡 提示:可能是API权限问题或者返回数据格式变更,请重试\n错误详情: " + error.message);
            }
        }

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

    }

    // 锁定按钮的方法
    function lockButton() {
        // 设置处理状态
        isProcessing = true;
        setButtonText(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;
        setButtonText(BTN_WAITING_TEXT);
        button.style.backgroundColor = buttonColorHex;
        button.style.cursor = 'pointer';
        button.style.boxShadow = `0 4px 12px ${buttonColorRgba}`;
    }

    // 解锁按钮的方法
    function setButtonText(text) {
        button.innerHTML = text;
        button.style.width = 'auto';
        void button.offsetWidth;
    }

    // 转换可读文件大小
    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];
    }
})();