您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Your best copilot for TronClass
// ==UserScript== // @name TronClass Copilot // @namespace Anong0u0 // @version 0.0.11.1 // @description Your best copilot for TronClass // @author Anong0u0 // @match https://eclass.yuntech.edu.tw/* // @match https://elearning.aeust.edu.tw/* // @match https://elearn.ntuspecs.ntu.edu.tw/* // @match https://elearn2.fju.edu.tw/* // @match https://iclass.tku.edu.tw/* // @match https://tronclass.ntou.edu.tw/* // @match https://tronclass.hk.edu.tw/* // @match https://tronclass.kh.usc.edu.tw/* // @match https://tronclass.usc.edu.tw/* // @match https://tronclass.cyut.edu.tw/* // @match https://tronclass.ypu.edu.tw/* // @match https://tccas.thu.edu.tw/* // @match https://ilearn.thu.edu.tw:8080/* // @match https://tronclass.cjcu.edu.tw/* // @match https://tronclass.asia.edu.tw/* // @match https://tronclass.mdu.edu.tw/* // @match https://ulearn.nfu.edu.tw/* // @match https://tc.nutc.edu.tw/* // @match https://tronclass.au.edu.tw/* // @match https://tronclass.cgust.edu.tw/* // @match https://tronclass.ocu.edu.tw/* // @match https://tc.stu.edu.tw/* // @match https://tronclass.ctust.edu.tw/* // @match https://tronclass.pu.edu.tw/* // @match https://nou.tronclass.com.tw/* // @match https://tronclass.must.edu.tw/* // @match https://tronclass.scu.edu.tw/* // @match https://ilearn.ttu.edu.tw/* // @match https://iclass.hwu.edu.tw/* // @icon https://tronclass.com.tw/static/assets/images/favicon-b420ac72.ico // @grant GM_xmlhttpRequest // @run-at document-start // @license Beerware // ==/UserScript== const _parse = JSON.parse const parseSet = {allow_download: true, allow_forward_seeking: true, pause_when_leaving_window: false} JSON.parse = (text, reviver) => _parse(text, (k, v) => { if(reviver) v = reviver(k, v) return parseSet[k] || v }) const _addEventListener = Window.prototype.addEventListener; Window.prototype.addEventListener = (eventName, fn, options) => { if (eventName === "blur" && String(fn).match(/pause/)) return; _addEventListener(eventName, fn, options); }; const delay = (ms = 0) => {return new Promise((r)=>{setTimeout(r, ms)})} const waitElementLoad = (elementSelector, selectCount = 1, tryTimes = 1, interval = 0) => { return new Promise(async (resolve, reject)=> { let t = 1, result; while(true) { if(selectCount != 1) {if((result = document.querySelectorAll(elementSelector)).length >= selectCount) break;} else {if(result = document.querySelector(elementSelector)) break;} if(tryTimes>0 && ++t>tryTimes) return reject(new Error("Wait Timeout")); await delay(interval); } resolve(result); }) } const requests = ({method, url, type = "", data = null, headers = {}}) => { return new Promise(async (resolve, reject) => { GM_xmlhttpRequest({ method: method, url: url, headers: headers, responseType: type, data: data, onload: resolve, onerror: reject, onabort: reject }); }); }; Node.prototype.catch = function () { const a = document.createElement("a") a.target = "_blank" this.insertAdjacentElement("beforebegin", a) a.appendChild(this) return a } const css = document.createElement("style") css.innerHTML = ` .title, .forum-category-title, .group-set > span {color:var(--primary-text-color)} ` const embedLink = async () => { const courseID = location.href.match(/(?<=course\/)\d+/) document.body.appendChild(css) if (location.href.match(/learning-activity\/full-screen/)) { const dict = {}; (await requests({method:"get", url:`/api/courses/${courseID}/activities`,type:"json"})) .response.activities.forEach((e)=>{dict[e.title] = e.type=="questionnaire"?`questionnaire/${e.id}`:e.id}); (await requests({method:"get", url:`/api/courses/${courseID}/exams`,type:"json"})) .response.exams.forEach((e)=>{dict[e.title] = `exam/${e.id}`}); document.querySelectorAll(".activity a[ng-click]").forEach((e) => { if(!(e.textContent.trim() in dict)) return; e.addEventListener("click", (e) => e.stopImmediatePropagation(), true) e.href = `/course/${courseID}/learning-activity/full-screen#/${dict[e.textContent.trim()]}` }) } else if (location.href.match(/homework/)) { const ids = (await requests({method:"get", url:`/api/courses/${courseID}/homework-activities?conditions=%7B%22itemsSortBy%22:%7B%22predicate%22:%22module%22,%22reverse%22:false%7D%7D&page_size=20`,type:"json"})) .response.homework_activities.map((e)=>e.id) document.querySelectorAll(".list-item").forEach((e, idx)=> { e.querySelectorAll("[ng-click]").forEach((e)=>{ if(!e.closest(".activity-operations-container")) e.addEventListener("click", (e) => e.stopImmediatePropagation(), true) }) e.catch().href = `/course/${courseID}/learning-activity#/${ids[idx]}` }) } else if (location.href.match(/(?<!learning-activity#\/)exam/)) { const ids = (await requests({method:"get", url:`/api/courses/${courseID}/exam-list?page_size=20`,type:"json"})) .response.exams.map((e)=>e.id).reverse() document.querySelectorAll(".sub-title").forEach((e, idx)=> { e.querySelector("[ng-click]").addEventListener("click", (e) => e.stopImmediatePropagation(), true) e.catch().href = `/course/${courseID}/learning-activity#/exam/${ids[idx]}` }) } else if(location.href.match(/forum(?!#\/topic-category)/)) { const ids = (await requests({method:"get", url:`/api/courses/${courseID}/topic-categories?page_size=20`,type:"json"})) .response.topic_categories.map((e)=>e.id).reverse() ids.unshift(ids.pop()) document.querySelectorAll(".list-item").forEach((e, idx)=> { e.addEventListener("click", (e) => e.stopImmediatePropagation(), true); e.catch().href = `/course/${courseID}/forum#/topic-category/${ids[idx]}` }) } else if(location.href.match(/forum#\/topic-category/)) { const sort = document.querySelector(".sort-operation .sort-active"), isReversed = sort.className.match(/down/) ? true : false, reversed = isReversed ? "&reverse" : "", sortTypeName = sort.parentElement.parentElement.className, sortType = {"last-updated-time":"lastUpdatedDate", "replies-number":"reply_count", "like-count":"like_count"}[ ["last-updated-time", "replies-number", "like-count"].filter((e)=>sortTypeName.match(e))[0]], page = document.querySelector(".pager-button.active").innerText, count = document.querySelector(".last-page").innerText.trim(), ids = (await requests({method:"get", url:`/api/forum/categories/${location.href.match(/(?<=category\/)\d+/)}?page=${page}&conditions=${ encodeURIComponent(`{"topic_sort_by":{"predicate":"${sortType}","reverse":${String(isReversed)}}}`)}`,type:"json"})) .response.result.topics.map((e)=>e.id), idList = ids.join(",") document.querySelectorAll(".topic-summary a[ng-click]").forEach((e, idx)=> { e.addEventListener("click", (e) => e.stopImmediatePropagation(), true); e.href = `/course/${courseID}/forum#/topics/${ids[idx]}?topicIds=${idList}&pageIndex=${page}&pageCount=${count}&predicate=${sortType}${reversed}` }) } else if (location.href.match(/\/user\/index/)) { const li = (await requests({method:"get", url:`/api/todos`,type:"json"})) .response.todo_list.sort((a,b)=>new Date(a.end_time)-new Date(b.end_time)).map((e)=>{return {cid:e.course_id, aid:e.id, type:e.type}}) document.querySelectorAll(".todo-list > a").forEach((e, idx)=> { switch(li[idx].type) { case "homework": e.href = `/course/${li[idx].cid}/learning-activity#/${li[idx].aid}` break case "exam": e.href = `/course/${li[idx].cid}/learning-activity#/exam/${li[idx].aid}` break case "questionnaire": e.href = `/course/${li[idx].cid}/learning-activity/full-screen#/questionnaire/${li[idx].aid}` break } e.addEventListener("click", (e) => e.stopImmediatePropagation(), true) }) } document.querySelectorAll("[id^=learning-activity]").forEach((e) => { const actID = e.id.match(/\d+/), collapse = e.querySelector(".expand-collapse-attachments") if(collapse) collapse.addEventListener("click", (e) => e.preventDefault()) e.querySelectorAll(".attachment-row").forEach((e)=>e.addEventListener("click", (e) => e.preventDefault())) e.querySelector("[ng-click]").addEventListener("click", (e) => { if(!(e.target == collapse || e.target.closest(".attachment-row"))) e.stopImmediatePropagation(); }, true); const type = e.querySelector("[ng-switch-when]").getAttribute("ng-switch-when"), a = e.catch() switch(type) { case "exam": a.href = `/course/${courseID}/learning-activity#/exam/${actID}` break case "web_link": requests({method:"get",url:`/api/activities/${actID}`,type:"json"}).then((r)=>{a.href = r.response.data.link}) break case "homework": a.href = `/course/${courseID}/learning-activity#/${actID}` break case "questionnaire": a.href = `/course/${courseID}/learning-activity/full-screen#/questionnaire/${actID}` break case "material": case "online_video": case "forum": default: a.href = `/course/${courseID}/learning-activity/full-screen#/${actID}` break /* TODO: 'slide': '微課程', 'lesson': '錄影教材', 'lesson_replay': '教室录播', 'chatroom': 'iSlide 直播', 'classroom': '隨堂測驗', 'page': '頁面', 'scorm': '第三方教材', 'interaction': '互動教材', 'feedback': '教學回饋', 'virtual_classroom': 'Connect 直播', 'zoom': 'Zoom直播', 'microsoft_teams_meeting': 'Teams 直播', 'google_meeting': 'Google Live', 'webex_meeting': 'Webex 直播', 'welink': 'Welink', 'classin': 'ClassIn 直播', 'live_record': '直播', 'select_student': '選人', 'race_answer': '搶答', 'number_rollcall': '数字点名', 'qr_rollcall': '二维码点名', 'virtual_experiment': '模擬實驗', 'wecom_meeting': 'WeCom會議', 'vocabulary': '詞彙表', */ } }) } const videoSpeedrun = async (element) => { const [userID, orgID] = st ? [st.userId, st.orgId] : (await requests({method:"get",url:"/api/profile",type:"json"}).then((e)=>[e.response.id, e.response.org.id])), courseID = st?.tags.course_id || location.href.match(/(?<=course\/)\d+/), actID = st?.tags.activity_id || location.href.split('/').pop(), endTime = element.innerText.split(":").map((e)=> Number(e)).reduce((acc, curr, index) => acc + curr * [3600,60,1][index], 0), need = Number(document.querySelector(".completion-criterion > .attribute-value").innerText.match(/\d+(?=%)/)), now = (await requests({method:"POST", url:`/api/course/activities-read/${actID}`, type:"json"})).response.data?.completeness || 0 await requests({ // increase student stat times for watch video method:"post", url:"/statistics/api/online-videos", data:`{"user_id":${userID},"org_id":${orgID},"course_id":${courseID},"activity_id":${actID},"action_type":"view","ts":${Date.now()}}` }) if(now >= need) { console.log("速刷已完成"); return; } const title = document.querySelector("span.title"), origText = title.innerText let res; for(let nowTime=Math.floor(endTime*0.01*Math.max(now-10,0)); nowTime!=endTime;) { const dur = endTime-nowTime<120 ? endTime-nowTime : Math.floor(120-Math.random()*66), newTime = nowTime + dur res = await requests({ // watch video api method: "POST", url: `/api/course/activities-read/${actID}`, data: `{"start":${nowTime},"end":${newTime}}`, headers: {"Content-Type": "application/json"}, type: "json" }).catch(()=>alert("速刷失敗,請聯絡作者")) await requests({ // increase student stat video watching time method:"post", url:"/statistics/api/online-videos", data:`{"user_id":${userID},"org_id":${orgID},"course_id":${courseID},"activity_id":${actID},"action_type":"play","ts":${Date.now()},"start_at":${nowTime},"end_at":${newTime},"duration":${dur}}` }) await requests({ // increase student stat times for access course method:"post", url:"/statistics/api/user-visits", data:`{"user_id":"${userID}","org_id":${orgID},"course_id":"${courseID}","visit_duration":${dur}}` }) console.log(`${res.response.data.completeness}% ${nowTime}-${newTime} (${endTime})`) title.innerHTML = `<b>[速刷中] (${res.response.data.completeness}%) ${newTime}/${endTime}s</b> ${origText}` nowTime = newTime } await delay(100) if(res.response.data.completeness < need) alert("速刷失敗,請聯絡作者") location.reload() } const myStat = async () => { const courseID = location.href.match(/(?<=course\/)\d+/) } let lock = false; waitElementLoad("#ngProgress",1,0,50).then((e)=>{ new MutationObserver(()=>{ if(lock==false && e.style.width=="100%") { lock = true // embedLink() waitElementLoad("span[ng-bind='ui.duration|formatTime']").then(videoSpeedrun).catch(()=>{}) // if(location.href.match(/course\/\d+/)) myStat() } else if (e.style.width=="0%") lock = false; }).observe(e, {attributes:true}) }) waitElementLoad("[data-category=tronclass-footer]",1,10,200).then((e)=>e.remove())
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址