// ==UserScript== // @name 头歌助手 Educoder Helper // @namespace https://github.com/lcandy2/user.js/tree/main/websites/educoder.net/educoder-helper // @version 2.8 // @author 甜檸Cirtron (lcandy2) // @description 【本脚本需配合《头歌复制助手 Educoder Copy Helper》使用,使用脚本前请确保复制助手已安装】📝解除头歌复制粘贴限制,解除头哥复制缩短限制,支持考试;✨增加“一键复制”、“一键全部文件复制”、“导出全部文件”、“一键完成视频任务”等功能。🧹简单高效代码,无需权限配置,清除广告界面,全自动化签到,安装即用。💛安全开源可读,无论是编译前后的代码均保持开源和易读性,保护隐私与账号安全 // @license AGPL-3.0-or-later // @copyright lcandy2 All Rights Reserved // @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAACXBIWXMAAAsTAAALEwEAmpwYAAABsUlEQVR4nO2ZzUrDQBCAA3kM23NfJKuC4FW6vYjZV/DiK7SaeCxYKiRQE5qNB7EPUoT6A3op9GBFi5eeIhsMjNbW2s2apMwHc2t35tud3RxG0xTQ6IRlyw26thtMRFgu55ZzWdGKQKMTlm2Hj22XR1/C4eNTJyhpeceKdz4u+ur43N8QRdtOcP0p4Wt5x47bhkdwt+NTcXlkucFbJkVt7u2XCTW7RpVNCGVREjNtAuLopLmT/P/Qau4u+i0Ba4ocBmXhVu2gklrxBmVjmGQZgXr7YiokRPH1tj9dVoAkItR8EbmlBcTO/5TgN4G/BJmzPqFM/s7Atrm5fYggaQlA+oN72E7ydwbuyHfOeE+6+BbvzawLcyoVeByOpCRavBc9DUfZCaiCoAAAT2AFCLbQki2ke+9SkfkJoMAc8A5A8BldAYLPKABfIS/H3wFVEBRQdAdydwJ60QVUQVAAgN8BD78Dcqz1JdaL/ozqRRdQBUEBAJ5A1i1kgAGHGD6opj+4AwLmq7wAZeGCEZDiMD1pATEtFAO3fy++yp63a6yU4piV+WJmpbpwI85heqkVjyBrzgdOSyKlYdgYdgAAAABJRU5ErkJggg== // @homepage https://greasyfork.org/scripts/495493 // @homepageURL https://greasyfork.org/scripts/495493 // @source https://github.com/lcandy2/user.js/tree/main/websites/educoder.net/educoder-helper // @match *://www.educoder.net/tasks/* // @match *://www.educoder.net/classrooms/* // @match *://www.educoder.net/* // @require https://registry.npmmirror.com/vue/3.4.27/files/dist/vue.global.prod.js // @require data:application/javascript,%3Bwindow.Vue%3DVue%3B // @require https://registry.npmmirror.com/vuetify/3.6.6/files/dist/vuetify.min.js // @require data:application/javascript,%3B // @require https://registry.npmmirror.com/js-base64/3.7.7/files/base64.js // @resource VuetifyStyle https://registry.npmmirror.com/vuetify/3.6.6/files/dist/vuetify.min.css // @grant GM_addStyle // @grant GM_getResourceText // @grant unsafeWindow // @run-at document-idle // ==/UserScript== (function (vue, vuetify, jsBase64) { 'use strict'; const cssLoader = (e) => { const t = GM_getResourceText(e); return GM_addStyle(t), t; }; cssLoader("VuetifyStyle"); const getTaskInfo = () => { const href2 = window.location.href; const hrefUrl = new URL(href2); const pathname2 = hrefUrl.pathname; const parts = pathname2.substring(1).split("/"); const courseId = parts[1]; const shixunId = parts[2]; const taskId = parts[3]; return { courseId, shixunId, taskId }; }; const getVideoInfo = () => { const href2 = window.location.href; const hrefUrl = new URL(href2); const pathname2 = hrefUrl.pathname; const parts = pathname2.substring(1).split("/"); const courseId = parts[1]; const searchParams = hrefUrl.searchParams; const videoId = Number(searchParams.get("new_video_id")); const durationElement = document.querySelector("#duration"); const durationText = durationElement == null ? void 0 : durationElement.textContent; let duration = -1; if (durationText) { const timeParts = durationText.split(":"); const minutes = Number(timeParts[0]); const seconds = Number(timeParts[1]); const totalSeconds = minutes * 60 + seconds; const randomDecimal = Number(Math.random().toFixed(1)); duration = totalSeconds + randomDecimal; } return { courseId, videoId, duration }; }; var _unsafeWindow = /* @__PURE__ */ (() => typeof unsafeWindow != "undefined" ? unsafeWindow : void 0)(); const _hoisted_1$4 = { key: 0, style: { "display": "flex", "flex-direction": "row", "align-items": "center", "gap": "1em" } }; const _hoisted_2$2 = { class: "text-body-2", style: { "margin": "0" } }; const _sfc_main$8 = /* @__PURE__ */ vue.defineComponent({ __name: "copy-all-content", props: { isActive: {} }, setup(__props) { const props = __props; const closeDialog = () => { props.isActive.value = false; }; const files = vue.ref([]); const isLoading = vue.ref(true); const progress = vue.ref(0); const progressMessage = vue.ref(""); const isError = vue.ref(false); const allChecked = vue.ref(false); const helperNotInstalled = vue.ref(false); vue.onMounted(async () => { const window2 = _unsafeWindow; if (window2.educoderCopyHelper === void 0) { helperNotInstalled.value = true; isLoading.value = false; isError.value = true; return; } const paths = window2.taskChallengePath && window2.taskChallengePath.split(";").filter((value) => value !== ""); if (paths) { for (const [index, path] of paths.entries()) { progress.value = (index + 1) / paths.length * 100; progressMessage.value = `正在获取文件:${path}`; const taskId = window2.taskId || getTaskInfo().taskId; const response = await fetch( `https://data.educoder.net/api/tasks/${taskId}/rep_content.json?path=${path}`, { credentials: "include", headers: { "X-EDU-Signature": window2.xEduSignature || "", "X-EDU-Timestamp": window2.xEduTimestamp || "", "X-EDU-Type": window2.xEduType || "pc" } } ); const res = await response.json(); if (res && res.content && res.content.content) { const file = { name: path, content: jsBase64.decode(res.content.content), visible: true }; files.value.push(file); } } } isLoading.value = false; if (files.value.length === 0) { isError.value = true; } }); const filesContent = vue.computed(() => { if (isError.value) { if (helperNotInstalled.value) { return `获取代码失败! 本插件需要《头歌复制助手 EduCoder Copy Helper》安装并启用后方可使用。 请安装并启用后刷新页面再试。 Greasy Fork 安装地址:https://greasyfork.org/scripts/495490 ScriptCat脚本猫 安装地址:https://scriptcat.org/script-show-page/1860`; } return "获取代码失败,请刷新再试。"; } return files.value.filter((file) => file.visible).map((file) => `${file.name} \`\`\` ${file.content}\`\`\``).join("\n\n"); }); vue.watch(allChecked, (newValue) => { if (newValue) { files.value.forEach((file) => file.visible = newValue); } }); vue.watch( () => files.value.map((file) => file.visible), (newValues) => { allChecked.value = newValues.every(Boolean); }, { deep: true } ); return (_ctx, _cache) => { const _component_v_checkbox_btn = vue.resolveComponent("v-checkbox-btn"); const _component_v_textarea = vue.resolveComponent("v-textarea"); const _component_v_card_text = vue.resolveComponent("v-card-text"); const _component_v_progress_circular = vue.resolveComponent("v-progress-circular"); const _component_v_btn = vue.resolveComponent("v-btn"); const _component_v_spacer = vue.resolveComponent("v-spacer"); const _component_v_card_actions = vue.resolveComponent("v-card-actions"); const _component_v_card = vue.resolveComponent("v-card"); return vue.openBlock(), vue.createBlock(_component_v_card, { loading: isLoading.value, title: "全部代码" }, { default: vue.withCtx(() => [ vue.createVNode(_component_v_card_text, null, { default: vue.withCtx(() => [ files.value.length ? (vue.openBlock(), vue.createBlock(_component_v_checkbox_btn, { key: 0, label: "全选", modelValue: allChecked.value, "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => allChecked.value = $event), onClick: _cache[1] || (_cache[1] = () => { allChecked.value && files.value.forEach((file) => file.visible = false); }) }, null, 8, ["modelValue"])) : vue.createCommentVNode("", true), (vue.openBlock(true), vue.createElementBlock(vue.Fragment, null, vue.renderList(files.value, (file, index) => { return vue.openBlock(), vue.createBlock(_component_v_checkbox_btn, { key: index, label: file.name, modelValue: file.visible, "onUpdate:modelValue": ($event) => file.visible = $event }, null, 8, ["label", "modelValue", "onUpdate:modelValue"]); }), 128)), vue.createVNode(_component_v_textarea, { "auto-grow": "", modelValue: filesContent.value, "onUpdate:modelValue": _cache[2] || (_cache[2] = ($event) => filesContent.value = $event), disabled: isLoading.value, readonly: isError.value || isLoading.value, placeholder: "全部代码将在这里显示", loading: isLoading.value, "persistent-placeholder": "" }, null, 8, ["modelValue", "disabled", "readonly", "loading"]) ]), _: 1 }), vue.createVNode(_component_v_card_actions, null, { default: vue.withCtx(() => [ isLoading.value ? (vue.openBlock(), vue.createElementBlock("div", _hoisted_1$4, [ vue.createVNode(_component_v_progress_circular, { "model-value": progress.value }, null, 8, ["model-value"]), vue.createElementVNode("p", _hoisted_2$2, vue.toDisplayString(progressMessage.value), 1) ])) : vue.createCommentVNode("", true), helperNotInstalled.value ? (vue.openBlock(), vue.createBlock(_component_v_btn, { key: 1, text: "安装《头歌复制助手 EduCoder Copy Helper》", variant: "elevated", color: "primary", href: "https://greasyfork.org/scripts/495490", target: "_blank" })) : vue.createCommentVNode("", true), helperNotInstalled.value ? (vue.openBlock(), vue.createBlock(_component_v_btn, { key: 2, text: "通过 ScriptCat 脚本猫安装", variant: "text", color: "primary", href: "https://scriptcat.org/script-show-page/1860", target: "_blank" })) : vue.createCommentVNode("", true), vue.createVNode(_component_v_spacer), vue.createVNode(_component_v_btn, { text: "关闭", onClick: closeDialog }) ]), _: 1 }) ]), _: 1 }, 8, ["loading"]); }; } }); const _sfc_main$7 = /* @__PURE__ */ vue.defineComponent({ __name: "copy-all", setup(__props) { return (_ctx, _cache) => { const _component_v_btn = vue.resolveComponent("v-btn"); const _component_v_dialog = vue.resolveComponent("v-dialog"); return vue.openBlock(), vue.createBlock(_component_v_dialog, { "max-width": "800", scrollable: "" }, { activator: vue.withCtx(({ props: activatorProps }) => [ vue.createVNode(_component_v_btn, vue.mergeProps(activatorProps, { color: "surface-variant", text: "复制全部", variant: "flat" }), null, 16) ]), default: vue.withCtx(({ isActive }) => [ vue.createVNode(_sfc_main$8, { "is-active": isActive }, null, 8, ["is-active"]) ]), _: 1 }); }; } }); const _hoisted_1$3 = { style: { "display": "flex", "flex-direction": "row", "align-items": "center", "gap": "1em" } }; const _hoisted_2$1 = { class: "text-body-1", style: { "margin": "0" } }; const _hoisted_3$1 = /* @__PURE__ */ vue.createElementVNode("br", null, null, -1); const _hoisted_4$1 = /* @__PURE__ */ vue.createElementVNode("br", null, null, -1); const _hoisted_5$1 = /* @__PURE__ */ vue.createElementVNode("br", null, null, -1); const _hoisted_6$1 = /* @__PURE__ */ vue.createElementVNode("br", null, null, -1); const _hoisted_7$1 = /* @__PURE__ */ vue.createElementVNode("br", null, null, -1); const _hoisted_8$1 = /* @__PURE__ */ vue.createElementVNode("br", null, null, -1); const _hoisted_9$1 = /* @__PURE__ */ vue.createElementVNode("br", null, null, -1); const _sfc_main$6 = /* @__PURE__ */ vue.defineComponent({ __name: "reset-all-content", props: { isActive: {}, setIsPersistent: { type: Function } }, setup(__props) { const props = __props; const closeDialog = () => { props.isActive.value = false; }; const inProgress = vue.ref(false); const progress = vue.ref(-1); const progressMessage = vue.ref("准备开始"); const isAvailable = vue.ref(false); const isError = vue.ref(false); const isWaitingForRefresh = vue.ref(false); const allPaths = vue.ref([]); const handleReset = async () => { inProgress.value = true; props.setIsPersistent(true); if (allPaths.value.length === 0) { inProgress.value = false; isError.value = true; props.setIsPersistent(false); return; } for (const [index, path] of allPaths.value.entries()) { if (allPaths.value.length > 1) { progress.value = (index + 1) / allPaths.value.length * 100; } else { progress.value = -1; } progressMessage.value = `正在重置:${path}`; const window2 = _unsafeWindow; const taskId = window2.taskId || getTaskInfo().taskId; const response = await fetch( `https://data.educoder.net/api/tasks/${taskId}/reset_original_code.json?path=${path}`, { credentials: "include", headers: { "X-EDU-Signature": window2.xEduSignature || "", "X-EDU-Timestamp": window2.xEduTimestamp || "", "X-EDU-Type": window2.xEduType || "pc" }, cache: "no-store" } ); const res = await response.json(); if (res && res.content) { console.info(`重置成功:${path}`); } else { console.error(`重置失败:${path}`, res); } await new Promise((resolve) => setTimeout(resolve, 250)); } progress.value = -1; progressMessage.value = "等待刷新"; isWaitingForRefresh.value = true; window.onbeforeunload = null; await new Promise((resolve) => setTimeout(resolve, 1e3)); window.location.reload(); progressMessage.value = "重置完成,等待页面刷新"; props.setIsPersistent(false); }; vue.onMounted(() => { const window2 = _unsafeWindow; if (window2.educoderCopyHelper === void 0) { isAvailable.value = false; return; } isAvailable.value = true; const paths = window2.taskChallengePath && window2.taskChallengePath.split(";").filter((value) => value !== ""); if (paths) { allPaths.value = paths; } }); const handleRefresh = () => { window.location.reload(); }; return (_ctx, _cache) => { const _component_v_progress_circular = vue.resolveComponent("v-progress-circular"); const _component_v_card_text = vue.resolveComponent("v-card-text"); const _component_v_btn = vue.resolveComponent("v-btn"); const _component_v_spacer = vue.resolveComponent("v-spacer"); const _component_v_card = vue.resolveComponent("v-card"); return vue.openBlock(), vue.createBlock(_component_v_card, { "prepend-icon": "mdi-alert", title: isError.value ? "重置失败" : isAvailable.value ? "重置全部代码?" : "依赖插件未安装", loading: inProgress.value }, { actions: vue.withCtx(() => [ !isAvailable.value ? (vue.openBlock(), vue.createBlock(_component_v_btn, { key: 0, text: "安装插件", variant: "elevated", color: "primary", href: "https://greasyfork.org/scripts/495490", target: "_blank" })) : vue.createCommentVNode("", true), !isAvailable.value ? (vue.openBlock(), vue.createBlock(_component_v_btn, { key: 1, text: "脚本猫", variant: "text", color: "primary", href: "https://scriptcat.org/script-show-page/1860", target: "_blank" })) : vue.createCommentVNode("", true), vue.createVNode(_component_v_spacer), vue.createVNode(_component_v_btn, { disabled: inProgress.value, onClick: closeDialog }, { default: vue.withCtx(() => [ vue.createTextVNode(vue.toDisplayString(isError.value ? "完成" : "取消"), 1) ]), _: 1 }, 8, ["disabled"]), isAvailable.value && !isError.value && !isWaitingForRefresh.value ? (vue.openBlock(), vue.createBlock(_component_v_btn, { key: 2, disabled: inProgress.value, color: "error", variant: "tonal", onClick: handleReset }, { default: vue.withCtx(() => [ vue.createTextVNode(" 重置所有代码 ") ]), _: 1 }, 8, ["disabled"])) : vue.createCommentVNode("", true), isWaitingForRefresh.value ? (vue.openBlock(), vue.createBlock(_component_v_btn, { key: 3, color: "primary", variant: "tonal", onClick: handleRefresh }, { default: vue.withCtx(() => [ vue.createTextVNode(" 手动刷新页面 ") ]), _: 1 })) : vue.createCommentVNode("", true) ]), default: vue.withCtx(() => [ inProgress.value ? (vue.openBlock(), vue.createBlock(_component_v_card_text, { key: 0 }, { default: vue.withCtx(() => [ vue.createElementVNode("div", _hoisted_1$3, [ vue.createVNode(_component_v_progress_circular, { "model-value": progress.value, indeterminate: progress.value === -1 }, null, 8, ["model-value", "indeterminate"]), vue.createElementVNode("p", _hoisted_2$1, vue.toDisplayString(progressMessage.value), 1) ]) ]), _: 1 })) : isError.value ? (vue.openBlock(), vue.createBlock(_component_v_card_text, { key: 1 }, { default: vue.withCtx(() => [ vue.createTextVNode(" 重置失败,请刷新再试。 ") ]), _: 1 })) : isAvailable.value ? (vue.openBlock(), vue.createBlock(_component_v_card_text, { key: 2 }, { default: vue.withCtx(() => [ vue.createTextVNode(" 你确定要将所有代码恢复为初始状态?"), _hoisted_3$1, _hoisted_4$1, vue.createTextVNode(" 请注意,此操作不可撤销,所有未保存的代码将会丢失。 ") ]), _: 1 })) : (vue.openBlock(), vue.createBlock(_component_v_card_text, { key: 3 }, { default: vue.withCtx(() => [ vue.createTextVNode(" 本插件需要《头歌复制助手 EduCoder Copy Helper》安装并启用后方可使用。"), _hoisted_5$1, vue.createTextVNode("请安装并启用后刷新页面再试。 "), _hoisted_6$1, _hoisted_7$1, vue.createTextVNode(" Greasy Fork 安装地址:https://greasyfork.org/scripts/495490 "), _hoisted_8$1, _hoisted_9$1, vue.createTextVNode(" ScriptCat脚本猫 安装地址:https://scriptcat.org/script-show-page/1860 ") ]), _: 1 })) ]), _: 1 }, 8, ["title", "loading"]); }; } }); const _sfc_main$5 = /* @__PURE__ */ vue.defineComponent({ __name: "reset-all", setup(__props) { const isPersistent = vue.ref(false); const setIsPersistent = (value) => { isPersistent.value = value; }; return (_ctx, _cache) => { const _component_v_btn = vue.resolveComponent("v-btn"); const _component_v_dialog = vue.resolveComponent("v-dialog"); return vue.openBlock(), vue.createBlock(_component_v_dialog, { "max-width": "400", persistent: isPersistent.value }, { activator: vue.withCtx(({ props: activatorProps }) => [ vue.createVNode(_component_v_btn, vue.mergeProps(activatorProps, { "prepend-icon": "mdi-restart", color: "warning", text: "全部重置", variant: "plain" }), null, 16) ]), default: vue.withCtx(({ isActive }) => [ vue.createVNode(_sfc_main$6, { "is-active": isActive, "set-is-persistent": setIsPersistent }, null, 8, ["is-active"]) ]), _: 1 }, 8, ["persistent"]); }; } }); const _hoisted_1$2 = /* @__PURE__ */ vue.createElementVNode("br", null, null, -1); const _hoisted_2 = /* @__PURE__ */ vue.createElementVNode("br", null, null, -1); const _hoisted_3 = /* @__PURE__ */ vue.createElementVNode("br", null, null, -1); const _hoisted_4 = /* @__PURE__ */ vue.createElementVNode("br", null, null, -1); const _hoisted_5 = /* @__PURE__ */ vue.createElementVNode("br", null, null, -1); const _hoisted_6 = /* @__PURE__ */ vue.createElementVNode("br", null, null, -1); const _hoisted_7 = /* @__PURE__ */ vue.createElementVNode("b", null, "该门实验成绩无效。", -1); const _hoisted_8 = /* @__PURE__ */ vue.createElementVNode("br", null, null, -1); const _hoisted_9 = /* @__PURE__ */ vue.createElementVNode("br", null, null, -1); const _hoisted_10 = /* @__PURE__ */ vue.createElementVNode("br", null, null, -1); const _hoisted_11 = /* @__PURE__ */ vue.createElementVNode("br", null, null, -1); const _hoisted_12 = /* @__PURE__ */ vue.createElementVNode("br", null, null, -1); const _hoisted_13 = /* @__PURE__ */ vue.createElementVNode("br", null, null, -1); const _hoisted_14 = /* @__PURE__ */ vue.createElementVNode("br", null, null, -1); const _hoisted_15 = /* @__PURE__ */ vue.createElementVNode("br", null, null, -1); const _hoisted_16 = /* @__PURE__ */ vue.createElementVNode("br", null, null, -1); const _hoisted_17 = /* @__PURE__ */ vue.createElementVNode("br", null, null, -1); const _hoisted_18 = /* @__PURE__ */ vue.createElementVNode("br", null, null, -1); const _hoisted_19 = /* @__PURE__ */ vue.createElementVNode("b", null, "该门实验成绩无效。", -1); const _hoisted_20 = /* @__PURE__ */ vue.createElementVNode("br", null, null, -1); const _hoisted_21 = /* @__PURE__ */ vue.createElementVNode("br", null, null, -1); const _hoisted_22 = /* @__PURE__ */ vue.createElementVNode("br", null, null, -1); const _hoisted_23 = /* @__PURE__ */ vue.createElementVNode("br", null, null, -1); const _hoisted_24 = /* @__PURE__ */ vue.createElementVNode("br", null, null, -1); const _hoisted_25 = /* @__PURE__ */ vue.createElementVNode("br", null, null, -1); const _hoisted_26 = /* @__PURE__ */ vue.createElementVNode("br", null, null, -1); const _hoisted_27 = /* @__PURE__ */ vue.createElementVNode("br", null, null, -1); const _hoisted_28 = /* @__PURE__ */ vue.createElementVNode("br", null, null, -1); const _sfc_main$4 = /* @__PURE__ */ vue.defineComponent({ __name: "get-anser-content", props: { isActive: {}, setIsPersistent: { type: Function } }, setup(__props) { const props = __props; const closeDialog = () => { props.isActive.value = false; }; const waitingForReading = vue.ref(-1); const isInFirstCheck = vue.ref(true); const isInSecondCheck = vue.ref(false); const isInFinalResult = vue.ref(false); const isHidden = vue.ref(true); const answerInfo = vue.ref({ answer_id: -1, answer_radio: -1, answer_score: -1 }); const answerContent = vue.ref(""); const inProgress = vue.ref(false); const isError = vue.ref(false); const errorMessages = vue.ref(""); const isWaitingForRefresh = vue.ref(false); const allPaths = vue.ref([]); const handleGetAnswerInfo = async () => { inProgress.value = true; const window2 = _unsafeWindow; const taskId = window2.taskId || getTaskInfo().taskId; const response = await fetch( `https://data.educoder.net/api/tasks/${taskId}/get_answer_info.json`, { credentials: "include", headers: { "X-EDU-Signature": window2.xEduSignature || "", "X-EDU-Timestamp": window2.xEduTimestamp || "", "X-EDU-Type": window2.xEduType || "pc" } } ); const res = await response.json(); if (res.status && res.status === 1 && res.message && res.message.answer_id) { isInSecondCheck.value = true; const answer_id = res.message.answer_id; const answer_radio = res.message.answer_radio || -1; const answer_score = res.message.answer_score || -1; answerInfo.value = { answer_id, answer_radio, answer_score }; } else if (res.status && res.status === 3 && res.message && res.message[0] && res.message[0].answer_contents) { answerContent.value = res.message[0].answer_contents; isInFinalResult.value = true; } else { isError.value = true; errorMessages.value = JSON.stringify(res); } inProgress.value = false; }; const handleUnclockAnswer = async () => { inProgress.value = true; const window2 = _unsafeWindow; const taskId = window2.taskId || getTaskInfo().taskId; const answer_id = answerInfo.value.answer_id; const url = new URL( `https://data.educoder.net/api/tasks/${taskId}/unlock_answer.json` ); url.searchParams.append("answer_id", answer_id.toString()); const response = await fetch(url.href, { credentials: "include", headers: { "X-EDU-Signature": window2.xEduSignature || "", "X-EDU-Timestamp": window2.xEduTimestamp || "", "X-EDU-Type": window2.xEduType || "pc" } }); const res = await response.json(); if (res && res.contents) { answerContent.value = res.contents; isInFinalResult.value = true; } else { isError.value = true; errorMessages.value = JSON.stringify(res); } inProgress.value = false; }; const waitingForReadingDisabled = vue.computed(() => waitingForReading.value > 0); const isAvailable = vue.computed(() => isInFirstCheck || isInSecondCheck); vue.watch( () => waitingForReading.value, (value) => { if (value > 0) { setTimeout(() => { waitingForReading.value = value - 1; }, 1e3); } } ); vue.watch( () => isInFirstCheck.value, (value) => { if (value) { isInFirstCheck.value = true; isInSecondCheck.value = false; isInFinalResult.value = false; } } ); vue.watch( () => isInSecondCheck.value, (value) => { if (value) { isInFirstCheck.value = false; isInSecondCheck.value = true; isInFinalResult.value = false; } } ); vue.watch( () => isInFinalResult.value, (value) => { if (value) { isInFirstCheck.value = false; isInSecondCheck.value = false; isInFinalResult.value = true; } } ); vue.onMounted(() => { waitingForReading.value = 0; const window2 = _unsafeWindow; if (window2.educoderCopyHelper === void 0) { isInFirstCheck.value = false; return; } isInFirstCheck.value = true; if (window2.educoderAnswerHelper === void 0) { isHidden.value = true; return; } isHidden.value = false; const paths = window2.taskChallengePath && window2.taskChallengePath.split(";").filter((value) => value !== ""); if (paths) { allPaths.value = paths; } }); return (_ctx, _cache) => { const _component_v_card_text = vue.resolveComponent("v-card-text"); const _component_v_textarea = vue.resolveComponent("v-textarea"); const _component_v_btn = vue.resolveComponent("v-btn"); const _component_v_spacer = vue.resolveComponent("v-spacer"); const _component_v_card = vue.resolveComponent("v-card"); return vue.openBlock(), vue.createBlock(_component_v_card, { "prepend-icon": "mdi-alert", title: isHidden.value ? "存在疑问?" : isError.value ? "查看答案失败" : isInFinalResult.value ? "答案已解锁" : isAvailable.value ? "查看答案?这将会留下记录!" : "依赖插件未安装", loading: inProgress.value }, { actions: vue.withCtx(() => [ !isAvailable.value ? (vue.openBlock(), vue.createBlock(_component_v_btn, { key: 0, text: "安装插件", variant: "elevated", color: "primary", href: "https://greasyfork.org/scripts/495490", target: "_blank" })) : vue.createCommentVNode("", true), !isAvailable.value ? (vue.openBlock(), vue.createBlock(_component_v_btn, { key: 1, text: "脚本猫", variant: "text", color: "primary", href: "https://scriptcat.org/script-show-page/1860", target: "_blank" })) : vue.createCommentVNode("", true), vue.createVNode(_component_v_spacer), vue.createVNode(_component_v_btn, { disabled: inProgress.value, onClick: closeDialog }, { default: vue.withCtx(() => [ vue.createTextVNode(vue.toDisplayString(isHidden.value ? "了解" : isError.value || isInFinalResult.value ? "完成" : "取消"), 1) ]), _: 1 }, 8, ["disabled"]), isAvailable.value && !isError.value && !isWaitingForRefresh.value && isInFirstCheck.value && !isHidden.value ? (vue.openBlock(), vue.createBlock(_component_v_btn, { key: 2, disabled: inProgress.value || waitingForReadingDisabled.value, loading: inProgress.value, color: "error", variant: "tonal", onClick: handleGetAnswerInfo }, { default: vue.withCtx(() => [ vue.createTextVNode(vue.toDisplayString(waitingForReadingDisabled.value ? `请等待 ${waitingForReading.value} 秒` : "已知晓上述内容,查看答案,并留下记录"), 1) ]), _: 1 }, 8, ["disabled", "loading"])) : vue.createCommentVNode("", true), isAvailable.value && !isError.value && !isWaitingForRefresh.value && isInSecondCheck.value && !isHidden.value ? (vue.openBlock(), vue.createBlock(_component_v_btn, { key: 3, disabled: inProgress.value || waitingForReadingDisabled.value, loading: inProgress.value, color: "error", variant: "tonal", onClick: handleUnclockAnswer }, { default: vue.withCtx(() => [ vue.createTextVNode(" 已知晓,解锁答案,并留下记录 ") ]), _: 1 }, 8, ["disabled", "loading"])) : vue.createCommentVNode("", true) ]), default: vue.withCtx(() => [ isHidden.value ? (vue.openBlock(), vue.createBlock(_component_v_card_text, { key: 0 }, { default: vue.withCtx(() => [ vue.createTextVNode("如果有使用上的疑问,请联系开发者。") ]), _: 1 })) : isError.value ? (vue.openBlock(), vue.createBlock(_component_v_card_text, { key: 1 }, { default: vue.withCtx(() => [ vue.createTextVNode("查看答案失败,可能当前练习不存在答案,或刷新再试,信息未记录。"), _hoisted_1$2, _hoisted_2, vue.createTextVNode(vue.toDisplayString(errorMessages.value), 1) ]), _: 1 })) : isInFirstCheck.value ? (vue.openBlock(), vue.createBlock(_component_v_card_text, { key: 2 }, { default: vue.withCtx(() => [ vue.createTextVNode(" 你确定要查看当前练习答案?"), _hoisted_3, vue.createTextVNode("一旦查看答案,你的信息会被平台记录,可供老师查阅。"), _hoisted_4, _hoisted_5, vue.createTextVNode(" 被记录的查看答案操作会导致但不限于以下结果:"), _hoisted_6, _hoisted_7, _hoisted_8, _hoisted_9, vue.createTextVNode(" 请注意,此操作不可撤销!"), _hoisted_10, vue.createTextVNode("一旦查看答案,便会不可逆转地留下记录。 ") ]), _: 1 })) : isInSecondCheck.value ? (vue.openBlock(), vue.createBlock(_component_v_card_text, { key: 3 }, { default: vue.withCtx(() => [ vue.createTextVNode(" 将花费 " + vue.toDisplayString(answerInfo.value.answer_score) + " 积分查看答案", 1), _hoisted_11, _hoisted_12, vue.createTextVNode(" answer_id: " + vue.toDisplayString(answerInfo.value.answer_id), 1), _hoisted_13, vue.createTextVNode(" answer_radio: " + vue.toDisplayString(answerInfo.value.answer_radio), 1), _hoisted_14, vue.createTextVNode(" answer_score: " + vue.toDisplayString(answerInfo.value.answer_score), 1), _hoisted_15, _hoisted_16, vue.createTextVNode(" 一旦查看答案,你的信息会被平台记录,可供老师查阅。"), _hoisted_17, vue.createTextVNode(" 可能导致但不限于以下结果:"), _hoisted_18, _hoisted_19, _hoisted_20, _hoisted_21, vue.createTextVNode(" 你确定要继续吗? ") ]), _: 1 })) : isInFinalResult.value ? (vue.openBlock(), vue.createBlock(_component_v_card_text, { key: 4 }, { default: vue.withCtx(() => [ vue.createTextVNode(" 答案解锁已被记录。"), _hoisted_22, _hoisted_23, vue.createVNode(_component_v_textarea, { "auto-grow": "", modelValue: answerContent.value, "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => answerContent.value = $event), disabled: inProgress.value, readonly: isError.value || inProgress.value, placeholder: "答案将在这里显示", loading: inProgress.value, "persistent-placeholder": "" }, null, 8, ["modelValue", "disabled", "readonly", "loading"]) ]), _: 1 })) : (vue.openBlock(), vue.createBlock(_component_v_card_text, { key: 5 }, { default: vue.withCtx(() => [ vue.createTextVNode(" 本插件需要《头歌复制助手 EduCoder Copy Helper》安装并启用后方可使用。"), _hoisted_24, vue.createTextVNode("请安装并启用后刷新页面再试。 "), _hoisted_25, _hoisted_26, vue.createTextVNode(" Greasy Fork 安装地址:https://greasyfork.org/scripts/495490 "), _hoisted_27, _hoisted_28, vue.createTextVNode(" ScriptCat脚本猫 安装地址:https://scriptcat.org/script-show-page/1860 ") ]), _: 1 })) ]), _: 1 }, 8, ["title", "loading"]); }; } }); const _sfc_main$3 = /* @__PURE__ */ vue.defineComponent({ __name: "get-anser", setup(__props) { const isPersistent = vue.ref(false); const setIsPersistent = (value) => { isPersistent.value = value; }; return (_ctx, _cache) => { const _component_v_btn = vue.resolveComponent("v-btn"); const _component_v_dialog = vue.resolveComponent("v-dialog"); return vue.openBlock(), vue.createBlock(_component_v_dialog, { "max-width": "800", width: "600", scrollable: "", persistent: isPersistent.value }, { activator: vue.withCtx(({ props: activatorProps }) => [ vue.createVNode(_component_v_btn, vue.mergeProps(activatorProps, { color: "surface-variant", text: "?", variant: "plain" }), null, 16) ]), default: vue.withCtx(({ isActive }) => [ vue.createVNode(_sfc_main$4, { "is-active": isActive, "set-is-persistent": setIsPersistent }, null, 8, ["is-active"]) ]), _: 1 }, 8, ["persistent"]); }; } }); const _hoisted_1$1 = { style: { "display": "flex", "flex-direction": "row", "gap": "0.5em", "align-items": "center" } }; const _sfc_main$2 = /* @__PURE__ */ vue.defineComponent({ __name: "copy-entry", setup(__props) { return (_ctx, _cache) => { return vue.openBlock(), vue.createElementBlock("div", _hoisted_1$1, [ vue.createVNode(_sfc_main$3), vue.createVNode(_sfc_main$5), vue.createVNode(_sfc_main$7) ]); }; } }); const _hoisted_1 = { class: "d-flex flex-column ga-2" }; const _sfc_main$1 = /* @__PURE__ */ vue.defineComponent({ __name: "pass-video", setup(__props) { const isLoading = vue.ref(false); const isError = vue.ref(false); const isHelperNotInstalled = vue.ref(false); const progress = vue.ref(-1); const status = vue.ref(""); const message = vue.ref(""); const handlePassVideo = async () => { if (progress.value === -1) { observeTextContentChange(); } isLoading.value = true; isError.value = false; isHelperNotInstalled.value = false; progress.value = 1; status.value = "触发开始播放事件中……"; const window2 = _unsafeWindow; const educoderCopyHelper = Number(window2.educoderCopyHelper); if (window2.educoderCopyHelper === void 0 || educoderCopyHelper < 1.5) { isHelperNotInstalled.value = true; isLoading.value = false; isError.value = true; return; } const { duration } = getVideoInfo(); message.value = `duration: ${duration}`; const playButton = document.querySelector("button#play"); const muteButton = document.querySelector("button#volume-button"); await waitTime(800); if (!isLoading.value) { return; } if (playButton) { message.value = "playButton found!"; isLoading && muteButton.click(); isLoading && playButton.click(); progress.value = 2; status.value = "触发开始播放成功,等待触发完成播放视频事件……"; await waitTwoTime(); if (!isLoading.value) { return; } isLoading && playButton.click(); isLoading && muteButton.click(); progress.value = 3; status.value = "触发完成播放事件中……"; const videoId = window2.videoId; const logId = window2.videoLogId; message.value = `videoId: ${videoId}, logId: ${logId}`; await waitTime(1200); if (!isLoading.value) { return; } const body = { ed: "1", point: duration, log_id: logId, total_duration: duration, watch_duration: duration }; const response = await fetch( `https://data.educoder.net/api/watch_video_histories.json`, { credentials: "include", method: "POST", headers: { "Accept": "application/json", "Content-Type": "application/json; charset=utf-8", "X-EDU-Signature": window2.xEduSignature || "", "X-EDU-Timestamp": window2.xEduTimestamp || "", "X-EDU-Type": window2.xEduType || "pc" }, body: JSON.stringify(body) } ); const res = await response.json(); message.value = res; if (res.status === 0) { progress.value = 4; status.value = `视频已完成,共计学习时长 ${duration} 秒。`; isLoading.value = false; } else { isError.value = true; isLoading.value = false; } } else { isError.value = true; isLoading.value = false; } }; const observeTextContentChange = () => { var _a, _b; const vidContainer = document.querySelector("#video-container"); const targetElement = (_b = (_a = vidContainer == null ? void 0 : vidContainer.parentElement) == null ? void 0 : _a.parentElement) == null ? void 0 : _b.parentElement; const firstChild = targetElement == null ? void 0 : targetElement.firstElementChild; if (firstChild) { const observer = new MutationObserver((mutationsList) => { for (let mutation of mutationsList) { if (mutation.type === "characterData") { progress.value = 0; status.value = "点击“完成该视频”以开始"; message.value = ""; isLoading.value = false; isError.value = false; isHelperNotInstalled.value = false; } } }); const config = { characterData: true, subtree: true }; observer.observe(firstChild, config); } else { console.warn("First child element not found."); } }; return (_ctx, _cache) => { const _component_v_btn = vue.resolveComponent("v-btn"); const _component_v_card_actions = vue.resolveComponent("v-card-actions"); const _component_v_card = vue.resolveComponent("v-card"); const _component_v_card_subtitle = vue.resolveComponent("v-card-subtitle"); const _component_v_spacer = vue.resolveComponent("v-spacer"); return vue.openBlock(), vue.createElementBlock("div", _hoisted_1, [ !isHelperNotInstalled.value ? (vue.openBlock(), vue.createBlock(_component_v_btn, { key: 0, color: "surface-variant", "max-width": "110px", text: "完成该视频", variant: "flat", onClick: handlePassVideo, loading: isLoading.value, disabled: isLoading.value }, null, 8, ["loading", "disabled"])) : vue.createCommentVNode("", true), isHelperNotInstalled.value ? (vue.openBlock(), vue.createBlock(_component_v_card, { key: 1, variant: "outlined", title: "必要插件未安装", subtitle: "请安装《头歌复制助手 EduCoder Copy Helper》1.5 及更新版本以使用该功能,如已安装,请尝试刷新页面重试。" }, { default: vue.withCtx(() => [ vue.createVNode(_component_v_card_actions, null, { default: vue.withCtx(() => [ vue.createVNode(_component_v_btn, { text: "安装《头歌复制助手 EduCoder Copy Helper》", variant: "elevated", color: "surface-variant", href: "https://greasyfork.org/scripts/495490", target: "_blank" }), vue.createVNode(_component_v_btn, { text: "通过 ScriptCat 脚本猫安装", variant: "text", color: "surface-variant", href: "https://scriptcat.org/script-show-page/1860", target: "_blank" }) ]), _: 1 }) ]), _: 1 })) : progress.value >= 0 ? (vue.openBlock(), vue.createBlock(_component_v_card, { key: 2, loading: isLoading.value, variant: "outlined", title: progress.value === 4 ? "任务已完成" : progress.value === 0 ? "等待开始" : "正在完成该视频" }, { default: vue.withCtx(() => [ vue.createVNode(_component_v_card_subtitle, null, { default: vue.withCtx(() => [ vue.createTextVNode(vue.toDisplayString(isError.value ? "任务执行失败,请刷新再试。" : status.value), 1) ]), _: 1 }), vue.createVNode(_component_v_card_actions, null, { default: vue.withCtx(() => [ vue.createTextVNode(" 步骤 " + vue.toDisplayString(progress.value) + " / 4 ", 1), vue.createVNode(_component_v_spacer), vue.createTextVNode(" " + vue.toDisplayString(message.value), 1) ]), _: 1 }) ]), _: 1 }, 8, ["loading", "title"])) : vue.createCommentVNode("", true) ]); }; } }); const _sfc_main = /* @__PURE__ */ vue.defineComponent({ __name: "video-entry", setup(__props) { return (_ctx, _cache) => { return vue.openBlock(), vue.createBlock(_sfc_main$1); }; } }); const appendCopyAllButton = () => { const css = document.createElement("link"); css.rel = "stylesheet"; css.href = "https://registry.npmmirror.com/@mdi/font/7.4.47/files/css/materialdesignicons.min.css"; document.head.appendChild(css); const vuetify$1 = vuetify.createVuetify({}); const app = vue.createApp(_sfc_main$2); app.use(vuetify$1); const antRow = document.querySelectorAll(".ant-row"); const host = document.createElement("div"); let target = document.body; antRow.forEach((row) => { const innerDiv = row.querySelectorAll("div"); innerDiv.forEach((div) => { if (div.className.includes("action-bar")) { target = div; return; } }); }); target.prepend(host); app.mount(host); }; const appendPassVideoButton = () => { var _a, _b; const css = document.createElement("link"); css.rel = "stylesheet"; css.href = "https://registry.npmmirror.com/@mdi/font/7.4.47/files/css/materialdesignicons.min.css"; document.head.appendChild(css); const vuetify$1 = vuetify.createVuetify({}); const app = vue.createApp(_sfc_main); app.use(vuetify$1); const vidContainer = document.querySelector("#video-container"); const host = document.createElement("div"); host.id = "educoder-helper-video-entry"; let target = document.body; const targetElement = (_b = (_a = vidContainer.parentElement) == null ? void 0 : _a.parentElement) == null ? void 0 : _b.parentElement; const firstChild = targetElement == null ? void 0 : targetElement.firstElementChild; if (targetElement && firstChild) { targetElement.insertBefore(host, firstChild.nextSibling); } else { target.prepend(host); } app.mount(host); }; const removeBanner = () => { const antSpinContainer = document.querySelector(".ant-spin-container"); if (antSpinContainer && antSpinContainer.children.length >= 2) { const firstChild = antSpinContainer.children[0]; const secondChild = antSpinContainer.children[1]; if (!firstChild.className.includes("header") && secondChild.className.includes("header")) { firstChild.setAttribute("style", "display: none;"); } } }; const removeModal = () => { const adModal = document.querySelector("div.selfdomModal___doNCF"); adModal == null ? void 0 : adModal.setAttribute("style", "display: none;"); }; const removeAffix = () => { const affixContainer = document.querySelector(".affixContainer___CWtV9"); affixContainer == null ? void 0 : affixContainer.setAttribute("style", "display: none;"); }; const removeUserSelectLimit = () => { const qItemElement = document.querySelector(".questionItem___q6Hgu > *"); if (qItemElement) { qItemElement.style.userSelect = "text"; const firstChild = qItemElement.querySelector(":first-child"); if (firstChild) { firstChild.style.userSelect = "none"; } } }; const removeContextMenuLimit = () => { document.addEventListener("contextmenu", function(event) { event.stopPropagation(); }, true); }; const observerCopyAll = () => { const observer = new MutationObserver((mutationsList, observer2) => { for (let mutation of mutationsList) { if (mutation.type === "childList") { const antRow = document.querySelector(".ant-row"); if (antRow) { appendCopyAllButton(); observer2.disconnect(); break; } } } }); const config = { childList: true, subtree: true }; observer.observe(document, config); }; const observerPassVideo = () => { const observer = new MutationObserver((mutationsList, observer2) => { for (let mutation of mutationsList) { if (mutation.type === "childList") { const vidContainer = document.querySelector("#video-container"); const vidEntry = document.querySelector("#educoder-helper-video-entry"); if (vidContainer && !vidEntry) { appendPassVideoButton(); observer2.disconnect(); break; } } } }); const config = { childList: true, subtree: true }; observer.observe(document, config); }; const observerAdRemove = () => { const observer = new MutationObserver((mutationsList, observer2) => { for (let mutation of mutationsList) { if (mutation.type === "childList") { const headerElement = document.querySelector(".ant-layout-header"); const modalElement = document.querySelector(".selfdomModal___doNCF"); const affixElement = document.querySelector(".affixContainer___CWtV9"); if (headerElement || modalElement || affixElement) { removeBanner(); removeModal(); removeAffix(); break; } } } }); const config = { childList: true, subtree: true }; observer.observe(document, config); }; const observerExerciseCopyLimit = () => { const observer = new MutationObserver((mutationsList, observer2) => { for (let mutation of mutationsList) { if (mutation.type === "childList") { const qItemElement = document.querySelector(".questionItem___q6Hgu"); if (qItemElement) { removeUserSelectLimit(); removeContextMenuLimit(); break; } } } }); const config = { childList: true, subtree: true }; observer.observe(document, config); }; function waitTwoTime() { const randomTime = 2.5 + Math.random() * (5 - 2.5); const randomTimeInMilliseconds = randomTime * 1e3; return new Promise((resolve) => setTimeout(resolve, randomTimeInMilliseconds)); } function waitTime(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } const postCheckIn = () => { const asyncPostCheckIn = async () => { const window2 = _unsafeWindow; const educoderCopyHelper = Number(window2.educoderCopyHelper); if (window2.educoderCopyHelper === void 0 || educoderCopyHelper < 2) { return; } const response = await fetch( `https://data.educoder.net/api/users/attendance.json`, { credentials: "include", method: "POST", headers: { "Accept": "application/json", "Content-Type": "application/json; charset=utf-8", "X-EDU-Signature": window2.xEduSignature || "", "X-EDU-Timestamp": window2.xEduTimestamp || "", "X-EDU-Type": window2.xEduType || "pc" } } ); try { const data = await response.json(); console.debug("[educoder-helper] postCheckIn", data); } catch (e) { console.error("[educoder-helper] postCheckIn", e); } }; requestIdleCallback(() => { setTimeout(async () => { await asyncPostCheckIn(); }, 1e3); }); }; const href = window.location.href; const pathname = window.location.pathname; if (href.includes("tasks")) { observerCopyAll(); } if (href.includes("video_info")) { observerPassVideo(); } observerAdRemove(); if (pathname.includes("exercise")) { observerExerciseCopyLimit(); } postCheckIn(); console.debug("[educoder-helper] main.ts loaded!"); })(Vue, Vuetify, Base64);