// ==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
// @downloadURL https://update.greasyfork.icu/scripts/444744/%E8%B6%85%E6%98%9F%E5%AD%A6%E4%B9%A0%E9%80%9A%E8%B5%84%E6%BA%90%E4%B8%8B%E8%BD%BD.user.js
// @updateURL https://update.greasyfork.icu/scripts/444744/%E8%B6%85%E6%98%9F%E5%AD%A6%E4%B9%A0%E9%80%9A%E8%B5%84%E6%BA%90%E4%B8%8B%E8%BD%BD.meta.js
// ==/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 = '✔';
if (isError) icon = '✘';
div.innerHTML = `${icon}${message}`;
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