Greasy Fork is available in English.

超星学习通资源下载

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

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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 - 修复多选) 已加载。");

})();