// ==UserScript==
// @name 云邮教学空间助手
// @namespace http://tampermonkey.net/
// @version 0.16
// @description 作业显示相关课程,office文档预览切换到 view.officeapps.live.com,重命名下载文件名
// @author YouXam
// @match https://ucloud.bupt.edu.cn/uclass/course.html*
// @match https://ucloud.bupt.edu.cn/uclass/*
// @match https://ucloud.bupt.edu.cn/office/*
// @icon https://ucloud.bupt.edu.cn/favicon.ico
// @grant none
// @license MIT
// ==/UserScript==
window.loadJs = function loadJs(src) {
return new Promise((ret, rej) => {
fetch(src).then(res => res.text()).then(res => {
let s = document.createElement("script")
s.language = "javascript"
s.type = "text/javascript"
s.text = res
document.getElementsByTagName('HEAD')[0].appendChild(s)
ret(0)
}).catch(e => rej(e))
})
}
function loadCss() {
function addGlobalStyle(css) {
var head, style;
head = document.getElementsByTagName('head')[0];
if (!head) { return; }
style = document.createElement('style');
style.type = 'text/css';
style.innerHTML = css;
head.appendChild(style);
}
addGlobalStyle(`#nprogress{pointer-events:none}#nprogress .bar{background:#ffbe00;position:fixed;z-index:1031;top:0;left:0;width:100%;height:5px}#nprogress .peg,.nprogress-custom-parent #nprogress .bar,.nprogress-custom-parent #nprogress .spinner{position:absolute}#nprogress .peg{display:block;right:0;width:100px;height:100%;box-shadow:0 0 10px #ffbe00,0 0 5px #ffbe00;opacity:1;-webkit-transform:rotate(3deg) translate(0,-4px);-ms-transform:rotate(3deg) translate(0,-4px);transform:rotate(3deg) translate(0,-4px)}#nprogress .spinner{display:block;position:fixed;z-index:1031;top:15px;right:15px}#nprogress .spinner-icon{width:18px;height:18px;box-sizing:border-box;border:2px solid transparent;border-top-color:#ffbe00;border-left-color:#ffbe00;border-radius:50%;-webkit-animation:.4s linear infinite nprogress-spinner;animation:.4s linear infinite nprogress-spinner}.nprogress-custom-parent{overflow:hidden;position:relative}@-webkit-keyframes nprogress-spinner{0%{-webkit-transform:rotate(0)}100%{-webkit-transform:rotate(360deg)}}@keyframes nprogress-spinner{0%{transform:rotate(0)}100%{transform:rotate(360deg)}}`)
}
function getToken() {
const cookieMap = new Map();
document.cookie.split("; ").forEach((cookie) => {
const [key, value] = cookie.split("=");
cookieMap.set(key, value);
})
const token = cookieMap.get("iClass-token");
const userid = cookieMap.get('iClass-uuid');
return [userid, token]
}
let sumBytes = 0, loadedBytes = 0, downloading = false
async function downloadFile(url, filename) {
downloading = true
await jsp;
NProgress.configure({ trickle: false, speed: 0 });
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const contentLength = response.headers.get('content-length');
if (!contentLength) {
throw new Error('Content-Length response header unavailable');
}
const total = parseInt(contentLength, 10);
sumBytes += total
const reader = response.body.getReader();
const chunks = [];
while (true) {
const { done, value } = await reader.read();
if (done) break;
if (!downloading) {
NProgress.done();
return
}
chunks.push(value);
loadedBytes += value.length;
NProgress.set(loadedBytes / sumBytes)
}
NProgress.done();
sumBytes -= total;
loadedBytes -= total;
const blob = new Blob(chunks);
const downloadUrl = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = downloadUrl;
a.download = filename;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(downloadUrl);
} catch (error) {
console.error('Download failed:', error);
}
}
async function searchTask(siteId, keyword, token) {
const res = await fetch("https://apiucloud.bupt.edu.cn/ykt-site/work/student/list", {
"headers": {
"authorization": "Basic cG9ydGFsOnBvcnRhbF9zZWNyZXQ=",
"blade-auth": token,
'content-type': 'application/json;charset=UTF-8'
},
"body": JSON.stringify({
siteId,
keyword,
"current": 1,
"size": 5,
}),
"method": "POST"
});
const json = await res.json();
return json
}
async function searchCourse(userId, id, keyword, token) {
const res = await fetch("https://apiucloud.bupt.edu.cn/ykt-site/site/list/student/current?size=999999¤t=1&userId=" + userId + "&siteRoleCode=2", {
"headers": {
"authorization": "Basic cG9ydGFsOnBvcnRhbF9zZWNyZXQ=",
"blade-auth": token,
},
"body": null,
"method": "GET"
});
const json = await res.json();
const list = json.data.records.map(x => ({
id: x.id,
name: x.siteName,
teachers: x.teachers.map(y => y.name).join(', '),
}))
async function searchWithLimit(list, id, keyword, token, limit = 5) {
for (let i = 0; i < list.length; i += limit) {
const batch = list.slice(i, i + limit);
const jobs = batch.map(x => searchTask(x.id, keyword, token));
const ress = await Promise.all(jobs);
for (let j = 0; j < ress.length; j++) {
const res = ress[j];
if (res.data.records.length > 0) {
for (const item of res.data.records) {
if (item.id == id) {
return batch[j];
}
}
}
}
}
return null;
}
return await searchWithLimit(list, id, keyword, token);
}
async function getTasks(siteId, token) {
const res = await fetch("https://apiucloud.bupt.edu.cn/ykt-site/work/student/list", {
"headers": {
"authorization": "Basic cG9ydGFsOnBvcnRhbF9zZWNyZXQ=",
"blade-auth": token,
'content-type': 'application/json;charset=UTF-8'
},
"body": JSON.stringify({
siteId,
current: 1,
size: 9999,
}),
"method": "POST"
});
const json = await res.json();
return json
}
async function searchCourses(nids) {
const result = {}
let ids = []
for (let id of nids) {
const r = get(id)
if (r) result[id] = r;
else ids.push(id)
}
if (ids.length == 0) return result
const [userid, token] = getToken()
const res = await fetch("https://apiucloud.bupt.edu.cn/ykt-site/site/list/student/current?size=999999¤t=1&userId=" + userid + "&siteRoleCode=2", {
"headers": {
"authorization": "Basic cG9ydGFsOnBvcnRhbF9zZWNyZXQ=",
"blade-auth": token,
},
"body": null,
"method": "GET"
});
const json = await res.json();
const list = json.data.records.map((x) => ({
id: x.id,
name: x.siteName,
teachers: x.teachers.map((y) => y.name).join(', '),
}))
const hashMap = new Map();
let count = ids.length
for (let i = 0; i < ids.length; i++) {
hashMap.set(ids[i], i);
}
async function searchWithLimit(list, limit = 5) {
for (let i = 0; i < list.length; i += limit) {
const batch = list.slice(i, i + limit);
const jobs = batch.map((x) => getTasks(x.id, token));
const ress = await Promise.all(jobs);
for (let j = 0; j < ress.length; j++) {
const res = ress[j];
if (res.data.records.length > 0) {
for (const item of res.data.records) {
if (hashMap.has(item.id)) {
result[item.id] = batch[j];
set(item.id, batch[j])
if (--count == 0) {
return result;
}
}
}
}
}
}
return result;
}
return await searchWithLimit(list);
}
async function getUndoneList() {
const [userid, token] = getToken()
const res = await fetch("https://apiucloud.bupt.edu.cn/ykt-site/site/student/undone?userId=" + userid, {
"headers": {
"authorization": "Basic cG9ydGFsOnBvcnRhbF9zZWNyZXQ=",
"blade-auth": token,
},
"method": "GET"
});
const json = await res.json();
return json;
}
async function getDetail(id) {
const [userid, token] = getToken()
const res = await fetch("https://apiucloud.bupt.edu.cn/ykt-site/work/detail?assignmentId=" + id, {
"headers": {
"authorization": "Basic cG9ydGFsOnBvcnRhbF9zZWNyZXQ=",
"blade-auth": token,
},
"body": null,
"method": "GET"
})
const json = await res.json();
return json;
}
async function getSiteResource(id) {
const [userid, token] = getToken()
const res = await fetch("https://apiucloud.bupt.edu.cn/ykt-site/site-resource/tree/student?siteId=" + id + "&userId=" + userid, {
"headers": {
"authorization": "Basic cG9ydGFsOnBvcnRhbF9zZWNyZXQ=",
"blade-auth": token,
},
"body": null,
"method": "POST"
})
const json = await res.json();
const result = []
function foreach(data) {
console.log(data)
data.forEach(x => {
x.attachmentVOs.forEach(y => result.push(y.resource))
foreach(x.children)
})
}
foreach(json.data)
return result
}
function $x(xpath, context = document) {
const iterator = document.evaluate(xpath, context, null, XPathResult.ANY_TYPE, null);
const results = [];
let item;
while (item = iterator.iterateNext()) {
results.push(item);
}
return results;
}
function set(k, v) {
const h = JSON.parse(localStorage.getItem('zzxw') || '{}')
h[k] = v
localStorage.setItem('zzxw', JSON.stringify(h))
}
function get(k) {
const h = JSON.parse(localStorage.getItem('zzxw') || '{}')
return h[k];
}
function insert(x) {
if ($x('/html/body/div[1]/div/div[2]/div[2]/div/div/div[2]/div/div[2]/div[1]/div/div/div[1]/div/p').length > 2) return
const d = $x('/html/body/div[1]/div/div[2]/div[2]/div/div/div[2]/div/div[2]/div[1]/div/div/div[1]/div/p[1]');
if (!d.length) {
setTimeout(() => insert(x), 50);
return
}
const p = document.createElement('p')
const t = document.createTextNode(x.name + "(" + x.teachers + ")")
p.appendChild(t)
d[0].after(p)
}
function sleep(n) {
return new Promise(res => setTimeout(res, n))
}
async function wait(func) {
let r = func()
if (r instanceof Promise) r = await r
if (r) return r;
await sleep(50)
return await wait(func)
}
async function waitChange(func, value) {
const r = value
while (1) {
let t = func()
if (t instanceof Promise) t = await t
if (t != r) return t;
await sleep(50);
}
}
let onlinePreview = null
async function getPreviewURL(storageId) {
const res = await fetch("https://apiucloud.bupt.edu.cn/blade-source/resource/preview-url?resourceId=" + storageId)
const json = await res.json();
onlinePreview = json.data.onlinePreview
return json.data.previewUrl;
}
let setClicked = false;
let gpage = -1;
let glist = null;
let jsp
async function main() {
'use strict';
if (location.href.startsWith("https://ucloud.bupt.edu.cn/uclass/course.html#/student/assignmentDetails_fullpage")) {
const q = new URLSearchParams(location.href)
const id = q.get('assignmentId')
const r = get(id)
const [userid, token] = getToken()
if (r) {
insert(r)
} else {
const title = q.get('assignmentTitle')
if (!id || !title) return;
searchCourse(userid, id, title, token).then(x => {
insert(x)
set(id, x)
})
}
const detail = (await getDetail(id)).data
const filenames = detail.assignmentResource.map(x => x.resourceName)
const urls = await Promise.all(detail.assignmentResource.map(x => {
return getPreviewURL(x.resourceId)
}))
await wait(() => $x('//*[@id="assignment-info"]/div[2]/div[2]/div[2]/div').length > 0)
$x('//*[@id="assignment-info"]/div[2]/div[2]/div[2]/div').forEach((x, index) => {
const i = document.createElement('i')
i.title="预览"
i.classList.add("by-icon-eye-grey")
i.addEventListener("click", () => {
const url = urls[index]
if (url.endsWith(".doc") || url.endsWith(".docx") || url.endsWith(".ppt") || url.endsWith(".pptx"))
window.open("https://view.officeapps.live.com/op/view.aspx?src=" + encodeURIComponent(url))
else if (url.endsWith(".pdf"))
window.open(url)
else if (onlinePreview !== null)
window.open(onlinePreview + encodeURIComponent(url))
})
x.children[3].remove();
x.children[2].insertAdjacentElement("afterend", i)
const i2 = document.createElement('i')
i2.title="下载"
i2.classList.add("by-icon-yundown-grey")
i2.addEventListener("click", () => {
downloadFile(urls[index], filenames[index])
})
x.children[2].remove();
x.children[1].insertAdjacentElement("afterend", i2)
})
} else if (location.href.startsWith("https://ucloud.bupt.edu.cn/uclass/#/student/homePage")) {
async function getPage() {
const pageText = await wait(() => document.querySelector("#layout-container > div.main-content > div.router-container > div > div.teacher-home-page > div.home-left-container.home-inline-block > div.in-progress-section.home-card > div.in-progress-header > div > div:nth-child(2) > div > div.banner-indicator.home-inline-block"))
return parseInt(pageText.innerHTML.trim().split('/')[0])
}
const list = glist || (await getUndoneList()).data.undoneList;
let page = 1
glist = list
if (list.length > 6) {
page = await getPage();
gpage = page
if (!setClicked) {
setClicked = true;
async function cmain() {
await waitChange(getPage, gpage)
main();
}
(await wait(() => document.querySelector("#layout-container > div.main-content > div.router-container > div > div.teacher-home-page > div.home-left-container.home-inline-block > div.in-progress-section.home-card > div.in-progress-header > div > div:nth-child(2) > div > div:nth-child(2) > span")))
.addEventListener('click', cmain);
(await wait(() => document.querySelector("#layout-container > div.main-content > div.router-container > div > div.teacher-home-page > div.home-left-container.home-inline-block > div.in-progress-section.home-card > div.in-progress-header > div > div:nth-child(2) > div > div:nth-child(3) > span")))
.addEventListener('click', cmain);
}
}
const tlist = list.slice((page - 1) * 6, page * 6)
const ids = tlist.map(x => x.activityId)
const infos = await searchCourses(ids)
const texts = tlist.map(x => infos[x.activityId].name + "(" + infos[x.activityId].teachers + ")")
const titles = tlist.map(x => x.activityName)
await wait(() => $x('//*[@id="layout-container"]/div[2]/div[2]/div/div[2]/div[1]/div[3]/div[2]/div/div').map(x => x.children[0].innerText).every((e, i) => e == titles[i]))
const nodes = $x('//*[@id="layout-container"]/div[2]/div[2]/div/div[2]/div[1]/div[3]/div[2]/div/div')
for (let i = 0; i < nodes.length; i++) {
if (nodes[i].children[1].children.length == 0) {
const p = document.createElement('div')
const t = document.createTextNode(texts[i])
p.appendChild(t)
nodes[i].children[1].insertAdjacentElement('afterbegin',p)
} else {
nodes[i].children[1].children[0].innerHTML = texts[i]
}
}
} else if (location.href.startsWith("https://ucloud.bupt.edu.cn/uclass/course.html#/student/courseHomePage")) {
const site = JSON.parse(localStorage.getItem("site"))
const id = site.id
const resources = await getSiteResource(id)
$x('//div[@class="resource-item"]/div[@class="right"]').forEach((x, index) => {
const i = document.createElement('i')
i.title="下载"
i.classList.add("by-icon-download")
i.classList.add("btn-icon")
i.classList.add("visible")
i.setAttribute(Array.from(x.attributes).filter(x => x.localName.startsWith('data-v'))[0].localName,'')
i.addEventListener("click", async (e) => {
e.stopPropagation();
downloadFile(await getPreviewURL(resources[index].id), resources[index].name)
}, false)
if (x.children.length) x.children[0].remove();
x.insertAdjacentElement('afterbegin',i)
})
const downloadAllButton = `<div style="display: flex;flex-direction: row;justify-content: end;margin-right: 24px;margin-top: 20px;">
<button type="button" class="el-button submit-btn el-button--primary" id="downloadAllButton">
下载全部
</button>
</div>`
const resourceList = $x('/html/body/div/div/div[2]/div[2]/div/div/div')
const containerElement = document.createElement('div');
containerElement.innerHTML = downloadAllButton
resourceList[0].before(containerElement)
document.getElementById("downloadAllButton").onclick = async () => {
downloading = !downloading
if (downloading) {
document.getElementById("downloadAllButton").innerHTML = '取消下载'
for (let file of resources) {
if (!downloading) return
await downloadFile(await getPreviewURL(file.id), file.name)
}
} else {
document.getElementById("downloadAllButton").innerHTML = '下载全部'
}
}
}
}
(function () {
loadCss()
jsp = loadJs('https://unpkg.com/nprogress@0.2.0/nprogress.js')
if (location.href.startsWith("https://ucloud.bupt.edu.cn/office/")) {
const url = new URLSearchParams(location.search).get("furl")
const filename = new URLSearchParams(location.search).get("fullfilename") || url
const viewURL = new URL(url)
if (new URLSearchParams(location.search).get("oauthKey")) {
const viewURLsearch = new URLSearchParams(viewURL.search)
viewURLsearch.set("oauthKey", new URLSearchParams(location.search).get("oauthKey"))
viewURL.search = viewURLsearch.toString()
}
if (filename.endsWith(".doc") || filename.endsWith(".docx") || filename.endsWith(".ppt") || filename.endsWith(".pptx"))
location.href = "https://view.officeapps.live.com/op/view.aspx?src=" + encodeURIComponent(viewURL.toString())
else if (filename.endsWith(".pdf"))
location.href = viewURL.toString()
return
}
main()
let hash = location.hash;
setInterval(() => {
if (location.hash != hash) {
hash = location.hash;
main();
}
}, 50)
})();