动漫花园树状显示

将动漫花园的文件列表转换为树状视图,支持搜索、智能折叠等功能

目前为 2025-01-15 提交的版本。查看 最新版本

// ==UserScript==
// @name         动漫花园树状显示
// @name:zh-CN   动漫花园树状显示
// @name:en      DMHY Tree View
// @namespace    https://github.com/xkbkx5904/dmhy-tree-view
// @version      0.4.0
// @description  将动漫花园的文件列表转换为树状视图,支持搜索、智能折叠等功能
// @description:zh-CN  将动漫花园的文件列表转换为树状视图,支持搜索、智能折叠等功能
// @description:en  Convert DMHY file list into a tree view with search and smart collapse features
// @author       xkbkx5904
// @license      GPL-3.0
// @homepage     https://github.com/xkbkx5904/dmhy-tree-view
// @supportURL   https://github.com/xkbkx5904/dmhy-tree-view/issues
// @match        *://share.dmhy.org/topics/view/*
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/jquery.min.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/jstree.min.js
// @resource     customCSS https://cdn.jsdelivr.net/npm/[email protected]/dist/themes/default/style.min.css
// @grant        GM_addStyle
// @grant        GM_getResourceText
// @run-at       document-end
// @originalAuthor TautCony
// @originalURL  https://gf.qytechs.cn/zh-CN/scripts/26430-dmhy-tree-view
// ==/UserScript==

// 文件类型图标映射
const ICONS = {
    audio: "/images/icon/mp3.gif",
    bmp: "/images/icon/bmp.gif",
    image: "/images/icon/jpg.gif",
    png: "/images/icon/png.gif",
    rar: "/images/icon/rar.gif",
    text: "/images/icon/txt.gif",
    unknown: "/images/icon/unknown.gif",
    video: "/images/icon/mp4.gif"
};

// 文件扩展名分类
const FILE_TYPES = {
    audio: ["flac", "aac", "wav", "mp3", "m4a", "mka"],
    bmp: ["bmp"],
    image: ["jpg", "jpeg", "webp"],
    png: ["png", "gif"],
    rar: ["rar", "zip", "7z", "tar", "gz"],
    text: ["txt", "log", "cue", "ass", "ssa", "srt", "doc", "docx", "xls", "xlsx", "pdf"],
    video: ["mkv", "mp4", "avi", "wmv", "flv", "m2ts"]
};

// 设置样式
const setupCSS = () => {
    GM_addStyle(GM_getResourceText("customCSS"));
    GM_addStyle(`
        .jstree-node, .jstree-default .jstree-icon {
            background-image: url(https://cdn.jsdelivr.net/npm/[email protected]/dist/themes/default/32px.png);
        }

        .tree-container {
            background: #fff;
            border: 2px solid;
            border-color: #404040 #dfdfdf #dfdfdf #404040;
            padding: 5px;
        }

        .control-panel {
            background: #f0f0f0;
            border-bottom: 1px solid #ccc;
            padding: 5px;
            display: flex;
            align-items: center;
            gap: 10px;
        }

        .control-panel-left {
            display: flex;
            align-items: center;
            gap: 10px;
        }

        .control-panel-right {
            margin-left: auto;
            display: flex;
            align-items: center;
        }

        #search_input {
            border: 1px solid #ccc;
            padding: 2px 5px;
            width: 200px;
        }

        #switch {
            padding: 2px 5px;
            cursor: pointer;
        }

        #file_tree {
            padding: 5px;
            max-height: 600px;
            overflow: auto;
        }

        .filesize {
            padding-left: 8px;
            color: #666;
        }

        .smart-toggle {
            display: flex;
            align-items: center;
            gap: 4px;
            cursor: pointer;
            user-select: none;
        }

        .smart-toggle input {
            margin: 0;
        }
    `);
};

// 树节点基础类
class TreeNode {
    constructor(name) {
        this.name = name;
        this.length = 0;
        this.childNode = new Map();
        this._cache = new Map();
    }

