您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Place all information on a single page (https://canvas.example.com/all or https://example.instructure.com/all)
// ==UserScript== // @name Canvas All Info // @namespace https://theusaf.github.io // @version 1.4.0 // @icon https://canvas.instructure.com/favicon.ico // @copyright 2020-2021, Daniel Lau // @license MIT // @description Place all information on a single page (https://canvas.example.com/all or https://example.instructure.com/all) // @author theusaf // @include /^https:\/\/canvas\.[a-z0-9]*?\.[a-z]*?\/all\/?(\?.*)?$/ // @include /^https:\/\/[a-z0-9]*?\.instructure\.com\/all\/?(\?.*)?$/ // @inject-into page // @grant none // ==/UserScript== /* Note: - This userscript uses public APIs accessed by canvas - Gets class information - Gets assignments - Gets basic teacher information - This userscript does not store or upload any information gathered by the script - This userscript overwrites /all in canvas - This userscript was originally developed for Oregon State University */ /* Useful Links (for use later?) /api/v1/conversations?scope=inbox&filter_mode=and&include_private_conversation_enrollments=false - Canvas mail /api/v1/conversations/(mailbox_id)?include_participant_contexts=false&include_private_conversation_enrollments=false - Specific mail /courses/(class_id)/modules/items/assignment_info - Module items */ /** * log - logs info to console * @param {*} str * @param {...any} data */ const log = (str, ...data) => { if (data.length > 0) { console.log(`[CANVAS-ALL] ${str}`, data); } console.log(`[CANVAS-ALL] ${str}`); }; /** * load - Loads everything */ async function load() { document.title = "Dashboard - All"; /** * mainElement - the main application div * iFrameLoader - The div for loading iframes for getting data * styles - A style element */ const mainElement = document.getElementById("main"), iFrameLoader = document.createElement("div"); mainElement.innerHTML = `<style> #canvas-all-iframe-loader{ visibility: hidden; position: fixed; width: 100%; height: 100%; pointer-events: none; left: 0; } #canvas-all-iframe-loader > iframe{ width: 100%; height: 100%; position: absolute; left: 0; top: 0; } #main{ display: flex; flex-flow: column; padding: 1rem; } #main>span{ flex: 0; margin-bottom: 1rem; } #main>div{ flex: 1; } #canvasall_main_wrapper{ display: flex; } #canvasall_main_wrapper>div{ flex: 75%; } #canvasall_class_grades{ display: flex; flex-flow: column; background: #fff5e0; padding: 0.5rem; border-radius: 0.5rem; } #canvasall_assignments_wrapper{ padding: 0.5rem; background: #fff5e0; border-radius: 0.5rem; } #canvasall_main_wrapper>#canvasall_announcement_wrapper{ flex: 25%; padding: 0.5rem; } #canvasall_assignment_filter_list_chosen{ display: inline-block; } #canvasall_assignment_filter_list_chosen>option{ display: inline-block; background: grey; color: white; padding: 0.25rem; margin: 0.25rem; cursor: pointer; } #canvasall_assignment_filter_list_chosen>option::before{ content: "x "; } .canvasall_announcement_wrapper{ margin-bottom: 0.5rem; padding: 0.5rem; border-radius: 0.5rem; } .canvasall_announcement_wrapper:nth-child(2n+1){ background: #fff5e0; } .canvasall_announcement_wrapper:nth-child(2n){ background: #ddd; } .canvasall_announcement_title{ font-size: 1.25rem; font-weight: bold; } .canvasall_announcement_class, .canvasall_announcement_when{ font-size: 0.75rem; } .canvasall_announcement_class>a{ color: grey; } .canvasall_class_grade_wrapper:nth-child(2n+1){ background: white; } .canvasall_class_grade_wrapper:nth-child(2n){ background: #eee; } .canvasall_class_grade_wrapper{ flex: 1; display: flex; border-radius: 0.5rem; } .canvasall_class_grade_wrapper>div{ flex: 1; padding: 0.5rem; word-break: break-all; } .canvasall_assignment_wrapper:nth-child(2n+1){ background: rgba(255,255,255,0.8); } .canvasall_assignment_wrapper:nth-child(2n){ background: rgba(255,255,255,0.4); } .canvasall_assignment_wrapper{ display: flex; padding: 0.5rem; border-radius: 0.5rem; } .canvasall_assignment_wrapper>div{ flex: 1; padding-right: 0.25rem; padding-left: 0.25rem; } .canvasall_assignment_title{ display: flex; align-items: center; } .canvasall_assignment_title>img{ height: 1.5rem; width: 1.5rem; margin-right: 0.5rem; } .canvasall_assignment_icon { min-height: 1.5rem; min-width: 1.5rem; margin-right: 0.5rem; } .canvasall_assignment_type_assignment:before { content: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZlcnNpb249IjEuMSIgeD0iMCIgeT0iMCIgdmlld0JveD0iMCAwIDI4MCAyNTkiIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDI4MCAyNTkiIHhtbDpzcGFjZT0icHJlc2VydmUiPjxwYXRoIGQ9Ik03My4zMSwxOThjLTExLjkzLDAtMjIuMjIsOC0yNCwxOC43M2EyNi42NywyNi42NywwLDAsMC0uMywzLjYzdi4zYTIyLDIyLDAsMCwwLDUuNDQsMTQuNjUsMjIuNDcsMjIuNDcsMCwwLDAsMTcuMjIsOEgyMDBWMjI4LjE5aC0xMzRWMjEzLjA4SDIwMFYxOThabTIxLTEwNS43NGg5MC42NFY2Mkg5NC4zWk03OS4xOSwxMDcuMzRWNDYuOTJIMjAwdjYwLjQyWm03LjU1LDMwLjIxVjEyMi40NUgxOTIuNDl2MTUuMTFaTTcxLjY1LDE2LjcxQTIyLjcyLDIyLjcyLDAsMCwwLDQ5LDM5LjM2VjE5MC44OGE0MS4xMiw0MS4xMiwwLDAsMSwyNC4zMi04aDE1N1YxNi43MVpNMzMuODgsMzkuMzZBMzcuNzgsMzcuNzgsMCwwLDEsNzEuNjUsMS42SDI0NS4zNlYxOThIMjE1LjE1djQ1LjMyaDIyLjY2VjI1OC40SDcxLjY1YTM3Ljg1LDM3Ljg1LDAsMCwxLTM3Ljc2LTM3Ljc2WiI+PC9wYXRoPjwvc3ZnPgo="); } .canvasall_assignment_type_quiz:before { content: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZlcnNpb249IjEuMSIgeD0iMCIgeT0iMCIgdmlld0JveD0iMCAwIDI4MCAyODAiIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDI4MCAyODAiIHhtbDpzcGFjZT0icHJlc2VydmUiPjxwYXRoIGQ9Ik05MS43MiwxMjAuNzVoOTYuNTZWMTA0LjY1SDkxLjcyWm0wLDQ4LjI4aDgwLjQ3VjE1Mi45NEg5MS43MlptMC05Ni41Nmg4MC40N1Y1Ni4zN0g5MS43MlptMTYwLjk0LDM0Ljg4SDIyOC41MlYxMC43OGgtMTc3djk2LjU2SDI3LjM0QTI0LjE3LDI0LjE3LDAsMCwwLDMuMiwxMzEuNDhWMjQ0LjE0YTI0LjE3LDI0LjE3LDAsMCwwLDI0LjE0LDI0LjE0SDI1Mi42NmEyNC4xNywyNC4xNywwLDAsMCwyNC4xNC0yNC4xNFYxMzEuNDhBMjQuMTcsMjQuMTcsMCwwLDAsMjUyLjY2LDEwNy4zNFptMCwxNi4wOWE4LjA2LDguMDYsMCwwLDEsOCw4djUxLjc3bC0zMi4xOSwxOS4zMVYxMjMuNDRaTTY3LjU4LDIwMy45MXYtMTc3SDIxMi40MnYxNzdaTTI3LjM0LDEyMy40NEg1MS40OHY3OS4xM0wxOS4yOSwxODMuMjZWMTMxLjQ4QTguMDYsOC4wNiwwLDAsMSwyNy4zNCwxMjMuNDRaTTI1Mi42NiwyNTIuMTlIMjcuMzRhOC4wNiw4LjA2LDAsMCwxLTgtOFYyMDJsMzAsMThIMjMwLjc1bDMwLTE4djQyLjEyQTguMDYsOC4wNiwwLDAsMSwyNTIuNjYsMjUyLjE5WiI+PC9wYXRoPjwvc3ZnPgo="); } .canvasall_assignment_type_discussion_topic:before { content: url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB3aWR0aD0iMWVtIiBoZWlnaHQ9IjFlbSIgcm90YXRlPSIwIiBzdHlsZT0iaGVpZ2h0OjFlbTt3aWR0aDoxZW0iIHZpZXdCb3g9IjAgMCAxOTIwIDE5MjAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxnPgo8cGF0aCBkPSJtNjc3LjY1IDE2djMzOC45NGgxMTIuOTR2LTIyNS44OGgxMDE2LjV2NzkwLjQ4aC0yMjZ2MjU5Ljc2bC0yNTkuNjUtMjU5Ljc2aC03OS4xNzJ2LTQ1MS42NWgtNTY0LjU5LTY3Ny42NXYxMDE2LjVoMzM4LjcxdjQxOC45bDQxOC00MTguOWg0ODUuNTN2LTQ1MS44OGgzMi43NTNsNDE5LjEyIDQxOS4xMnYtNDE5LjEyaDIyNS44OHYtMTAxNi41aC0xMjQyLjR6bS0zMzguODYgOTAzLjU2aDU2NC43MXYtMTEyLjk0aC01NjQuNzF2MTEyLjk0em0wIDIyNS44OGgzMzguOTR2LTExMy4wNWgtMzM4Ljk0djExMy4wNXptLTIyNS44NS01NjQuNzRoMTAxNi41djc5MC43aC00MTkuMDFsLTI1OC43NSAyNTkuNjV2LTI1OS42NWgtMzM4Ljcxdi03OTAuN3oiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPgo8L2c+Cjwvc3ZnPgo=") } .canvasall_status_submit, .canvasall_status_submit a{ color: green; } .canvasall_status_done{ text-decoration: line-through; } .canvasall_status_late{ background: orange !important; color: white; } .canvasall_status_late a{ color: white; } .canvasall_status_missing{ background: red !important; color: white; } .canvasall_status_missing a{ color: white; } .canvasall_status_feedback>.canvasall_assignment_title::after{ content: " (Feedback available)" } </style> <span id="canvasall_fetching_information">Fetching information... please wait...</span> <div id="canvasall_main_wrapper"> <div> <div id="canvasall_class_wrapper"> <!-- Courses, Grades, etc. --> <h3>Courses</h3> <div id="canvasall_class_grades"> <div id="canvasall_class_grade_header" class="canvasall_class_grade_wrapper"> <div><span>Class</span></div> <div><span>Grade</span></div> <div><span>Professor</span></div> </div> </div> </div> <h3>Current Assignments</h3> <div> <span>Filters:</span> <select id="canvasall_assignment_filter_status"> <option value="">Select</option> <option value="submit">Hide Submitted</option> <option value="done">Hide Graded</option> <option value="late">Hide Late</option> <option value="missing">Hide Missing</option> <option value="quiz">Hide Quiz</option> <option value="assignment">Hide Assignment</option> </select> <div id="canvasall_assignment_filter_list_chosen"> </div> </div> <div id="canvasall_assignments_wrapper"> <div class="canvasall_assignment_wrapper"> <div> <span>Assignment Name</span> </div> <div> <span>Class</span> </div> <div> <span>Due Date</span> </div> </div> </div> </div> <div id="canvasall_announcement_wrapper"> <h3>Announcements</h3> </div> </div>`; iFrameLoader.id = "canvas-all-iframe-loader"; mainElement.append(iFrameLoader); /** * courses - Class information * classAssignments - Assignments for courses * mainFrame - The main iframe * ENV - Global variables with useful data * currentUserID - The current user id */ log("Getting courses"); const courses = await getCourses(), courseAssignments = [], courseGrades = {}, { ENV } = window, { currentUserID } = ENV, courseGradeDiv = mainElement.querySelector("#canvasall_class_grades"), assignmentsDiv = mainElement.querySelector("#canvasall_assignments_wrapper"), announcementsDiv = mainElement.querySelector( "#canvasall_announcement_wrapper" ), statusFilter = mainElement.querySelector( "#canvasall_assignment_filter_status" ), statusFilterList = mainElement.querySelector( "#canvasall_assignment_filter_list_chosen" ), localStorageConfigStr = localStorage.canvasAllConfig ?? "{}", localStorageConfig = JSON.parse(localStorageConfigStr); function hideType(value) { console.log(`Hiding '${value}'`); if (value === "") { // ignore reset to empty value console.log("Ignoring empty value"); return; } const elem = statusFilter.querySelector( `option[value="${value}"]` ), temp = document.createElement("template"), now = `${Math.random()}`.substring(2); if (!elem) { console.error("Could not find element"); return; } temp.innerHTML = `<style id="canvasall_filter_${now}"> .canvasall_status_${value}{ display: none; } </style>`; localStorageConfig[value] = true; localStorage.canvasAllConfig = JSON.stringify(localStorageConfig); setTimeout(() => { document.body.append(temp.content.cloneNode(true)); statusFilterList.append(elem); elem.addEventListener("click", click); statusFilter.value = ""; }); function click() { localStorageConfig[value] = false; localStorage.canvasAllConfig = JSON.stringify(localStorageConfig); // remove style document.querySelector(`#canvasall_filter_${now}`).remove(); elem.removeEventListener("click", click); setTimeout(() => { statusFilter.append(elem); statusFilter.value = ""; }); } } // Filters statusFilter.addEventListener("change", () => { hideType(statusFilter.value); }); // Begin writing class information for (const [i, course] of Object.entries(courses)) { const template = document.createElement("template"); template.innerHTML = `<div class="canvasall_class_grade_wrapper" canvasall-class-id="${ course.id }"> <div class="canvasall_class_grade_name"> <span>${+i + 1}. <a href="/courses/${course.id}">${ course.name }</a></span> </div> <div class="canvasall_class_grade_score"> <span>(Loading scores...)</span> </div> <div class="canvasall_class_grade_instructor"> <span>(Loading instructors...)</span> </div> </div>`; courseGradeDiv.append(template.content.cloneNode(true)); } log("Getting course assignments and grades"); let courseCount = 0, finishedCollections = 0; for (const course of courses) { courseCount++; getCourseAssignments(course.id, currentUserID).then(assignments => { courseAssignments.push.apply( courseAssignments, assignments ); finishedCollections++; if (finishedCollections === courseCount) { // Write assignments courseAssignments.sort((a, b) => { // sort by "due date" // also places non-due-date at end. (+1 week) const dueA = new Date( a.plannable.due_at || b.plannable.created_at || Date.now() + 604800e3 ), dueB = new Date( b.plannable.due_at || b.plannable.created_at || Date.now() + 604800e3 ); return dueA.getTime() - dueB.getTime(); }); log("Compiling assignments"); for (const assignment of courseAssignments) { const template = document.createElement("template"); if (assignment.plannable_type === "announcement") { // Put in announcement thing template.innerHTML = `<div class="canvasall_announcement_wrapper"> <div class="canvasall_announcement_title"> <a href="${assignment.html_url}">${assignment.plannable.title}</a> </div> <div class="canvasall_announcement_class"> <a href="/courses/${assignment.course_id}">${ assignment.context_name }</a> </div> <div class="canvasall_announcement_when"> <span>${new Date(assignment.plannable.created_at) .toString() .split(" ") .slice(0, 5) .join(" ") .replace(/:\d{2}$/, "") .replace(/\s(?=\w{3}\s\d{2})/, ", ") .replace(/\d{4}/, "at")}</span> </div> </div>`; announcementsDiv.append(template.content.cloneNode(true)); continue; } const divClasses = [], { submissions } = assignment; if (submissions) { const { excused, graded, has_feedback, late, missing, submitted } = submissions; if (excused || graded) { divClasses.push("canvasall_status_done"); } if (submitted) { divClasses.push("canvasall_status_submit"); } if (late) { divClasses.push("canvasall_status_late"); } if (missing) { divClasses.push("canvasall_status_missing"); } if (has_feedback) { divClasses.push("canvasall_status_feedback"); } } divClasses.push("canvasall_status_" + assignment.plannable_type); template.innerHTML = `<div class="canvasall_assignment_wrapper ${divClasses.join( " " )}" class-id="${assignment.course_id}"> <div class="canvasall_assignment_title"> <div class="canvasall_assignment_icon canvasall_assignment_type_${ assignment.plannable_type }"></div> <a href="${assignment.html_url}">${assignment.plannable.title}</a> </div> <div class="canvasall_assignment_class"> <a href="/courses/${assignment.course_id}">${ assignment.context_name }</a> </div> <div class="canvasall_assignment_due"> <span>${ assignment.plannable.due_at ? new Date(assignment.plannable.due_at) .toString() .split(" ") .slice(0, 5) .join(" ") .replace(/:\d{2}$/, "") .replace(/\s(?=\w{3}\s\d{2})/, ", ") .replace(/\d{4}/, "at") : "No due date" }</span> </div> </div>`; assignmentsDiv.append(template.content.cloneNode(true)); } // Restore hide for (const [key, value] of Object.entries(localStorageConfig)) { if (value && key !== "") { hideType(key); } } courseAssignments.splice(0); // Free up memory document.querySelector("#canvasall_fetching_information").outerHTML = ""; } }); // Get grades loadFrame(iFrameLoader, `/courses/${course.id}/grades`).then( (ClassGradeFrame) => { const { document: d, window: w, frame } = ClassGradeFrame; let overallGrade = d .querySelector( "#student-grades-right-content .student_assignment.final_grade > .grade" ) ?.textContent?.replace(/%|\s/g, ""), titles = d.querySelectorAll(".title"), possiblePoints = d.querySelectorAll(".points_possible"), dueDates = d.querySelectorAll(".due"); courseGrades[course.id] = { Grades: overallGrade + "%", Titles: titles, PossiblePoints: possiblePoints, DueDates: dueDates, }; // Write grades const gradeDiv = courseGradeDiv.querySelector( `[canvasall-class-id="${course.id}"] > .canvasall_class_grade_score` ), { ENV } = w, { grading_scheme } = ENV, [letter] = (grading_scheme ?? ["N/A", 0]).find((scheme) => { try { return +overallGrade / 100 >= scheme[1]; } catch (e) { console.error(e); } }) ?? ["?"]; gradeDiv.innerHTML = overallGrade !== "N/A" && overallGrade ? `<span>${letter} (${overallGrade}%)</span>` : `<span>N/A</span>`; // Done using data from iframe, attempt to clean up memory usage overallGrade = titles = possiblePoints = dueDates = null; for (const i in courseGrades) { delete courseGrades[i]; } w.location = "about:blank"; setTimeout(() => { frame.remove(); }, 500); } ); // Get instructors getCourseTeacher(course.id) .then((teacher) => { const teacherDiv = courseGradeDiv.querySelector( `[canvasall-class-id="${course.id}"] > .canvasall_class_grade_instructor` ); teacherDiv.innerHTML = `<span>${teacher.name}</span>`; }) .catch(() => { // no teacher, or failed to get teacher const teacherDiv = courseGradeDiv.querySelector( `[canvasall-class-id="${course.id}"] > .canvasall_class_grade_instructor` ); teacherDiv.innerHTML = "<span>No instructor found.</span>"; }); } } // /api/v1/users/:user_id/communication_channels /** * getCourses - Gets all the classes of the user * * @returns {Promise<Array>} A list of class information */ function getCourses() { return new Promise((res, rej) => { const x = new XMLHttpRequest(); x.open( "GET", "/api/v1/users/self/favorites/courses?include[]=term&exclude[]=enrollments&sort=nickname" ); x.setRequestHeader("X-Requested-With", "XMLHttpRequest"); x.setRequestHeader( "accept", "application/json, text/javascript, application/json+canvas-string-ids, */*; q=0.01" ); x.onload = () => { res(JSON.parse(x.responseText)); }; x.onerror = () => { rej(); }; x.send(); }); } /** * GetClassAssignments - Gets the class assignments * * @param {String} courseID The class id * @param {String} userID The user id * @returns {Promise<Array>} The list of assignments */ function getCourseAssignments(courseID, userID) { const x = new XMLHttpRequest(), now = new Date(), offset = now.getTimezoneOffset() / 60, nextUrlRegex = /[^<>]*(?=>; rel="next")/; now.addDays(-14); // To get assignments from 2 weeks ago now.setHours(8 - offset); now.setMinutes(0); now.setSeconds(0); now.setMilliseconds(0); function makeHttpRequest(url) { return new Promise((res, rej) => { x.open("GET", url); x.setRequestHeader("X-Requested-With", "XMLHttpRequest"); x.setRequestHeader( "accept", "application/json+canvas-string-ids, application/json+canvas-string-ids, application/json, text/plain, */*" ); x.onerror = () => {rej();} x.onload = () => { res(x); } x.send(); }); } async function gatherRecursiveRequests(url, list = []) { const response = await makeHttpRequest(url), data = JSON.parse(response.responseText), linkHeader = response.getResponseHeader("link"); list.push(...data); if (linkHeader) { const lastLink = linkHeader.match(nextUrlRegex); if (lastLink) { return gatherRecursiveRequests(lastLink[0], list); } else { return list; } } else { return list; } } return gatherRecursiveRequests(`/api/v1/planner/items?start_date=${now.toISOString()}&order=asc&context_codes[]=course_${courseID}`) //&context_codes[]=user_${userID} .catch(() => []); } /** * getCourseTeacher - Gets the teacher of the course * * @param {String} courseID The class id * @returns {Object} The teacher */ function getCourseTeacher(courseID) { return new Promise((res, rej) => { const x = new XMLHttpRequest(); x.open( "GET", `/api/v1/search/recipients?search=&per_page=20&permissions[]=send_messages_all&messageable_only=true&synthetic_contexts=true&context=course_${courseID}_teachers` ); x.setRequestHeader("X-Requested-With", "XMLHttpRequest"); x.setRequestHeader( "Accept", "application/json, text/javascript, application/json+canvas-string-ids, */*; q=0.01" ); x.onload = () => { res(JSON.parse(x.responseText)[0]); }; x.onerror = () => { rej(); }; x.send(); }); } /** * loadFrame - Loads an iframe * * @param {HTMLElement} iFrameLoader The element to append iframes to * @param {String} src The iframe url to load * @returns {Promise<Object>} returns an object with the "window" and "document" of the iframe */ function loadFrame(iFrameLoader, src) { return new Promise((res) => { const mainFrame = document.createElement("iframe"); mainFrame.src = src || "/"; iFrameLoader.append(mainFrame); mainFrame.addEventListener("load", () => { const frameContext = { document: mainFrame.contentDocument, window: mainFrame.contentWindow, frame: mainFrame }; res(frameContext); }); }); } load();