Greasy Fork

来自缓存

Greasy Fork is available in English.

动漫花园树状显示

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

安装此脚本
作者推荐脚本

您可能也喜欢动漫花园种子屏蔽助手

安装此脚本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         动漫花园树状显示
// @name:zh-CN   动漫花园文件列表树状显示
// @name:en      DMHY Tree View
// @namespace    https://github.com/xkbkx5904/dmhy-tree-view
// @version      0.5.3
// @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
// @icon         https://share.dmhy.org/favicon.ico
// @grant        GM_addStyle
// @grant        GM_getResourceText
// @run-at       document-end
// @originalAuthor TautCony
// @originalURL  http://greasyfork.icu/zh-CN/scripts/26430-dmhy-tree-view
// ==/UserScript==

/* 更新日志
 * v0.5.3
 * - 刚刚更新了日志但是代码复制了旧版本的,幽默了一下,更新版本号从新推送一下代码
 *
 * v0.5.2
 * - 修复了当文件无法提取到文件名时,文件大小被识别为文件名的问题
 * - 修复了智能展开模式下,单层级目录没有自动打开的问题
 *
 * v0.5.1
 * - 修复智能模式下单层目录展开/折叠的问题
 * 
 * v0.5.0
 * - 添加文件名和大小排序功能
 * - 优化搜索性能
 * - 修复搜索和排序功能的冲突问题
 * - 添加动漫花园网站图标
 * 
 * v0.4.0
 * - 初始版本发布
 * - 实现树状显示功能
 * - 添加搜索功能
 * - 添加智能展开功能
 */
 