    // 插入节点
    insert(path, size) {
        let currentNode = this;
        for (const node of path) {
            if (!currentNode.childNode.has(node)) {
                currentNode.childNode.set(node, new TreeNode(node));
            }
            currentNode = currentNode.childNode.get(node);
        }
        currentNode.length = this.toLength(size);
        return currentNode;
    }

    // 转换为显示文本
    toString() {
        const size = this.childNode.size > 0 ? this.calculateTotalSize() : this.length;
        return `<span class="filename">${this.name}</span><span class="filesize">${this.toSize(size)}</span>`;
    }

    // 计算总大小
    calculateTotalSize() {
        if (this._cache.has('totalSize')) return this._cache.get('totalSize');

        let total = this.length;
        for (const node of this.childNode.values()) {
            total += node.childNode.size === 0 ? node.length : node.calculateTotalSize();
        }

        this._cache.set('totalSize', total);
        return total;
    }

    // 转换为jstree对象
    toObject() {
        if (this._cache.has('object')) return this._cache.get('object');

        const ret = {
            text: this.toString(),
            children: [],
            state: { opened: false }
        };

        // 分别处理文件夹和文件
        const folders = [];
        const files = [];

        for (const [, value] of this.childNode) {
            if (value.childNode.size === 0) {
                files.push({
                    icon: value.icon,
                    length: value.length,
                    text: value.toString()
                });
            } else {
                const inner = value.toObject();
                folders.push({
                    ...inner,
                    text: `<span class="filename">${value.name}</span><span class="filesize">${this.toSize(value.calculateTotalSize())}</span>`,
                    state: { opened: false }
                });
            }
        }

        ret.children = [...folders, ...files];
        this._cache.set('object', ret);
        return ret;
    }

    // 获取文件扩展名
    get ext() {
        if (this._ext !== undefined) return this._ext;
        const dotIndex = this.name.lastIndexOf(".");
        this._ext = dotIndex > 0 ? this.name.substr(dotIndex + 1).toLowerCase() : "";
        return this._ext;
    }

    // 获取文件图标
    get icon() {
        if (this._icon !== undefined) return this._icon;
        this._icon = ICONS.unknown;
        for (const [type, extensions] of Object.entries(FILE_TYPES)) {
            if (extensions.includes(this.ext)) {
                this._icon = ICONS[type];
                break;
            }
        }
        return this._icon;
    }

    // 转换文件大小字符串为字节数
    toLength(size) {
        if (!size) return -1;
        const match = size.toLowerCase().match(/^([\d.]+)\s*([kmgt]?b(?:ytes)?)$/);
        if (!match) return -1;
        const [, value, unit] = match;
        const factors = { b: 0, bytes: 0, kb: 10, mb: 20, gb: 30, tb: 40 };
        return parseFloat(value) * Math.pow(2, factors[unit] || 0);
    }

    // 转换字节数为可读大小
    toSize(length) {
        if (length < 0) return "";
        const units = [[40, "TiB"], [30, "GiB"], [20, "MiB"], [10, "KiB"], [0, "Bytes"]];
        for (const [factor, unit] of units) {
            if (length >= Math.pow(2, factor)) {
                return (length / Math.pow(2, factor)).toFixed(unit === "Bytes" ? 0 : 3) + unit;
            }
        }
        return "0 Bytes";
    }
}

// 查找树中第一个分叉节点
function findFirstForkNode(tree) {
    const findForkInNode = (nodeId) => {
        const node = tree.get_node(nodeId);
        if (!node || !node.children) return null;
        if (node.children.length > 1) return node;
        if (node.children.length === 1) return findForkInNode(node.children[0]);
        return null;
    };
    return findForkInNode('#');
}

// 获取到指定节点的路径
function getPathToNode(tree, targetNode) {
    const path = [];
    let currentNode = targetNode;
    while (currentNode.id !== '#') {
        path.unshift(currentNode.id);
        currentNode = tree.get_node(tree.get_parent(currentNode));
    }
    return path;
}

