您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
点击按钮弹出模态框选择下载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 - 修复多选) 已加载。"); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址