Fuck bb

Optimizes your grading experience using bb.ustc.edu.cn, mainly for TAs.

  1. // ==UserScript==
  2. // @name Fuck bb
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.5.3
  5. // @description Optimizes your grading experience using bb.ustc.edu.cn, mainly for TAs.
  6. // @description:zh-CN 优化您在 bb.ustc.edu.cn 的评分体验(主要为了助教开发)
  7. // @author PRO
  8. // @match https://www.bb.ustc.edu.cn/webapps/assignment/*
  9. // @icon http://ustc.edu.cn/favicon.ico
  10. // @run-at document-start
  11. // @license gpl-3.0
  12. // @grant GM_setValue
  13. // @grant GM_getValue
  14. // @grant GM_deleteValue
  15. // @grant GM_registerMenuCommand
  16. // @grant GM_unregisterMenuCommand
  17. // @grant unsafeWindow
  18. // @require https://update.greasyfork.org/scripts/470224/1303666/Tampermonkey%20Config.js
  19. // ==/UserScript==
  20.  
  21. (function () {
  22. 'use strict';
  23. const config_desc = {
  24. "$default": {
  25. value: true,
  26. input: "current",
  27. processor: "not",
  28. formatter: "boolean",
  29. autoClose: false
  30. },
  31. "alternate-count": { name: "Alternate count", title: "Replace original count with a better one. Required for 'Group count' to work" },
  32. "group-cnt": {
  33. name: "Group count",
  34. value: 500,
  35. input: "prompt",
  36. processor: "int_range-1-",
  37. formatter: "normal",
  38. title: "Number of students in your group. Viewing grades of greater number of students will trigger an alert."
  39. },
  40. "focus-grade": { name: "Focus grade", title: "Automatically focuses on grade input" },
  41. "full-grade": { name: "Full grade", title: "Automatically fills in full grade" },
  42. "ignore-ungraded": { name: "Ignore un-graded", title: "Skips un-graded attemps" },
  43. "submit-draft": { name: "Submit draft", title: "Automatically submits drafts", value: false },
  44. "native-pdf": { name: "*Native PDF", title: "View students' pdf files with browser's solution and auto scrolls to the viewer" }
  45. };
  46. const config = GM_config(config_desc);
  47. const window = unsafeWindow;
  48. const $ = document.querySelector.bind(document);
  49. document.head.appendChild(document.createElement("style")).textContent = "html { scroll-behavior: smooth; }";
  50. if (config["native-pdf"]) {
  51. if (typeof window.gradeAssignment === "undefined") {
  52. Object.defineProperty(window, "gradeAssignment", {
  53. set: function (value) {
  54. console.log("[Fuck bb] Hooked gradeAssignment");
  55. value._handleInlineViewResponse = value.handleInlineViewResponse;
  56. value.handleInlineViewResponse = function (responseJSON) {
  57. const PDF = responseJSON.downloadUrl;
  58. if (PDF.endsWith(".pdf")) {
  59. fetch(PDF).then(res => res.blob()).then(blob => {
  60. const url = URL.createObjectURL(blob);
  61. responseJSON.viewUrl = url;
  62. value._handleInlineViewResponse(responseJSON);
  63. $("#inlineGrader")?.scrollIntoView();
  64. });
  65. } else {
  66. value._handleInlineViewResponse(responseJSON);
  67. $("#inlineGrader")?.scrollIntoView();
  68. }
  69. };
  70. window._gradeAssignment = value;
  71. },
  72. get: function () {
  73. return window._gradeAssignment;
  74. }
  75. });
  76. } else {
  77. window.gradeAssignment._handleInlineViewResponse = window.gradeAssignment.handleInlineViewResponse;
  78. window.gradeAssignment.handleInlineViewResponse = function (responseJSON) {
  79. const PDF = responseJSON.downloadUrl;
  80. if (PDF.endsWith(".pdf")) {
  81. fetch(PDF).then(res => res.blob()).then(blob => {
  82. const url = URL.createObjectURL(blob);
  83. responseJSON.viewUrl = url;
  84. window.gradeAssignment._handleInlineViewResponse(responseJSON);
  85. $("#inlineGrader")?.scrollIntoView();
  86. });
  87. } else {
  88. window.gradeAssignment._handleInlineViewResponse(responseJSON);
  89. $("#inlineGrader")?.scrollIntoView();
  90. }
  91. };
  92. console.log("[Fuck bb] Unable to hook, fallback");
  93. }
  94. }
  95. document.addEventListener("DOMContentLoaded", e => {
  96. const isSelf = config["alternate-count"] ? alternateCount() : true;
  97. const timer = window.setInterval(
  98. () => {
  99. if (typeof theAttemptNavController !== "undefined") {
  100. window.clearInterval(timer);
  101. waitForCtrl(isSelf);
  102. }
  103. }, 500
  104. );
  105. });
  106. // Alternate count
  107. function alternateCount() {
  108. const span = $("span.count");
  109. if (!span) return true;
  110. span.style.fontWeight = "bold";
  111. span.style.color = "red";
  112. const m = span.textContent.match(/正在查看 (\d+) 个可评分项目,共 (\d+) 个可评分项目/);
  113. if (!m) return true;
  114. const alternate = m[1] + " / " + m[2];
  115. span.textContent = alternate;
  116. if (parseInt(m[2]) > config["group-cnt"]) {
  117. alert("Viewing all students!");
  118. return false;
  119. }
  120. return true;
  121. }
  122. // Focus grade input
  123. function focusGrade() {
  124. $("#inlineGrader")?.scrollIntoView();
  125. $("#currentAttempt_grade")?.focus();
  126. }
  127. // Full grade
  128. function fullGrade() {
  129. const grade = $("#currentAttempt_grade");
  130. const max = $("#currentAttempt_pointsPossible");
  131. if (grade.value === "") {
  132. grade.value = max.textContent.slice(1);
  133. }
  134. }
  135. // Ignore un-graded attempts
  136. function ignoreUngraded() {
  137. if ($("#panelbutton2 div.students-pager img[alt='不计入用户的成绩']")) {
  138. console.log("Skip un-graded");
  139. theAttemptNavController.viewNext();
  140. }
  141. }
  142. // Auto submit drafts
  143. function submitDraft() {
  144. const grade = $("#currentAttempt_grade");
  145. const submit = $("#currentAttempt_submitButton");
  146. if (grade.value !== "" && submit) {
  147. submit.click();
  148. }
  149. }
  150. // Function to be called after the controller is loaded
  151. function waitForCtrl(isSelf) {
  152. if (config["focus-grade"]) focusGrade();
  153. if (!isSelf) return;
  154. if (config["ignore-ungraded"]) ignoreUngraded();
  155. if (config["submit-draft"]) submitDraft();
  156. if (config["full-grade"]) fullGrade();
  157. }
  158. // Shortcuts
  159. document.addEventListener("keydown", e => {
  160. if (e.key === "Escape")
  161. document.activeElement.blur();
  162. if (document.activeElement.nodeName != "INPUT") {
  163. switch (e.key) {
  164. case "ArrowLeft":
  165. theAttemptNavController.viewPrevious();
  166. break;
  167. case "ArrowRight":
  168. theAttemptNavController.viewNext();
  169. break;
  170. case "Enter": {
  171. const save = $(e.ctrlKey ? "#currentAttempt_submitButton" : "#currentAttempt_saveButton");
  172. if (save) save.click();
  173. break;
  174. }
  175. default:
  176. break;
  177. }
  178. }
  179. });
  180. // Listen to config changes
  181. const callbacks = {
  182. "alternate-count": alternateCount,
  183. "focus-grade": focusGrade,
  184. "full-grade": fullGrade,
  185. "ignore-ungraded": ignoreUngraded,
  186. "submit-draft": submitDraft,
  187. };
  188. window.addEventListener(GM_config_event, e => {
  189. if (e.detail.type === "set" && e.detail.after) {
  190. const callback = callbacks[e.detail.prop];
  191. if (callback) callback();
  192. }
  193. });
  194. })();