超星学习通资源下载

点击按钮弹出模态框选择下载PDF/课件/视频。按D键快速下载全部PDF。修复多选下载。

// ==UserScript==
// @name         超星学习通资源下载
// @namespace    http://tampermonkey.net/
// @version      1.15
// @description  点击按钮弹出模态框选择下载PDF/课件/视频。按D键快速下载全部PDF。修复多选下载。
// @author       西电网信院的废物rytter & 西电网信院的废物B4a
// @match        *://*/*mycourse/studentstudy*
// @match        *://*/*nodedetailcontroller/*
// @match        *://*/*mycourse/teacherstudy*
// @icon         data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
// @grant        GM_addStyle
// @grant        GM_getValue
// @grant        GM_setValue
// @license      MIT
// ==/UserScript==

(function () {
    "use strict";

    // --- Styles ---
    // (GM_addStyle remains the same as v1.21)
    GM_addStyle(`
        .cx-download-tips {
            position: fixed;
            top: 15px;
            right: 15px;
            background-color: rgba(230, 247, 255, 0.97);
            padding: 12px 18px;
            z-index: 10001;
            text-align: left; /* Align text left for better readability */
            border-radius: 8px;
            font-size: 13px;
            color: #333;
            border: 1px solid #b3e0ff;
            box-shadow: 0 4px 12px rgba(0, 123, 255, 0.2);
            max-width: 400px; /* Limit width */
            opacity: 1;
            transition: opacity 0.5s ease-out, transform 0.3s ease-out;
            transform: translateX(0);
        }
        .cx-download-tips.cx-hidden {
            opacity: 0;
            transform: translateX(20px); /* Slide out effect */
            pointer-events: none;
        }
        .cx-download-tips i { /* General message styling */
            font-style: normal;
            display: block;
        }
        .cx-download-tips .progress-container {
            margin-top: 8px;
        }
        .cx-download-tips .progress-info {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 4px;
            font-size: 12px;
        }
        .cx-download-tips .progress-filename {
            font-weight: bold;
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis;
            max-width: 250px; /* Limit filename width */
            display: inline-block;
        }
        .cx-download-tips .progress-size {
            color: #555;
            font-size: 11px;
        }
        .cx-download-tips progress {
            width: 100%;
            height: 8px;
            border-radius: 4px;
            overflow: hidden; /* Ensure rounded corners apply to value */
        }
        .cx-download-tips progress::-webkit-progress-bar {
            background-color: #e0e0e0;
            border-radius: 4px;
        }
        .cx-download-tips progress::-webkit-progress-value {
            background-color: #4CAF50; /* Green progress */
            border-radius: 4px;
            transition: width 0.1s linear;
        }
        .cx-download-tips progress::-moz-progress-bar { /* Firefox */
             background-color: #4CAF50;
             border-radius: 4px;
             transition: width 0.1s linear;
        }
        .cx-download-tips .status-icon {
            margin-right: 5px;
            font-weight: bold;
        }
        .cx-download-tips .status-success { color: #28a745; } /* Green */
        .cx-download-tips .status-error { color: #dc3545; } /* Red */

        /* Modal Styles */
        .cx-modal-overlay {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background-color: rgba(0, 0, 0, 0.6);
            z-index: 10010;
            display: flex;
            justify-content: center;
            align-items: center;
        }
        .cx-modal-content {
            background-color: white;
            padding: 25px;
            border-radius: 10px;
            box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
            width: 90%;
            max-width: 600px;
            max-height: 80vh;
            display: flex;
            flex-direction: column;
        }
        .cx-modal-title {
            font-size: 18px;
            font-weight: bold;
            margin-bottom: 15px;
            color: #333;
            border-bottom: 1px solid #eee;
            padding-bottom: 10px;
        }
        .cx-modal-list {
            overflow-y: auto;
            margin-bottom: 20px;
            flex-grow: 1; /* Allow list to take available space */
        }
        .cx-modal-list ul {
            list-style: none;
            padding: 0;
            margin: 0;
        }
        .cx-modal-list li {
            padding: 10px 8px; /* Slightly more padding */
            border-bottom: 1px solid #f0f0f0;
            display: flex;
            align-items: center;
            cursor: pointer; /* Make list item clickable */
            transition: background-color 0.15s ease;
        }
         .cx-modal-list li:hover {
            background-color: #f8f9fa;
         }
        .cx-modal-list li:last-child {
            border-bottom: none;
        }
        .cx-modal-list input[type="checkbox"] {
            margin-right: 12px;
            width: 16px;
            height: 16px;
            flex-shrink: 0; /* Prevent checkbox from shrinking */
            pointer-events: none; /* Let the LI handle the click */
        }
        .cx-modal-list label {
            font-size: 14px;
            color: #555;
            flex-grow: 1; /* Allow label to take space */
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;
            /* Remove cursor pointer from label, LI handles it */
            /* cursor: pointer; */
        }
        .cx-modal-list .file-type {
            font-size: 11px;
            color: #888;
            margin-left: 10px;
            background-color: #eee;
            padding: 2px 5px;
            border-radius: 3px;
            white-space: nowrap; /* Prevent type from wrapping */
            flex-shrink: 0; /* Prevent type from shrinking */
        }
         .cx-modal-list .file-error {
             font-size: 11px;
             color: #dc3545;
             margin-left: 10px;
             font-style: italic;
         }
        .cx-modal-actions {
            text-align: right;
            margin-top: 10px; /* Add space above buttons */
        }
        .cx-modal-button {
            padding: 8px 16px;
            margin-left: 10px;
            border: none;
            border-radius: 6px;
            cursor: pointer;
            font-size: 14px;
            transition: background-color 0.2s ease, box-shadow 0.2s ease;
        }
        .cx-modal-button-primary {
            background-color: #007bff;
            color: white;
            box-shadow: 0 2px 4px rgba(0, 123, 255, 0.3);
        }
        .cx-modal-button-primary:hover {
            background-color: #0056b3;
            box-shadow: 0 3px 6px rgba(0, 123, 255, 0.4);
        }
        .cx-modal-button-secondary {
            background-color: #6c757d;
            color: white;
             box-shadow: 0 2px 4px rgba(108, 117, 125, 0.3);
        }
        .cx-modal-button-secondary:hover {
            background-color: #5a6268;
            box-shadow: 0 3px 6px rgba(108, 117, 125, 0.4);
        }
        /* Button Styles */
        .cx-action-button {
             margin: 6px 0;
             padding: 8px 10px;
             border: none;
             border-radius: 8px;
             cursor: pointer;
             font-size: 12px;
             font-weight: bold;
             width: 85px;
             text-align: center;
             box-shadow: 0 3px 6px rgba(0,0,0,0.15);
             transition: background-color 0.2s ease, transform 0.1s ease, box-shadow 0.2s ease;
             color: white;
         }
         .cx-action-button:hover {
             opacity: 0.9;
             box-shadow: 0 4px 8px rgba(0,0,0,0.2);
         }
          .cx-action-button:active {
             transform: scale(0.97);
             box-shadow: 0 1px 3px rgba(0,0,0,0.1);
         }
    `);

    // --- Globals ---
    let tipsDiv = null;
    let tipTimeout = null;
    let modalInstance = null;

    // --- Helper Functions ---
    // get_objectids, getTipsDiv, showTip, hideTipsDiv, cleanFilename, fetchResourceDetails, delay
    // (Keep these functions as they were in v1.21)
    function get_objectids() {
        // ... (keep the existing get_objectids function as is) ...
        let objectids = [];
        let ans_classes = document.getElementsByClassName("ans-attach-ct");
        // support old version used by learning.xidian.edu.cn
        if (ans_classes.length === 0) {
            ans_classes = document.getElementsByClassName("ans-cc");
        }

        if (ans_classes.length > 0) {
            // console.log("模式1: 找到资源容器数量:", ans_classes.length);
            for (let j = 0; j < ans_classes.length; j++) {
                const iframe = ans_classes[j].getElementsByTagName("iframe")[0];
                if (iframe && iframe.getAttribute("objectid") != undefined) {
                    objectids.push(iframe.getAttribute("objectid"));
                }
            }
        } else {
            // Try finding objectid in the main iframe content (common case)
            const mainIframe = document.getElementsByTagName("iframe")[0];
            if (mainIframe && mainIframe.contentDocument) {
                 try {
                    ans_classes = mainIframe.contentDocument.body.getElementsByClassName("ans-attach-ct");
                     if (ans_classes.length === 0) {
                         ans_classes = mainIframe.contentDocument.body.getElementsByClassName("ans-cc");
                     }
                    // console.log("模式2: Iframe内找到资源容器数量:", ans_classes.length);
                    for (let j = 0; j < ans_classes.length; j++) {
                        const iframe = ans_classes[j].getElementsByTagName("iframe")[0];
                        if (iframe && iframe.getAttribute("objectid") != undefined) {
                            objectids.push(iframe.getAttribute("objectid"));
                        }
                    }
                 } catch(e) {
                     console.warn("访问iframe内容时出错 (可能是跨域限制):", e);
                     if (mainIframe.getAttribute("objectid") != undefined) {
                         objectids.push(mainIframe.getAttribute("objectid"));
                         // console.log("模式3: 直接在主iframe上找到objectid");
                     }
                 }
            }
        }
        // Remove duplicates
        objectids = [...new Set(objectids)];
        console.log("找到的所有任务对象IDs:", objectids);
        return objectids;
    }

    function getTipsDiv() {
        if (!tipsDiv) {
            tipsDiv = document.createElement('div');
            tipsDiv.className = 'cx-download-tips cx-hidden'; // Start hidden
            document.body.appendChild(tipsDiv);
        }
        return tipsDiv;
    }

    function showTip(message, duration = 3000, isError = false, isSuccess = false) {
        const div = getTipsDiv();
        clearTimeout(tipTimeout); // Clear any existing hide timeout

        let icon = '';
        if (isSuccess) icon = '<span class="status-icon status-success">✔</span>';
        if (isError) icon = '<span class="status-icon status-error">✘</span>';

        div.innerHTML = `<i>${icon}${message}</i>`;
        div.classList.remove('cx-hidden'); // Make visible

        if (duration > 0) {
            tipTimeout = setTimeout(hideTipsDiv, duration);
        }
    }

    function hideTipsDiv() {
        const div = getTipsDiv();
        clearTimeout(tipTimeout);
        div.classList.add('cx-hidden');
    }

    function cleanFilename(filename) {
         return filename.replace(/[\/\\?%*:|"<>]/g, '-').replace(/\s+/g, ' ');
    }

    /**
     * Fetches details for a single resource object ID.
     * @param {string} objectid
     * @returns {Promise<object>} Resource details object
     */
    async function fetchResourceDetails(objectid) {
        const protocolStr = document.location.protocol;
        const domain = window.location.href.includes("xueyinonline") ? "xueyinonline" : "chaoxing";
        const url = `${protocolStr}//mooc1.${domain}.com/ananas/status/${objectid}?flag=normal`;
        let resource = {
            objectid: objectid,
            filename: `未知资源_${objectid}`,
            downloadUrl: null,
            type: '未知',
            error: null,
            openDirectly: false // Added flag
        };

        try {
            const response = await fetch(url);
            if (!response.ok) {
                throw new Error(`HTTP ${response.status}`);
            }
            const json = await response.json();
            // console.log("Fetched info for", objectid, json);

            let fileUrl = null;
            let fileExtension = 'bin';
            let filename = json.filename || `资源_${objectid}`;

            // Determine type and URL priority
            if (json.pdf) {
                resource.type = 'PDF';
                fileUrl = json.pdf;
                fileExtension = 'pdf';
            } else if (json.http && (json.filename?.toLowerCase().endsWith('.mp4') || json.http.includes('.mp4') || json.mimetype?.includes('video'))) {
                resource.type = '视频';
                fileUrl = json.http;
                fileExtension = json.filename?.split('.').pop() || 'mp4';
            } else if (json.filename?.toLowerCase().includes(".ppt") && json.download) {
                resource.type = 'PPT'; // Special PPT case handled differently (opening link)
                fileUrl = json.download.startsWith('http') ? json.download : 'https://' + json.download;
                fileExtension = json.filename?.split('.').pop() || 'ppt'; // Use original ext
                 // Mark this as needing direct open, not XHR download
                 resource.openDirectly = true;
            } else if (json.download) {
                resource.type = '文件'; // General file
                fileUrl = json.download;
                fileExtension = json.filename?.split('.').pop() || 'bin';
                if (fileExtension.match(/ppt|pptx/i)) resource.type = 'PPT';
                else if (fileExtension.match(/doc|docx/i)) resource.type = '文档';
                else if (fileExtension.match(/xls|xlsx/i)) resource.type = '表格';
                else if (fileExtension.match(/zip|rar|7z/i)) resource.type = '压缩包';
            } else if (json.http) {
                // Treat other http links as potential downloads, but maybe less reliable
                resource.type = '链接/其他';
                fileUrl = json.http;
                fileExtension = json.filename?.split('.').pop() || 'bin';
            }

            // Construct final filename
            let baseName = filename.replace(/\.[^.]+$/, '');
            const prevTitleElement = document.querySelector('.prev_title') || document.querySelector('.tab_highlight');
            const prevTitle = prevTitleElement?.textContent?.trim() || document.title.split(/[ \-_]/)[0] || '课程文件';
            resource.filename = cleanFilename(`${prevTitle}_${baseName}.${fileExtension}`);

            // Ensure protocol for non-direct links
            if (fileUrl && !resource.openDirectly && !fileUrl.startsWith('http')) {
                fileUrl = protocolStr + fileUrl;
            }
             resource.downloadUrl = fileUrl;


        } catch (error) {
            console.error(`获取资源 ${objectid} 信息失败:`, error);
            resource.error = `获取失败 (${error.message})`;
        }
        return resource;
    }

    function delay(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }


    // --- Core Download Logic ---
    /**
     * Downloads a file using XMLHttpRequest. Can show progress or run silently.
     * @param {string} fileUrl The URL of the file to download.
     * @param {string} fullFileName The desired filename for the download.
     * @param {boolean} [showProgress=true] Whether to display detailed progress in the tips div.
     */
    function downloadFile(fileUrl, fullFileName, showProgress = true) {
        const div = getTipsDiv();
        if (showProgress) {
            clearTimeout(tipTimeout); // Prevent auto-hide during download
            div.innerHTML = ''; // Clear previous content for detailed progress
            div.classList.remove('cx-hidden'); // Ensure visible

            console.log(`开始下载 (带进度): ${fullFileName} from ${fileUrl}`);

            // Create progress UI elements
            const container = document.createElement('div');
            container.className = 'progress-container';
            const infoDiv = document.createElement('div');
            infoDiv.className = 'progress-info';
            const fileNameSpan = document.createElement('span');
            fileNameSpan.className = 'progress-filename';
            fileNameSpan.textContent = fullFileName;
            fileNameSpan.title = fullFileName;
            const fileSizeSpan = document.createElement('span');
            fileSizeSpan.className = 'progress-size';
            fileSizeSpan.textContent = '(计算中...)';
            infoDiv.appendChild(fileNameSpan);
            infoDiv.appendChild(fileSizeSpan);
            const progressBar = document.createElement('progress');
            progressBar.value = 0;
            progressBar.max = 100;
            container.appendChild(infoDiv);
            container.appendChild(progressBar);
            div.appendChild(container);
        } else {
            // For batch downloads, maybe just log or show a brief starting message if needed
            console.log(`开始下载 (批量): ${fullFileName} from ${fileUrl}`);
            // Optionally: showTip(`正在尝试下载: ${fullFileName}`, 1500); // Very brief message
        }

        const xhr = new XMLHttpRequest();
        xhr.open('GET', fileUrl, true);
        xhr.responseType = 'blob';

        xhr.onprogress = function (event) {
            if (showProgress && event.lengthComputable) {
                 const percentComplete = (event.loaded / event.total) * 100;
                 const progressBar = div.querySelector('progress'); // Find progress bar within the tips div
                 const fileSizeSpan = div.querySelector('.progress-size');
                 if(progressBar) progressBar.value = percentComplete;
                 if (fileSizeSpan && fileSizeSpan.textContent === '(计算中...)') {
                      fileSizeSpan.textContent = `(${(event.total / 1024 / 1024).toFixed(2)} MB)`;
                 }
            } else if (showProgress) {
                 // Handle indeterminate progress if needed
                 const progressBar = div.querySelector('progress');
                 const fileSizeSpan = div.querySelector('.progress-size');
                 if(progressBar) progressBar.removeAttribute('value');
                 if(fileSizeSpan) fileSizeSpan.textContent = `(${(event.loaded / 1024 / 1024).toFixed(2)} MB / 未知)`;
            }
        };

        xhr.onload = function (event) {
            if (showProgress) {
                 const progressBar = div.querySelector('progress');
                 if(progressBar) progressBar.value = 100; // Ensure 100% on success
            }

            if (this.status === 200) {
                let blob = this.response;
                let finalFileName = fullFileName;

                // Mime type check and correction (especially for PDF)
                if (blob.type === 'application/pdf' && !finalFileName.toLowerCase().endsWith('.pdf')) {
                    console.log(`Blob type is PDF, correcting filename: ${finalFileName}`);
                    finalFileName = finalFileName.replace(/\.[^.]+$/, '') + '.pdf';
                }
                // Add more mime type checks if necessary

                const downloadUrl = window.URL.createObjectURL(blob);
                const a = document.createElement("a");
                a.style.display = 'none';
                a.href = downloadUrl;
                a.download = finalFileName;
                document.body.appendChild(a);
                a.click(); // This triggers the download
                document.body.removeChild(a);
                window.URL.revokeObjectURL(downloadUrl);

                console.log(`下载成功: ${finalFileName}`);
                if (showProgress) {
                    showTip(`下载完成: ${finalFileName}`, 4000, false, true);
                } else {
                    // Optionally provide minimal feedback for batch success
                    // console.log(`Batch download success: ${finalFileName}`);
                }

            } else {
                 console.error(`下载失败 (${this.status}): ${finalFileName}`);
                 if (showProgress) {
                     showTip(`下载失败 (${this.status}): ${finalFileName}`, 5000, true, false);
                 } else {
                     // Show error for batch download failure
                     showTip(`下载失败 (${this.status}): ${finalFileName.substring(0, 30)}...`, 4000, true);
                 }
            }
        };

        xhr.onerror = function () {
            if (showProgress) {
                 const progressBar = div.querySelector('progress');
                 if(progressBar) progressBar.value = 0; // Reset progress on error
            }
            console.error("下载时发生网络错误: ", fullFileName);
            if (showProgress) {
                showTip(`下载网络错误: ${fullFileName}`, 5000, true, false);
            } else {
                 showTip(`网络错误: ${fullFileName.substring(0, 30)}...`, 4000, true);
            }
        };

        xhr.send();
    }

    // --- Modal Logic ---
    // createModalElement, populateModalList (Keep as is from v1.21)
    function createModalElement() {
        // If modal already exists, just return it
        if (modalInstance) return modalInstance;

        const overlay = document.createElement('div');
        overlay.className = 'cx-modal-overlay';
        overlay.style.display = 'none'; // Start hidden

        const content = document.createElement('div');
        content.className = 'cx-modal-content';

        const title = document.createElement('div');
        title.className = 'cx-modal-title';
        title.textContent = '选择要下载的资源';

        const listContainer = document.createElement('div');
        listContainer.className = 'cx-modal-list';
        const list = document.createElement('ul');
        listContainer.appendChild(list);

        const actions = document.createElement('div');
        actions.className = 'cx-modal-actions';

        const downloadButton = document.createElement('button');
        downloadButton.className = 'cx-modal-button cx-modal-button-primary';
        downloadButton.textContent = '下载选中';

        const cancelButton = document.createElement('button');
        cancelButton.className = 'cx-modal-button cx-modal-button-secondary';
        cancelButton.textContent = '取消';

        actions.appendChild(cancelButton);
        actions.appendChild(downloadButton); // Primary button last

        content.appendChild(title);
        content.appendChild(listContainer);
        content.appendChild(actions);
        overlay.appendChild(content);

        // Close modal listeners
        cancelButton.onclick = () => overlay.style.display = 'none';
        overlay.onclick = (e) => {
            if (e.target === overlay) { // Only close if clicking the overlay itself
                overlay.style.display = 'none';
            }
        };

        document.body.appendChild(overlay);
        modalInstance = { overlay, list, downloadButton }; // Store the instance
        return modalInstance;
    }

    function populateModalList(listElement, resources, preselectPdf = false) {
        listElement.innerHTML = ''; // Clear previous items
        if (resources.length === 0) {
             listElement.innerHTML = '<li><i>未找到可识别的资源。</i></li>';
             return;
        }

        resources.forEach((res, index) => {
            const li = document.createElement('li');
            const checkbox = document.createElement('input');
            checkbox.type = 'checkbox';
            checkbox.id = `cx-res-${index}`;
            checkbox.value = index; // Store index to retrieve resource data later
            checkbox.checked = (preselectPdf && res.type === 'PDF' && !res.error); // Preselect PDFs if requested and valid

            const label = document.createElement('label');
            label.htmlFor = `cx-res-${index}`; // Keep htmlFor for accessibility
            label.textContent = res.filename || '未知文件';
            label.title = res.filename || '未知文件'; // Full name on hover

            li.appendChild(checkbox);
            li.appendChild(label);

            if (res.error) {
                const errorSpan = document.createElement('span');
                errorSpan.className = 'file-error';
                errorSpan.textContent = `(${res.error})`;
                li.appendChild(errorSpan);
                checkbox.disabled = true; // Disable checkbox for errored items
                li.style.opacity = '0.6';
                li.style.cursor = 'not-allowed';
            } else {
                const typeSpan = document.createElement('span');
                typeSpan.className = 'file-type';
                typeSpan.textContent = res.type || '未知';
                li.appendChild(typeSpan);
                // **MODIFIED:** Make clicking the list item toggle the checkbox
                li.onclick = (e) => {
                    // Don't toggle if the click was on the disabled error span
                    if (e.target.classList.contains('file-error')) return;
                    // Toggle the checkbox state directly
                    if (!checkbox.disabled) {
                        checkbox.checked = !checkbox.checked;
                    }
                };
            }
            listElement.appendChild(li);
        });
    }

    async function showResourceSelectionModal(preselectPdf = false) {
        showTip("正在查找资源列表...", 0);

        const objectids = get_objectids();
        if (objectids.length === 0) {
            hideTipsDiv();
            showTip("未找到任何资源对象ID。", 3000, true);
            return;
        }

        const resourcePromises = objectids.map(id => fetchResourceDetails(id));
        const resources = await Promise.all(resourcePromises);

        hideTipsDiv();

        const validResources = resources.filter(res => !res.error && (res.downloadUrl || res.openDirectly));

        if (validResources.length === 0) {
            showTip("未找到可下载的资源。", 3000, true);
            return;
        }

        const modal = createModalElement();
        populateModalList(modal.list, validResources, preselectPdf);

        // **MODIFIED:** Download button click handler using downloadFile for batch
        modal.downloadButton.onclick = async () => {
            const selectedIndices = Array.from(modal.list.querySelectorAll('input[type="checkbox"]:checked'))
                                       .map(cb => parseInt(cb.value, 10));

            if (selectedIndices.length === 0) {
                alert("请至少选择一个要下载的资源。");
                return;
            }

            modal.overlay.style.display = 'none';

            if (selectedIndices.length === 1) {
                // Single download: use downloadFile with progress
                const resource = validResources[selectedIndices[0]];
                 if (resource.openDirectly) {
                     console.log("打开直接链接:", resource.filename);
                     window.open(resource.downloadUrl);
                     showTip(`已尝试打开: ${resource.filename}`, 3000);
                 } else if (resource.downloadUrl) {
                    downloadFile(resource.downloadUrl, resource.filename, true); // Show progress
                 } else {
                     showTip(`无法处理 ${resource.filename}: 无有效链接`, 4000, true);
                 }
            } else {
                // Multiple downloads: use downloadFile without progress, with delay
                const directOpenQueue = [];
                const downloadQueue = [];

                selectedIndices.forEach(index => {
                    const resource = validResources[index];
                    if (resource.openDirectly) {
                        directOpenQueue.push(resource);
                    } else if (resource.downloadUrl) {
                        downloadQueue.push(resource);
                    } else {
                        console.warn(`无法处理 ${resource.filename}: 无有效链接`);
                    }
                });

                let openCount = directOpenQueue.length;
                let downloadTotal = downloadQueue.length;
                showTip(`准备处理 ${openCount} 个直接链接和 ${downloadTotal} 个下载...`, 0);

                // 1. Open direct links
                if (openCount > 0) {
                    console.log(`正在打开 ${openCount} 个直接链接...`);
                    directOpenQueue.forEach(res => {
                        console.log("打开直接链接:", res.filename);
                        window.open(res.downloadUrl);
                    });
                }

                // 2. Process download queue with delay using downloadFile(..., false)
                if (downloadTotal > 0) {
                    console.log(`开始处理 ${downloadTotal} 个下载 (带延迟)...`);
                    let initiatedCount = 0;
                    for (const res of downloadQueue) {
                        // Call downloadFile WITHOUT progress indicator
                        downloadFile(res.downloadUrl, res.filename, false);
                        initiatedCount++;
                        console.log(`已尝试启动下载 (${initiatedCount}/${downloadTotal}): ${res.filename}`);
                        // Update status occasionally
                        if (initiatedCount % 3 === 0 || initiatedCount === downloadTotal) {
                             showTip(`已尝试启动 ${initiatedCount}/${downloadTotal} 个下载...`, 0);
                        }
                        await delay(500); // Wait 500ms between initiating XHR downloads
                    }
                     // Final message after loop
                     setTimeout(() => showTip(`处理完成: ${openCount} 个链接已打开, ${initiatedCount} 个下载已尝试启动。`, 6000), 500);
                } else if (openCount > 0) {
                     // If only direct links were opened
                     setTimeout(() => showTip(`已尝试打开 ${openCount} 个链接。`, 4000), 500);
                } else {
                     showTip(`未找到有效文件进行处理。`, 4000, true);
                }
            }
        };

        modal.overlay.style.display = 'flex';
    }


    // --- Quick Download All PDFs (D key) ---
    // **MODIFIED:** Use downloadFile(..., false) for consistency and robustness
    async function downloadAllPdfsDirectly() {
        showTip("正在查找并下载所有PDF...", 0);

        const objectids = get_objectids();
        if (objectids.length === 0) {
            hideTipsDiv();
            showTip("未找到任何资源对象ID。", 3000, true);
            return;
        }

        const resourcePromises = objectids.map(id => fetchResourceDetails(id));
        const resources = await Promise.all(resourcePromises);

        const pdfResources = resources.filter(res => res.type === 'PDF' && !res.error && res.downloadUrl);

        if (pdfResources.length === 0) {
            hideTipsDiv();
            showTip("未找到可下载的PDF文件。", 3000);
            return;
        }

        hideTipsDiv();
        showTip(`开始下载 ${pdfResources.length} 个PDF文件...`, 0);
        let downloadCount = 0;
        for (const res of pdfResources) {
             // Use downloadFile without progress for D key shortcut as well
             downloadFile(res.downloadUrl, res.filename, false);
             downloadCount++;
             await delay(300); // Keep a small delay
        }

        setTimeout(() => showTip(`已尝试启动 ${downloadCount} 个PDF文件的下载。`, 5000), 500);
    }


    // --- UI and Event Listeners ---
    // (Button creation and keydown listener remain the same as v1.21)
    // Create main action buttons container
    var buttonContainer = document.createElement('div');
    buttonContainer.style.position = 'fixed';
    buttonContainer.style.left = '10px';
    buttonContainer.style.top = '50%';
    buttonContainer.style.transform = 'translateY(-50%)';
    buttonContainer.style.display = 'flex';
    buttonContainer.style.flexDirection = 'column';
    buttonContainer.style.zIndex = '10000';

    // **MODIFIED:** Only one button now
    var downloadButton = document.createElement('button');
    downloadButton.className = 'cx-action-button';
    downloadButton.textContent = '下载资源'; // Changed text
    downloadButton.title = '点击选择要下载的文件'; // Changed title
    downloadButton.style.backgroundColor = '#81C784'; // Green color
    downloadButton.addEventListener('click', () => showResourceSelectionModal(false)); // Don't preselect PDF

    // Add the single button to container
    buttonContainer.appendChild(downloadButton);
    document.body.appendChild(buttonContainer);

    // Keyboard shortcut (D key for downloading all PDFs directly)
    document.addEventListener('keydown', function (e) {
        if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.isContentEditable) {
            return;
        }
        // Check if modal is open, if so, don't trigger D key shortcut
        const modalOverlay = document.querySelector('.cx-modal-overlay');
        if (modalOverlay && modalOverlay.style.display !== 'none') {
            return;
        }

        if (e.key === 'd' || e.key === 'D' || e.keyCode === 68) {
            console.log("检测到 'D' 键按下,开始直接下载所有PDF...");
            e.preventDefault();
            downloadAllPdfsDirectly(); // Call the direct download function
        }
    });


    console.log("超星学习通下载脚本 (v1.22 - 修复多选) 已加载。");

})();