// 文件类型图标映射
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;
        }
 
        .sort-controls {
            display: flex;
            align-items: center;
            gap: 5px;
        }
 
        .sort-btn {
            padding: 2px 8px;
            cursor: pointer;
            border: 1px solid #ccc;
            background: #f8f8f8;
            display: flex;
            align-items: center;
            gap: 4px;
        }
 
        .sort-btn.active {
            background: #e0e0e0;
        }
 
        .sort-direction {
            display: inline-block;
            width: 12px;
        }
    `);
};
 
// 树节点基础类
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, treeDepth) {
    // 如果是单层目录,直接使用普通折叠
    if (treeDepth <= 1) {
        tree.close_all();
        return;
    }
    
    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);
        }
    });
}
 
// 检查文件树的最大层级
function checkTreeDepth(tree) {
    const getNodeDepth = (nodeId, currentDepth = 0) => {
        const node = tree.get_node(nodeId);
        // 如果节点不存在,或者是文件节点(没有子节点),返回当前深度
        if (!node || !node.children || node.children.length === 0) {
            return currentDepth - 1; // 文件节点不计入深度
        }
        return Math.max(...node.children.map(childId => 
            getNodeDepth(childId, currentDepth + 1)
        ));
    };
    return Math.max(0, getNodeDepth('#'));
}
 
// 主程序入口
(() => {
    // 设置样式
    setupCSS();
 
    // 创建树数据
    const data = new TreeNode($(".topic-title > h3").text());
    const pattern = /^(.+?) (\d+(?:\.\d+)?[TGMK]?B(?:ytes)?)$/;
 
    // 解析文件列表
    let unnamedCounter = 1;  // 添加计数器用于区分未命名文件

    $(".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 {
                // 检查是否只是一个文件大小
                const sizeMatch = /^\d+(?:\.\d+)?[TGMK]?B(?:ytes)?$/.test(text);
                if (sizeMatch) {
                    // 如果只是文件大小,使用带编号的占位符作为文件名
                    data.insert([`unknown (${unnamedCounter++})`], text);
                } else {
                    // 如果不是文件大小,则视为纯文件名
                    data.insert(text.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="sort-controls"></div>')
                .append('<button class="sort-btn" data-sort="name">名称<span class="sort-direction">↑</span></button>')
                .append('<button class="sort-btn" data-sort="size">大小<span class="sort-direction">↓</span></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 treeDepth = checkTreeDepth(tree);
            
            if (treeDepth > 1) {
                // 多层目录时执行智能展开
                const firstFork = findFirstForkNode(tree);
                if (firstFork) {
                    const pathToFork = getPathToNode(tree, firstFork);
                    pathToFork.forEach(nodeId => tree.open_node(nodeId));
                }
            } else {
                // 单层目录时全部展开
                tree.open_all();
            }
        }
    });
 
    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;
        let searchTimeout = null;
        let treeNodes = null;

        // 1. 更新展开/折叠按钮状态的函数
        const updateSwitchButton = () => {
            $("#switch").text(isExpanded ? "折叠全部" : "展开全部");
        };

        // 2. 绑定展开/折叠按钮事件
        $("#switch").click(function() {
            isExpanded = !isExpanded;
            const treeDepth = checkTreeDepth(tree);
            
            if (isSmartMode) {
                if (isExpanded) {
                    tree.open_all();
                } else {
                    if (treeDepth > 1) {
                        // 多层目录时使用智能折叠
                        smartCollapse(tree, treeDepth);
                    } else {
                        // 单层目录时全部折叠
                        tree.close_all();
                    }
                }
            } else {
                if (isExpanded) {
                    tree.open_all();
                } else {
                    tree.close_all();
                }
            }
            
            updateSwitchButton();
        });

        // 3. 绑定智能模式切换事件
        $("#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();
        });

        // 4. 初始化排序
        const rootNode = tree.get_node('#');
        $('.sort-btn[data-sort="name"]').addClass('active').find('.sort-direction').text('↑');

        const sortNodes = (node, sortType, isAsc) => {
            if (node.children && node.children.length) {
                node.children.sort((a, b) => {
                    const nodeA = tree.get_node(a);
                    const nodeB = tree.get_node(b);
                    
                    // 文件夹始终排在前面
                    const isAFolder = nodeA.children.length > 0;
                    const isBFolder = nodeB.children.length > 0;
                    if (isAFolder !== isBFolder) {
                        return isAFolder ? -1 : 1;
                    }

                    let result = 0;
                    if (sortType === 'size') {
                        const sizeA = parseFloat(nodeA.text.match(/[\d.]+(?=[TGMK]iB|Bytes)/)) || 0;
                        const sizeB = parseFloat(nodeB.text.match(/[\d.]+(?=[TGMK]iB|Bytes)/)) || 0;
                        const unitA = nodeA.text.match(/[TGMK]iB|Bytes/)?.[0] || '';
                        const unitB = nodeB.text.match(/[TGMK]iB|Bytes/)?.[0] || '';
                        
                        const units = { 'TiB': 4, 'GiB': 3, 'MiB': 2, 'KiB': 1, 'Bytes': 0 };
                        const unitCompare = (units[unitA] || 0) - (units[unitB] || 0);
                        
                        result = unitCompare !== 0 ? unitCompare : sizeA - sizeB;
                    } else {
                        const nameA = nodeA.text.match(/class="filename">([^<]+)/)?.[1] || '';
                        const nameB = nodeB.text.match(/class="filename">([^<]+)/)?.[1] || '';
                        result = nameA.localeCompare(nameB, undefined, { numeric: true });
                    }
                    
                    return isAsc ? result : -result;
                });

                node.children.forEach(childId => {
                    sortNodes(tree.get_node(childId), sortType, isAsc);
                });
            }
        };

        // 执行初始排序(按文件名升序)
        sortNodes(rootNode, 'name', true);
        tree.redraw(true);

        // 绑定排序按钮事件
        $('.sort-btn').on('click', function() {
            const $this = $(this);
            const $direction = $this.find('.sort-direction');
            const sortType = $this.data('sort');
            
            if ($this.hasClass('active')) {
                $direction.text($direction.text() === '↑' ? '↓' : '↑');
            } else {
                $('.sort-btn').removeClass('active').find('.sort-direction').text('↓');
                $this.addClass('active');
            }
            
            const isAsc = $direction.text() === '↑';
            sortNodes(rootNode, sortType, isAsc);
            tree.redraw(true);
        });

        // 5. 初始化搜索功能
        treeNodes = tree.get_json('#', { flat: true });  // 缓存已排序的节点
        const searchDebounceTime = 250;

        $('#search_input').keyup(function() {
            if (searchTimeout) {
                clearTimeout(searchTimeout);
            }
            
            searchTimeout = setTimeout(() => {
                const searchText = $(this).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));
                        }
                    });

                    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);
        });
    });
})();