omomo / AnkiWeb Deck Collapse

// ==UserScript==
// @name         AnkiWeb Deck Collapse
// @name:zh-CN   AnkiWeb Deck 折叠功能
// @namespace    https://github.com/deepseek-ai/DeepSeek-V3
// @version      1.0
// @description  Add collapse/expand functionality to AnkiWeb deck pages with hierarchical indentation.
// @description:zh-CN 在 AnkiWeb 的 deck 页面添加折叠/展开功能,并支持层级缩进。
// @author       DeepSeek-V3 | ovo
// @match        https://ankiweb.net/*
// @grant        none
// @license      MIT
// @updateURL https://openuserjs.org/meta/omomo/AnkiWeb_Deck_Collapse.meta.js
// @downloadURL https://openuserjs.org/install/omomo/AnkiWeb_Deck_Collapse.user.js
// ==/UserScript==

(function() {
    // 初始化函数
    function initializeDeckCollapse() {
        // 只在/decks路径下运行
        if (window.location.pathname !== '/decks') {
            return;
        }

        // 获取所有的deck行
        const deckRows = document.querySelectorAll('.row.light-bottom-border');

        // 用于存储每个deck的层级关系和子deck
        const deckMap = new Map();

        // 遍历每个deck行,构建层级关系
        deckRows.forEach((row, index) => {
            const button = row.querySelector('button');
            const textContent = button.textContent;

            // 计算空格数量来确定层级
            const indentLevel = (textContent.match(/^\s*/) || [''])[0].length;

            // 存储当前deck的层级和子deck
            deckMap.set(row, {
                indentLevel,
                children: [],
                isCollapsed: true // 默认收起
            });

            // 找到当前deck的父deck
            if (indentLevel > 0) {
                for (let i = index - 1; i >= 0; i--) {
                    const parentRow = deckRows[i];
                    const parentIndentLevel = deckMap.get(parentRow).indentLevel;

                    // 如果找到父deck
                    if (parentIndentLevel < indentLevel) {
                        deckMap.get(parentRow).children.push(row);
                        break;
                    }
                }
            }
        });

        // 遍历每个deck行,添加展开/收起按钮和缩进
        deckRows.forEach(row => {
            const { indentLevel, children } = deckMap.get(row);
            const button = row.querySelector('button');
            const deckName = button.textContent.trim();

            // 修改button的文本为deckName
            button.textContent = deckName;

            // 计算margin-left
            const marginLeft = Math.max((indentLevel - 4), 0) + 'em';

            // 如果当前deck有子deck,则添加展开/收起按钮
            if (children.length > 0) {
                // 创建展开/收起按钮
                const toggleButton = document.createElement('button');
                toggleButton.textContent = '+'; // 默认收起
                toggleButton.style.background = 'none';
                toggleButton.style.border = 'none';
                toggleButton.style.cursor = 'pointer';
                toggleButton.style.marginLeft = marginLeft; // 设置缩进

                // 检查localStorage中是否存储了该deck的展开状态
                const isCollapsed = localStorage.getItem(`deckCollapsed_${deckName}`) !== 'false';

                // 如果存储的状态是展开,则展开直接子deck
                if (!isCollapsed) {
                    toggleButton.textContent = '-';
                    toggleDirectChildren(row, false); // 展开直接子deck
                } else {
                    toggleDirectChildren(row, true); // 收起直接子deck
                }

                // 添加点击事件来切换展开/收起状态
                toggleButton.addEventListener('click', () => {
                    const isCollapsed = toggleButton.textContent === '+';
                    if (isCollapsed) {
                        toggleButton.textContent = '-';
                        toggleDirectChildren(row, false); // 展开直接子deck
                        localStorage.setItem(`deckCollapsed_${deckName}`, 'false');
                    } else {
                        toggleButton.textContent = '+';
                        toggleDirectChildren(row, true); // 收起直接子deck
                        localStorage.setItem(`deckCollapsed_${deckName}`, 'true');
                    }
                });

                // 将按钮插入到button之前
                button.parentNode.insertBefore(toggleButton, button);
            } else {
                // 最低层级deck,只设置margin-left
                button.style.marginLeft = marginLeft;
            }
        });

        // 切换直接子deck的显示状态
        function toggleDirectChildren(parentRow, isCollapsed) {
            const children = deckMap.get(parentRow).children;
            children.forEach(child => {
                child.style.display = isCollapsed ? 'none' : '';
            });
        }
    }

    // 监听点击事件,检测导航变化
    function observeNavigation() {
        document.body.addEventListener('click', (event) => {
            const target = event.target;
            if (target.tagName === 'A' && target.hostname === window.location.hostname) {
                // 如果点击的是当前站点的链接,重新初始化脚本
                reinitializeScript();
            }
        });
    }

    // 重新初始化脚本
    function reinitializeScript() {
        // 重新观察 DOM 变化
        observer.disconnect();
        observer.observe(document.body, {
            childList: true, // 观察子节点的变化
            subtree: true // 观察所有后代节点
        });
    }

    // 监听 DOM 变化
    const observer = new MutationObserver((mutationsList, observer) => {
        // 检查是否有动态内容加载
        const deckRows = document.querySelectorAll('.row.light-bottom-border');
        if (deckRows.length > 0) {
            // 停止观察,避免重复运行
            observer.disconnect();

            // 执行主逻辑
            initializeDeckCollapse();
        }
    });

    // 开始观察整个文档
    observer.observe(document.body, {
        childList: true, // 观察子节点的变化
        subtree: true // 观察所有后代节点
    });

    // 监听导航变化
    observeNavigation();
})();