// 获取第一个分叉点及其路径信息
function getFirstForkInfo(tree) {
    const firstFork = findFirstForkNode(tree);
    if (!firstFork) return null;
    
    const pathToFork = getPathToNode(tree, firstFork);
    const protectedNodes = new Set(pathToFork);
    protectedNodes.add(firstFork.id);
    
    return {
        fork: firstFork,
        pathToFork,
        protectedNodes
    };
}

// 智能折叠:只折叠分叉点以下的节点
function smartCollapse(tree) {
    const forkInfo = getFirstForkInfo(tree);
    if (!forkInfo) return;

    // 获取所有打开的节点
    const openNodes = tree.get_json('#', { flat: true })
        .filter(node => tree.is_open(node.id))
        .map(node => node.id);
    
    // 只折叠不在保护名单中的节点
    openNodes.forEach(nodeId => {
        if (!forkInfo.protectedNodes.has(nodeId)) {
            tree.close_node(nodeId);
        }
    });
}

// 主程序入口
(() => {
    // 设置样式
    setupCSS();

    // 创建树数据
    const data = new TreeNode($(".topic-title > h3").text());
    const pattern = /^(.+?) (\d+(?:\.\d+)?[TGMK]?B(?:ytes)?)$/;

    // 解析文件列表
    $(".file_list:first > ul li").each(function() {
        const text = $(this).text().trim();
        const line = text.replace(/\t+/i, "\t").split("\t");

        if (line.length === 2) {
            data.insert(line[0].split("/"), line[1]);
        } else if (line.length === 1) {
            const match = pattern.exec(text);
            if (match) {
                data.insert(match[1].split("/"), match[2]);
            } else {
                data.insert(line[0].split("/"), "");
            }
        }
    });

    // 创建UI
    const fragment = document.createDocumentFragment();
    const treeContainer = $('<div class="tree-container"></div>').appendTo(fragment);
    
    const controlPanel = $('<div class="control-panel"></div>')
        .append($('<div class="control-panel-left"></div>')
            .append('<input type="text" id="search_input" placeholder="搜索文件..." />')
            .append('<button id="switch">展开全部</button>')
        )
        .append($('<div class="control-panel-right"></div>')
            .append('<label class="smart-toggle"><input type="checkbox" id="smart_mode" />智能展开</label>')
        )
        .appendTo(treeContainer);
    
    const fileTree = $('<div id="file_tree"></div>').appendTo(treeContainer);
    $('.file_list:first').replaceWith(fragment);

    // 创建树实例
    const treeInstance = fileTree.jstree({
        core: { 
            data: data.toObject(),
            themes: { variant: "large" }
        },
        plugins: ["search", "wholerow", "contextmenu"],
        contextmenu: {
            select_node: false,
            show_at_node: false,
            items: {
                getText: {
                    label: "复制",
                    action: selected => {
                        const text = selected.reference.find(".filename").text();
                        navigator.clipboard.writeText(text);
                    }
                }
            }
        }
    });

    // 绑定事件
    treeInstance.on("ready.jstree", function() {
        const tree = treeInstance.jstree(true);
        const isSmartMode = localStorage.getItem('dmhy_smart_mode') !== 'false';
        
        if (isSmartMode) {
            const firstFork = findFirstForkNode(tree);
            if (firstFork) {
                const pathToFork = getPathToNode(tree, firstFork);
                pathToFork.forEach(nodeId => tree.open_node(nodeId));
            }
        }
    });

    treeInstance.on("loaded.jstree", function() {
        const tree = treeInstance.jstree(true);
        let isExpanded = false;
        let isSmartMode = localStorage.getItem('dmhy_smart_mode') !== 'false';
        let previousState = null;
        let hasSearched = false;
        
        // 更新展开/折叠按钮状态
        const updateSwitchButton = () => {
            $("#switch").text(isExpanded ? "折叠全部" : "展开全部");
        };

        // 绑定展开/折叠按钮事件
        $("#switch").click(function() {
            isExpanded = !isExpanded;
            
            if (isSmartMode) {
                if (isExpanded) {
                    tree.open_all();
                } else {
                    smartCollapse(tree);  // 使用新的智能折叠函数
                }
            } else {
                if (isExpanded) {
                    tree.open_all();
                } else {
                    tree.close_all();
                }
            }
            
            updateSwitchButton();
        });

        // 绑定智能模式切换事件
        $("#smart_mode").prop('checked', isSmartMode).change(function() {
            isSmartMode = this.checked;
            localStorage.setItem('dmhy_smart_mode', isSmartMode);
            
            isExpanded = false;
            localStorage.setItem('dmhy_tree_expanded', isExpanded);
            
            if (isSmartMode) {
                tree.close_all();
                const firstFork = findFirstForkNode(tree);
                if (firstFork) {
                    const pathToFork = getPathToNode(tree, firstFork);
                    pathToFork.forEach(nodeId => tree.open_node(nodeId));
                }
            } else {
                tree.close_all();
            }
            
            updateSwitchButton();
        });

        // 绑定搜索事件
        const searchDebounceTime = 250;
        let treeNodes = null;  // 缓存所有节点

        // 初始化时缓存所有节点
        treeNodes = tree.get_json('#', { flat: true });

        $('#search_input').keyup(function() {
            if(searchTimeout) clearTimeout(searchTimeout);
            searchTimeout = setTimeout(() => {
                const searchText = $('#search_input').val().toLowerCase();
                
                if (searchText) {
                    if (!hasSearched) {
                        // 仅在第一次搜索时保存状态
                        previousState = {
                            isExpanded,
                            openNodes: treeNodes.filter(node => tree.is_open(node.id))
                                .map(node => node.id)
                        };
                        hasSearched = true;
                    }
                    
                    // 使用缓存的节点数据进行过滤
                    const matchedNodes = new Set();
                    treeNodes.forEach(node => {
                        const nodeText = tree.get_text(node.id).toLowerCase();
                        if (nodeText.includes(searchText)) {
                            matchedNodes.add(node.id);
                            // 一次性获取所有父节点
                            let parent = tree.get_parent(node.id);
                            while (parent !== '#') {
                                matchedNodes.add(parent);
                                parent = tree.get_parent(parent);
                            }
                        }
                    });

                    // 批量处理节点显示/隐藏
                    const operations = [];
                    treeNodes.forEach(node => {
                        if (matchedNodes.has(node.id)) {
                            operations.push(() => {
                                tree.show_node(node.id);
                                tree.open_node(node.id);
                            });
                        } else {
                            operations.push(() => tree.hide_node(node.id));
                        }
                    });

                    // 使用 requestAnimationFrame 分批执行操作
                    const batchSize = 50;
                    const executeBatch = (startIndex) => {
                        const endIndex = Math.min(startIndex + batchSize, operations.length);
                        for (let i = startIndex; i < endIndex; i++) {
                            operations[i]();
                        }
                        if (endIndex < operations.length) {
                            requestAnimationFrame(() => executeBatch(endIndex));
                        }
                    };
                    executeBatch(0);
                    
                    isExpanded = true;
                } else {
                    // 还原状态
                    if (previousState) {
                        tree.show_all();
                        tree.close_all();
                        // 批量还原打开的节点
                        const restoreNodes = () => {
                            const batch = previousState.openNodes.splice(0, 50);
                            batch.forEach(nodeId => tree.open_node(nodeId, false));
                            if (previousState.openNodes.length > 0) {
                                requestAnimationFrame(restoreNodes);
                            }
                        };
                        restoreNodes();
                        
                        isExpanded = previousState.isExpanded;
                        previousState = null;
                        hasSearched = false;
                    }
                }
                
                updateSwitchButton();
            }, searchDebounceTime);
        });
    });
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址