Full Code of ginnnnnncc/GinsMooc for AI

master c2ccb5a03fbe cached
70 files
183.7 KB
58.0k tokens
144 symbols
1 requests
Download .txt
Showing preview only (203K chars total). Download the full file or copy to clipboard to get everything.
Repository: ginnnnnncc/GinsMooc
Branch: master
Commit: c2ccb5a03fbe
Files: 70
Total size: 183.7 KB

Directory structure:
gitextract_klorelqm/

├── .eslintrc.cjs
├── .gitignore
├── .prettierrc.json
├── .vscode/
│   └── extensions.json
├── README.md
├── env.d.ts
├── extension/
│   ├── .eslintrc.cjs
│   ├── .prettierrc.json
│   ├── README.md
│   ├── env.d.ts
│   ├── index.html
│   ├── package.json
│   ├── release/
│   │   ├── content-scripts/
│   │   │   └── index-ad710f80.js
│   │   └── manifest.json
│   ├── src/
│   │   ├── main.ts
│   │   ├── newExam.ts
│   │   ├── plugins/
│   │   │   ├── apiAccess.ts
│   │   │   ├── mooc.ts
│   │   │   ├── react.ts
│   │   │   └── tool.ts
│   │   └── type/
│   │       ├── api.ts
│   │       └── mooc.ts
│   ├── tsconfig.config.json
│   ├── tsconfig.json
│   └── vite.config.ts
├── index.html
├── package.json
├── public/
│   ├── background.html
│   ├── css/
│   │   ├── main.css
│   │   └── noscript.css
│   ├── guess.java
│   └── sass/
│       ├── libs/
│       │   ├── _breakpoints.scss
│       │   ├── _functions.scss
│       │   ├── _mixins.scss
│       │   ├── _vars.scss
│       │   └── _vendor.scss
│       ├── main.scss
│       └── noscript.scss
├── src/
│   ├── App.vue
│   ├── components/
│   │   ├── CourseCard.vue
│   │   ├── QuestionCard.vue
│   │   ├── icon/
│   │   │   ├── Extension.vue
│   │   │   ├── Github.vue
│   │   │   └── index.ts
│   │   ├── index.ts
│   │   └── question/
│   │       ├── Completion.vue
│   │       ├── Homework.vue
│   │       ├── MultipleChoice.vue
│   │       ├── OnlineJudge.vue
│   │       ├── SingleChoice.vue
│   │       └── index.ts
│   ├── main.ts
│   ├── plugins/
│   │   ├── apiAccess.ts
│   │   └── tool.ts
│   ├── router/
│   │   └── index.ts
│   ├── type/
│   │   ├── api.ts
│   │   ├── globleProperties.ts
│   │   └── mooc.ts
│   └── views/
│       ├── BlogView.vue
│       ├── HomeView.vue
│       ├── MoocAside.vue
│       ├── MoocCourseDetail.vue
│       ├── MoocHeader.vue
│       ├── MoocTest.vue
│       ├── MoocView.vue
│       ├── VideoView.vue
│       └── index.ts
├── tsconfig.config.json
├── tsconfig.json
└── vite.config.ts

================================================
FILE CONTENTS
================================================

================================================
FILE: .eslintrc.cjs
================================================
/* eslint-env node */
require("@rushstack/eslint-patch/modern-module-resolution")

module.exports = {
    root: true,
    extends: [
        "plugin:vue/vue3-essential",
        "eslint:recommended",
        "@vue/eslint-config-typescript",
        "@vue/eslint-config-prettier"
    ],
    parserOptions: {
        ecmaVersion: "latest"
    },
    rules: {
        "no-unused-vars": ["off", { "vars": "all", "args": "after-used", "ignoreRestSiblings": false }]
    }
}


================================================
FILE: .gitignore
================================================
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
.DS_Store
dist
dist-ssr
coverage
*.local

/cypress/videos/
/cypress/screenshots/

# Editor directories and files
**/.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
**/*.pem


================================================
FILE: .prettierrc.json
================================================
{
    "tabWidth": 4,
    "useTabs": false,
    "semi": false,
    "trailingComma": "none",
    "bracketSameLine": false,
    "printWidth": 120
}

================================================
FILE: .vscode/extensions.json
================================================
{
  "recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
}


================================================
FILE: README.md
================================================
# 不支持 spoc

# 反馈所需信息

反馈课程更新问题,需要提新 issue,带上课程 id、课程链接和测验截止时间,我会在截止时间前修,上班太忙了

# 插件简介

实现对于中国大学MOOC的
- 非在线测评题的自动答案查询,包括单选题、多选题、判断题、填空题、简答题,支持测验与作业及考试
- 互评阶段的自动评分、自动点评

下载地址:[Github release v2.2.1](https://github.com/ginnnnnn666/GinsMooc/releases/tag/v2.2.1)

在线使用:[GinsMooc](https://ginnnnnn.top/mooc/)

# 功能介绍

在测试的准备页面,将会自动检查是否准备就绪,若为否将自动更新课程

![](/public/extension-updating.png)

进入测验后,将显示“获取答案”按钮,点击即可

![](/public/extension-single-choice.png)
![](/public/extension-multiple-choice.png)
![](/public/extension-completion.png)
![](/public/extension-homework.png)

作业的互评阶段支持自动评分、自动点评

![](/public/extension-auto-evaluate-1.png)
![](/public/extension-auto-evaluate-2.png)


# 安装介绍

下载安装包后,将其解压至文件夹内

在浏览器地址栏中输入`edge://extensions`(谷歌浏览器为`chrome://extensions`)

打开开发者模式

![](/public/extension-developer-mode.png)

点击“加载解压缩的扩展”,选择刚刚解压到的文件夹,即可开始使用

![](/public/extension-load-decompression.png)


================================================
FILE: env.d.ts
================================================
/// <reference types="vite/client" />


================================================
FILE: extension/.eslintrc.cjs
================================================
/* eslint-env node */
require('@rushstack/eslint-patch/modern-module-resolution')

module.exports = {
    root: true,
    'extends': [
        'plugin:vue/vue3-essential',
        'eslint:recommended',
        '@vue/eslint-config-typescript',
        '@vue/eslint-config-prettier'
    ],
    parserOptions: {
        ecmaVersion: 'latest'
    }
}


================================================
FILE: extension/.prettierrc.json
================================================
{
    "tabWidth": 4,
    "useTabs": false,
    "semi": false,
    "trailingComma": "none",
    "bracketSameLine": false,
    "printWidth": 120
}

================================================
FILE: extension/README.md
================================================
# GinsMooc Extention

This template should help get you started developing with Vue 3 in Vite.

## Recommended IDE Setup

[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).

## Type Support for `.vue` Imports in TS

TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types.

If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps:

1. Disable the built-in TypeScript Extension
    1) Run `Extensions: Show Built-in Extensions` from VSCode's command palette
    2) Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)`
2. Reload the VSCode window by running `Developer: Reload Window` from the command palette.

## Customize configuration

See [Vite Configuration Reference](https://vitejs.dev/config/).

## Project Setup

```sh
npm install
```

### Compile and Hot-Reload for Development

```sh
npm run dev
```

### Type-Check, Compile and Minify for Production

```sh
npm run build
```

### Lint with [ESLint](https://eslint.org/)

```sh
npm run lint
```


================================================
FILE: extension/env.d.ts
================================================
/// <reference types="vite/client" />


================================================
FILE: extension/index.html
================================================
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <link rel="icon" href="/favicon.ico">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Vite App</title>
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.ts"></script>
  </body>
</html>


================================================
FILE: extension/package.json
================================================
{
    "name": "ginsmooc-extention",
    "version": "0.0.0",
    "private": true,
    "scripts": {
        "dev": "vite",
        "build": "run-p type-check build-only",
        "preview": "vite preview",
        "build-only": "vite build",
        "type-check": "vue-tsc --noEmit",
        "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore"
    },
    "dependencies": {
        "axios": "^1.3.2",
        "element-plus": "^2.2.29",
        "jssha": "^3.3.0",
        "vue": "^3.2.45",
        "vue-router": "^4.1.6"
    },
    "devDependencies": {
        "@rushstack/eslint-patch": "^1.1.4",
        "@types/chrome": "^0.0.212",
        "@types/node": "^18.11.12",
        "@vitejs/plugin-vue": "^4.0.0",
        "@vue/eslint-config-prettier": "^7.0.0",
        "@vue/eslint-config-typescript": "^11.0.0",
        "@vue/tsconfig": "^0.1.3",
        "eslint": "^8.22.0",
        "eslint-plugin-vue": "^9.3.0",
        "npm-run-all": "^4.1.5",
        "prettier": "^2.7.1",
        "typescript": "~4.7.4",
        "vite": "^4.0.0",
        "vue-tsc": "^1.0.12"
    }
}


================================================
FILE: extension/release/content-scripts/index-ad710f80.js
================================================
var lt=Object.defineProperty;var ut=(e,t,n)=>t in e?lt(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n;var k=(e,t,n)=>(ut(e,typeof t!="symbol"?t+"":t,n),n);(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const s of document.querySelectorAll('link[rel="modulepreload"]'))r(s);new MutationObserver(s=>{for(const o of s)if(o.type==="childList")for(const i of o.addedNodes)i.tagName==="LINK"&&i.rel==="modulepreload"&&r(i)}).observe(document,{childList:!0,subtree:!0});function n(s){const o={};return s.integrity&&(o.integrity=s.integrity),s.referrerPolicy&&(o.referrerPolicy=s.referrerPolicy),s.crossOrigin==="use-credentials"?o.credentials="include":s.crossOrigin==="anonymous"?o.credentials="omit":o.credentials="same-origin",o}function r(s){if(s.ep)return;s.ep=!0;const o=n(s);fetch(s.href,o)}})();const Fe=async e=>new Promise(t=>{setTimeout(()=>{t("")},e)}),ge=async e=>new Promise(t=>{const n=setInterval(()=>{console.log("check wait for"),e()&&(clearInterval(n),t(""))},50)}),W=e=>{const t=new RegExp("(^|&)"+e+"=([^&]*)(&|$)"),n=window.location.search.substring(1).match(t)||window.location.hash.substring(window.location.hash.search(/\?/)+1).match(t);return n?decodeURIComponent(n[2]):null},_e=e=>{const t="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";let n="";for(let r=0;r<e;r++)n+=t[Math.floor(Math.random()*t.length)];return n};class dt extends Array{constructor(){super();k(this,"id");k(this,"node");this.node=document.createElement("gin"),this.id=_e(8),this.node.id=`gin-auto-${this.id}`,document.body.appendChild(this.node)}add(n){const r=new ft(this,n);return super.push(r),r}}class ft{constructor(t,n){k(this,"id");k(this,"node");k(this,"parent");k(this,"value");this.parent=t,this.node=document.createElement("gin"),this.id=_e(8),this.node.id=this.id,this.parent.node.appendChild(this.node),this.value=n}get(){return this.value}set(t){if(this.value!==t){const n=this.value;this.value=t,this.node.dispatchEvent(new CustomEvent("change",{detail:{oldValue:n,newValue:this.value}}))}this.node.dispatchEvent(new CustomEvent("set"))}addEventListenr(t,n){this.node.addEventListener(t,n)}}function je(e,t){return function(){return e.apply(t,arguments)}}const{toString:pt}=Object.prototype,{getPrototypeOf:he}=Object,ee=(e=>t=>{const n=pt.call(t);return e[n]||(e[n]=n.slice(8,-1).toLowerCase())})(Object.create(null)),T=e=>(e=e.toLowerCase(),t=>ee(t)===e),te=e=>t=>typeof t===e,{isArray:q}=Array,$=te("undefined");function ht(e){return e!==null&&!$(e)&&e.constructor!==null&&!$(e.constructor)&&S(e.constructor.isBuffer)&&e.constructor.isBuffer(e)}const De=T("ArrayBuffer");function mt(e){let t;return typeof ArrayBuffer<"u"&&ArrayBuffer.isView?t=ArrayBuffer.isView(e):t=e&&e.buffer&&De(e.buffer),t}const yt=te("string"),S=te("function"),Ue=te("number"),ne=e=>e!==null&&typeof e=="object",wt=e=>e===!0||e===!1,V=e=>{if(ee(e)!=="object")return!1;const t=he(e);return(t===null||t===Object.prototype||Object.getPrototypeOf(t)===null)&&!(Symbol.toStringTag in e)&&!(Symbol.iterator in e)},Et=T("Date"),gt=T("File"),bt=T("Blob"),St=T("FileList"),xt=e=>ne(e)&&S(e.pipe),Ot=e=>{let t;return e&&(typeof FormData=="function"&&e instanceof FormData||S(e.append)&&((t=ee(e))==="formdata"||t==="object"&&S(e.toString)&&e.toString()==="[object FormData]"))},Tt=T("URLSearchParams"),At=e=>e.trim?e.trim():e.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,"");function z(e,t,{allOwnKeys:n=!1}={}){if(e===null||typeof e>"u")return;let r,s;if(typeof e!="object"&&(e=[e]),q(e))for(r=0,s=e.length;r<s;r++)t.call(null,e[r],r,e);else{const o=n?Object.getOwnPropertyNames(e):Object.keys(e),i=o.length;let c;for(r=0;r<i;r++)c=o[r],t.call(null,e[c],c,e)}}function qe(e,t){t=t.toLowerCase();const n=Object.keys(e);let r=n.length,s;for(;r-- >0;)if(s=n[r],t===s.toLowerCase())return s;return null}const Ie=(()=>typeof globalThis<"u"?globalThis:typeof self<"u"?self:typeof window<"u"?window:global)(),ve=e=>!$(e)&&e!==Ie;function le(){const{caseless:e}=ve(this)&&this||{},t={},n=(r,s)=>{const o=e&&qe(t,s)||s;V(t[o])&&V(r)?t[o]=le(t[o],r):V(r)?t[o]=le({},r):q(r)?t[o]=r.slice():t[o]=r};for(let r=0,s=arguments.length;r<s;r++)arguments[r]&&z(arguments[r],n);return t}const Rt=(e,t,n,{allOwnKeys:r}={})=>(z(t,(s,o)=>{n&&S(s)?e[o]=je(s,n):e[o]=s},{allOwnKeys:r}),e),Nt=e=>(e.charCodeAt(0)===65279&&(e=e.slice(1)),e),Ct=(e,t,n,r)=>{e.prototype=Object.create(t.prototype,r),e.prototype.constructor=e,Object.defineProperty(e,"super",{value:t.prototype}),n&&Object.assign(e.prototype,n)},Lt=(e,t,n,r)=>{let s,o,i;const c={};if(t=t||{},e==null)return t;do{for(s=Object.getOwnPropertyNames(e),o=s.length;o-- >0;)i=s[o],(!r||r(i,e,t))&&!c[i]&&(t[i]=e[i],c[i]=!0);e=n!==!1&&he(e)}while(e&&(!n||n(e,t))&&e!==Object.prototype);return t},Pt=(e,t,n)=>{e=String(e),(n===void 0||n>e.length)&&(n=e.length),n-=t.length;const r=e.indexOf(t,n);return r!==-1&&r===n},kt=e=>{if(!e)return null;if(q(e))return e;let t=e.length;if(!Ue(t))return null;const n=new Array(t);for(;t-- >0;)n[t]=e[t];return n},Bt=(e=>t=>e&&t instanceof e)(typeof Uint8Array<"u"&&he(Uint8Array)),Ft=(e,t)=>{const r=(e&&e[Symbol.iterator]).call(e);let s;for(;(s=r.next())&&!s.done;){const o=s.value;t.call(e,o[0],o[1])}},_t=(e,t)=>{let n;const r=[];for(;(n=e.exec(t))!==null;)r.push(n);return r},jt=T("HTMLFormElement"),Dt=e=>e.toLowerCase().replace(/[-_\s]([a-z\d])(\w*)/g,function(n,r,s){return r.toUpperCase()+s}),be=(({hasOwnProperty:e})=>(t,n)=>e.call(t,n))(Object.prototype),Ut=T("RegExp"),He=(e,t)=>{const n=Object.getOwnPropertyDescriptors(e),r={};z(n,(s,o)=>{let i;(i=t(s,o,e))!==!1&&(r[o]=i||s)}),Object.defineProperties(e,r)},qt=e=>{He(e,(t,n)=>{if(S(e)&&["arguments","caller","callee"].indexOf(n)!==-1)return!1;const r=e[n];if(S(r)){if(t.enumerable=!1,"writable"in t){t.writable=!1;return}t.set||(t.set=()=>{throw Error("Can not rewrite read-only method '"+n+"'")})}})},It=(e,t)=>{const n={},r=s=>{s.forEach(o=>{n[o]=!0})};return q(e)?r(e):r(String(e).split(t)),n},vt=()=>{},Ht=(e,t)=>(e=+e,Number.isFinite(e)?e:t),oe="abcdefghijklmnopqrstuvwxyz",Se="0123456789",Me={DIGIT:Se,ALPHA:oe,ALPHA_DIGIT:oe+oe.toUpperCase()+Se},Mt=(e=16,t=Me.ALPHA_DIGIT)=>{let n="";const{length:r}=t;for(;e--;)n+=t[Math.random()*r|0];return n};function $t(e){return!!(e&&S(e.append)&&e[Symbol.toStringTag]==="FormData"&&e[Symbol.iterator])}const zt=e=>{const t=new Array(10),n=(r,s)=>{if(ne(r)){if(t.indexOf(r)>=0)return;if(!("toJSON"in r)){t[s]=r;const o=q(r)?[]:{};return z(r,(i,c)=>{const d=n(i,s+1);!$(d)&&(o[c]=d)}),t[s]=void 0,o}}return r};return n(e,0)},Jt=T("AsyncFunction"),Vt=e=>e&&(ne(e)||S(e))&&S(e.then)&&S(e.catch),a={isArray:q,isArrayBuffer:De,isBuffer:ht,isFormData:Ot,isArrayBufferView:mt,isString:yt,isNumber:Ue,isBoolean:wt,isObject:ne,isPlainObject:V,isUndefined:$,isDate:Et,isFile:gt,isBlob:bt,isRegExp:Ut,isFunction:S,isStream:xt,isURLSearchParams:Tt,isTypedArray:Bt,isFileList:St,forEach:z,merge:le,extend:Rt,trim:At,stripBOM:Nt,inherits:Ct,toFlatObject:Lt,kindOf:ee,kindOfTest:T,endsWith:Pt,toArray:kt,forEachEntry:Ft,matchAll:_t,isHTMLForm:jt,hasOwnProperty:be,hasOwnProp:be,reduceDescriptors:He,freezeMethods:qt,toObjectSet:It,toCamelCase:Dt,noop:vt,toFiniteNumber:Ht,findKey:qe,global:Ie,isContextDefined:ve,ALPHABET:Me,generateString:Mt,isSpecCompliantForm:$t,toJSONObject:zt,isAsyncFn:Jt,isThenable:Vt};function m(e,t,n,r,s){Error.call(this),Error.captureStackTrace?Error.captureStackTrace(this,this.constructor):this.stack=new Error().stack,this.message=e,this.name="AxiosError",t&&(this.code=t),n&&(this.config=n),r&&(this.request=r),s&&(this.response=s)}a.inherits(m,Error,{toJSON:function(){return{message:this.message,name:this.name,description:this.description,number:this.number,fileName:this.fileName,lineNumber:this.lineNumber,columnNumber:this.columnNumber,stack:this.stack,config:a.toJSONObject(this.config),code:this.code,status:this.response&&this.response.status?this.response.status:null}}});const $e=m.prototype,ze={};["ERR_BAD_OPTION_VALUE","ERR_BAD_OPTION","ECONNABORTED","ETIMEDOUT","ERR_NETWORK","ERR_FR_TOO_MANY_REDIRECTS","ERR_DEPRECATED","ERR_BAD_RESPONSE","ERR_BAD_REQUEST","ERR_CANCELED","ERR_NOT_SUPPORT","ERR_INVALID_URL"].forEach(e=>{ze[e]={value:e}});Object.defineProperties(m,ze);Object.defineProperty($e,"isAxiosError",{value:!0});m.from=(e,t,n,r,s,o)=>{const i=Object.create($e);return a.toFlatObject(e,i,function(d){return d!==Error.prototype},c=>c!=="isAxiosError"),m.call(i,e.message,t,n,r,s),i.cause=e,i.name=e.name,o&&Object.assign(i,o),i};const Gt=null;function ue(e){return a.isPlainObject(e)||a.isArray(e)}function Je(e){return a.endsWith(e,"[]")?e.slice(0,-2):e}function xe(e,t,n){return e?e.concat(t).map(function(s,o){return s=Je(s),!n&&o?"["+s+"]":s}).join(n?".":""):t}function Kt(e){return a.isArray(e)&&!e.some(ue)}const Wt=a.toFlatObject(a,{},null,function(t){return/^is[A-Z]/.test(t)});function re(e,t,n){if(!a.isObject(e))throw new TypeError("target must be an object");t=t||new FormData,n=a.toFlatObject(n,{metaTokens:!0,dots:!1,indexes:!1},!1,function(h,E){return!a.isUndefined(E[h])});const r=n.metaTokens,s=n.visitor||u,o=n.dots,i=n.indexes,d=(n.Blob||typeof Blob<"u"&&Blob)&&a.isSpecCompliantForm(t);if(!a.isFunction(s))throw new TypeError("visitor must be a function");function f(p){if(p===null)return"";if(a.isDate(p))return p.toISOString();if(!d&&a.isBlob(p))throw new m("Blob is not supported. Use a Buffer instead.");return a.isArrayBuffer(p)||a.isTypedArray(p)?d&&typeof Blob=="function"?new Blob([p]):Buffer.from(p):p}function u(p,h,E){let g=p;if(p&&!E&&typeof p=="object"){if(a.endsWith(h,"{}"))h=r?h:h.slice(0,-2),p=JSON.stringify(p);else if(a.isArray(p)&&Kt(p)||(a.isFileList(p)||a.endsWith(h,"[]"))&&(g=a.toArray(p)))return h=Je(h),g.forEach(function(L,ct){!(a.isUndefined(L)||L===null)&&t.append(i===!0?xe([h],ct,o):i===null?h:h+"[]",f(L))}),!1}return ue(p)?!0:(t.append(xe(E,h,o),f(p)),!1)}const l=[],w=Object.assign(Wt,{defaultVisitor:u,convertValue:f,isVisitable:ue});function b(p,h){if(!a.isUndefined(p)){if(l.indexOf(p)!==-1)throw Error("Circular reference detected in "+h.join("."));l.push(p),a.forEach(p,function(g,C){(!(a.isUndefined(g)||g===null)&&s.call(t,g,a.isString(C)?C.trim():C,h,w))===!0&&b(g,h?h.concat(C):[C])}),l.pop()}}if(!a.isObject(e))throw new TypeError("data must be an object");return b(e),t}function Oe(e){const t={"!":"%21","'":"%27","(":"%28",")":"%29","~":"%7E","%20":"+","%00":"\0"};return encodeURIComponent(e).replace(/[!'()~]|%20|%00/g,function(r){return t[r]})}function me(e,t){this._pairs=[],e&&re(e,this,t)}const Ve=me.prototype;Ve.append=function(t,n){this._pairs.push([t,n])};Ve.toString=function(t){const n=t?function(r){return t.call(this,r,Oe)}:Oe;return this._pairs.map(function(s){return n(s[0])+"="+n(s[1])},"").join("&")};function Qt(e){return encodeURIComponent(e).replace(/%3A/gi,":").replace(/%24/g,"$").replace(/%2C/gi,",").replace(/%20/g,"+").replace(/%5B/gi,"[").replace(/%5D/gi,"]")}function Ge(e,t,n){if(!t)return e;const r=n&&n.encode||Qt,s=n&&n.serialize;let o;if(s?o=s(t,n):o=a.isURLSearchParams(t)?t.toString():new me(t,n).toString(r),o){const i=e.indexOf("#");i!==-1&&(e=e.slice(0,i)),e+=(e.indexOf("?")===-1?"?":"&")+o}return e}class Xt{constructor(){this.handlers=[]}use(t,n,r){return this.handlers.push({fulfilled:t,rejected:n,synchronous:r?r.synchronous:!1,runWhen:r?r.runWhen:null}),this.handlers.length-1}eject(t){this.handlers[t]&&(this.handlers[t]=null)}clear(){this.handlers&&(this.handlers=[])}forEach(t){a.forEach(this.handlers,function(r){r!==null&&t(r)})}}const Te=Xt,Ke={silentJSONParsing:!0,forcedJSONParsing:!0,clarifyTimeoutError:!1},Yt=typeof URLSearchParams<"u"?URLSearchParams:me,Zt=typeof FormData<"u"?FormData:null,en=typeof Blob<"u"?Blob:null,tn={isBrowser:!0,classes:{URLSearchParams:Yt,FormData:Zt,Blob:en},protocols:["http","https","file","blob","url","data"]},We=typeof window<"u"&&typeof document<"u",nn=(e=>We&&["ReactNative","NativeScript","NS"].indexOf(e)<0)(typeof navigator<"u"&&navigator.product),rn=(()=>typeof WorkerGlobalScope<"u"&&self instanceof WorkerGlobalScope&&typeof self.importScripts=="function")(),sn=Object.freeze(Object.defineProperty({__proto__:null,hasBrowserEnv:We,hasStandardBrowserEnv:nn,hasStandardBrowserWebWorkerEnv:rn},Symbol.toStringTag,{value:"Module"})),x={...sn,...tn};function on(e,t){return re(e,new x.classes.URLSearchParams,Object.assign({visitor:function(n,r,s,o){return x.isNode&&a.isBuffer(n)?(this.append(r,n.toString("base64")),!1):o.defaultVisitor.apply(this,arguments)}},t))}function an(e){return a.matchAll(/\w+|\[(\w*)]/g,e).map(t=>t[0]==="[]"?"":t[1]||t[0])}function cn(e){const t={},n=Object.keys(e);let r;const s=n.length;let o;for(r=0;r<s;r++)o=n[r],t[o]=e[o];return t}function Qe(e){function t(n,r,s,o){let i=n[o++];if(i==="__proto__")return!0;const c=Number.isFinite(+i),d=o>=n.length;return i=!i&&a.isArray(s)?s.length:i,d?(a.hasOwnProp(s,i)?s[i]=[s[i],r]:s[i]=r,!c):((!s[i]||!a.isObject(s[i]))&&(s[i]=[]),t(n,r,s[i],o)&&a.isArray(s[i])&&(s[i]=cn(s[i])),!c)}if(a.isFormData(e)&&a.isFunction(e.entries)){const n={};return a.forEachEntry(e,(r,s)=>{t(an(r),s,n,0)}),n}return null}function ln(e,t,n){if(a.isString(e))try{return(t||JSON.parse)(e),a.trim(e)}catch(r){if(r.name!=="SyntaxError")throw r}return(n||JSON.stringify)(e)}const ye={transitional:Ke,adapter:["xhr","http"],transformRequest:[function(t,n){const r=n.getContentType()||"",s=r.indexOf("application/json")>-1,o=a.isObject(t);if(o&&a.isHTMLForm(t)&&(t=new FormData(t)),a.isFormData(t))return s?JSON.stringify(Qe(t)):t;if(a.isArrayBuffer(t)||a.isBuffer(t)||a.isStream(t)||a.isFile(t)||a.isBlob(t))return t;if(a.isArrayBufferView(t))return t.buffer;if(a.isURLSearchParams(t))return n.setContentType("application/x-www-form-urlencoded;charset=utf-8",!1),t.toString();let c;if(o){if(r.indexOf("application/x-www-form-urlencoded")>-1)return on(t,this.formSerializer).toString();if((c=a.isFileList(t))||r.indexOf("multipart/form-data")>-1){const d=this.env&&this.env.FormData;return re(c?{"files[]":t}:t,d&&new d,this.formSerializer)}}return o||s?(n.setContentType("application/json",!1),ln(t)):t}],transformResponse:[function(t){const n=this.transitional||ye.transitional,r=n&&n.forcedJSONParsing,s=this.responseType==="json";if(t&&a.isString(t)&&(r&&!this.responseType||s)){const i=!(n&&n.silentJSONParsing)&&s;try{return JSON.parse(t)}catch(c){if(i)throw c.name==="SyntaxError"?m.from(c,m.ERR_BAD_RESPONSE,this,null,this.response):c}}return t}],timeout:0,xsrfCookieName:"XSRF-TOKEN",xsrfHeaderName:"X-XSRF-TOKEN",maxContentLength:-1,maxBodyLength:-1,env:{FormData:x.classes.FormData,Blob:x.classes.Blob},validateStatus:function(t){return t>=200&&t<300},headers:{common:{Accept:"application/json, text/plain, */*","Content-Type":void 0}}};a.forEach(["delete","get","head","post","put","patch"],e=>{ye.headers[e]={}});const we=ye,un=a.toObjectSet(["age","authorization","content-length","content-type","etag","expires","from","host","if-modified-since","if-unmodified-since","last-modified","location","max-forwards","proxy-authorization","referer","retry-after","user-agent"]),dn=e=>{const t={};let n,r,s;return e&&e.split(`
`).forEach(function(i){s=i.indexOf(":"),n=i.substring(0,s).trim().toLowerCase(),r=i.substring(s+1).trim(),!(!n||t[n]&&un[n])&&(n==="set-cookie"?t[n]?t[n].push(r):t[n]=[r]:t[n]=t[n]?t[n]+", "+r:r)}),t},Ae=Symbol("internals");function H(e){return e&&String(e).trim().toLowerCase()}function G(e){return e===!1||e==null?e:a.isArray(e)?e.map(G):String(e)}function fn(e){const t=Object.create(null),n=/([^\s,;=]+)\s*(?:=\s*([^,;]+))?/g;let r;for(;r=n.exec(e);)t[r[1]]=r[2];return t}const pn=e=>/^[-_a-zA-Z0-9^`|~,!#$%&'*+.]+$/.test(e.trim());function ie(e,t,n,r,s){if(a.isFunction(r))return r.call(this,t,n);if(s&&(t=n),!!a.isString(t)){if(a.isString(r))return t.indexOf(r)!==-1;if(a.isRegExp(r))return r.test(t)}}function hn(e){return e.trim().toLowerCase().replace(/([a-z\d])(\w*)/g,(t,n,r)=>n.toUpperCase()+r)}function mn(e,t){const n=a.toCamelCase(" "+t);["get","set","has"].forEach(r=>{Object.defineProperty(e,r+n,{value:function(s,o,i){return this[r].call(this,t,s,o,i)},configurable:!0})})}let se=class{constructor(t){t&&this.set(t)}set(t,n,r){const s=this;function o(c,d,f){const u=H(d);if(!u)throw new Error("header name must be a non-empty string");const l=a.findKey(s,u);(!l||s[l]===void 0||f===!0||f===void 0&&s[l]!==!1)&&(s[l||d]=G(c))}const i=(c,d)=>a.forEach(c,(f,u)=>o(f,u,d));return a.isPlainObject(t)||t instanceof this.constructor?i(t,n):a.isString(t)&&(t=t.trim())&&!pn(t)?i(dn(t),n):t!=null&&o(n,t,r),this}get(t,n){if(t=H(t),t){const r=a.findKey(this,t);if(r){const s=this[r];if(!n)return s;if(n===!0)return fn(s);if(a.isFunction(n))return n.call(this,s,r);if(a.isRegExp(n))return n.exec(s);throw new TypeError("parser must be boolean|regexp|function")}}}has(t,n){if(t=H(t),t){const r=a.findKey(this,t);return!!(r&&this[r]!==void 0&&(!n||ie(this,this[r],r,n)))}return!1}delete(t,n){const r=this;let s=!1;function o(i){if(i=H(i),i){const c=a.findKey(r,i);c&&(!n||ie(r,r[c],c,n))&&(delete r[c],s=!0)}}return a.isArray(t)?t.forEach(o):o(t),s}clear(t){const n=Object.keys(this);let r=n.length,s=!1;for(;r--;){const o=n[r];(!t||ie(this,this[o],o,t,!0))&&(delete this[o],s=!0)}return s}normalize(t){const n=this,r={};return a.forEach(this,(s,o)=>{const i=a.findKey(r,o);if(i){n[i]=G(s),delete n[o];return}const c=t?hn(o):String(o).trim();c!==o&&delete n[o],n[c]=G(s),r[c]=!0}),this}concat(...t){return this.constructor.concat(this,...t)}toJSON(t){const n=Object.create(null);return a.forEach(this,(r,s)=>{r!=null&&r!==!1&&(n[s]=t&&a.isArray(r)?r.join(", "):r)}),n}[Symbol.iterator](){return Object.entries(this.toJSON())[Symbol.iterator]()}toString(){return Object.entries(this.toJSON()).map(([t,n])=>t+": "+n).join(`
`)}get[Symbol.toStringTag](){return"AxiosHeaders"}static from(t){return t instanceof this?t:new this(t)}static concat(t,...n){const r=new this(t);return n.forEach(s=>r.set(s)),r}static accessor(t){const r=(this[Ae]=this[Ae]={accessors:{}}).accessors,s=this.prototype;function o(i){const c=H(i);r[c]||(mn(s,i),r[c]=!0)}return a.isArray(t)?t.forEach(o):o(t),this}};se.accessor(["Content-Type","Content-Length","Accept","Accept-Encoding","User-Agent","Authorization"]);a.reduceDescriptors(se.prototype,({value:e},t)=>{let n=t[0].toUpperCase()+t.slice(1);return{get:()=>e,set(r){this[n]=r}}});a.freezeMethods(se);const R=se;function ae(e,t){const n=this||we,r=t||n,s=R.from(r.headers);let o=r.data;return a.forEach(e,function(c){o=c.call(n,o,s.normalize(),t?t.status:void 0)}),s.normalize(),o}function Xe(e){return!!(e&&e.__CANCEL__)}function J(e,t,n){m.call(this,e??"canceled",m.ERR_CANCELED,t,n),this.name="CanceledError"}a.inherits(J,m,{__CANCEL__:!0});function yn(e,t,n){const r=n.config.validateStatus;!n.status||!r||r(n.status)?e(n):t(new m("Request failed with status code "+n.status,[m.ERR_BAD_REQUEST,m.ERR_BAD_RESPONSE][Math.floor(n.status/100)-4],n.config,n.request,n))}const wn=x.hasStandardBrowserEnv?{write(e,t,n,r,s,o){const i=[e+"="+encodeURIComponent(t)];a.isNumber(n)&&i.push("expires="+new Date(n).toGMTString()),a.isString(r)&&i.push("path="+r),a.isString(s)&&i.push("domain="+s),o===!0&&i.push("secure"),document.cookie=i.join("; ")},read(e){const t=document.cookie.match(new RegExp("(^|;\\s*)("+e+")=([^;]*)"));return t?decodeURIComponent(t[3]):null},remove(e){this.write(e,"",Date.now()-864e5)}}:{write(){},read(){return null},remove(){}};function En(e){return/^([a-z][a-z\d+\-.]*:)?\/\//i.test(e)}function gn(e,t){return t?e.replace(/\/?\/$/,"")+"/"+t.replace(/^\/+/,""):e}function Ye(e,t){return e&&!En(t)?gn(e,t):t}const bn=x.hasStandardBrowserEnv?function(){const t=/(msie|trident)/i.test(navigator.userAgent),n=document.createElement("a");let r;function s(o){let i=o;return t&&(n.setAttribute("href",i),i=n.href),n.setAttribute("href",i),{href:n.href,protocol:n.protocol?n.protocol.replace(/:$/,""):"",host:n.host,search:n.search?n.search.replace(/^\?/,""):"",hash:n.hash?n.hash.replace(/^#/,""):"",hostname:n.hostname,port:n.port,pathname:n.pathname.charAt(0)==="/"?n.pathname:"/"+n.pathname}}return r=s(window.location.href),function(i){const c=a.isString(i)?s(i):i;return c.protocol===r.protocol&&c.host===r.host}}():function(){return function(){return!0}}();function Sn(e){const t=/^([-+\w]{1,25})(:?\/\/|:)/.exec(e);return t&&t[1]||""}function xn(e,t){e=e||10;const n=new Array(e),r=new Array(e);let s=0,o=0,i;return t=t!==void 0?t:1e3,function(d){const f=Date.now(),u=r[o];i||(i=f),n[s]=d,r[s]=f;let l=o,w=0;for(;l!==s;)w+=n[l++],l=l%e;if(s=(s+1)%e,s===o&&(o=(o+1)%e),f-i<t)return;const b=u&&f-u;return b?Math.round(w*1e3/b):void 0}}function Re(e,t){let n=0;const r=xn(50,250);return s=>{const o=s.loaded,i=s.lengthComputable?s.total:void 0,c=o-n,d=r(c),f=o<=i;n=o;const u={loaded:o,total:i,progress:i?o/i:void 0,bytes:c,rate:d||void 0,estimated:d&&i&&f?(i-o)/d:void 0,event:s};u[t?"download":"upload"]=!0,e(u)}}const On=typeof XMLHttpRequest<"u",Tn=On&&function(e){return new Promise(function(n,r){let s=e.data;const o=R.from(e.headers).normalize();let{responseType:i,withXSRFToken:c}=e,d;function f(){e.cancelToken&&e.cancelToken.unsubscribe(d),e.signal&&e.signal.removeEventListener("abort",d)}let u;if(a.isFormData(s)){if(x.hasStandardBrowserEnv||x.hasStandardBrowserWebWorkerEnv)o.setContentType(!1);else if((u=o.getContentType())!==!1){const[h,...E]=u?u.split(";").map(g=>g.trim()).filter(Boolean):[];o.setContentType([h||"multipart/form-data",...E].join("; "))}}let l=new XMLHttpRequest;if(e.auth){const h=e.auth.username||"",E=e.auth.password?unescape(encodeURIComponent(e.auth.password)):"";o.set("Authorization","Basic "+btoa(h+":"+E))}const w=Ye(e.baseURL,e.url);l.open(e.method.toUpperCase(),Ge(w,e.params,e.paramsSerializer),!0),l.timeout=e.timeout;function b(){if(!l)return;const h=R.from("getAllResponseHeaders"in l&&l.getAllResponseHeaders()),g={data:!i||i==="text"||i==="json"?l.responseText:l.response,status:l.status,statusText:l.statusText,headers:h,config:e,request:l};yn(function(L){n(L),f()},function(L){r(L),f()},g),l=null}if("onloadend"in l?l.onloadend=b:l.onreadystatechange=function(){!l||l.readyState!==4||l.status===0&&!(l.responseURL&&l.responseURL.indexOf("file:")===0)||setTimeout(b)},l.onabort=function(){l&&(r(new m("Request aborted",m.ECONNABORTED,e,l)),l=null)},l.onerror=function(){r(new m("Network Error",m.ERR_NETWORK,e,l)),l=null},l.ontimeout=function(){let E=e.timeout?"timeout of "+e.timeout+"ms exceeded":"timeout exceeded";const g=e.transitional||Ke;e.timeoutErrorMessage&&(E=e.timeoutErrorMessage),r(new m(E,g.clarifyTimeoutError?m.ETIMEDOUT:m.ECONNABORTED,e,l)),l=null},x.hasStandardBrowserEnv&&(c&&a.isFunction(c)&&(c=c(e)),c||c!==!1&&bn(w))){const h=e.xsrfHeaderName&&e.xsrfCookieName&&wn.read(e.xsrfCookieName);h&&o.set(e.xsrfHeaderName,h)}s===void 0&&o.setContentType(null),"setRequestHeader"in l&&a.forEach(o.toJSON(),function(E,g){l.setRequestHeader(g,E)}),a.isUndefined(e.withCredentials)||(l.withCredentials=!!e.withCredentials),i&&i!=="json"&&(l.responseType=e.responseType),typeof e.onDownloadProgress=="function"&&l.addEventListener("progress",Re(e.onDownloadProgress,!0)),typeof e.onUploadProgress=="function"&&l.upload&&l.upload.addEventListener("progress",Re(e.onUploadProgress)),(e.cancelToken||e.signal)&&(d=h=>{l&&(r(!h||h.type?new J(null,e,l):h),l.abort(),l=null)},e.cancelToken&&e.cancelToken.subscribe(d),e.signal&&(e.signal.aborted?d():e.signal.addEventListener("abort",d)));const p=Sn(w);if(p&&x.protocols.indexOf(p)===-1){r(new m("Unsupported protocol "+p+":",m.ERR_BAD_REQUEST,e));return}l.send(s||null)})},de={http:Gt,xhr:Tn};a.forEach(de,(e,t)=>{if(e){try{Object.defineProperty(e,"name",{value:t})}catch{}Object.defineProperty(e,"adapterName",{value:t})}});const Ne=e=>`- ${e}`,An=e=>a.isFunction(e)||e===null||e===!1,Ze={getAdapter:e=>{e=a.isArray(e)?e:[e];const{length:t}=e;let n,r;const s={};for(let o=0;o<t;o++){n=e[o];let i;if(r=n,!An(n)&&(r=de[(i=String(n)).toLowerCase()],r===void 0))throw new m(`Unknown adapter '${i}'`);if(r)break;s[i||"#"+o]=r}if(!r){const o=Object.entries(s).map(([c,d])=>`adapter ${c} `+(d===!1?"is not supported by the environment":"is not available in the build"));let i=t?o.length>1?`since :
`+o.map(Ne).join(`
`):" "+Ne(o[0]):"as no adapter specified";throw new m("There is no suitable adapter to dispatch the request "+i,"ERR_NOT_SUPPORT")}return r},adapters:de};function ce(e){if(e.cancelToken&&e.cancelToken.throwIfRequested(),e.signal&&e.signal.aborted)throw new J(null,e)}function Ce(e){return ce(e),e.headers=R.from(e.headers),e.data=ae.call(e,e.transformRequest),["post","put","patch"].indexOf(e.method)!==-1&&e.headers.setContentType("application/x-www-form-urlencoded",!1),Ze.getAdapter(e.adapter||we.adapter)(e).then(function(r){return ce(e),r.data=ae.call(e,e.transformResponse,r),r.headers=R.from(r.headers),r},function(r){return Xe(r)||(ce(e),r&&r.response&&(r.response.data=ae.call(e,e.transformResponse,r.response),r.response.headers=R.from(r.response.headers))),Promise.reject(r)})}const Le=e=>e instanceof R?e.toJSON():e;function _(e,t){t=t||{};const n={};function r(f,u,l){return a.isPlainObject(f)&&a.isPlainObject(u)?a.merge.call({caseless:l},f,u):a.isPlainObject(u)?a.merge({},u):a.isArray(u)?u.slice():u}function s(f,u,l){if(a.isUndefined(u)){if(!a.isUndefined(f))return r(void 0,f,l)}else return r(f,u,l)}function o(f,u){if(!a.isUndefined(u))return r(void 0,u)}function i(f,u){if(a.isUndefined(u)){if(!a.isUndefined(f))return r(void 0,f)}else return r(void 0,u)}function c(f,u,l){if(l in t)return r(f,u);if(l in e)return r(void 0,f)}const d={url:o,method:o,data:o,baseURL:i,transformRequest:i,transformResponse:i,paramsSerializer:i,timeout:i,timeoutMessage:i,withCredentials:i,withXSRFToken:i,adapter:i,responseType:i,xsrfCookieName:i,xsrfHeaderName:i,onUploadProgress:i,onDownloadProgress:i,decompress:i,maxContentLength:i,maxBodyLength:i,beforeRedirect:i,transport:i,httpAgent:i,httpsAgent:i,cancelToken:i,socketPath:i,responseEncoding:i,validateStatus:c,headers:(f,u)=>s(Le(f),Le(u),!0)};return a.forEach(Object.keys(Object.assign({},e,t)),function(u){const l=d[u]||s,w=l(e[u],t[u],u);a.isUndefined(w)&&l!==c||(n[u]=w)}),n}const et="1.6.7",Ee={};["object","boolean","number","function","string","symbol"].forEach((e,t)=>{Ee[e]=function(r){return typeof r===e||"a"+(t<1?"n ":" ")+e}});const Pe={};Ee.transitional=function(t,n,r){function s(o,i){return"[Axios v"+et+"] Transitional option '"+o+"'"+i+(r?". "+r:"")}return(o,i,c)=>{if(t===!1)throw new m(s(i," has been removed"+(n?" in "+n:"")),m.ERR_DEPRECATED);return n&&!Pe[i]&&(Pe[i]=!0,console.warn(s(i," has been deprecated since v"+n+" and will be removed in the near future"))),t?t(o,i,c):!0}};function Rn(e,t,n){if(typeof e!="object")throw new m("options must be an object",m.ERR_BAD_OPTION_VALUE);const r=Object.keys(e);let s=r.length;for(;s-- >0;){const o=r[s],i=t[o];if(i){const c=e[o],d=c===void 0||i(c,o,e);if(d!==!0)throw new m("option "+o+" must be "+d,m.ERR_BAD_OPTION_VALUE);continue}if(n!==!0)throw new m("Unknown option "+o,m.ERR_BAD_OPTION)}}const fe={assertOptions:Rn,validators:Ee},P=fe.validators;let Q=class{constructor(t){this.defaults=t,this.interceptors={request:new Te,response:new Te}}async request(t,n){try{return await this._request(t,n)}catch(r){if(r instanceof Error){let s;Error.captureStackTrace?Error.captureStackTrace(s={}):s=new Error;const o=s.stack?s.stack.replace(/^.+\n/,""):"";r.stack?o&&!String(r.stack).endsWith(o.replace(/^.+\n.+\n/,""))&&(r.stack+=`
`+o):r.stack=o}throw r}}_request(t,n){typeof t=="string"?(n=n||{},n.url=t):n=t||{},n=_(this.defaults,n);const{transitional:r,paramsSerializer:s,headers:o}=n;r!==void 0&&fe.assertOptions(r,{silentJSONParsing:P.transitional(P.boolean),forcedJSONParsing:P.transitional(P.boolean),clarifyTimeoutError:P.transitional(P.boolean)},!1),s!=null&&(a.isFunction(s)?n.paramsSerializer={serialize:s}:fe.assertOptions(s,{encode:P.function,serialize:P.function},!0)),n.method=(n.method||this.defaults.method||"get").toLowerCase();let i=o&&a.merge(o.common,o[n.method]);o&&a.forEach(["delete","get","head","post","put","patch","common"],p=>{delete o[p]}),n.headers=R.concat(i,o);const c=[];let d=!0;this.interceptors.request.forEach(function(h){typeof h.runWhen=="function"&&h.runWhen(n)===!1||(d=d&&h.synchronous,c.unshift(h.fulfilled,h.rejected))});const f=[];this.interceptors.response.forEach(function(h){f.push(h.fulfilled,h.rejected)});let u,l=0,w;if(!d){const p=[Ce.bind(this),void 0];for(p.unshift.apply(p,c),p.push.apply(p,f),w=p.length,u=Promise.resolve(n);l<w;)u=u.then(p[l++],p[l++]);return u}w=c.length;let b=n;for(l=0;l<w;){const p=c[l++],h=c[l++];try{b=p(b)}catch(E){h.call(this,E);break}}try{u=Ce.call(this,b)}catch(p){return Promise.reject(p)}for(l=0,w=f.length;l<w;)u=u.then(f[l++],f[l++]);return u}getUri(t){t=_(this.defaults,t);const n=Ye(t.baseURL,t.url);return Ge(n,t.params,t.paramsSerializer)}};a.forEach(["delete","get","head","options"],function(t){Q.prototype[t]=function(n,r){return this.request(_(r||{},{method:t,url:n,data:(r||{}).data}))}});a.forEach(["post","put","patch"],function(t){function n(r){return function(o,i,c){return this.request(_(c||{},{method:t,headers:r?{"Content-Type":"multipart/form-data"}:{},url:o,data:i}))}}Q.prototype[t]=n(),Q.prototype[t+"Form"]=n(!0)});const K=Q;let Nn=class tt{constructor(t){if(typeof t!="function")throw new TypeError("executor must be a function.");let n;this.promise=new Promise(function(o){n=o});const r=this;this.promise.then(s=>{if(!r._listeners)return;let o=r._listeners.length;for(;o-- >0;)r._listeners[o](s);r._listeners=null}),this.promise.then=s=>{let o;const i=new Promise(c=>{r.subscribe(c),o=c}).then(s);return i.cancel=function(){r.unsubscribe(o)},i},t(function(o,i,c){r.reason||(r.reason=new J(o,i,c),n(r.reason))})}throwIfRequested(){if(this.reason)throw this.reason}subscribe(t){if(this.reason){t(this.reason);return}this._listeners?this._listeners.push(t):this._listeners=[t]}unsubscribe(t){if(!this._listeners)return;const n=this._listeners.indexOf(t);n!==-1&&this._listeners.splice(n,1)}static source(){let t;return{token:new tt(function(s){t=s}),cancel:t}}};const Cn=Nn;function Ln(e){return function(n){return e.apply(null,n)}}function Pn(e){return a.isObject(e)&&e.isAxiosError===!0}const pe={Continue:100,SwitchingProtocols:101,Processing:102,EarlyHints:103,Ok:200,Created:201,Accepted:202,NonAuthoritativeInformation:203,NoContent:204,ResetContent:205,PartialContent:206,MultiStatus:207,AlreadyReported:208,ImUsed:226,MultipleChoices:300,MovedPermanently:301,Found:302,SeeOther:303,NotModified:304,UseProxy:305,Unused:306,TemporaryRedirect:307,PermanentRedirect:308,BadRequest:400,Unauthorized:401,PaymentRequired:402,Forbidden:403,NotFound:404,MethodNotAllowed:405,NotAcceptable:406,ProxyAuthenticationRequired:407,RequestTimeout:408,Conflict:409,Gone:410,LengthRequired:411,PreconditionFailed:412,PayloadTooLarge:413,UriTooLong:414,UnsupportedMediaType:415,RangeNotSatisfiable:416,ExpectationFailed:417,ImATeapot:418,MisdirectedRequest:421,UnprocessableEntity:422,Locked:423,FailedDependency:424,TooEarly:425,UpgradeRequired:426,PreconditionRequired:428,TooManyRequests:429,RequestHeaderFieldsTooLarge:431,UnavailableForLegalReasons:451,InternalServerError:500,NotImplemented:501,BadGateway:502,ServiceUnavailable:503,GatewayTimeout:504,HttpVersionNotSupported:505,VariantAlsoNegotiates:506,InsufficientStorage:507,LoopDetected:508,NotExtended:510,NetworkAuthenticationRequired:511};Object.entries(pe).forEach(([e,t])=>{pe[t]=e});const kn=pe;function nt(e){const t=new K(e),n=je(K.prototype.request,t);return a.extend(n,K.prototype,t,{allOwnKeys:!0}),a.extend(n,t,null,{allOwnKeys:!0}),n.create=function(s){return nt(_(e,s))},n}const y=nt(we);y.Axios=K;y.CanceledError=J;y.CancelToken=Cn;y.isCancel=Xe;y.VERSION=et;y.toFormData=re;y.AxiosError=m;y.Cancel=y.CanceledError;y.all=function(t){return Promise.all(t)};y.spread=Ln;y.isAxiosError=Pn;y.mergeConfig=_;y.AxiosHeaders=R;y.formToJSON=e=>Qe(a.isHTMLForm(e)?new FormData(e):e);y.getAdapter=Ze.getAdapter;y.HttpStatusCode=kn;y.default=y;const rt=y,{Axios:Vn,AxiosError:Gn,CanceledError:Kn,isCancel:Wn,CancelToken:Qn,VERSION:Xn,all:Yn,Cancel:Zn,isAxiosError:Bn,spread:er,toFormData:tr,AxiosHeaders:nr,HttpStatusCode:rr,formToJSON:sr,getAdapter:or,mergeConfig:ir}=rt,ke={checkTestExist:{url:"/mooc/test/:tid",method:"GET"},selectQustion:{url:"/mooc/test/:tid",method:"POST"},getAnnouncement:{url:"/mooc/announcement",method:"GET"},getNewExamInfo:{url:"https://www.icourse163.org/mm-tiku/web/j/mocExamBean.getPaper.rpc",method:"POST"},getNotice:{url:"/mooc/notice/extension",method:"GET"}},Fn="https://ginnnnnn.top/api";async function _n(e,t,n){try{return await new Promise((r,s)=>{let o=ke[e].url;if(t)for(const[i,c]of Object.entries(t)){const d=RegExp(`(/):${i}(/)?`,"g");d.test(o)&&(o=o.replaceAll(d,`$1${c}$2`),Reflect.deleteProperty(t,i))}if(o.indexOf("http")!=0&&(o=`${Fn}${o}`),n)for(const[i,c]of Object.entries(n))typeof c=="object"&&Reflect.set(n,i,JSON.stringify(c));rt({url:o,method:ke[e].method,params:t||{},data:n||{},headers:{"Content-Type":"application/x-www-form-urlencoded"}}).then(i=>{let c="",d=!1;i.status!==200||!i.data?c="请求出错":i.data.msg&&(c=i.data.msg,i.data.status===200&&(d=!0)),c&&console.log(c),r(i.data)}).catch(i=>{let c=i;Bn(i)&&(c=i.message),console.log(c),s(i)})})}catch{return{}}}const st=()=>_n,jn=()=>{const[e,t]=[new Array,new Array],n=document.querySelectorAll('input[id^="op_"]');for(let s=0;s<n.length;s++){const o=n.item(s);e.push(Number.parseInt(o.id.split("_")[2].slice(0,-13)))}const r=document.getElementsByClassName("m-FillBlank examMode u-questionItem");for(let s=0;s<r.length;s++){const i=r.item(s).querySelector(".j-richTxt");t.push(i.innerText)}return{oidList:e,titleList:t}},Dn=(e,t)=>{if(e)for(const r of e)document.querySelector(`input[id*="_${r}"]`).classList.add("gin-answer");const n=document.getElementsByClassName("m-FillBlank examMode u-questionItem");for(let r=0;r<n.length;r++){const o=n.item(r).querySelector(".j-richTxt"),i=Reflect.get(t,o.innerText),c=document.createElement("div");c.innerHTML=i.title,o.appendChild(c);const d=i.stdAnswer.split("##%_YZPRLFH_%##"),f=document.createElement("div");for(let u=0;u<d.length;u++){const l=document.createElement("span");l.classList.add("gin-answer-item"),l.innerHTML=d[u],f.append(l),u!==d.length-1&&f.append(" / ")}o.append(f)}},Un=e=>{var n,r;const t=document.getElementsByClassName("f-richEditorText j-richTxt f-fl");for(let s=0;s<t.length;s++){const o=document.createElement("div");o.classList.add("gin-answer-item"),o.innerHTML=(n=Reflect.get(e,`${s}`))==null?void 0:n.answer,(r=t.item(s))==null||r.append(o)}},ot=()=>{var t,n,r,s;const e=document.getElementsByClassName("u-questionItem u-analysisQuestion analysisMode");for(let o=0;o<e.length;o++){const i=(t=e.item(o))==null?void 0:t.getElementsByClassName("s");for(let d=0;d<i.length;d++){const f=(r=(n=i.item(d))==null?void 0:n.lastElementChild)==null?void 0:r.querySelector("input");f.checked=!0}const c=(s=e.item(o))==null?void 0:s.querySelector("textarea");c.value="666"}},qn=async(e,t)=>{const n=document.querySelector(".u-homework-evaAction .bottombtnwrap .j-submitbtn"),r=document.querySelector(".u-homework-evaAction .xlinfo"),s=document.querySelector(".u-homework-evaAction .xlinfo .j-gotonext");for(let o=0;o<e;o++)await ge(()=>r.style.display==="none"),ot(),await Fe(1e3),console.log(new Date),n.click(),await ge(()=>r.style.display!=="none"),t(o+1,e),s.click()},In=async()=>{var s,o;const e=st();let t=(s=document.getElementById("app"))==null?void 0:s.getElementsByTagName("form").item(0);for(;!t;)await Fe(1e3),t=(o=document.getElementById("app"))==null?void 0:o.getElementsByTagName("form").item(0);const n=async()=>{const i=await e("getNewExamInfo",{csrfKey:document.cookie.match(/NTESSTUDYSI=([a-z0-9]+);/)[1]},{answerformId:W("aid"),examId:W("eid")});let c=[];for(let u of i.result.questions)for(let l of u.optionDtos)c.push(l.id);const d=await e("selectQustion",{tid:i.result.tid},{oidList:c}),f=document.querySelectorAll(".ant-checkbox-group>div, .ant-radio-group>div");for(let u of d.data.choiceAns)f[c.indexOf(u)].classList.add("gin-answer-item")},r=document.createElement("button");r.className="ant-btn ant-btn-primary",r.setAttribute("style","margin-bottom: 16px"),r.onclick=n,r.innerText="获取答案",t==null||t.before(r)},X=st(),M=new dt,N=M.add(void 0),A=M.add(-1);let B=null;const[Y,F,Z]=[M.add(!1),M.add(!1),M.add(!1)];location.href.indexOf("newExam")!==-1&&In();const vn=async()=>{if(O.innerText!=="正在获取答案,请稍后..."){if(O.innerText="正在获取答案,请稍后...",N.get()==="quiz"){const e=await X("selectQustion",{tid:B},jn());Dn(e.data.choiceAns,e.data.completionAns)}else if(N.get()==="homework"){const e=await X("selectQustion",{tid:B},{});Un(e.data.homeworkAns)}O.innerText=""}},it=document.createElement("style");it.innerText=`
    input.gin-answer:not(:checked) + label, #GinsMooc, .gin-answer-item {
        background-color: #d9ecff;
    }
    .learnPageContentLeft {
        background: rgb(240, 242, 245);
    }
    #GinsMooc {
        margin-bottom: 12px !important;
    }
    .gin-function {
        display: flex;
        align-items: center;
    }
    .gin-function .u-btn {
        margin-right: 16px;
    }
    .gin-state-tips {
        font-size: 14px;
    }
`;document.head.append(it);const I=document.createElement("div");I.id="GinsMooc";I.classList.add("m-learnbox");var Be;(Be=document.querySelector(".learnPageContentLeft"))==null||Be.prepend(I);if(location.href.indexOf("/spoc")!==-1){const e=document.createElement("div");e.innerHTML="当前课程为 SPOC 课程,可能无法获取答案。SPOC 课程可能会有关联的对外课程,你可以尝试搜索并加入,题目大概率是一样的",I.prepend()}const Hn=async()=>{var t;const e=(await X("getNotice",{version:"v2.2.1"},void 0)).data;if(console.log(e),!((t=localStorage.getItem("Gins-ignore-notice"))!=null&&t.split(",").find(n=>Number.parseInt(n)===e.id))){const n=document.createElement("div");n.innerHTML=e.content;const r=document.createElement("a");r.innerText="不再提醒",r.onclick=()=>{const s=localStorage.getItem("Gins-ignore-notice");localStorage.setItem("Gins-ignore-notice",s?`${s},${e.id}`:`${e.id}`),n.remove(),r.remove()},r.style.marginLeft="16px",n.append(r),I.prepend(n)}};Hn();const v=document.createElement("div");v.classList.add("gin-function");I.append(v);const j=document.createElement("button");j.classList.add("u-btn","u-btn-default","f-dn");j.onclick=vn;j.innerText="获取答案";v.append(j);const D=document.createElement("button");D.classList.add("u-btn","u-btn-default","f-dn");D.onclick=()=>{ot(),window.scroll({top:document.documentElement.scrollHeight,behavior:"smooth"})};D.innerText="一键互评";v.append(D);const U=document.createElement("button");U.classList.add("u-btn","u-btn-default","f-dn");U.onclick=()=>{qn(5,(e,t)=>{e>=t?O.innerText=`已完成 ${t} 次互评`:O.innerText=`自动互评中(${e} / ${t})`})};U.innerText="自动互评";v.append(U);const O=document.createElement("div");O.classList.add("gin-state-tips");v.append(O);window.addEventListener("hashchange",()=>{B=W("id"),location.hash.indexOf("quiz")!==-1||location.hash.indexOf("examObject")!==-1?N.set("quiz"):location.hash.indexOf("hw")!==-1||location.hash.indexOf("examSubjective")!==-1?N.set("homework"):N.set(void 0)});window.setInterval(()=>{Z.set(location.hash.indexOf("examlist")!==-1);const e=document.querySelector(".j-prepare.prepare");F.set(e&&!e.classList.contains("f-dn")||document.querySelector(".j-homework-paper")!==null||Z.get()),Y.set(document.querySelector(".u-questionItem.u-analysisQuestion.analysisMode")!==null)},100);const at=async()=>{if(console.log("onTestChange",N.get(),F.get(),Z.get(),B),N.get()==="quiz"&&!F.get()||N.get()==="homework"?j.classList.remove("f-dn"):j.classList.add("f-dn"),A.set(-2),F.get()&&B)if((await X("checkTestExist",{tid:B,type:"isExisting"},void 0)).data.existing){A.set(-1);return}else{const t=new EventSource(`https://ginnnnnn.top/api/mooc/course/refresh/${W("tid")}`);t.onmessage=n=>{console.log(n.data);const r=JSON.parse(n.data);r&&r.total>0&&A.set(Math.round(r.finished/r.total*100)),(A.value===100||r.status===400)&&(t.close(),r.msg&&A.set(-1))}}else if(!Z.get()){A.set(-1);return}},Mn=()=>{console.log("onModeChange",Y.get()),Y.get()?(D.classList.remove("f-dn"),U.classList.remove("f-dn")):(D.classList.add("f-dn"),U.classList.add("f-dn"))};Y.addEventListenr("change",Mn);F.addEventListenr("change",at);N.addEventListenr("change",at);A.addEventListenr("set",()=>{switch(A.get()){case-2:O.innerText="正在检查课程...";break;case-1:O.innerText=F.get()?"已准备就绪":"";break;default:O.innerText=`正在更新课程...${A.get()}%`;break}});


================================================
FILE: extension/release/manifest.json
================================================
{
    "manifest_version": 3,
    "name": "GinsMooc Extension",
    "version": "2.2.1",
    "description": "A Chrome extension to get the mooc answers and evaluate auto automatically.",
    "icons": {
        "16": "icons/favicon16.png",
        "32": "icons/favicon32.png",
        "48": "icons/favicon48.png",
        "128": "icons/favicon128.png"
    },
    "author": { "email": "ginnnnnn@qq.com" },
    "homepage_url": "https://ginnnnnn.top/mooc",
    "content_scripts": [
        {
            "matches": ["https://www.icourse163.org/*learn/*?tid=*", "https://www.icourse163.org/mooc/main/newExam*"],
            "js": ["content-scripts/index-ad710f80.js"],
            "run_at": "document_end"
        }
    ]
}

================================================
FILE: extension/src/main.ts
================================================
import { CustomRefList } from "./plugins/react"
import { useApiAccess } from "./plugins/apiAccess"
import { sleep, getUrlParam } from "./plugins/tool"
import { getQuizQuestionKeys, setQuizAnswer, setHomeworkAnswer, autoEvaluate, batchEvaluate } from "./plugins/mooc"
import { newExamHandle } from "./newExam"

const apiAccess = useApiAccess()
const refList = new CustomRefList()
const testType = refList.add(<"quiz" | "homework" | undefined>undefined)
const newCourseState = refList.add(-1)
let testId: string | null = null
const [analysis, prepare, examlist] = [refList.add(false), refList.add(false), refList.add(false)]

if (location.href.indexOf("newExam") !== -1) {
    newExamHandle()
}

const getAnswer = async () => {
    if (stateTips.innerText === "正在获取答案,请稍后...") {
        return
    }
    stateTips.innerText = "正在获取答案,请稍后..."
    if (testType.get() === "quiz") {
        const answers = await apiAccess("selectQustion", { tid: testId as string }, getQuizQuestionKeys())
        setQuizAnswer(answers.data.choiceAns as number[], answers.data.completionAns as Object)
    } else if (testType.get() === "homework") {
        const answers = await apiAccess("selectQustion", { tid: testId as string }, {})
        setHomeworkAnswer(answers.data.homeworkAns as Object)
    }
    stateTips.innerText = ""
}

const styleNode = document.createElement("style")
styleNode.innerText = `
    input.gin-answer:not(:checked) + label, #GinsMooc, .gin-answer-item {
        background-color: #d9ecff;
    }
    .learnPageContentLeft {
        background: rgb(240, 242, 245);
    }
    #GinsMooc {
        margin-bottom: 12px !important;
    }
    .gin-function {
        display: flex;
        align-items: center;
    }
    .gin-function .u-btn {
        margin-right: 16px;
    }
    .gin-state-tips {
        font-size: 14px;
    }
`
document.head.append(styleNode)

const wrapperNode = document.createElement("div")
wrapperNode.id = "GinsMooc"
wrapperNode.classList.add("m-learnbox")
document.querySelector(".learnPageContentLeft")?.prepend(wrapperNode)

if (location.href.indexOf("/spoc") !== -1) {
    const spocTipsNode = document.createElement("div")
    spocTipsNode.innerHTML = "当前课程为 SPOC 课程,可能无法获取答案。SPOC 课程可能会有关联的对外课程,你可以尝试搜索并加入,题目大概率是一样的"
    wrapperNode.prepend()
}

const setNotice = async () => {
    const notice = (await apiAccess("getNotice", { version: "v2.2.1" }, undefined)).data
    console.log(notice)
    if (
        !localStorage
            .getItem("Gins-ignore-notice")
            ?.split(",")
            .find((item) => Number.parseInt(item) === notice.id)
    ) {
        const noticeNode = document.createElement("div")
        noticeNode.innerHTML = notice.content
        const closeBtn = document.createElement("a")
        closeBtn.innerText = "不再提醒"
        closeBtn.onclick = () => {
            const origin = localStorage.getItem("Gins-ignore-notice")
            localStorage.setItem("Gins-ignore-notice", origin ? `${origin},${notice.id}` : `${notice.id}`)
            noticeNode.remove()
            closeBtn.remove()
        }
        closeBtn.style.marginLeft = "16px"
        noticeNode.append(closeBtn)
        wrapperNode.prepend(noticeNode)
    }
}
setNotice()

const functionNode = document.createElement("div")
functionNode.classList.add("gin-function")
wrapperNode.append(functionNode)

const getAnswerBtn = document.createElement("button")
getAnswerBtn.classList.add("u-btn", "u-btn-default", "f-dn")
getAnswerBtn.onclick = getAnswer
getAnswerBtn.innerText = "获取答案"
functionNode.append(getAnswerBtn)

const evaluateBtn = document.createElement("button")
evaluateBtn.classList.add("u-btn", "u-btn-default", "f-dn")
evaluateBtn.onclick = () => {
    autoEvaluate()
    window.scroll({ top: document.documentElement.scrollHeight, behavior: "smooth" })
}
evaluateBtn.innerText = "一键互评"
functionNode.append(evaluateBtn)

const batchEvaluateBtn = document.createElement("button")
batchEvaluateBtn.classList.add("u-btn", "u-btn-default", "f-dn")
batchEvaluateBtn.onclick = () => {
    batchEvaluate(5, (finish: number, total: number) => {
        if (finish >= total) {
            stateTips.innerText = `已完成 ${total} 次互评`
        } else {
            stateTips.innerText = `自动互评中(${finish} / ${total})`
        }
    })
}
batchEvaluateBtn.innerText = "自动互评"
functionNode.append(batchEvaluateBtn)


const stateTips = document.createElement("div")
stateTips.classList.add("gin-state-tips")
functionNode.append(stateTips)

window.addEventListener("hashchange", () => {
    testId = getUrlParam("id")
    if (location.hash.indexOf("quiz") !== -1 || location.hash.indexOf("examObject") !== -1) {
        testType.set("quiz")
    } else if (location.hash.indexOf("hw") !== -1 || location.hash.indexOf("examSubjective") !== -1) {
        testType.set("homework")
    } else {
        testType.set(undefined)
    }
})

window.setInterval(() => {
    examlist.set(location.hash.indexOf("examlist") !== -1)
    const prepareNode = document.querySelector(".j-prepare.prepare")
    prepare.set(
        (prepareNode && !prepareNode.classList.contains("f-dn")) ||
            document.querySelector(".j-homework-paper") !== null ||
            examlist.get()
    )
    analysis.set(document.querySelector(".u-questionItem.u-analysisQuestion.analysisMode") !== null)
}, 100);

const onTestChange = async () => {
    console.log("onTestChange", testType.get(), prepare.get(), examlist.get(), testId)
    if ((testType.get() === "quiz" && !prepare.get()) || testType.get() === "homework") {
        getAnswerBtn.classList.remove("f-dn")
    } else {
        getAnswerBtn.classList.add("f-dn")
    }

    newCourseState.set(-2)
    if (prepare.get() && testId) {
        const res = await apiAccess("checkTestExist", { tid: testId, type: "isExisting" }, undefined)
        if (res.data.existing) {
            newCourseState.set(-1)
            return
        } else {
            const eventSource = new EventSource(`https://ginnnnnn.top/api/mooc/course/refresh/${getUrlParam("tid")}`)
            eventSource.onmessage = (event) => {
                console.log(event.data)
                const state = JSON.parse(event.data)
                if (state && state.total > 0) {
                    newCourseState.set(Math.round((state.finished / state.total) * 100))
                }
                if (newCourseState.value === 100 || state.status === 400) {
                    eventSource.close()
                    if (state.msg) {
                        newCourseState.set(-1)
                    }
                }
            }
        }
    } else if (!examlist.get()) {
        newCourseState.set(-1)
        return
    }
}

const onModeChange = () => {
    console.log("onModeChange", analysis.get())
    if (analysis.get()) {
        evaluateBtn.classList.remove("f-dn")
        batchEvaluateBtn.classList.remove("f-dn")
    } else {
        evaluateBtn.classList.add("f-dn")
        batchEvaluateBtn.classList.add("f-dn")
    }
}

analysis.addEventListenr("change", onModeChange)
prepare.addEventListenr("change", onTestChange)
testType.addEventListenr("change", onTestChange)
newCourseState.addEventListenr("set", () => {
    switch (newCourseState.get()) {
        case -2:
            stateTips.innerText = "正在检查课程..."
            break
        case -1:
            stateTips.innerText = prepare.get() ? "已准备就绪" : ""
            break
        default:
            stateTips.innerText = `正在更新课程...${newCourseState.get()}%`
            break
    }
})


================================================
FILE: extension/src/newExam.ts
================================================
import { useApiAccess } from "./plugins/apiAccess"
import { sleep, getUrlParam } from "./plugins/tool"

export const newExamHandle = async () => {
    const apiAccess = useApiAccess()
    let form = document.getElementById("app")?.getElementsByTagName("form").item(0)

    while (!form) {
        await sleep(1000)
        form = document.getElementById("app")?.getElementsByTagName("form").item(0)
    }

    const getAnswer = async () => {
        const info = await apiAccess(
            "getNewExamInfo",
            { csrfKey: document.cookie.match(/NTESSTUDYSI=([a-z0-9]+);/)![1] },
            { answerformId: getUrlParam("aid")!, examId: getUrlParam("eid")! }
        )
        let oidList: Array<number> = []
        for (let question of info.result.questions) {
            for (let option of question.optionDtos) {
                oidList.push(option.id)
            }
        }
        const answers = await apiAccess("selectQustion", { tid: info.result.tid }, { oidList: oidList })
        const optionElements = document.querySelectorAll(
            ".ant-checkbox-group>div, .ant-radio-group>div"
        ) as NodeListOf<HTMLDivElement>
        // console.log(optionElements)
        for (let id of answers.data.choiceAns!) {
            optionElements[oidList.indexOf(id)].classList.add("gin-answer-item")
        }
    }

    const getAnswerBtn = document.createElement("button")
    getAnswerBtn.className = "ant-btn ant-btn-primary"
    getAnswerBtn.setAttribute("style", "margin-bottom: 16px")
    getAnswerBtn.onclick = getAnswer
    getAnswerBtn.innerText = "获取答案"
    form?.before(getAnswerBtn)
}


================================================
FILE: extension/src/plugins/apiAccess.ts
================================================
import type { ApiKeyType, ApiResponseType, ApiRequestType } from "../type/api"
import type { App } from "vue"
import { isAxiosError } from "axios"
import apiInfo from "../type/api"
import axios from "axios"

const baseUrl = "https://ginnnnnn.top/api"

async function apiAccess<T extends ApiKeyType>(api: T): Promise<ApiResponseType[T]>
async function apiAccess<T extends ApiKeyType>(
    api: T,
    params: ApiRequestType[T]["params"],
    data: ApiRequestType[T]["data"]
): Promise<ApiResponseType[T]>

/** 函数重载 */
async function apiAccess<T extends ApiKeyType>(
    api: T,
    params?: ApiRequestType[T]["params"],
    data?: ApiRequestType[T]["data"]
) {
    /** 错误处理,主要catch 404,调用者不再需要try-catch */
    try {
        return await new Promise<ApiResponseType[T]>((resolve, reject) => {
            /** 查询参数转动态路由参数 */
            let url = apiInfo[api].url
            if (params) {
                for (const [key, val] of Object.entries(params)) {
                    const reg = RegExp(`(/):${key}(/)?`, "g")
                    if (reg.test(url)) {
                        url = url.replaceAll(reg, `$1${val}$2`)
                        Reflect.deleteProperty(params, key)
                    }
                }
            }
            if (url.indexOf("http") != 0) {
                url = `${baseUrl}${url}`
            }
            /** 将对象转为json字符串 */
            if (data) {
                for (const [key, val] of Object.entries(data)) {
                    if (typeof val === "object") {
                        Reflect.set(data, key, JSON.stringify(val))
                    }
                }
            }
            /** 异步发送请求 */
            axios<ApiResponseType[T]>({
                url: url,
                method: apiInfo[api].method,
                params: params || {},
                data: data || {},
                headers: { "Content-Type": "application/x-www-form-urlencoded" }
            })
                .then((res) => {
                    let message = "",
                        success = false
                    if (res.status !== 200 || !res.data) {
                        message = "请求出错"
                    } else if (res.data.msg) {
                        message = res.data.msg
                        if (res.data.status === 200) {
                            success = true
                        }
                    }
                    if (message) {
                        console.log(message)
                    }
                    resolve(res.data)
                })
                .catch((error) => {
                    let message = error
                    if (isAxiosError(error)) {
                        message = error.message
                    }
                    console.log(message)
                    reject(error)
                })
        })
    } catch {
        return {}
    }
}

export const useApiAccess = () => apiAccess

export default {
    install: (app: App) => {
        app.config.globalProperties.$apiAccess = apiAccess
    }
}


================================================
FILE: extension/src/plugins/mooc.ts
================================================
import type { quiz, option } from "../type/mooc"
import { sleep, waitFor } from "./tool"

const getQuizQuestionKeys = () => {
    const [oidList, titleList] = [new Array<number>(), new Array<string>()]
    const choices = document.querySelectorAll('input[id^="op_"]')
    for (let i = 0; i < choices.length; i++) {
        const choice = choices.item(i) as HTMLInputElement
        oidList.push(Number.parseInt(choice.id.split("_")[2].slice(0, -13)))
    }
    const completions = document.getElementsByClassName("m-FillBlank examMode u-questionItem")
    for (let i = 0; i < completions.length; i++) {
        const questionNode = completions.item(i) as HTMLDivElement
        const titleNode = questionNode.querySelector(".j-richTxt") as HTMLDivElement
        titleList.push(titleNode.innerText)
    }
    return { oidList, titleList }
}

const setQuizAnswer = (choiceAns: number[], completionAns: Object) => {
    if (choiceAns) {
        for (const id of choiceAns) {
            const node = document.querySelector(`input[id*="_${id}"]`) as HTMLInputElement
            node.classList.add("gin-answer")
        }
    }
    const completions = document.getElementsByClassName("m-FillBlank examMode u-questionItem")
    for (let i = 0; i < completions.length; i++) {
        const questionNode = completions.item(i) as HTMLDivElement
        const titleNode = questionNode.querySelector(".j-richTxt") as HTMLDivElement
        const question = Reflect.get(completionAns, titleNode.innerText) as quiz

        const newTitleNode = document.createElement("div")
        newTitleNode.innerHTML = question.title
        titleNode.appendChild(newTitleNode)

        const answerList = (<string>question.stdAnswer).split("##%_YZPRLFH_%##")
        const answerListNode = document.createElement("div")
        for (let j = 0; j < answerList.length; j++) {
            const answerNode = document.createElement("span")
            answerNode.classList.add("gin-answer-item")
            answerNode.innerHTML = answerList[j]
            answerListNode.append(answerNode)
            if (j !== answerList.length - 1) {
                answerListNode.append(" / ")
            }
        }
        titleNode.append(answerListNode)
    }
}

const setHomeworkAnswer = (homeworkAns: Object) => {
    const homeworks = document.getElementsByClassName("f-richEditorText j-richTxt f-fl")
    for (let i = 0; i < homeworks.length; i++) {
        const answerNode = document.createElement("div")
        answerNode.classList.add('gin-answer-item')
        answerNode.innerHTML = Reflect.get(homeworkAns, `${i}`)?.answer as string
        homeworks.item(i)?.append(answerNode)
    }
}

const autoEvaluate = () => {
    const analysis = document.getElementsByClassName("u-questionItem u-analysisQuestion analysisMode")
    for (let i = 0; i < analysis.length; i++) {
        const radioGroup = analysis.item(i)?.getElementsByClassName("s") as HTMLCollection
        for (let j = 0; j < radioGroup.length; j++) {
            const radio = radioGroup.item(j)?.lastElementChild?.querySelector("input") as HTMLInputElement
            radio.checked = true
        }
        const textarea = analysis.item(i)?.querySelector("textarea") as HTMLTextAreaElement
        textarea.value = "666"
    }
}

const batchEvaluate = async (times: number, updateCaller: (finish: number, total: number) => any) => {
    const submitBtn = document.querySelector(".u-homework-evaAction .bottombtnwrap .j-submitbtn") as HTMLDivElement
    const xlinfo = document.querySelector(".u-homework-evaAction .xlinfo") as HTMLDivElement
    const nextBtn = document.querySelector(".u-homework-evaAction .xlinfo .j-gotonext") as HTMLDivElement
    for (let i = 0; i < times; i++) {
        await waitFor(() => xlinfo.style.display === 'none')
        autoEvaluate()
        await sleep(1000)
        console.log(new Date())
        submitBtn.click()
        await waitFor(() => xlinfo.style.display !== 'none')
        updateCaller(i + 1, times)
        nextBtn.click()
    }
}

export { getQuizQuestionKeys, setHomeworkAnswer, setQuizAnswer, autoEvaluate, batchEvaluate }


================================================
FILE: extension/src/plugins/react.ts
================================================
import { randomString } from "./tool"

class CustomRefList extends Array<CustomRef<any>> {
    id: string
    node: HTMLElement

    constructor() {
        super()
        this.node = document.createElement("gin")
        this.id = randomString(8)
        this.node.id = `gin-auto-${this.id}`
        document.body.appendChild(this.node)
    }

    add<T>(value: T): CustomRef<T> {
        const add = new CustomRef(this, value)
        super.push(add)
        return add
    }
}

class CustomRef<T> {
    id: string
    node: HTMLElement
    parent: CustomRefList
    value: T

    constructor(parent: CustomRefList, value: T) {
        this.parent = parent
        this.node = document.createElement("gin")
        this.id = randomString(8)
        this.node.id = this.id
        this.parent.node.appendChild(this.node)
        this.value = value
    }

    get(): T {
        return this.value
    }

    set(value: T): void {
        if (this.value !== value) {
            const oldValue = this.value
            this.value = value
            this.node.dispatchEvent(new CustomEvent("change", { detail: { oldValue, newValue: this.value } }))
        }
        this.node.dispatchEvent(new CustomEvent("set"))
    }

    addEventListenr(eventName: "change" | "set", callback: (ev: Event) => void): void {
        this.node.addEventListener(eventName, callback)
    }
}

export { CustomRefList }

================================================
FILE: extension/src/plugins/tool.ts
================================================
const sleep = async (ms: number) => {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve('')
        }, ms)
    })
}

const waitFor = async (checker: any) => {
    return new Promise((resolve) => {
        const checkDisplay = setInterval(() => {
            console.log('check wait for')
            if (checker()) {
                clearInterval(checkDisplay);
                resolve('');
            }
        }, 50);
    });
}

const getUrlParam = (key: string) => {
    const reg = new RegExp('(^|&)' + key + '=([^&]*)(&|$)')
    const result = window.location.search.substring(1).match(reg)
        || window.location.hash.substring((window.location.hash.search(/\?/)) + 1).match(reg)
    return result ? decodeURIComponent(result[2]) : null
}

const randomString = (length: number) => {
    const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890'
    let result = ''
    for (let i = 0; i < length; i++) {
        result += chars[Math.floor(Math.random() * chars.length)]
    }
    return result
}

export { sleep, getUrlParam, randomString, waitFor }

================================================
FILE: extension/src/type/api.ts
================================================
import type { homework, quiz, notice } from "./mooc"

type RequestType = {
    params?: Object
    data?: Object
}

type Response<T = any> = {
    status: number
    data: T
    msg: string
}

const apiInfo = {
    checkTestExist: {
        url: "/mooc/test/:tid",
        method: "GET"
    },
    selectQustion: {
        url: "/mooc/test/:tid",
        method: "POST"
    },
    getAnnouncement: {
        url: "/mooc/announcement",
        method: "GET"
    },
    getNewExamInfo: {
        url: "https://www.icourse163.org/mm-tiku/web/j/mocExamBean.getPaper.rpc",
        method: "POST"
    },
    getNotice: {
        url: "/mooc/notice/extension",
        method: "GET"
    }
}

export type ApiKeyType = keyof typeof apiInfo

export interface ApiResponseType {
    checkTestExist: Response<{
        existing: boolean
    }>
    selectQustion: Response<{
        choiceAns?: number[]
        completionAns?: Object
        homeworkAns?: Object
    }>
    getAnnouncement: Response<string>
    getNotice: Response<notice>
    getNewExamInfo: {
        code: number
        result: {
            tid: number
            questions: { optionDtos: { id: number }[] }[]
        }
    }
}

export interface ApiRequestType {
    checkTestExist: RequestType & {
        params: { tid: number | string; type: "isExisting" }
    }
    selectQustion: RequestType & {
        params: { tid: number | string }
        data: {
            oidList?: number[]
            titleList?: string[]
        }
    }
    getNotice: RequestType & {
        params: { version: string }
    }
    getNewExamInfo: RequestType & {
        params: { csrfKey: string }
        data: {
            answerformId: number | string
            examId: number | string
        }
    }
}

export default apiInfo


================================================
FILE: extension/src/type/mooc.ts
================================================
export enum QuestionTypeEnumList {
    SingleChoice = "SINGLE_CHOICE",
    MultipleChoice = "MULTIPLE_CHOICE",
    Completion = "COMPLETION",
    Judge = "JUDGEMENT",
    Homework = "HOMEWORK",
    OnlineJudge = "ONLINE_JUDGE"
}

interface course extends Object {
    id: number
    name: string
    school: string
    imageUrl: string
}

interface test extends Object {
    id: number
    name: string
    objective: boolean
    releaseTime: string
    deadline: string
    chapterId: number
    chapterName: string
}

interface option extends Object {
    id: number
    content: string
    answer: boolean
}

interface quiz extends Object {
    id: number
    type:
        | QuestionTypeEnumList.SingleChoice
        | QuestionTypeEnumList.MultipleChoice
        | QuestionTypeEnumList.Completion
        | QuestionTypeEnumList.Judge
    title: string
    stdAnswer: string | null
    optionList: option[] | null
}

interface homework extends Object {
    id: number
    type: QuestionTypeEnumList.Homework | QuestionTypeEnumList.OnlineJudge
    title: string
    answer: string | null
    description: string | null
    memoryLimit: number | null
    timeLimit: number | null
}

interface notice extends Object {
    id: number
    content: string
}

export type QuestionTypeEnum = typeof QuestionTypeEnumList
export type { course, test, option, quiz, homework, notice }


================================================
FILE: extension/tsconfig.config.json
================================================
{
    "extends": "@vue/tsconfig/tsconfig.node.json",
    "include": [
        "vite.config.*",
        "vitest.config.*",
        "cypress.config.*",
        "playwright.config.*"
    ],
    "compilerOptions": {
        "composite": true,
        "types": [
            "node"
        ]
    }
}

================================================
FILE: extension/tsconfig.json
================================================
{
    "extends": "@vue/tsconfig/tsconfig.web.json",
    "include": [
        "env.d.ts",
        "src/**/*",
        "src/**/*.vue"
    ],
    "compilerOptions": {
        "baseUrl": ".",
        "paths": {
            "@/*": [
                "./src/*"
            ]
        }
    },
    "references": [
        {
            "path": "./tsconfig.config.json"
        }
    ]
}

================================================
FILE: extension/vite.config.ts
================================================
import { fileURLToPath, URL } from 'node:url'

import { defineConfig } from 'vite'

// https://vitejs.dev/config/
export default defineConfig({
    plugins: [],
    resolve: {
        alias: {
            '@': fileURLToPath(new URL('./src', import.meta.url))
        }
    },
    build: {
        "outDir": "./release/content-scripts",
        "assetsDir": "./"
    }
})


================================================
FILE: index.html
================================================
<!DOCTYPE html>
<html lang="zh">

<head>
    <meta charset="UTF-8">
    <link rel="icon" href="/favicon.png">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <!-- <meta name="referrer" content="no-referrer"> -->
    <script type="module" src="/src/main.ts"></script>
    <title>Gins</title>
    <script>
        window.location.hash = ""
    </script>
    <script>
        var _hmt = _hmt || [];
        (function () {
            var hm = document.createElement("script");
            hm.src = "https://hm.baidu.com/hm.js?803f8581ce82c43a7f51eab256958890";
            var s = document.getElementsByTagName("script")[0];
            s.parentNode.insertBefore(hm, s);
        })();
    </script>
    <script>
        (function () {
            var meta = document.createElement('meta')
            meta.content = 'no-referrer'
            meta.name = 'referrer'
            document.getElementsByTagName('head')[0].appendChild(meta)
        })()
    </script>
</head>

<body>
</body>

</html>

================================================
FILE: package.json
================================================
{
  "name": "ginsmooc",
  "version": "0.0.0",
  "private": true,
  "scripts": {
    "dev": "vite",
    "build": "run-p type-check build-only",
    "preview": "vite preview",
    "build-only": "vite build",
    "type-check": "vue-tsc --noEmit",
    "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore"
  },
  "dependencies": {
    "@element-plus/icons-vue": "^2.1.0",
    "@jridgewell/sourcemap-codec": "^1.4.14",
    "@vueuse/core": "^9.11.1",
    "axios": "^1.2.3",
    "d3-cloud": "^1.2.5",
    "element-plus": "^2.2.28",
    "fuzzysort": "^2.0.4",
    "vue": "^3.2.45",
    "vue-router": "^4.1.6"
  },
  "devDependencies": {
    "@rushstack/eslint-patch": "^1.1.4",
    "@types/node": "^18.15.5",
    "@vitejs/plugin-vue": "^4.0.0",
    "@vue/eslint-config-prettier": "^7.0.0",
    "@vue/eslint-config-typescript": "^11.0.0",
    "@vue/tsconfig": "^0.1.3",
    "eslint": "^8.22.0",
    "eslint-plugin-vue": "^9.3.0",
    "npm-run-all": "^4.1.5",
    "prettier": "^2.7.1",
    "typescript": "~4.7.4",
    "vite": "^4.0.0",
    "vue-tsc": "^1.0.12"
  }
}


================================================
FILE: public/background.html
================================================
<!DOCTYPE html>
<!--
	Aerial by HTML5 UP
	html5up.net | @ajlkn
	Free for personal and commercial use under the CCA 3.0 license (html5up.net/license)
-->
<html>
    <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" />
        <link rel="stylesheet" href="css/main.css" />
        <noscript><link rel="stylesheet" href="css/noscript.css" /></noscript>
    </head>
    <body class="is-preload">
        <div id="wrapper">
            <div id="bg"></div>
            <div id="overlay"></div>
            <div id="main">
                <!-- Header -->
                <header id="header" style="text-shadow: 5px 5px 5px grey;">
                    <h1>Welcome</h1>
                    <p>新年好!</p>
                </header>
            </div>
        </div>
        <script>
            window.onload = function () {
                document.body.classList.remove("is-preload")
            }
            window.ontouchmove = function () {
                return false
            }
            window.onorientationchange = function () {
                document.body.scrollTop = 0
            }
        </script>
        <script src="https://unpkg.com/magic-snowflakes/dist/snowflakes.min.js"></script>
        <script>
            var sf = new Snowflakes();
        </script>
    </body>
</html>


================================================
FILE: public/css/main.css
================================================
@import url("https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,900");
@import url("fontawesome-all.min.css");

/*
	Aerial by HTML5 UP
	html5up.net | @ajlkn
	Free for personal and commercial use under the CCA 3.0 license (html5up.net/license)
*/

html, body, div, span, applet, object,
iframe, h1, h2, h3, h4, h5, h6, p, blockquote,
pre, a, abbr, acronym, address, big, cite,
code, del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var, b,
u, i, center, dl, dt, dd, ol, ul, li, fieldset,
form, label, legend, table, caption, tbody,
tfoot, thead, tr, th, td, article, aside,
canvas, details, embed, figure, figcaption,
footer, header, hgroup, menu, nav, output, ruby,
section, summary, time, mark, audio, video {
	margin: 0;
	padding: 0;
	border: 0;
	font-size: 100%;
	font: inherit;
	vertical-align: baseline;}

article, aside, details, figcaption, figure,
footer, header, hgroup, menu, nav, section {
	display: block;}

body {
	line-height: 1;
}

ol, ul {
	list-style: none;
}

blockquote, q {
	quotes: none;
}

	blockquote:before, blockquote:after, q:before, q:after {
		content: '';
		content: none;
	}

table {
	border-collapse: collapse;
	border-spacing: 0;
}

body {
	-webkit-text-size-adjust: none;
}

mark {
	background-color: transparent;
	color: inherit;
}

input::-moz-focus-inner {
	border: 0;
	padding: 0;
}

input, select, textarea {
	-moz-appearance: none;
	-webkit-appearance: none;
	-ms-appearance: none;
	appearance: none;
}

/* Basic */

	html {
		box-sizing: border-box;
	}

	*, *:before, *:after {
		box-sizing: inherit;
	}

	body {
		background: #fff;
		overflow: hidden;
	}

		body.is-preload *, body.is-preload *:before, body.is-preload *:after {
			-moz-animation: none !important;
			-webkit-animation: none !important;
			-ms-animation: none !important;
			animation: none !important;
			-moz-transition: none !important;
			-webkit-transition: none !important;
			-ms-transition: none !important;
			transition: none !important;
		}

	body, input, select, textarea {
		color: #fff;
		font-family: 'Source Sans Pro', sans-serif;
		font-size: 15pt;
		font-weight: 300 !important;
		letter-spacing: -0.025em;
		line-height: 1.75em;
	}

	a {
		-moz-transition: border-color 0.2s ease-in-out;
		-webkit-transition: border-color 0.2s ease-in-out;
		-ms-transition: border-color 0.2s ease-in-out;
		transition: border-color 0.2s ease-in-out;
		border-bottom: dotted 1px;
		color: inherit;
		outline: 0;
		text-decoration: none;
	}

		a:hover {
			border-color: transparent;
		}

/* Icon */

	.icon {
		text-decoration: none;
		position: relative;
	}

		.icon:before {
			-moz-osx-font-smoothing: grayscale;
			-webkit-font-smoothing: antialiased;
			display: inline-block;
			font-style: normal;
			font-variant: normal;
			text-rendering: auto;
			line-height: 1;
			text-transform: none !important;
			font-family: 'Font Awesome 5 Free';
			font-weight: 400;
		}

		.icon > .label {
			display: none;
		}

		.icon.solid:before {
			font-weight: 900;
		}

		.icon.brands:before {
			font-family: 'Font Awesome 5 Brands';
		}

/* Wrapper */

	@-moz-keyframes wrapper {
		0% {
			opacity: 0;
		}

		100% {
			opacity: 1;
		}
	}

	@-webkit-keyframes wrapper {
		0% {
			opacity: 0;
		}

		100% {
			opacity: 1;
		}
	}

	@-ms-keyframes wrapper {
		0% {
			opacity: 0;
		}

		100% {
			opacity: 1;
		}
	}

	@keyframes wrapper {
		0% {
			opacity: 0;
		}

		100% {
			opacity: 1;
		}
	}

	#wrapper {
		-moz-animation: wrapper 3s forwards;
		-webkit-animation: wrapper 3s forwards;
		-ms-animation: wrapper 3s forwards;
		animation: wrapper 3s forwards;
		height: 100%;
		left: 0;
		opacity: 0;
		position: fixed;
		top: 0;
		width: 100%;
	}

/* BG */

	#bg {
		-moz-animation: bg 60s linear infinite;
		-webkit-animation: bg 60s linear infinite;
		-ms-animation: bg 60s linear infinite;
		animation: bg 60s linear infinite;
		-moz-backface-visibility: hidden;
		-webkit-backface-visibility: hidden;
		-ms-backface-visibility: hidden;
		backface-visibility: hidden;
		-moz-transform: translate3d(0,0,0);
		-webkit-transform: translate3d(0,0,0);
		-ms-transform: translate3d(0,0,0);
		transform: translate3d(0,0,0);
	/* Set your background with this */

		background: #348cb2 url("https://gins-1255964181.cos.ap-beijing.myqcloud.com/bg.jpg") bottom left;
		background-repeat: repeat-x;
		height: 100%;
		left: 0;
		opacity: 1;
		position: fixed;
		top: 0;
	}

	@-moz-keyframes bg {
		0% {
			-moz-transform: translate3d(0,0,0);
			-webkit-transform: translate3d(0,0,0);
			-ms-transform: translate3d(0,0,0);
			transform: translate3d(0,0,0);
		}

		100% {
			-moz-transform: translate3d(-2250px,0,0);
			-webkit-transform: translate3d(-2250px,0,0);
			-ms-transform: translate3d(-2250px,0,0);
			transform: translate3d(-2250px,0,0);
		}
	}

	@-webkit-keyframes bg {
		0% {
			-moz-transform: translate3d(0,0,0);
			-webkit-transform: translate3d(0,0,0);
			-ms-transform: translate3d(0,0,0);
			transform: translate3d(0,0,0);
		}

		100% {
			-moz-transform: translate3d(-2250px,0,0);
			-webkit-transform: translate3d(-2250px,0,0);
			-ms-transform: translate3d(-2250px,0,0);
			transform: translate3d(-2250px,0,0);
		}
	}

	@-ms-keyframes bg {
		0% {
			-moz-transform: translate3d(0,0,0);
			-webkit-transform: translate3d(0,0,0);
			-ms-transform: translate3d(0,0,0);
			transform: translate3d(0,0,0);
		}

		100% {
			-moz-transform: translate3d(-2250px,0,0);
			-webkit-transform: translate3d(-2250px,0,0);
			-ms-transform: translate3d(-2250px,0,0);
			transform: translate3d(-2250px,0,0);
		}
	}

	@keyframes bg {
		0% {
			-moz-transform: translate3d(0,0,0);
			-webkit-transform: translate3d(0,0,0);
			-ms-transform: translate3d(0,0,0);
			transform: translate3d(0,0,0);
		}

		100% {
			-moz-transform: translate3d(-2250px,0,0);
			-webkit-transform: translate3d(-2250px,0,0);
			-ms-transform: translate3d(-2250px,0,0);
			transform: translate3d(-2250px,0,0);
		}
	}

	#bg {
		background-size: auto 100%;
		width: 6750px;
	}

/* Overlay */

	@-moz-keyframes overlay {
		0% {
			opacity: 0;
		}

		100% {
			opacity: 1;
		}
	}

	@-webkit-keyframes overlay {
		0% {
			opacity: 0;
		}

		100% {
			opacity: 1;
		}
	}

	@-ms-keyframes overlay {
		0% {
			opacity: 0;
		}

		100% {
			opacity: 1;
		}
	}

	@keyframes overlay {
		0% {
			opacity: 0;
		}

		100% {
			opacity: 1;
		}
	}

	#overlay {
		-moz-animation: overlay 1.5s 1.5s forwards;
		-webkit-animation: overlay 1.5s 1.5s forwards;
		-ms-animation: overlay 1.5s 1.5s forwards;
		animation: overlay 1.5s 1.5s forwards;
		background-attachment: fixed, fixed;
		background-image: url("images/overlay-pattern.png"), url("images/overlay.svg");
		background-position: top left, center center;
		background-repeat: repeat, no-repeat;
		background-size: auto, cover;
		height: 100%;
		left: 0;
		opacity: 0;
		position: fixed;
		top: 0;
		width: 100%;
	}

/* Main */

	#main {
		height: 100%;
		left: 0;
		position: fixed;
		text-align: center;
		top: 0;
		width: 100%;
	}

		#main:before {
			content: '';
			display: inline-block;
			height: 100%;
			margin-right: 0;
			vertical-align: middle;
			width: 1px;
		}

/* Header */

	@-moz-keyframes header {
		0% {
			-moz-transform: translate3d(0,1em,0);
			-webkit-transform: translate3d(0,1em,0);
			-ms-transform: translate3d(0,1em,0);
			transform: translate3d(0,1em,0);
			opacity: 0;
		}

		100% {
			-moz-transform: translate3d(0,0,0);
			-webkit-transform: translate3d(0,0,0);
			-ms-transform: translate3d(0,0,0);
			transform: translate3d(0,0,0);
			opacity: 1;
		}
	}

	@-webkit-keyframes header {
		0% {
			-moz-transform: translate3d(0,1em,0);
			-webkit-transform: translate3d(0,1em,0);
			-ms-transform: translate3d(0,1em,0);
			transform: translate3d(0,1em,0);
			opacity: 0;
		}

		100% {
			-moz-transform: translate3d(0,0,0);
			-webkit-transform: translate3d(0,0,0);
			-ms-transform: translate3d(0,0,0);
			transform: translate3d(0,0,0);
			opacity: 1;
		}
	}

	@-ms-keyframes header {
		0% {
			-moz-transform: translate3d(0,1em,0);
			-webkit-transform: translate3d(0,1em,0);
			-ms-transform: translate3d(0,1em,0);
			transform: translate3d(0,1em,0);
			opacity: 0;
		}

		100% {
			-moz-transform: translate3d(0,0,0);
			-webkit-transform: translate3d(0,0,0);
			-ms-transform: translate3d(0,0,0);
			transform: translate3d(0,0,0);
			opacity: 1;
		}
	}

	@keyframes header {
		0% {
			-moz-transform: translate3d(0,1em,0);
			-webkit-transform: translate3d(0,1em,0);
			-ms-transform: translate3d(0,1em,0);
			transform: translate3d(0,1em,0);
			opacity: 0;
		}

		100% {
			-moz-transform: translate3d(0,0,0);
			-webkit-transform: translate3d(0,0,0);
			-ms-transform: translate3d(0,0,0);
			transform: translate3d(0,0,0);
			opacity: 1;
		}
	}

	@-moz-keyframes nav-icons {
		0% {
			-moz-transform: translate3d(0,1em,0);
			-webkit-transform: translate3d(0,1em,0);
			-ms-transform: translate3d(0,1em,0);
			transform: translate3d(0,1em,0);
			opacity: 0;
		}

		100% {
			-moz-transform: translate3d(0,0,0);
			-webkit-transform: translate3d(0,0,0);
			-ms-transform: translate3d(0,0,0);
			transform: translate3d(0,0,0);
			opacity: 1;
		}
	}

	@-webkit-keyframes nav-icons {
		0% {
			-moz-transform: translate3d(0,1em,0);
			-webkit-transform: translate3d(0,1em,0);
			-ms-transform: translate3d(0,1em,0);
			transform: translate3d(0,1em,0);
			opacity: 0;
		}

		100% {
			-moz-transform: translate3d(0,0,0);
			-webkit-transform: translate3d(0,0,0);
			-ms-transform: translate3d(0,0,0);
			transform: translate3d(0,0,0);
			opacity: 1;
		}
	}

	@-ms-keyframes nav-icons {
		0% {
			-moz-transform: translate3d(0,1em,0);
			-webkit-transform: translate3d(0,1em,0);
			-ms-transform: translate3d(0,1em,0);
			transform: translate3d(0,1em,0);
			opacity: 0;
		}

		100% {
			-moz-transform: translate3d(0,0,0);
			-webkit-transform: translate3d(0,0,0);
			-ms-transform: translate3d(0,0,0);
			transform: translate3d(0,0,0);
			opacity: 1;
		}
	}

	@keyframes nav-icons {
		0% {
			-moz-transform: translate3d(0,1em,0);
			-webkit-transform: translate3d(0,1em,0);
			-ms-transform: translate3d(0,1em,0);
			transform: translate3d(0,1em,0);
			opacity: 0;
		}

		100% {
			-moz-transform: translate3d(0,0,0);
			-webkit-transform: translate3d(0,0,0);
			-ms-transform: translate3d(0,0,0);
			transform: translate3d(0,0,0);
			opacity: 1;
		}
	}

	#header {
		-moz-animation: header 1s 2.25s forwards;
		-webkit-animation: header 1s 2.25s forwards;
		-ms-animation: header 1s 2.25s forwards;
		animation: header 1s 2.25s forwards;
		-moz-backface-visibility: hidden;
		-webkit-backface-visibility: hidden;
		-ms-backface-visibility: hidden;
		backface-visibility: hidden;
		-moz-transform: translate3d(0,0,0);
		-webkit-transform: translate3d(0,0,0);
		-ms-transform: translate3d(0,0,0);
		transform: translate3d(0,0,0);
		cursor: default;
		display: inline-block;
		opacity: 0;
		position: relative;
		text-align: center;
		top: -1em;
		vertical-align: middle;
		width: 90%;
	}

		#header h1 {
			font-size: 4.35em;
			font-weight: 900;
			letter-spacing: -0.035em;
			line-height: 1em;
		}

		#header p {
			font-size: 1.25em;
			margin: 0.75em 0 0.25em 0;
			opacity: 0.75;
		}

		#header nav {
			margin: 1.5em 0 0 0;
		}

			#header nav li {
				-moz-animation: nav-icons 0.5s ease-in-out forwards;
				-webkit-animation: nav-icons 0.5s ease-in-out forwards;
				-ms-animation: nav-icons 0.5s ease-in-out forwards;
				animation: nav-icons 0.5s ease-in-out forwards;
				-moz-backface-visibility: hidden;
				-webkit-backface-visibility: hidden;
				-ms-backface-visibility: hidden;
				backface-visibility: hidden;
				-moz-transform: translate3d(0,0,0);
				-webkit-transform: translate3d(0,0,0);
				-ms-transform: translate3d(0,0,0);
				transform: translate3d(0,0,0);
				display: inline-block;
				height: 5.35em;
				line-height: 5.885em;
				opacity: 0;
				position: relative;
				top: 0;
				width: 5.35em;
			}

				#header nav li:nth-child(1) {
					-moz-animation-delay: 2.5s;
					-webkit-animation-delay: 2.5s;
					-ms-animation-delay: 2.5s;
					animation-delay: 2.5s;
				}

				#header nav li:nth-child(2) {
					-moz-animation-delay: 2.75s;
					-webkit-animation-delay: 2.75s;
					-ms-animation-delay: 2.75s;
					animation-delay: 2.75s;
				}

				#header nav li:nth-child(3) {
					-moz-animation-delay: 3s;
					-webkit-animation-delay: 3s;
					-ms-animation-delay: 3s;
					animation-delay: 3s;
				}

				#header nav li:nth-child(4) {
					-moz-animation-delay: 3.25s;
					-webkit-animation-delay: 3.25s;
					-ms-animation-delay: 3.25s;
					animation-delay: 3.25s;
				}

				#header nav li:nth-child(5) {
					-moz-animation-delay: 3.5s;
					-webkit-animation-delay: 3.5s;
					-ms-animation-delay: 3.5s;
					animation-delay: 3.5s;
				}

				#header nav li:nth-child(6) {
					-moz-animation-delay: 3.75s;
					-webkit-animation-delay: 3.75s;
					-ms-animation-delay: 3.75s;
					animation-delay: 3.75s;
				}

				#header nav li:nth-child(7) {
					-moz-animation-delay: 4s;
					-webkit-animation-delay: 4s;
					-ms-animation-delay: 4s;
					animation-delay: 4s;
				}

				#header nav li:nth-child(8) {
					-moz-animation-delay: 4.25s;
					-webkit-animation-delay: 4.25s;
					-ms-animation-delay: 4.25s;
					animation-delay: 4.25s;
				}

				#header nav li:nth-child(9) {
					-moz-animation-delay: 4.5s;
					-webkit-animation-delay: 4.5s;
					-ms-animation-delay: 4.5s;
					animation-delay: 4.5s;
				}

				#header nav li:nth-child(10) {
					-moz-animation-delay: 4.75s;
					-webkit-animation-delay: 4.75s;
					-ms-animation-delay: 4.75s;
					animation-delay: 4.75s;
				}

			#header nav a {
				-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
				-webkit-touch-callout: none;
				border: 0;
				display: inline-block;
			}

				#header nav a:before {
					-moz-transition: all 0.2s ease-in-out;
					-webkit-transition: all 0.2s ease-in-out;
					-ms-transition: all 0.2s ease-in-out;
					transition: all 0.2s ease-in-out;
					border-radius: 100%;
					border: solid 1px #fff;
					display: block;
					font-size: 1.75em;
					height: 2.5em;
					line-height: 2.5em;
					position: relative;
					text-align: center;
					top: 0;
					width: 2.5em;
				}

				#header nav a:hover {
					font-size: 1.1em;
				}

					#header nav a:hover:before {
						background-color: rgba(255, 255, 255, 0.175);
						color: #fff;
					}

				#header nav a:active {
					font-size: 0.95em;
					background: none;
				}

					#header nav a:active:before {
						background-color: rgba(255, 255, 255, 0.35);
						color: #fff;
					}

				#header nav a span {
					display: none;
				}

/* Footer */

	#footer {
		background-image: -moz-linear-gradient(top, rgba(0,0,0,0), rgba(0,0,0,0.5) 75%);
		background-image: -webkit-linear-gradient(top, rgba(0,0,0,0), rgba(0,0,0,0.5) 75%);
		background-image: -ms-linear-gradient(top, rgba(0,0,0,0), rgba(0,0,0,0.5) 75%);
		background-image: linear-gradient(top, rgba(0,0,0,0), rgba(0,0,0,0.5) 75%);
		bottom: 0;
		cursor: default;
		height: 6em;
		left: 0;
		line-height: 8em;
		position: absolute;
		text-align: center;
		width: 100%;
	}

/* Wide */

	@media screen and (max-width: 1680px) {

		/* Basic */

			body, input, select, textarea {
				font-size: 13pt;
			}

		/* BG */

			@-moz-keyframes bg {
				0% {
					-moz-transform: translate3d(0,0,0);
					-webkit-transform: translate3d(0,0,0);
					-ms-transform: translate3d(0,0,0);
					transform: translate3d(0,0,0);
				}

				100% {
					-moz-transform: translate3d(-1500px,0,0);
					-webkit-transform: translate3d(-1500px,0,0);
					-ms-transform: translate3d(-1500px,0,0);
					transform: translate3d(-1500px,0,0);
				}

	}

		@-webkit-keyframes bg {
			0% {
				-moz-transform: translate3d(0,0,0);
				-webkit-transform: translate3d(0,0,0);
				-ms-transform: translate3d(0,0,0);
				transform: translate3d(0,0,0);
			}

			100% {
				-moz-transform: translate3d(-1500px,0,0);
				-webkit-transform: translate3d(-1500px,0,0);
				-ms-transform: translate3d(-1500px,0,0);
				transform: translate3d(-1500px,0,0);
			}
			}

		@-ms-keyframes bg {
			0% {
				-moz-transform: translate3d(0,0,0);
				-webkit-transform: translate3d(0,0,0);
				-ms-transform: translate3d(0,0,0);
				transform: translate3d(0,0,0);
			}

			100% {
				-moz-transform: translate3d(-1500px,0,0);
				-webkit-transform: translate3d(-1500px,0,0);
				-ms-transform: translate3d(-1500px,0,0);
				transform: translate3d(-1500px,0,0);
			}
		}

		@keyframes bg {
			0% {
				-moz-transform: translate3d(0,0,0);
				-webkit-transform: translate3d(0,0,0);
				-ms-transform: translate3d(0,0,0);
				transform: translate3d(0,0,0);
			}

			100% {
				-moz-transform: translate3d(-1500px,0,0);
				-webkit-transform: translate3d(-1500px,0,0);
				-ms-transform: translate3d(-1500px,0,0);
				transform: translate3d(-1500px,0,0);
			}
		}

		#bg {
			background-size: 1500px auto;
			width: 4500px;
		} }

/* Normal */

	@media screen and (max-width: 1280px) {

		/* Basic */

			body, input, select, textarea {
				font-size: 12pt;
			}

		/* BG */

			@-moz-keyframes bg {
				0% {
					-moz-transform: translate3d(0,0,0);
					-webkit-transform: translate3d(0,0,0);
					-ms-transform: translate3d(0,0,0);
					transform: translate3d(0,0,0);
				}

				100% {
					-moz-transform: translate3d(-750px,0,0);
					-webkit-transform: translate3d(-750px,0,0);
					-ms-transform: translate3d(-750px,0,0);
					transform: translate3d(-750px,0,0);
				}

	}

		@-webkit-keyframes bg {
			0% {
				-moz-transform: translate3d(0,0,0);
				-webkit-transform: translate3d(0,0,0);
				-ms-transform: translate3d(0,0,0);
				transform: translate3d(0,0,0);
			}

			100% {
				-moz-transform: translate3d(-750px,0,0);
				-webkit-transform: translate3d(-750px,0,0);
				-ms-transform: translate3d(-750px,0,0);
				transform: translate3d(-750px,0,0);
			}
			}

		@-ms-keyframes bg {
			0% {
				-moz-transform: translate3d(0,0,0);
				-webkit-transform: translate3d(0,0,0);
				-ms-transform: translate3d(0,0,0);
				transform: translate3d(0,0,0);
			}

			100% {
				-moz-transform: translate3d(-750px,0,0);
				-webkit-transform: translate3d(-750px,0,0);
				-ms-transform: translate3d(-750px,0,0);
				transform: translate3d(-750px,0,0);
			}
		}

		@keyframes bg {
			0% {
				-moz-transform: translate3d(0,0,0);
				-webkit-transform: translate3d(0,0,0);
				-ms-transform: translate3d(0,0,0);
				transform: translate3d(0,0,0);
			}

			100% {
				-moz-transform: translate3d(-750px,0,0);
				-webkit-transform: translate3d(-750px,0,0);
				-ms-transform: translate3d(-750px,0,0);
				transform: translate3d(-750px,0,0);
			}
		}

		#bg {
			background-size: 750px auto;
			width: 2250px;
		} }

/* Mobile */

	@media screen and (max-width: 736px) {

		/* Basic */

			body {
				min-width: 320px;
			}

			body, input, select, textarea {
				font-size: 11pt;
			}

		/* BG */

			@-moz-keyframes bg {
				0% {
					-moz-transform: translate3d(0,0,0);
					-webkit-transform: translate3d(0,0,0);
					-ms-transform: translate3d(0,0,0);
					transform: translate3d(0,0,0);
				}

				100% {
					-moz-transform: translate3d(-300px,0,0);
					-webkit-transform: translate3d(-300px,0,0);
					-ms-transform: translate3d(-300px,0,0);
					transform: translate3d(-300px,0,0);
				}

	}

		@-webkit-keyframes bg {
			0% {
				-moz-transform: translate3d(0,0,0);
				-webkit-transform: translate3d(0,0,0);
				-ms-transform: translate3d(0,0,0);
				transform: translate3d(0,0,0);
			}

			100% {
				-moz-transform: translate3d(-300px,0,0);
				-webkit-transform: translate3d(-300px,0,0);
				-ms-transform: translate3d(-300px,0,0);
				transform: translate3d(-300px,0,0);
			}
			}

		@-ms-keyframes bg {
			0% {
				-moz-transform: translate3d(0,0,0);
				-webkit-transform: translate3d(0,0,0);
				-ms-transform: translate3d(0,0,0);
				transform: translate3d(0,0,0);
			}

			100% {
				-moz-transform: translate3d(-300px,0,0);
				-webkit-transform: translate3d(-300px,0,0);
				-ms-transform: translate3d(-300px,0,0);
				transform: translate3d(-300px,0,0);
			}
		}

		@keyframes bg {
			0% {
				-moz-transform: translate3d(0,0,0);
				-webkit-transform: translate3d(0,0,0);
				-ms-transform: translate3d(0,0,0);
				transform: translate3d(0,0,0);
			}

			100% {
				-moz-transform: translate3d(-300px,0,0);
				-webkit-transform: translate3d(-300px,0,0);
				-ms-transform: translate3d(-300px,0,0);
				transform: translate3d(-300px,0,0);
			}
		}

		#bg {
			background-size: 300px auto;
			width: 900px;
		}

	/* Header */

		#header h1 {
			font-size: 2.5em;
		}

		#header p {
			font-size: 1em;
		}

		#header nav {
			font-size: 1em;
		}

			#header nav a:hover {
				font-size: 1em;
			}

			#header nav a:active {
				font-size: 1em;
			} }

/* Mobile (Portrait) */

	@media screen and (max-width: 480px) {

		/* BG */

			@-moz-keyframes bg {
				0% {
					-moz-transform: translate3d(0,0,0);
					-webkit-transform: translate3d(0,0,0);
					-ms-transform: translate3d(0,0,0);
					transform: translate3d(0,0,0);
				}

				100% {
					-moz-transform: translate3d(-412.5px,0,0);
					-webkit-transform: translate3d(-412.5px,0,0);
					-ms-transform: translate3d(-412.5px,0,0);
					transform: translate3d(-412.5px,0,0);
				}

	}

		@-webkit-keyframes bg {
			0% {
				-moz-transform: translate3d(0,0,0);
				-webkit-transform: translate3d(0,0,0);
				-ms-transform: translate3d(0,0,0);
				transform: translate3d(0,0,0);
			}

			100% {
				-moz-transform: translate3d(-412.5px,0,0);
				-webkit-transform: translate3d(-412.5px,0,0);
				-ms-transform: translate3d(-412.5px,0,0);
				transform: translate3d(-412.5px,0,0);
			}
			}

		@-ms-keyframes bg {
			0% {
				-moz-transform: translate3d(0,0,0);
				-webkit-transform: translate3d(0,0,0);
				-ms-transform: translate3d(0,0,0);
				transform: translate3d(0,0,0);
			}

			100% {
				-moz-transform: translate3d(-412.5px,0,0);
				-webkit-transform: translate3d(-412.5px,0,0);
				-ms-transform: translate3d(-412.5px,0,0);
				transform: translate3d(-412.5px,0,0);
			}
		}

		@keyframes bg {
			0% {
				-moz-transform: translate3d(0,0,0);
				-webkit-transform: translate3d(0,0,0);
				-ms-transform: translate3d(0,0,0);
				transform: translate3d(0,0,0);
			}

			100% {
				-moz-transform: translate3d(-412.5px,0,0);
				-webkit-transform: translate3d(-412.5px,0,0);
				-ms-transform: translate3d(-412.5px,0,0);
				transform: translate3d(-412.5px,0,0);
			}
		}

		#bg {
			background-size: 412.5px auto;
			width: 1237.5px;
		}

	/* Header */

		#header nav {
			padding: 0 1em;
		} }

================================================
FILE: public/css/noscript.css
================================================
/*
	Aerial by HTML5 UP
	html5up.net | @ajlkn
	Free for personal and commercial use under the CCA 3.0 license (html5up.net/license)
*/

/* Wrapper */

	#wrapper {
		opacity: 1 !important;
	}

/* Overlay */

	#overlay {
		opacity: 1 !important;
	}

/* Header */

	#header {
		opacity: 1 !important;
	}

		#header nav li {
			opacity: 1 !important;
		}

================================================
FILE: public/guess.java
================================================
package top.ginnnnnn.mooc.service.implement;

import java.security.SecureRandom;
import java.util.HashMap;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import top.ginnnnnn.mooc.service.GameService;

@Service
public class GameServiceImpl implements GameService {

    private static final SecureRandom RNG = new SecureRandom();
    private static final String GUESS_GAME_PREFIX = "GinsGame-guess-";
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    public HashMap<String, Object> guess(String token, Double guess) {
        final String key = GUESS_GAME_PREFIX + token;
        init(key);
        HashOperations<String, String, Object> hashOperations = stringRedisTemplate.opsForHash();

        Double number = Double.valueOf((String)hashOperations.get(key, "number"));
        boolean isLess = number > guess + 1e-6 / 2;
        boolean isMore = number < guess - 1e-6 / 2;

        boolean isPassed = !isLess && !isMore;
        boolean isTalented = isPassed && !hashOperations.hasKey(key, "previous");

        HashMap<String, Object> state = new HashMap<>(5);
        state.put("less", isLess);
        state.put("more", isMore);

        hashOperations.putIfAbsent(key, "previous", "0");
        hashOperations.increment(key, "previous", 1);
        if (isPassed) {
            hashOperations.put(key, "number", String.format("%f", RNG.nextInt(1, 1000000) * 1e-6));
            hashOperations.delete(key, "previous");
            hashOperations.increment(key, "passed", 1);
        }
        if (isTalented) {
            hashOperations.increment(key, "talented", 1);
            state.put("reward", Boolean.TRUE);
        } else {
            state.put("reward", Boolean.FALSE);
        }

        HashMap<String, Object> ret = new HashMap<>(3);
        ret.put("info", infoBuild(key));
        ret.put("state", state);
        return ret;

    }

    public HashMap<String, Object> getState(String token) {
        final String key = GUESS_GAME_PREFIX + token;
        init(key);
        return infoBuild(key);
    }

    public boolean refresh(String token) {
        final String key = GUESS_GAME_PREFIX + token;
        init(key);
        HashOperations<String, String, Object> hashOperations = stringRedisTemplate.opsForHash();

        hashOperations.put(key, "passed", "0");
        hashOperations.put(key, "talented", "0");
        hashOperations.put(key, "number", String.format("%f", RNG.nextInt(1, 1000000) * 1e-6));
        hashOperations.delete(key, "previous");
        return true;
    }

    private void init(String key) {
        HashOperations<String, String, Object> hashOperations = stringRedisTemplate.opsForHash();
        hashOperations.putIfAbsent(key, "passed", "0");
        hashOperations.putIfAbsent(key, "talented", "0");
        hashOperations.putIfAbsent(key, "number", String.format("%f", RNG.nextInt(1, 1000000) * 1e-6));
    }

    private HashMap<String, Object> infoBuild(String key) {
        HashOperations<String, String, Object> hashOperations = stringRedisTemplate.opsForHash();
        HashMap<String, Object> info = new HashMap<>(3);
        info.put("passed", hashOperations.get(key, "passed"));
        info.put("talented", hashOperations.get(key, "talented"));
        return info;
    }

}


================================================
FILE: public/sass/libs/_breakpoints.scss
================================================
// breakpoints.scss v1.0 | @ajlkn | MIT licensed */

// Vars.

	/// Breakpoints.
	/// @var {list}
	$breakpoints: () !global;

// Mixins.

	/// Sets breakpoints.
	/// @param {map} $x Breakpoints.
	@mixin breakpoints($x: ()) {
		$breakpoints: $x !global;
	}

	/// Wraps @content in a @media block targeting a specific orientation.
	/// @param {string} $orientation Orientation.
	@mixin orientation($orientation) {
		@media screen and (orientation: #{$orientation}) {
			@content;
		}
	}

	/// Wraps @content in a @media block using a given query.
	/// @param {string} $query Query.
	@mixin breakpoint($query: null) {

		$breakpoint: null;
		$op: null;
		$media: null;

		// Determine operator, breakpoint.

			// Greater than or equal.
				@if (str-slice($query, 0, 2) == '>=') {

					$op: 'gte';
					$breakpoint: str-slice($query, 3);

				}

			// Less than or equal.
				@elseif (str-slice($query, 0, 2) == '<=') {

					$op: 'lte';
					$breakpoint: str-slice($query, 3);

				}

			// Greater than.
				@elseif (str-slice($query, 0, 1) == '>') {

					$op: 'gt';
					$breakpoint: str-slice($query, 2);

				}

			// Less than.
				@elseif (str-slice($query, 0, 1) == '<') {

					$op: 'lt';
					$breakpoint: str-slice($query, 2);

				}

			// Not.
				@elseif (str-slice($query, 0, 1) == '!') {

					$op: 'not';
					$breakpoint: str-slice($query, 2);

				}

			// Equal.
				@else {

					$op: 'eq';
					$breakpoint: $query;

				}

		// Build media.
			@if ($breakpoint and map-has-key($breakpoints, $breakpoint)) {

				$a: map-get($breakpoints, $breakpoint);

				// Range.
					@if (type-of($a) == 'list') {

						$x: nth($a, 1);
						$y: nth($a, 2);

						// Max only.
							@if ($x == null) {

								// Greater than or equal (>= 0 / anything)
									@if ($op == 'gte') {
										$media: 'screen';
									}

								// Less than or equal (<= y)
									@elseif ($op == 'lte') {
										$media: 'screen and (max-width: ' + $y + ')';
									}

								// Greater than (> y)
									@elseif ($op == 'gt') {
										$media: 'screen and (min-width: ' + ($y + 1) + ')';
									}

								// Less than (< 0 / invalid)
									@elseif ($op == 'lt') {
										$media: 'screen and (max-width: -1px)';
									}

								// Not (> y)
									@elseif ($op == 'not') {
										$media: 'screen and (min-width: ' + ($y + 1) + ')';
									}

								// Equal (<= y)
									@else {
										$media: 'screen and (max-width: ' + $y + ')';
									}

							}

						// Min only.
							@else if ($y == null) {

								// Greater than or equal (>= x)
									@if ($op == 'gte') {
										$media: 'screen and (min-width: ' + $x + ')';
									}

								// Less than or equal (<= inf / anything)
									@elseif ($op == 'lte') {
										$media: 'screen';
									}

								// Greater than (> inf / invalid)
									@elseif ($op == 'gt') {
										$media: 'screen and (max-width: -1px)';
									}

								// Less than (< x)
									@elseif ($op == 'lt') {
										$media: 'screen and (max-width: ' + ($x - 1) + ')';
									}

								// Not (< x)
									@elseif ($op == 'not') {
										$media: 'screen and (max-width: ' + ($x - 1) + ')';
									}

								// Equal (>= x)
									@else {
										$media: 'screen and (min-width: ' + $x + ')';
									}

							}

						// Min and max.
							@else {

								// Greater than or equal (>= x)
									@if ($op == 'gte') {
										$media: 'screen and (min-width: ' + $x + ')';
									}

								// Less than or equal (<= y)
									@elseif ($op == 'lte') {
										$media: 'screen and (max-width: ' + $y + ')';
									}

								// Greater than (> y)
									@elseif ($op == 'gt') {
										$media: 'screen and (min-width: ' + ($y + 1) + ')';
									}

								// Less than (< x)
									@elseif ($op == 'lt') {
										$media: 'screen and (max-width: ' + ($x - 1) + ')';
									}

								// Not (< x and > y)
									@elseif ($op == 'not') {
										$media: 'screen and (max-width: ' + ($x - 1) + '), screen and (min-width: ' + ($y + 1) + ')';
									}

								// Equal (>= x and <= y)
									@else {
										$media: 'screen and (min-width: ' + $x + ') and (max-width: ' + $y + ')';
									}

							}

					}

				// String.
					@else {

						// Missing a media type? Prefix with "screen".
							@if (str-slice($a, 0, 1) == '(') {
								$media: 'screen and ' + $a;
							}

						// Otherwise, use as-is.
							@else {
								$media: $a;
							}

					}

			}

		// Output.
	        @media #{$media} {
				@content;
			}

	}

================================================
FILE: public/sass/libs/_functions.scss
================================================
/// Removes a specific item from a list.
/// @author Hugo Giraudel
/// @param {list} $list List.
/// @param {integer} $index Index.
/// @return {list} Updated list.
@function remove-nth($list, $index) {

	$result: null;

	@if type-of($index) != number {
		@warn "$index: #{quote($index)} is not a number for `remove-nth`.";
	}
	@else if $index == 0 {
		@warn "List index 0 must be a non-zero integer for `remove-nth`.";
	}
	@else if abs($index) > length($list) {
		@warn "List index is #{$index} but list is only #{length($list)} item long for `remove-nth`.";
	}
	@else {

		$result: ();
		$index: if($index < 0, length($list) + $index + 1, $index);

		@for $i from 1 through length($list) {

			@if $i != $index {
				$result: append($result, nth($list, $i));
			}

		}

	}

	@return $result;

}

/// Gets a value from a map.
/// @author Hugo Giraudel
/// @param {map} $map Map.
/// @param {string} $keys Key(s).
/// @return {string} Value.
@function val($map, $keys...) {

	@if nth($keys, 1) == null {
		$keys: remove-nth($keys, 1);
	}

	@each $key in $keys {
		$map: map-get($map, $key);
	}

	@return $map;

}

/// Gets a duration value.
/// @param {string} $keys Key(s).
/// @return {string} Value.
@function _duration($keys...) {
	@return val($duration, $keys...);
}

/// Gets a font value.
/// @param {string} $keys Key(s).
/// @return {string} Value.
@function _font($keys...) {
	@return val($font, $keys...);
}

/// Gets a misc value.
/// @param {string} $keys Key(s).
/// @return {string} Value.
@function _misc($keys...) {
	@return val($misc, $keys...);
}

/// Gets a palette value.
/// @param {string} $keys Key(s).
/// @return {string} Value.
@function _palette($keys...) {
	@return val($palette, $keys...);
}

/// Gets a size value.
/// @param {string} $keys Key(s).
/// @return {string} Value.
@function _size($keys...) {
	@return val($size, $keys...);
}

================================================
FILE: public/sass/libs/_mixins.scss
================================================
/// Makes an element's :before pseudoelement a FontAwesome icon.
/// @param {string} $content Optional content value to use.
/// @param {string} $category Optional category to use.
/// @param {string} $where Optional pseudoelement to target (before or after).
@mixin icon($content: false, $category: regular, $where: before) {

	text-decoration: none;

	&:#{$where} {

		@if $content {
			content: $content;
		}

		-moz-osx-font-smoothing: grayscale;
		-webkit-font-smoothing: antialiased;
		display: inline-block;
		font-style: normal;
		font-variant: normal;
		text-rendering: auto;
		line-height: 1;
		text-transform: none !important;

		@if ($category == brands) {
			font-family: 'Font Awesome 5 Brands';
		}
		@elseif ($category == solid) {
			font-family: 'Font Awesome 5 Free';
			font-weight: 900;
		}
		@else {
			font-family: 'Font Awesome 5 Free';
			font-weight: 400;
		}

	}

}

/// Applies padding to an element, taking the current element-margin value into account.
/// @param {mixed} $tb Top/bottom padding.
/// @param {mixed} $lr Left/right padding.
/// @param {list} $pad Optional extra padding (in the following order top, right, bottom, left)
/// @param {bool} $important If true, adds !important.
@mixin padding($tb, $lr, $pad: (0,0,0,0), $important: null) {

	@if $important {
		$important: '!important';
	}

	$x: 0.1em;

	@if unit(_size(element-margin)) == 'rem' {
		$x: 0.1rem;
	}

	padding: ($tb + nth($pad,1)) ($lr + nth($pad,2)) max($x, $tb - _size(element-margin) + nth($pad,3)) ($lr + nth($pad,4)) #{$important};

}

/// Encodes a SVG data URL so IE doesn't choke (via codepen.io/jakob-e/pen/YXXBrp).
/// @param {string} $svg SVG data URL.
/// @return {string} Encoded SVG data URL.
@function svg-url($svg) {

	$svg: str-replace($svg, '"', '\'');
	$svg: str-replace($svg, '%', '%25');
	$svg: str-replace($svg, '<', '%3C');
	$svg: str-replace($svg, '>', '%3E');
	$svg: str-replace($svg, '&', '%26');
	$svg: str-replace($svg, '#', '%23');
	$svg: str-replace($svg, '{', '%7B');
	$svg: str-replace($svg, '}', '%7D');
	$svg: str-replace($svg, ';', '%3B');

	@return url("data:image/svg+xml;charset=utf8,#{$svg}");

}

================================================
FILE: public/sass/libs/_vars.scss
================================================
// Misc.
	$misc: (
		bg:					#348cb2 url("images/bg.jpg") bottom left,
		bg-width:			1500px
	);

// Duration.
	$duration: (
		bg:					60s,
		wrapper:			3s,
		overlay:			1.5s,
		header:				1s,
		nav-icons:			0.5s
	);

// Size.
	$size: (
		nav-icon-wrapper:	5.35em,
		nav-icon:			1.75em
	);

// Font.
	$font: (
	);

// Palette.
	$palette: (
		bg:					#fff,
		fg:					#fff,

		nav-icon: (
			hover-bg:		rgba(255,255,255,0.175),
			hover-fg:		#fff,
			active-bg:		rgba(255,255,255,0.35),
			active-fg:		#fff
		)
	);

================================================
FILE: public/sass/libs/_vendor.scss
================================================
// vendor.scss v1.0 | @ajlkn | MIT licensed */

// Vars.

	/// Vendor prefixes.
	/// @var {list}
	$vendor-prefixes: (
		'-moz-',
		'-webkit-',
		'-ms-',
		''
	);

	/// Properties that should be vendorized.
	/// Data via caniuse.com, github.com/postcss/autoprefixer, and developer.mozilla.org
	/// @var {list}
	$vendor-properties: (

		// Animation.
			'animation',
			'animation-delay',
			'animation-direction',
			'animation-duration',
			'animation-fill-mode',
			'animation-iteration-count',
			'animation-name',
			'animation-play-state',
			'animation-timing-function',

		// Appearance.
			'appearance',

		// Backdrop filter.
			'backdrop-filter',

		// Background image options.
			'background-clip',
			'background-origin',
			'background-size',

		// Box sizing.
			'box-sizing',

		// Clip path.
			'clip-path',

		// Filter effects.
			'filter',

		// Flexbox.
			'align-content',
			'align-items',
			'align-self',
			'flex',
			'flex-basis',
			'flex-direction',
			'flex-flow',
			'flex-grow',
			'flex-shrink',
			'flex-wrap',
			'justify-content',
			'order',

		// Font feature.
			'font-feature-settings',
			'font-language-override',
			'font-variant-ligatures',

		// Font kerning.
			'font-kerning',

		// Fragmented borders and backgrounds.
			'box-decoration-break',

		// Grid layout.
			'grid-column',
			'grid-column-align',
			'grid-column-end',
			'grid-column-start',
			'grid-row',
			'grid-row-align',
			'grid-row-end',
			'grid-row-start',
			'grid-template-columns',
			'grid-template-rows',

		// Hyphens.
			'hyphens',
			'word-break',

		// Masks.
			'mask',
			'mask-border',
			'mask-border-outset',
			'mask-border-repeat',
			'mask-border-slice',
			'mask-border-source',
			'mask-border-width',
			'mask-clip',
			'mask-composite',
			'mask-image',
			'mask-origin',
			'mask-position',
			'mask-repeat',
			'mask-size',

		// Multicolumn.
			'break-after',
			'break-before',
			'break-inside',
			'column-count',
			'column-fill',
			'column-gap',
			'column-rule',
			'column-rule-color',
			'column-rule-style',
			'column-rule-width',
			'column-span',
			'column-width',
			'columns',

		// Object fit.
			'object-fit',
			'object-position',

		// Regions.
			'flow-from',
			'flow-into',
			'region-fragment',

		// Scroll snap points.
			'scroll-snap-coordinate',
			'scroll-snap-destination',
			'scroll-snap-points-x',
			'scroll-snap-points-y',
			'scroll-snap-type',

		// Shapes.
			'shape-image-threshold',
			'shape-margin',
			'shape-outside',

		// Tab size.
			'tab-size',

		// Text align last.
			'text-align-last',

		// Text decoration.
			'text-decoration-color',
			'text-decoration-line',
			'text-decoration-skip',
			'text-decoration-style',

		// Text emphasis.
			'text-emphasis',
			'text-emphasis-color',
			'text-emphasis-position',
			'text-emphasis-style',

		// Text size adjust.
			'text-size-adjust',

		// Text spacing.
			'text-spacing',

		// Transform.
			'transform',
			'transform-origin',

		// Transform 3D.
			'backface-visibility',
			'perspective',
			'perspective-origin',
			'transform-style',

		// Transition.
			'transition',
			'transition-delay',
			'transition-duration',
			'transition-property',
			'transition-timing-function',

		// Unicode bidi.
			'unicode-bidi',

		// User select.
			'user-select',

		// Writing mode.
			'writing-mode',

	);

	/// Values that should be vendorized.
	/// Data via caniuse.com, github.com/postcss/autoprefixer, and developer.mozilla.org
	/// @var {list}
	$vendor-values: (

		// Cross fade.
			'cross-fade',

		// Element function.
			'element',

		// Filter function.
			'filter',

		// Flexbox.
			'flex',
			'inline-flex',

		// Grab cursors.
			'grab',
			'grabbing',

		// Gradients.
			'linear-gradient',
			'repeating-linear-gradient',
			'radial-gradient',
			'repeating-radial-gradient',

		// Grid layout.
			'grid',
			'inline-grid',

		// Image set.
			'image-set',

		// Intrinsic width.
			'max-content',
			'min-content',
			'fit-content',
			'fill',
			'fill-available',
			'stretch',

		// Sticky position.
			'sticky',

		// Transform.
			'transform',

		// Zoom cursors.
			'zoom-in',
			'zoom-out',

	);

// Functions.

	/// Removes a specific item from a list.
	/// @author Hugo Giraudel
	/// @param {list} $list List.
	/// @param {integer} $index Index.
	/// @return {list} Updated list.
	@function remove-nth($list, $index) {

		$result: null;

		@if type-of($index) != number {
			@warn "$index: #{quote($index)} is not a number for `remove-nth`.";
		}
		@else if $index == 0 {
			@warn "List index 0 must be a non-zero integer for `remove-nth`.";
		}
		@else if abs($index) > length($list) {
			@warn "List index is #{$index} but list is only #{length($list)} item long for `remove-nth`.";
		}
		@else {

			$result: ();
			$index: if($index < 0, length($list) + $index + 1, $index);

			@for $i from 1 through length($list) {

				@if $i != $index {
					$result: append($result, nth($list, $i));
				}

			}

		}

		@return $result;

	}

	/// Replaces a substring within another string.
	/// @author Hugo Giraudel
	/// @param {string} $string String.
	/// @param {string} $search Substring.
	/// @param {string} $replace Replacement.
	/// @return {string} Updated string.
	@function str-replace($string, $search, $replace: '') {

		$index: str-index($string, $search);

		@if $index {
			@return str-slice($string, 1, $index - 1) + $replace + str-replace(str-slice($string, $index + str-length($search)), $search, $replace);
		}

		@return $string;

	}

	/// Replaces a substring within each string in a list.
	/// @param {list} $strings List of strings.
	/// @param {string} $search Substring.
	/// @param {string} $replace Replacement.
	/// @return {list} Updated list of strings.
	@function str-replace-all($strings, $search, $replace: '') {

		@each $string in $strings {
			$strings: set-nth($strings, index($strings, $string), str-replace($string, $search, $replace));
		}

		@return $strings;

	}

// Mixins.

	/// Wraps @content in vendorized keyframe blocks.
	/// @param {string} $name Name.
	@mixin keyframes($name) {

		@-moz-keyframes #{$name} { @content; }
		@-webkit-keyframes #{$name} { @content; }
		@-ms-keyframes #{$name} { @content; }
		@keyframes #{$name} { @content; }

	}

	/// Vendorizes a declaration's property and/or value(s).
	/// @param {string} $property Property.
	/// @param {mixed} $value String/list of value(s).
	@mixin vendor($property, $value) {

		// Determine if property should expand.
			$expandProperty: index($vendor-properties, $property);

		// Determine if value should expand (and if so, add '-prefix-' placeholder).
			$expandValue: false;

			@each $x in $value {
				@each $y in $vendor-values {
					@if $y == str-slice($x, 1, str-length($y)) {

						$value: set-nth($value, index($value, $x), '-prefix-' + $x);
						$expandValue: true;

					}
				}
			}

		// Expand property?
			@if $expandProperty {
			    @each $vendor in $vendor-prefixes {
			        #{$vendor}#{$property}: #{str-replace-all($value, '-prefix-', $vendor)};
			    }
			}

		// Expand just the value?
			@elseif $expandValue {
			    @each $vendor in $vendor-prefixes {
			        #{$property}: #{str-replace-all($value, '-prefix-', $vendor)};
			    }
			}

		// Neither? Treat them as a normal declaration.
			@else {
		        #{$property}: #{$value};
			}

	}

================================================
FILE: public/sass/main.scss
================================================
@import 'libs/vars';
@import 'libs/functions';
@import 'libs/mixins';
@import 'libs/vendor';
@import 'libs/breakpoints';
@import url("https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,900");
@import url("fontawesome-all.min.css");

/*
	Aerial by HTML5 UP
	html5up.net | @ajlkn
	Free for personal and commercial use under the CCA 3.0 license (html5up.net/license)
*/

// Breakpoints.

	@include breakpoints((
		wide:      ( 1281px,  1680px ),
		normal:    ( 737px,   1280px ),
		mobile:    ( 481px,   736px  ),
		mobilep:   ( null,    480px  )
	));

// Mixins.

	@mixin bg($width) {
		@include keyframes('bg') {
			0%		{ @include vendor('transform', 'translate3d(0,0,0)'); }
			100%	{ @include vendor('transform', 'translate3d(#{$width * -1},0,0)'); }
		}

		#bg {
			background-size: $width auto;
			width: ($width * 3);
		}
	}

	$delay-wrapper:			_duration(wrapper) - 1s;
	$delay-overlay:			$delay-wrapper - 0.5s;
	$delay-header:			$delay-overlay + _duration(overlay) - 0.75s;
	$delay-nav-icons:		$delay-header + _duration(header) - 1s;
	$delay-nav-icon:		0.25s;

// Reset.
// Based on meyerweb.com/eric/tools/css/reset (v2.0 | 20110126 | License: public domain)

	html, body, div, span, applet, object,
	iframe, h1, h2, h3, h4, h5, h6, p, blockquote,
	pre, a, abbr, acronym, address, big, cite,
	code, del, dfn, em, img, ins, kbd, q, s, samp,
	small, strike, strong, sub, sup, tt, var, b,
	u, i, center, dl, dt, dd, ol, ul, li, fieldset,
	form, label, legend, table, caption, tbody,
	tfoot, thead, tr, th, td, article, aside,
	canvas, details, embed, figure, figcaption,
	footer, header, hgroup, menu, nav, output, ruby,
	section, summary, time, mark, audio, video {
		margin: 0;
		padding: 0;
		border: 0;
		font-size: 100%;
		font: inherit;
		vertical-align: baseline;
	}

	article, aside, details, figcaption, figure,
	footer, header, hgroup, menu, nav, section {
		display: block;
	}

	body {
		line-height: 1;
	}

	ol, ul {
		list-style:none;
	}

	blockquote,	q {
		quotes: none;

		&:before,
		&:after {
			content: '';
			content: none;
		}
	}

	table {
		border-collapse: collapse;
		border-spacing: 0;
	}

	body {
		-webkit-text-size-adjust: none;
	}

	mark {
		background-color: transparent;
		color: inherit;
	}

	input::-moz-focus-inner {
		border: 0;
		padding: 0;
	}

	input, select, textarea {
		-moz-appearance: none;
		-webkit-appearance: none;
		-ms-appearance: none;
		appearance: none;
	}

/* Basic */

	// Set box model to border-box.
	// Based on css-tricks.com/inheriting-box-sizing-probably-slightly-better-best-practice
		html {
			box-sizing: border-box;
		}

		*, *:before, *:after {
			box-sizing: inherit;
		}

	body {
		background: _palette(bg);
		overflow: hidden;

		// Stops initial animations until page loads.
			&.is-preload {
				*, *:before, *:after {
					@include vendor('animation', 'none !important');
					@include vendor('transition', 'none !important');
				}
			}

	}

	body, input, select, textarea {
		color: _palette(fg);
		font-family: 'Source Sans Pro', sans-serif;
		font-size: 15pt;
		font-weight: 300 !important;
		letter-spacing: -0.025em;
		line-height: 1.75em;
	}

	a {
		@include vendor('transition', 'border-color 0.2s ease-in-out');
		border-bottom: dotted 1px;
		color: inherit;
		outline: 0;
		text-decoration: none;

		&:hover {
			border-color: transparent;
		}
	}

/* Icon */

	.icon {
		@include icon;
		position: relative;

		> .label {
			display: none;
		}

		&.solid {
			&:before {
				font-weight: 900;
			}
		}

		&.brands {
			&:before {
				font-family: 'Font Awesome 5 Brands';
			}
		}
	}

/* Wrapper */

	@include keyframes('wrapper') {
		0%		{ opacity: 0; }
		100%	{ opacity: 1; }
	}

	#wrapper {
		@include vendor('animation', 'wrapper #{_duration(wrapper)} forwards');
		height: 100%;
		left: 0;
		opacity: 0;
		position: fixed;
		top: 0;
		width: 100%;
	}

/* BG */

	#bg {
		@include vendor('animation', 'bg #{_duration(bg)} linear infinite');
		@include vendor('backface-visibility', 'hidden');
		@include vendor('transform', 'translate3d(0,0,0)');

		/* Set your background with this */
		background: _misc(bg);

		background-repeat: repeat-x;
		height: 100%;
		left: 0;
		opacity: 1;
		position: fixed;
		top: 0;
	}

	@include bg(_misc(bg-width) * 1.5);

/* Overlay */

	@include keyframes('overlay') {
		0%		{ opacity: 0; }
		100%	{ opacity: 1; }
	}

	#overlay {
		@include vendor('animation', 'overlay #{_duration(overlay)} #{$delay-overlay} forwards');
		background-attachment: fixed, fixed;
		background-image: url('images/overlay-pattern.png'), url('images/overlay.svg');
		background-position: top left, center center;
		background-repeat: repeat, no-repeat;
		background-size: auto, cover;
		height: 100%;
		left: 0;
		opacity: 0;
		position: fixed;
		top: 0;
		width: 100%;
	}

/* Main */

	#main {
		height: 100%;
		left: 0;
		position: fixed;
		text-align: center;
		top: 0;
		width: 100%;

		&:before {
			content: '';
			display: inline-block;
			height: 100%;
			margin-right: 0;
			vertical-align: middle;
			width: 1px;
		}
	}

/* Header */

	@include keyframes('header') {
		0%		{ @include vendor('transform', 'translate3d(0,1em,0)'); opacity: 0; }
		100%	{ @include vendor('transform', 'translate3d(0,0,0)'); opacity: 1; }
	}

	@include keyframes('nav-icons') {
		0%		{ @include vendor('transform', 'translate3d(0,1em,0)'); opacity: 0; }
		100%	{ @include vendor('transform', 'translate3d(0,0,0)'); opacity: 1; }
	}

	#header {
		@include vendor('animation', 'header #{_duration(header)} #{$delay-header} forwards');
		@include vendor('backface-visibility', 'hidden');
		@include vendor('transform', 'translate3d(0,0,0)');
		cursor: default;
		display: inline-block;
		opacity: 0;
		position: relative;
		text-align: center;
		top: -1em;
		vertical-align: middle;
		width: 90%;

		h1 {
			font-size: 4.35em;
			font-weight: 900;
			letter-spacing: -0.035em;
			line-height: 1em;
		}

		p {
			font-size: 1.25em;
			margin: 0.75em 0 0.25em 0;
			opacity: 0.75;
		}

		nav {
			margin: 1.5em 0 0 0;

			li {
				@include vendor('animation', 'nav-icons #{_duration(nav-icons)} ease-in-out forwards');
				@include vendor('backface-visibility', 'hidden');
				@include vendor('transform', 'translate3d(0,0,0)');
				display: inline-block;
				height: _size(nav-icon-wrapper);
				line-height: _size(nav-icon-wrapper) * 1.1;
				opacity: 0;
				position: relative;
				top: 0;
				width: _size(nav-icon-wrapper);

				@for $x from 1 through 10 {
					&:nth-child(#{$x}) {
						@include vendor('animation-delay', ($delay-nav-icons + ($x * $delay-nav-icon)) + '');
					}
				}
			}

			a {
				-webkit-tap-highlight-color: rgba(0,0,0,0);
				-webkit-touch-callout: none;
				border: 0;
				display: inline-block;

				&:before {
					@include vendor('transition', 'all 0.2s ease-in-out');
					border-radius: 100%;
					border: solid 1px _palette(fg);
					display: block;
					font-size: _size(nav-icon);
					height: 2.5em;
					line-height: 2.5em;
					position: relative;
					text-align: center;
					top: 0;
					width: 2.5em;
				}

				&:hover {
					font-size: 1.1em;

					&:before {
						background-color: _palette(nav-icon, hover-bg);
						color: _palette(nav-icon, hover-fg);
					}
				}

				&:active {
					font-size: 0.95em;
					background: none;

					&:before {
						background-color: _palette(nav-icon, active-bg);
						color: _palette(nav-icon, active-fg);
					}
				}

				span {
					display: none;
				}
			}
		}
	}

/* Footer */

	#footer {
		@include vendor('background-image', 'linear-gradient(top, rgba(0,0,0,0), rgba(0,0,0,0.5) 75%)');
		bottom: 0;
		cursor: default;
		height: 6em;
		left: 0;
		line-height: 8em;
		position: absolute;
		text-align: center;
		width: 100%;
	}

/* Wide */

	@include breakpoint('<=wide') {

		/* Basic */

			body, input, select, textarea {
				font-size: 13pt;
			}

		/* BG */

			@include bg(_misc(bg-width));

	}

/* Normal */

	@include breakpoint('<=normal') {

		/* Basic */

			body, input, select, textarea {
				font-size: 12pt;
			}

		/* BG */

			@include bg(_misc(bg-width) * 0.5);

	}

/* Mobile */

	@include breakpoint('<=mobile') {

		/* Basic */

			body {
				min-width: 320px;
			}

			body, input, select, textarea {
				font-size: 11pt;
			}

		/* BG */

			@include bg(_misc(bg-width) * 0.2);

		/* Header */

			#header {
				h1 {
					font-size: 2.5em;
				}

				p {
					font-size: 1em;
				}

				nav {
					font-size: 1em;

					a {
						&:hover {
							font-size: 1em;
						}

						&:active {
							font-size: 1em;
						}
					}
				}
			}

	}

/* Mobile (Portrait) */

	@include breakpoint('<=mobilep') {

		/* BG */

			@include bg(_misc(bg-width) * 0.275);

		/* Header */

			#header {
				nav {
					padding: 0 1em;
				}
			}

	}

================================================
FILE: public/sass/noscript.scss
================================================
@import 'libs/vars';
@import 'libs/functions';
@import 'libs/mixins';
@import 'libs/vendor';
@import 'libs/breakpoints';

/*
	Aerial by HTML5 UP
	html5up.net | @ajlkn
	Free for personal and commercial use under the CCA 3.0 license (html5up.net/license)
*/

/* Wrapper */

	#wrapper {
		opacity: 1 !important;
	}

/* Overlay */

	#overlay {
		opacity: 1 !important;
	}

/* Header */

	#header {
		opacity: 1 !important;

		nav {
			li {
				opacity: 1 !important;
			}
		}
	}

================================================
FILE: src/App.vue
================================================
<script setup lang="ts">
import { ref } from "vue"
import { useDark } from "@vueuse/core"
import {  Moon, Sunny } from "@element-plus/icons-vue"
import { Github } from "@/components/icon"
import router from "@/router"

const isDark = useDark()

const themeStyle = {
    "--el-switch-on-color": "var(--el-fill-color)",
    "--el-switch-off-color": "var(--el-fill-color)",
    "--el-switch-border-color": "var(--el-border-color-dark)"
}

const goIndex = () => router.push("/")
const openGithub = () => window.open("https://github.com/ginnnnnn666/GinsMooc")
const spaceCount = () => window.innerWidth < 768 ? 8 : 16

const defaultColor = "var(--el-color-primary-dark-2)"
const hoverColor = "var(--el-color-primary)"
const color = [defaultColor, hoverColor]
const githubBtnColor = ref(0)
</script>

<template>
    <ElContainer>
        <ElHeader class="header">
            <ElSpace :size="8" class="nav">
                <ElMenu :default-active="$route.path" mode="horizontal" router class="nav-start" :ellipsis="false">
                    <ElImage class="icon hidden-xs-only" src="/favicon.png" @click="goIndex()"></ElImage>
                    <ElMenuItem class="menu-item hidden-xs-only" index="/mooc">Mooc</ElMenuItem>
                    <ElMenuItem class="menu-item hidden-xs-only" index="/video">Video</ElMenuItem>

                    <ElImage class="icon small hidden-sm-and-up" src="/favicon.png" @click="goIndex()"></ElImage>
                    <ElMenuItem class="menu-item small hidden-sm-and-up" index="/mooc">Mooc</ElMenuItem>
                    <!-- <ElMenuItem class="menu-item small hidden-sm-and-up" index="/video">Video</ElMenuItem> -->
                </ElMenu>
                <div class="nav-center">
                    <RouterView name="header" />
                </div>
                <ElSpace class="nav-end" :size="spaceCount()">
                    <ElSwitch v-model="isDark" inline-prompt :active-icon="Moon" :inactive-icon="Sunny" :style="themeStyle"
                        class="theme-switch"></ElSwitch>
                    <ElIcon :color="color[githubBtnColor % 2]" @click="openGithub" @mouseenter="githubBtnColor++"
                        @mouseleave="githubBtnColor++" style="cursor: pointer" class="github-icon">
                        <Github />
                    </ElIcon>
                    <ElAvatar class="avatar" :size="25" src="/headicon.png"></ElAvatar>
                </ElSpace>
            </ElSpace>
        </ElHeader>
        <ElMain class="main">
            <Suspense>
                <RouterView />
            </Suspense>
        </ElMain>
    </ElContainer>
</template>

<style scoped>
.header {
    height: 50px;
    padding: 0;
    border-bottom: 1px solid var(--el-border-color-light);
    background-color: var(--el-bg-color);
}

@media only screen and (max-width: 768px) {
    .header {
        height: 40px;
    }

    .nav-start {
        height: 40px !important;
    }

    .menu-item.small {
        padding: 0 8px;
        font-size: 13px;
    }

    .icon.small {
        height: 20px;
        width: 20px;
        margin: 10px 16px 10px 16px;
    }

    .github-icon {
        font-size: 26px !important;
    }

    .avatar {
        height: 22px;
        width: 22px;
    }
}

.main {
    padding: 0;
    display: flex;
    justify-content: center;
}

.nav {
    display: flex;
}

.nav :nth-child(2) {
    flex: 1;
}

.nav-start {
    height: 49px;
    border-bottom: 0;
    background-color: transparent;
    color: var(--el-text-color-primary);
}

.nav-end {
    display: flex;
    justify-content: end;
}

.github-icon {
    font-size: 30px;
}
</style>

<style>
body {
    margin: 0;
    height: 100vh;
    transition: margin 0s;
}

i,
p {
    margin: 0 !important;
}

::selection {
    background-color: var(--el-color-primary-light-3);
    color: var(--el-color-white);
}

.el-header,
.el-footer,
.el-main,
.el-aside {
    transition: all var(--el-transition-duration) !important;
}

.el-container {
    height: 100%;
    width: 100%;
}

.icon {
    height: 24px;
    width: 24px;
    margin: 13px 30px;
    cursor: pointer;
}

.theme-switch .el-switch__core .el-switch__inner .is-icon,
.el-switch__core {
    color: var(--el-text-color-primary);
}

.theme-switch .el-switch__core .el-switch__action {
    background-color: var(--el-bg-color);
}
</style>


================================================
FILE: src/components/CourseCard.vue
================================================
<script setup lang="ts">
import type { course } from "@/type/mooc"
import { ElImage } from "element-plus"

const props = defineProps<{
    course: course
}>()

const emit = defineEmits<{
    (e: 'click', course: course, event: MouseEvent): void
}>()
</script>

<template>
    <li :class="{ 'course-card': true }" @click="$emit('click', course, $event)">
        <ElImage :src="course.imageUrl" class="course-card__img" lazy></ElImage>
        <div class="course-card__info">
            <div class="course-card__info-name">{{ course.name }}</div>
            <div class="course-card__info-school">{{ course.school }}</div>
        </div>
    </li>
</template>

<style scoped>
.course-card {
    margin-bottom: 8px;
    height: auto;
    padding: 18px 12px 18px 24px;
    display: flex;
    border-radius: 8px;
    transition: all 0.2s;
    cursor: pointer;
}
.course-card:last-child {
    margin-bottom: 0;
}

.course-card:active {
    transform: scale(0.98);
}

.course-card.is-selected, .course-card:not(.is-selected):active {
    background-color: var(--el-color-primary-light-5);
    z-index: 5;
}

.course-card:not(.is-selected):hover {
    background-color: var(--el-color-primary-light-7);
}

.course-card__img {
    height: 48px;
    width: 85px;
    border-radius: 8px;
    margin-right: 16px;
}

.course-card__info {
    flex: 1;
    display: flex;
    width: 186px;
    white-space: nowrap;
    text-overflow: ellipsis;
    overflow: hidden;
    flex-direction: column;
}

.course-card__info-name {
    flex: 1;
    font-size: 18px;
    color: var(--el-text-color-primary);
    white-space: nowrap;
    text-overflow: ellipsis;
    overflow: hidden;
}

.course-card__info-school {
    font-size: 12px;
    color: var(--el-text-color-secondary);
}
</style>


================================================
FILE: src/components/QuestionCard.vue
================================================
<script setup lang="ts">
import type { quiz, homework, option } from "@/type/mooc"
import { QuestionTypeEnumList } from "@/type/mooc"
import { Completion, SingleChoice, MultipleChoice, Homework, OnlineJudge } from "@/components/question"

const props = defineProps<{
    data: quiz | homework
    order: number
}>()

const seq = (() => {
    const ret = props.order.toString()
    if (ret.length < 2) {
        return "0" + ret
    }
    return ret
})()

const isSingleChoice = (data: quiz | homework): data is quiz => {
    return data.type === QuestionTypeEnumList.SingleChoice || data.type === QuestionTypeEnumList.Judge
}
const isMultipleChoice = (data: quiz | homework): data is quiz => data.type === QuestionTypeEnumList.MultipleChoice
const isCompletion = (data: quiz | homework): data is quiz => data.type === QuestionTypeEnumList.Completion
const isRegularHomework = (data: quiz | homework): data is homework => data.type === QuestionTypeEnumList.Homework
const isOnlineJudge = (data: quiz | homework): data is homework => data.type === QuestionTypeEnumList.OnlineJudge
</script>

<template>
    <ElContainer class="question-card">
        <SingleChoice v-if="isSingleChoice(data)" :data="data" :seq="seq"></SingleChoice>
        <MultipleChoice v-else-if="isMultipleChoice(data)" :data="data" :seq="seq"></MultipleChoice>
        <Completion v-else-if="isCompletion(data)" :data="data" :seq="seq"></Completion>
        <Homework v-else-if="isRegularHomework(data)" :data="data" :seq="seq"></Homework>
        <OnlineJudge v-else-if="isOnlineJudge(data)" :data="data" :seq="seq"></OnlineJudge>
    </ElContainer>
</template>

<style scoped>
.question-card {
    height: fit-content;
    display: flex;
    flex-direction: column;
    padding: 20px 40px;
    margin-top: 20px;
    background-color: var(--el-bg-color);
    box-shadow: var(--el-box-shadow-light);
    transition: all 0.2s;
}

.question-card:hover {
    transform: scale(1.02);
}

.question-card :deep(.question-card-header) {
    margin-bottom: 14px;
    color: var(--el-text-color-primary);
    line-height: 1.5;
}

.question-card :deep(.question-card-body) {
    color: var(--el-text-color-primary);
    line-height: 1.5;
}

.question-card :deep(.question-card-body img) {
    max-width: 100%;
    height: auto;
}

.question-card :deep(.el-checkbox__label),
.question-card :deep(.el-radio__label) {
    white-space: normal;
    line-height: 1.5;
    color: var(--el-text-color-primary);
}

.question-card :deep(.question-card__seq) {
    float: left;
    margin-right: 8px;
    font-weight: 700;
    font-size: 16px;
    color: var(--el-text-color-primary);
}

.question-card :deep(.question-card__title img) {
    max-width: 100%;
}

@media only screen and (max-width: 768px) {
    .question-card {
        padding: 10px 20px;
        margin-top: 8px;
    }

    .question-card :deep(.question-card-header) {
        margin-bottom: 8px;
    }

    .question-card :deep(.question-card__seq),
    .question-card :deep(.question-card__title) {
        font-size: 14px;
    }

    .question-card :deep(.el-radio),
    .question-card :deep(.el-checkbox) {
        height: 24px;
    }

    .question-card :deep(.el-radio .el-radio__inner),
    .question-card :deep(.el-checkbox .el-checkbox__inner) {
        width: 12px;
        height: 12px;
    }

    .question-card :deep(.el-checkbox .el-checkbox__inner:after) {
        width: 2px;
        height: 6px;
    }

    .question-card :deep(.el-radio .el-radio__label),
    .question-card :deep(.el-checkbox .el-checkbox__label),
    .question-card :deep(.question-card-body) {
        font-size: 12px;
    }

    .question-card :deep(.el-input) {
        --el-input-height: var(--el-component-size-small);
        font-size: 12px;
    }

    .question-card :deep(.el-input__wrapper) {
        padding: 1px 7px;
    }

    .question-card :deep(.el-input__inner) {
        --el-input-inner-height: calc(var(--el-input-height, 24px) - 2px);
    }
}</style>


================================================
FILE: src/components/icon/Extension.vue
================================================
<template>
    <svg viewBox="0 0 1024 1024" width="200" height="200">
        <path
            d="M873.984 470.016q43.989333 0 75.989333 31.018667t32 75.008-32 75.008-75.989333 31.018667l-64 0 0 171.989333q0 34.005333-25.002667 59.008t-59.008 25.002667l-162.005333 0 0-64q0-48-34.005333-80.981333t-82.005333-32.981333-82.005333 32.981333-34.005333 80.981333l0 64-162.005333 0q-34.005333 0-59.008-25.002667t-25.002667-59.008l0-162.005333 64 0q48 0 80.981333-34.005333t32.981333-82.005333-32.981333-82.005333-80.981333-34.005333l-64 0 0-162.005333q0-34.005333 25.002667-59.008t59.008-25.002667l171.989333 0 0-64q0-43.989333 31.018667-75.989333t75.008-32 75.008 32 31.018667 75.989333l0 64 171.989333 0q34.005333 0 59.008 25.002667t25.002667 59.008l0 171.989333 64 0z"
            p-id="5381"></path>
    </svg>
</template>


================================================
FILE: src/components/icon/Github.vue
================================================
<template>
    <svg viewBox="0 0 24 24" width="1.2em" height="1.2em">
        <path
            fill="currentColor"
            d="M12 2C6.475 2 2 6.475 2 12a9.994 9.994 0 0 0 6.838 9.488c.5.087.687-.213.687-.476c0-.237-.013-1.024-.013-1.862c-2.512.463-3.162-.612-3.362-1.175c-.113-.288-.6-1.175-1.025-1.413c-.35-.187-.85-.65-.013-.662c.788-.013 1.35.725 1.538 1.025c.9 1.512 2.338 1.087 2.912.825c.088-.65.35-1.087.638-1.337c-2.225-.25-4.55-1.113-4.55-4.938c0-1.088.387-1.987 1.025-2.688c-.1-.25-.45-1.275.1-2.65c0 0 .837-.262 2.75 1.026a9.28 9.28 0 0 1 2.5-.338c.85 0 1.7.112 2.5.337c1.912-1.3 2.75-1.024 2.75-1.024c.55 1.375.2 2.4.1 2.65c.637.7 1.025 1.587 1.025 2.687c0 3.838-2.337 4.688-4.562 4.938c.362.312.675.912.675 1.85c0 1.337-.013 2.412-.013 2.75c0 .262.188.574.688.474A10.016 10.016 0 0 0 22 12c0-5.525-4.475-10-10-10z"
        ></path>
    </svg>
</template>


================================================
FILE: src/components/icon/index.ts
================================================
import Github from "./Github.vue"
import Extension from "./Extension.vue"

export { Github, Extension }

================================================
FILE: src/components/index.ts
================================================
import CourseCard from "./CourseCard.vue"
import QuestionCard from "./QuestionCard.vue"

export {CourseCard, QuestionCard}

================================================
FILE: src/components/question/Completion.vue
================================================
<script setup lang="ts">
import type { quiz } from "@/type/mooc"
import { ref } from "vue"

const props = defineProps<{
    data: quiz
    seq: string
}>()
const stdAnswerArray = (props.data.stdAnswer as string).split("##%_YZPRLFH_%##")
const [answer, backgroundColor, color] = [ref(""), ref("transparent"), ref("")]

const checkAnswer = () => {
    const answerArray = answer.value.split(" / ")
    for (const item of answerArray) {
        if (stdAnswerArray.indexOf(item) === -1) {
            backgroundColor.value = "transparent"
            color.value = ""
            return
        }
    }
    backgroundColor.value = "var(--el-color-success-light-8)"
    color.value = "var(--el-color-primary)"
}

const setAnswer = () => {
    let splicedAnswer = ""
    const len = stdAnswerArray.length
    for (let i = 0; i < len; i++) {
        if (i > 0) {
            splicedAnswer += " / "
        }
        splicedAnswer += stdAnswerArray[i]
    }
    answer.value = splicedAnswer
    checkAnswer()
}
props.data.title = props.data.title.replaceAll(/img\d\.ph\.126\.net/g, 'img-ph-mirror.nosdn.127.net')
</script>

<template>
    <div class="question-card-header" @click="setAnswer">
        <span class="question-card__seq">{{ seq }}</span>
        <div class="question-card__title" v-html="data.title"></div>
    </div>
    <div class="question-card-body">
        <ElInput
            size="large"
            class="answer-input"
            v-model="answer"
            placeholder="在这里输入答案"
            @input="checkAnswer"
        ></ElInput>
    </div>
</template>

<style scoped>
.answer-input {
    background-color: v-bind(backgroundColor);
}
.answer-input :deep(.el-input__wrapper, .el-input__inner) {
    background-color: transparent;
}
.answer-input :deep(.el-input__inner) {
    color: v-bind(color);
}
</style>


================================================
FILE: src/components/question/Homework.vue
================================================
<script setup lang="ts">
import type { homework } from "@/type/mooc"

const props = defineProps<{
    data: homework
    seq: string
}>()
props.data.title = props.data.title.replaceAll(/img\d\.ph\.126\.net/g, 'img-ph-mirror.nosdn.127.net')
props.data.answer = <string>props.data.answer?.replaceAll(/img\d\.ph\.126\.net/g, 'img-ph-mirror.nosdn.127.net')
</script>

<template>
    <div class="question-card-header">
        <span class="question-card__seq">{{ seq }}</span>
        <div class="question-card__title" v-html="data.title"></div>
    </div>
    <div class="question-card-body">
        <span v-html="data.answer"></span>
    </div>
</template>

<style scoped></style>


================================================
FILE: src/components/question/MultipleChoice.vue
================================================
<script setup lang="ts">
import type { quiz, option } from "@/type/mooc"
import { ref } from "vue"

const props = defineProps<{
    data: quiz
    seq: string
}>()
const checkedList = ref(<Array<number>>new Array())
const answerList = (() => {
    let ret = <Array<number>>new Array()
    for (const item of <option[]>props.data.optionList) {
        if (item.answer) {
            ret.push(item.id)
        }
    }
    return ret
})()

const inAnswerList = (element: number) => answerList.indexOf(element) >= 0
const inCheckedList = (element: number) => checkedList.value.indexOf(element) >= 0
const checkedColor = (option: option) => {
    if (checkedList.value.length !== answerList.length) {
        return "transparent"
    }
    if (checkedList.value.every(inAnswerList)) {
        return inAnswerList(option.id) ? "var(--el-color-success-light-8)" : "transparent"
    }
}
const setAnswer = () => {
    checkedList.value.length = 0
    answerList.forEach((value) => checkedList.value.push(value))
}
props.data.title = props.data.title.replaceAll(/img\d\.ph\.126\.net/g, 'img-ph-mirror.nosdn.127.net')
props.data.optionList?.forEach((item) => {
    item.content = item.content.replaceAll(/img\d\.ph\.126\.net/g, 'img-ph-mirror.nosdn.127.net')
})
</script>

<template>
    <div class="question-card-header" @click="setAnswer">
        <span class="question-card__seq">{{ seq }}</span>
        <div class="question-card__title" v-html="data.title"></div>
    </div>
    <div class="question-card-body">
        <ElCheckboxGroup v-model="checkedList" class="checkbox-group">
            <ElCheckbox
                class="checkbox-item"
                :class="{ 'is-selected': inCheckedList(option.id) }"
                v-for="option in data.optionList"
                :label="option.id"
                size="large"
                style="margin: 0;"
                :style="{ 'background-color': checkedColor(option) }"
                ><span class="checkbox-item__content" v-html="option.content"></span
            ></ElCheckbox>
        </ElCheckboxGroup>
    </div>
</template>

<style scoped>
.checkbox-group {
    display: flex;
    flex-direction: column;
    align-items: stretch;
}
.checkbox-item {
    padding: 8px 8px;
    height: auto !important;
}
.checkbox-item:not(.is-selected):hover {
    background-color: var(--el-color-primary-light-8) !important;
}
</style>


================================================
FILE: src/components/question/OnlineJudge.vue
================================================
<script setup lang="ts">
import type { homework } from "@/type/mooc"
import { computed } from "vue"

const props = defineProps<{
    data: homework
    seq: string
}>()
const timeLimit = computed(() => `${props.data.memoryLimit! / 1000}s`)
const memoryLimit = computed(() => {
    let memValue = props.data.memoryLimit! / 1024
    return memValue < 1024 ? `${memValue}KB` : `${memValue / 1024}MB`
})
props.data.title = props.data.title.replaceAll(/img\d\.ph\.126\.net/g, 'img-ph-mirror.nosdn.127.net')
props.data.description = <string>props.data.description?.replaceAll(/img\d\.ph\.126\.net/g, 'img-ph-mirror.nosdn.127.net')
</script>

<template>
    <div class="question-card-header">
        <span class="question-card__seq">{{ seq }}</span>
        <div class="question-card__title" v-html="data.title"></div>
    </div>
    <div class="question-card-body">
        <div>
            <ElTag>描述</ElTag>
            <span class="question-desc" v-html="data.description"></span>
        </div>
        <ElRow>
            <ElCol :span="12"
                ><ElTag type="danger">时间限制</ElTag><span>{{ timeLimit }}</span></ElCol
            >
            <ElCol :span="12"
                ><ElTag type="danger">内存限制</ElTag><span>{{ memoryLimit }}</span></ElCol
            >
        </ElRow>
    </div>
</template>

<style scoped>
</style>


================================================
FILE: src/components/question/SingleChoice.vue
================================================
<script setup lang="ts">
import type { option, quiz } from "@/type/mooc"
import { ref } from "vue"

const props = defineProps<{
    data: quiz
    seq: string
}>()
const isChecked = ref(0)
const answer = (() => {
    for (const item of <option[]>props.data.optionList) {
        if (item.answer) {
            return item.id
        }
    }
    return 0
})()

const checkedColor = (option: option) => {
    if (option.id !== isChecked.value) {
        return "transparent"
    }
    return option.answer ? "var(--el-color-success-light-8)" : "var(--el-color-error-light-8)"
}
const setAnswer = () => (isChecked.value = answer)
props.data.title = props.data.title.replaceAll(/img\d\.ph\.126\.net/g, 'img-ph-mirror.nosdn.127.net')
props.data.optionList?.forEach((item) => {
    item.content = item.content.replaceAll(/img\d\.ph\.126\.net/g, 'img-ph-mirror.nosdn.127.net')
})
</script>

<template>
    <div class="question-card-header" @click="setAnswer">
        <span class="question-card__seq">{{ seq }}</span>
        <div class="question-card__title" v-html="data.title"></div>
    </div>
    <div class="question-card-body">
        <ElRadioGroup v-model="isChecked" class="radio-group">
            <ElRadio
                class="radio-item"
                :class="{ 'is-selected': option.id === isChecked }"
                v-for="option in data.optionList"
                :label="option.id"
                size="large"
                style="margin: 0"
                :style="{ 'background-color': checkedColor(option) }"
                ><span class="radio-item__content" v-html="option.content"></span
            ></ElRadio>
        </ElRadioGroup>
    </div>
</template>

<style scoped>
.radio-group {
    display: flex;
    flex-direction: column;
    align-items: stretch;
}
.radio-item {
    padding: 8px 8px;
    height: auto !important;
}
.radio-item :deep(img) {
    max-width: 100%;
}
.radio-item:not(.is-selected):hover {
    background-color: var(--el-color-primary-light-8) !important;
}
</style>


================================================
FILE: src/components/question/index.ts
================================================
import SingleChoice from "./SingleChoice.vue"
import MultipleChoice from "./MultipleChoice.vue"
import Completion from "./Completion.vue"
import Homework from "./Homework.vue"
import OnlineJudge from "./OnlineJudge.vue"

export { SingleChoice, MultipleChoice, Completion, Homework, OnlineJudge }

================================================
FILE: src/main.ts
================================================
import { createApp } from "vue"
import ElementPlus from 'element-plus'
import router from "./router"
import App from "./App.vue"
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import 'element-plus/dist/index.css'
import 'element-plus/theme-chalk/dark/css-vars.css'
import 'element-plus/theme-chalk/display.css'

const app = createApp(App)

app.use(ElementPlus)
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
    app.component(key, component)
}

app.use(router)

app.mount("body")


================================================
FILE: src/plugins/apiAccess.ts
================================================
import type { ApiKeyType, ApiResponseType, ApiRequestType } from "../type/api"
import type { App } from "vue"
import { isAxiosError } from "axios"
import { ElMessage } from "element-plus"
import apiInfo from "../type/api"
import axios from "axios"

async function apiAccess<T extends ApiKeyType>(api: T): Promise<ApiResponseType[T]>
async function apiAccess<T extends ApiKeyType>(
    api: T,
    params: ApiRequestType[T]["params"],
    data: ApiRequestType[T]["data"]
): Promise<ApiResponseType[T]>

/** 函数重载 */
async function apiAccess<T extends ApiKeyType>(
    api: T,
    params?: ApiRequestType[T]["params"],
    data?: ApiRequestType[T]["data"]
) {
    /** 错误处理,主要catch 404,调用者不再需要try-catch */
    try {
        return await new Promise<ApiResponseType[T]>((resolve, reject) => {
            /** 查询参数转动态路由参数 */
            let url = apiInfo[api].url
            if (params) {
                for (const [key, val] of Object.entries(params)) {
                    const reg = RegExp(`(/):${key}(/)?`, "g")
                    if (reg.test(url)) {
                        url = url.replaceAll(reg, `$1${val}$2`)
                        Reflect.deleteProperty(params, key)
                    }
                }
            }
            /** 将对象转为json字符串 */
            if (data) {
                for (const [key, val] of Object.entries(data)) {
                    if (typeof val === "object") {
                        Reflect.set(data, key, JSON.stringify(val))
                    }
                }
            }
            /** 异步发送请求 */
            let urlPrefix = "https://ginnnnnn.top/api"
            axios<ApiResponseType[T]>({
                url: urlPrefix + url,
                method: apiInfo[api].method,
                params: params || {},
                data: data || {},
                headers: { "Content-Type": "application/x-www-form-urlencoded" }
            })
                .then((res) => {
                    let message = "",
                        success = false
                    if (res.status !== 200 || !res.data) {
                        message = "请求出错"
                    } else if (res.data.msg) {
                        message = res.data.msg
                        if (res.data.status === 200) {
                            success = true
                        }
                    }
                    if (message) {
                        ElMessage({ message, type: success ? "success" : "error" })
                    }
                    resolve(res.data)
                })
                .catch((error) => {
                    let message = error
                    if (isAxiosError(error)) {
                        message = error.message
                    }
                    ElMessage({ message, type: "error" })
                    reject(error)
                })
        })
    } catch {
        return {}
    }
}

export const useApiAccess = () => apiAccess

export default {
    install: (app: App) => {
        app.config.globalProperties.$apiAccess = apiAccess
    }
}


================================================
FILE: src/plugins/tool.ts
================================================
const sleep = async (ms: number) => {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve('');
        }, ms)
    })
}

export { sleep } 

================================================
FILE: src/router/index.ts
================================================
import { createRouter, createWebHistory } from "vue-router"
import { HomeView, MoocView, MoocHeader, MoocCourseDetail, MoocTest, VideoView } from "@/views"
import { ElMessage } from "element-plus"

const router = createRouter({
    history: createWebHistory(import.meta.env.BASE_URL),
    routes: [
        {
            path: "/",
            name: "Home",
            meta: { title: "Gins" },
            component: HomeView
        },
        {
            path: "/ginnnnnn/video",
            name: "Video",
            meta: { title: "GinsVideo" },
            component: VideoView
        },
        {
            path: "/mooc",
            name: "Mooc",
            components: {
                default: MoocView,
                header: MoocHeader
            },
            meta: { title: "GinsMooc" },
            children: [
                { path: "course/:cid", name: "MoocCourse", component: MoocCourseDetail, props: true },
                { path: "test/:tid", name: "MoocTest", component: MoocTest, props: true }
            ]
        }
    ]
})

router.beforeEach((to, from) => {
    if (!to.name) {
        ElMessage.error("路由错误")
        return { name: "Home" }
    }
    if (to.meta.title) {
        document.title = <string>to.meta.title
    }
    return true
})

export default router

const checkMoocItemId = (id: string) => /[0-9]/.test(id)

================================================
FILE: src/type/api.ts
================================================
import type { course, homework, notice, quiz, test } from "./mooc"

type RequestType = {
    params?: Object
    data?: Object
}

type Response<T = any> = {
    status: number
    data: T
    msg: string
}

export interface courseList extends Object {
    courseList: course[]
    totalPages: number
    currentPage: number
}

export interface courseDetail extends Object {
    course: course
    testList: test[]
}

export interface testDetail extends Object {
    course: course
    test: test
    questionList: quiz[] | homework[]
    totalPages: number
}

const apiInfo = {
    getCourseList: {
        url: "/mooc/course",
        method: "GET"
    },
    getCourseDetail: {
        url: "/mooc/course/:cid",
        method: "GET"
    },
    getTestDetail: {
        url: "/mooc/test/:tid",
        method: "GET"
    },
    getNotice: {
        url: "/mooc/notice/website",
        method: "GET"
    }
}

export type ApiKeyType = keyof typeof apiInfo

export interface ApiResponseType {
    getCourseList: Response<courseList>
    getCourseDetail: Response<courseDetail>
    getTestDetail: Response<testDetail>
    getNotice: Response<notice>
}

export interface ApiRequestType {
    getCourseList: RequestType & {
        params: {
            page?: number | string
            search?: string
            cid?: number | string
        }
    }
    getCourseDetail: RequestType & {
        params: { cid: number | string }
    }
    getTestDetail: RequestType & {
        params: {
            tid: number | string
            page: number | string
            search?: string
        }
    }
    getNotice: RequestType & {
        params: { version: string }
    }
}

export default apiInfo


================================================
FILE: src/type/globleProperties.ts
================================================
import { useApiAccess } from "@/plugins/apiAccess"

const apiAccess = useApiAccess()
declare module 'vue' {
    interface ComponentCustomProperties {
        $apiAccess: typeof apiAccess
    }
}

================================================
FILE: src/type/mooc.ts
================================================
export enum QuestionTypeEnumList {
    SingleChoice = "SINGLE_CHOICE",
    MultipleChoice = "MULTIPLE_CHOICE",
    Completion = "COMPLETION",
    Judge = "JUDGEMENT",
    Homework = "HOMEWORK",
    OnlineJudge = "ONLINE_JUDGE"
}

interface course extends Object {
    id: number
    name: string
    school: string
    imageUrl: string
}

interface test extends Object {
    id: number
    name: string
    objective: boolean
    releaseTime: string
    deadline: string
    chapterId: number
    chapterName: string
}

interface option extends Object {
    id: number
    content: string
    answer: boolean
}

interface quiz extends Object {
    id: number
    type:
        | QuestionTypeEnumList.SingleChoice
        | QuestionTypeEnumList.MultipleChoice
        | QuestionTypeEnumList.Completion
        | QuestionTypeEnumList.Judge
    title: string
    stdAnswer: string | null
    optionList: option[] | null
}

interface homework extends Object {
    id: number
    type: QuestionTypeEnumList.Homework | QuestionTypeEnumList.OnlineJudge
    title: string
    answer: string | null
    description: string | null
    memoryLimit: number | null
    timeLimit: number | null
}

interface notice extends Object {
    id: number
    content: string
}

export type QuestionTypeEnum = typeof QuestionTypeEnumList
export type { course, test, option, quiz, homework, notice }


================================================
FILE: src/views/BlogView.vue
================================================
<template>
    <iframe class="background" src="background.html" allowtransparency="true"></iframe>
</template>

<style scoped>
.background {
    width: 100vw;
    height: 100%;
    border: none;
    background-color: transparent;
}
</style>

================================================
FILE: src/views/HomeView.vue
================================================
<script setup lang="ts">
import BlogView from "@/views/BlogView.vue"
import { ref } from "vue"

const disabled = ref(true)
const c = () => {
    if (disabled.value === false) {
        console.log("click")
    }
}
</script>

<template>
    <div class="home">
        <div class="home-loading" v-loading="true" element-loading-text="Loading..."></div>
        <BlogView></BlogView>
        <div class="home-footer">
            <ElLink href="https://beian.miit.gov.cn" :underline="false" target="_blank" style="font-size: 12px">
                粤ICP备2022090073号</ElLink>
            <span class="copyright">©2022-2024 by gin</span>
        </div>
    </div>
</template>

<style scoped>
.home-loading {
    z-index: -1;
    position: absolute;
    height: calc(100vh - 50px);
    width: 100%;
}

.home {
    display: flex;
    flex-direction: column;
    align-items: center;
}

.home-footer {
    z-index: 10;
    position: absolute;
    bottom: 0;
    display: flex;
    justify-content: center;
    font-size: 12px;
    padding-bottom: 12px;
}

.copyright {
    margin-left: 16px;
    display: flex;
    align-items: center;
    color: var(--el-text-color-regular);
}
</style>


================================================
FILE: src/views/MoocAside.vue
================================================
<script setup lang="ts">
import type { course } from "@/type/mooc"
import type { courseList } from "@/type/api"
import { ref, watch } from "vue"
import { useApiAccess } from "@/plugins/apiAccess"
import { Plus, Search } from "@element-plus/icons-vue"
import { CourseCard } from "@/components"
import { sleep } from "@/plugins/tool"
import { useDark } from "@vueuse/core"
import router from "@/router"
import { ElMessage, ElScrollbar } from 'element-plus'
import axios from "axios"

const checkExist = async () => {
    let total_page
    let data: course[] = (await apiAccess("getCourseList", { page: 0 }, undefined)).data.courseList

}

const isDark = useDark()
const apiAccess = useApiAccess()
const props = defineProps<{
    currentCourse: course
}>()
const drawerVisible = ref(false)
const data = ref(<course[]>[])
const query = ref("")
const refresh = ref(false)
const clicked = ref(false)
const cidCourseLoading = ref(false)

const showDrawer = () => (drawerVisible.value = true)
defineExpose({ showDrawer })
const defaultColor = "var(--el-color-primary-dark-2)"
const hoverColor = "var(--el-color-primary)"
const color = [defaultColor, hoverColor]

const newCourseId = ref("")
const newCourseState = ref(-1)
const newCourseBtnColor = ref(0)
const newCourseDialogVisible = ref(false)
const newCourseDialogLocked = ref(false)

const onNewCourseIdInput = () => (newCourseId.value = newCourseId.value.replace(/[^0-9]/g, ""))
const onNewCourseClick = async () => {
    newCourseDialogLocked.value = true
    const eventSource = new EventSource(`https://ginnnnnn.top/api/mooc/course/refresh/${newCourseId.value}`)
    eventSource.onmessage = (event) => {
        console.log(event.data)
        const state = JSON.parse(event.data)
        if (state && state.total > 0) {
            newCourseState.value = Math.round((state.finished / state.total) * 100)
        }
        if (newCourseState.value === 100 || state.status !== undefined) {
            eventSource.close()
            if (state.msg) {
                state.status === 200 ? ElMessage.success(state.msg) : ElMessage.info(state.msg);
            }
            newCourseDialogLocked.value = false
            newCourseState.value = -1
            newCourseId.value = ""
            newCourseDialogVisible.value = false
            refresh.value = true
            loadData()
        }
    }
}
const onCourseClick = (course: course) => {
    clicked.value = true
    router.push({ path: `/mooc/course/${course.id}` })
    drawerVisible.value = false
}

watch(
    () => query.value,
    () => {
        refresh.value = true
        loadData()
    }
)

watch(
    () => props.currentCourse?.id,
    () => {
        if (!clicked.value) {
            refresh.value = true
            loadData(props.currentCourse?.id)
        }
    }
)

let [currentPage, totalPages] = [[0, -1], 0]
const loading = ref(false)
const disabled = () => loading.value || currentPage[1] >= totalPages - 1
const scrollbarRef1 = ref<InstanceType<typeof ElScrollbar>>()
const scrollbarRef2 = ref<InstanceType<typeof ElScrollbar>>()

const loadData = async (cid?: number, up?: boolean) => {
    const scrollTop = (value: number) => {
        scrollbarRef1.value && scrollbarRef1.value.setScrollTop(value)
        scrollbarRef2.value && scrollbarRef2.value.setScrollTop(value)
    }

    if (cid) {
        cidCourseLoading.value = true
    } else if (refresh.value) {
        currentPage = [0, -1]
    }
    loading.value = true
    const res = (await apiAccess("getCourseList", cid ? { cid } : { page: up ? --currentPage[0] : ++currentPage[1], search: query.value }, undefined)).data
    loading.value = false
    if (!cid && cidCourseLoading.value) {
        return
    }

    if (cid) {
        cidCourseLoading.value = false
        const order = data.value.findIndex((course) => course.id === cid)
        order !== -1 && scrollTop(order * 92)
    } else if (refresh.value) {
        scrollTop(0)
    }

    if (refresh.value) {
        data.value = res.courseList
        refresh.value = false
        currentPage = [res.currentPage, res.currentPage]
    } else if (up) {
        data.value = res.courseList.concat(data.value)
        currentPage[0] = res.currentPage
    } else {
        data.value = data.value.concat(res.courseList)
        currentPage[1] = res.currentPage
    }

    totalPages = res.totalPages
}

let courseListAside = document.querySelectorAll('.course-list .el-scrollbar__view')
const scrollCallback = (arg: { scrollLeft: number, scrollTop: number }) => {
    if (courseListAside.length === 0) {
        courseListAside = document.querySelectorAll('.course-list .el-scrollbar__view')
    }
    courseListAside.forEach((value) => {
        if (!query.value && !loading.value && currentPage[0] > 0 && value.getBoundingClientRect().top > -500) {
            loadData(undefined, true)
        }
    })
}

loadData(props.currentCourse?.id)
</script>

<template>
    <ElContainer>
        <ElMain class="aside-main">
            <ElScrollbar v-if="data" class="course-list" ref="scrollbarRef1" :noresize="true" @scroll="scrollCallback">
                <div v-infinite-scroll="loadData" :infinite-scroll-disabled="disabled()" :infinite-scroll-distance="500"
                    :infinite-scroll-immediate="false">
                    <CourseCard v-for="course in data" :key="course.id" :course="course"
                        :class="{ 'is-selected': currentCourse && course.id === currentCourse.id }"
                        @click="onCourseClick(course)"></CourseCard>
                </div>
            </ElScrollbar>
        </ElMain>
        <ElFooter class="aside-footer" height="50px">
            <ElIcon :size="20" @click="newCourseDialogVisible = true" style="cursor: pointer" class="new-course-btn"
                :color="color[newCourseBtnColor % 2]" @mouseenter="newCourseBtnColor++" @mouseleave="newCourseBtnColor++">
                <Plus />
            </ElIcon>
            <ElInput v-model="query" :suffix-icon="Search" placeholder="按课程名或学校搜索"></ElInput>
        </ElFooter>
    </ElContainer>
    <ElDrawer v-model="drawerVisible" :show-close="false" title="课程列表" :lock-scroll="false" direction="ltr"
        class="aside-drawer" :append-to-body="true">
        <ElScrollbar v-if="data" class="course-list" ref="scrollbarRef2" @scroll="scrollCallback">
            <div v-infinite-scroll="loadData" :infinite-scroll-disabled="disabled()" :infinite-scroll-distance="500"
                :infinite-scroll-immediate="false">
                <CourseCard v-for="course in data" :key="course.id" :course="course"
                    :class="{ 'is-selected': currentCourse && course.id === currentCourse.id }"
                    @click="onCourseClick(course)"></CourseCard>
            </div>
        </ElScrollbar>
        <template #footer>
            <ElIcon :size="20" @click="newCourseDialogVisible = true" style="cursor: pointer" class="new-course-btn"
                :color="color[newCourseBtnColor % 2]" @mouseenter="newCourseBtnColor++" @mouseleave="newCourseBtnColor++">
                <Plus />
            </ElIcon>
            <ElInput v-model="query" :suffix-icon="Search" placeholder="按课程名或学校搜索"></ElInput>
        </template>
    </ElDrawer>
    <ElDialog v-model="newCourseDialogVisible" :close-on-click-modal="!newCourseDialogLocked"
        :close-on-press-escape="!newCourseDialogLocked" :show-close="!newCourseDialogLocked" :append-to-body="true">
        <template #header><span>添加新课程</span></template>
        <ElRow :gutter="16">
            <ElCol :span="6">
                <ElInput v-model="newCourseId" placeholder="在此输入课程id" maxlength="10" @input="onNewCourseIdInput"
                    :disabled="newCourseDialogLocked"></ElInput>
            </ElCol>
            <ElCol :span="2">
                <ElButton type="primary" @click="onNewCourseClick" :disabled="newCourseDialogLocked">确认</ElButton>
            </ElCol>
            <ElCol :span="16" v-if="newCourseDialogLocked">
                <ElProgress :text-inside="true" :stroke-width="32" :percentage="newCourseState" status="success">
                </ElProgress>
            </ElCol>
        </ElRow>
        <ElImage src="/new-course-help.png" style="margin-top: 8px; box-shadow: var(--el-box-shadow-light)"></ElImage>
    </ElDialog>
</template>

<style scoped>
.aside-header {
    color: var(--el-text-color-primary);
    padding: 0;
}

.aside-main {
    padding: 16px 0 16px 16px;
    overflow: hidden;
}

.course-list {
    padding-right: 16px;
}

.aside-header-img {
    width: 355px;
    cursor: pointer;
}

.aside-footer {
    display: flex;
    align-items: center;
    padding: 15px 16px;
    border-top: 1px solid var(--el-border-color-light);
}

.new-course-btn {
    margin-right: 16px !important;
}
</style>

<style>
.course-card.is-selected {
    position: sticky;
    top: 0;
    bottom: 0;
}

.aside-drawer {
    width: 355px !important;
}

.aside-drawer .el-drawer__body {
    padding: 16px;
}

.aside-drawer .el-drawer__footer {
    padding: 15px 16px;
    display: flex;
    align-items: center;
}
</style>


================================================
FILE: src/views/MoocCourseDetail.vue
================================================
<script setup lang="ts">
import type { course, test } from "@/type/mooc"
import { watch, ref } from "vue"
import { useRoute, useRouter } from "vue-router"
import { useApiAccess } from "@/plugins/apiAccess"
import { ElMessage } from "element-plus";

const props = defineProps<{
    cid: number | string
    updateCurrentCourse: Function
    updateCurrentTest: Function
}>()

const [apiAccess, route, router] = [useApiAccess(), useRoute(), useRouter()]
const data = ref(<test[][]>new Array())

const courseDetailSetup = async (cid: number | string) => {
    if (!/[0-9]+/g.test(String(cid))) {
        ElMessage.error("id错误")
        router.replace({ name: "Home" })
    }
    const res = (await apiAccess("getCourseDetail", { cid }, undefined)).data
    const [testList, course] = [res.testList, res.course]
    props.updateCurrentCourse(course)
    props.updateCurrentTest(null)
    const len = testList.length
    let chapterIndex = -1
    data.value.length = 0
    for (let i = 0; i < len; i++) {
        if (i === 0 || testList[i].chapterId !== testList[i - 1].chapterId) {
            chapterIndex++
            data.value.push(new Array())
        }
        data.value[chapterIndex].push(testList[i])
    }
}

courseDetailSetup(props.cid)

watch(
    () => route.params,
    async (toParams) => {
        if (toParams["cid"]) {
            courseDetailSetup(<string>toParams["cid"])
        }
    }
)

const onTestClick = (test: test) => router.push({ path: `/mooc/test/${test.id}` })
const sizeCount = () => window.innerWidth < 768 ? 'small' : 'default'
</script>

<template>
    <ElScrollbar v-if="data" class="course-detail" :always="true">
        <div class="chapter" v-for="chapter in data">
            <div class="chapter__title">{{ chapter[0].chapterName }}</div>
            <div class="chapter__detail">
                <div class="test" v-for="test in chapter">
                    <ElLink class="test__title" :underline="false" @click="onTestClick(test)">{{ test.name }}</ElLink>
                    <br class="hidden-sm-and-up"/>
                    <ElTag class="test__release-time" type="success" :size="sizeCount()">{{ test.releaseTime }}</ElTag>
                    <ElTag class="test__deadline" type="danger" :size="sizeCount()">{{ test.deadline }}</ElTag>
                </div>
            </div>
        </div>
    </ElScrollbar>
</template>

<style scoped>
.course-detail {
    margin: 20px;
    width: calc(100% - 40px);
    height: calc(100% - 40px);
    background-color: var(--el-bg-color);
    box-shadow: var(--el-box-shadow-light);
}
.chapter {
    max-width: 900px;
    margin: 40px 0 0 40px;
    padding-right: 40px;
    color: var(--el-text-color-primary);
}
.chapter:last-child {
    margin-bottom: 40px;
}
.chapter__title {
    padding-left: 8px;
    border-bottom: 1px solid var(--el-border-color);
    font-size: 18px;
    line-height: 1.5;
}
.chapter__detail {
    padding: 0 20px 0 40px;
}
.test {
    margin-top: 12px;
    line-height: 28px;
    vertical-align: middle;
}
.test__title {
    font-size: 16px;
    margin-bottom: 4px;
    transition: all 0.2s;
    line-height: 1.5;
}
.test__release-time {
    margin-left: 20px;
}
.test__deadline {
    margin-left: 12px;
}
.test__release-time,
.test__deadline {
    transition: all 0s;
}

@media screen and (max-width: 768px) {
    .course-detail {
        margin: 0;
        width: 100%;
        height: 100%;
    }
    .course-detail
    .chapter {
        margin: 16px 0 0 16px;
        padding-right: 16px;
    }
    .chapter__title {
        font-size: 16px;
    }
    .chapter__detail {
        padding: 0 8px 0 16px;
    }
    .test {
        margin-top: 8px;
        line-height: 24px;
    }
    .test__title {
        font-size: 14px;
        margin-bottom: 2px;
    }
    .test__release-time {
        margin-left: 16px;
    }
    .test__deadline {
        margin-left: 8px;
    }
    .test__release-time {
        margin-left: 0;
    }
}
</style>


================================================
FILE: src/views/MoocHeader.vue
================================================
<script setup lang="ts">
import { ref } from "vue"
import { Extension } from "@/components/icon"
import { useApiAccess } from "@/plugins/apiAccess"
import type { notice } from "@/type/mooc"

const defaultColor = "var(--el-color-primary-dark-2)"
const hoverColor = "var(--el-color-primary)"
const color = [defaultColor, hoverColor]
const extensionBtnColor = ref(0)
const extensionDialogVisible = ref(false)
const extensionCollapseValue = ref("1")

const sizeCount = () => window.innerWidth < 768 ? 20 : 25

const apiAccess = useApiAccess()
const data = ref<notice>()
const getNotice = async () => {
    data.value = (await apiAccess("getNotice", { version: 'v1.0.0' }, undefined)).data
}

getNotice()
</script>

<template>
    <div class="mooc-header">
        <div class="mooc-notice"><span class="hidden-xs-only">{{ data?.content }}</span></div>
        <div class="mooc-function">
            <ElIcon :color="color[extensionBtnColor % 2]" :size="sizeCount()" @mouseenter="extensionBtnColor++"
                @mouseleave="extensionBtnColor++" @click="extensionDialogVisible = true" style="cursor: pointer;">
                <Extension style="margin: 0;" />
            </ElIcon>
        </div>
    </div>
    <ElDialog v-model="extensionDialogVisible">
        <template #header>GinsMooc Extension Helper</template>
        <ElCollapse accordion style="line-height: 1.5;" v-model="extensionCollapseValue">
            <ElCollapseItem title="简介" name="1">
                <p>实现对于中国大学MOOC的</p>
                <ul>
                    <li>非在线测评题的自动答案查询,包括单选题、多选题、判断题、填空题、简答题,支持测验与作业及考试</li>
                    <li>互评阶段的自动评分、自动点评</li>
                </ul>
                <p>下载地址:<ElLink href="/download/GinsMoocExtension_v2.2.1.zip">
                        https://ginnnnnn.top/download/GinsMoocExtension_v2.2.1.zip
                    </ElLink>
                </p>
            </ElCollapseItem>
            <ElCollapseItem title="功能介绍" name="2">
                <p>在测试的准备页面,将会自动检查是否准备就绪,若为否将自动更新课程</p>
                <ElImage class="extension-image" src="/extension-updating.png"></ElImage>
                <p>进入测验后,将显示“获取答案”按钮,点击即可</p>
                <ElImage class="extension-image" src="/extension-single-choice.png"></ElImage>
                <ElImage class="extension-image" src="/extension-multiple-choice.png"></ElImage>
                <ElImage class="extension-image" src="/extension-completion.png"></ElImage>
                <ElImage class="extension-image" src="/extension-homework.png"></ElImage>
                <p>作业的互评阶段支持自动评分、自动点评</p>
                <ElImage class="extension-image" src="/extension-auto-evaluate-1.png"></ElImage>
                <ElImage class="extension-image" src="/extension-auto-evaluate-2.png"></ElImage>
            </ElCollapseItem>
            <ElCollapseItem title="安装介绍" name="3">
                <p>下载安装包后,将其解压至文件夹内</p>
                <p>在浏览器地址栏中输入edge://extensions(谷歌浏览器为chrome://extensions)</p>
                <p>打开开发者模式</p>
                <ElImage class="extension-image" src="/extension-developer-mode.png"></ElImage>
                <p>点击“加载解压缩的扩展”,选择刚刚解压到的文件夹,即可开始使用</p>
                <ElImage class="extension-image" src="/extension-load-decompression.png"></ElImage>
            </ElCollapseItem>
        </ElCollapse>
    </ElDialog>
</template>

<style scoped>
.mooc-header {
    display: flex;
    align-items: center;
}

.mooc-notice {
    flex: 1;
    text-align: center;
    color: var(--el-color-primary);
    font-size: 14px;
}

.mooc-function {
    flex: 0 !important;
    width: min-content;
    display: flex;
    align-items: center;
    justify-content: flex-end;
    padding-right: 8px;
}

@media screen and (max-width: 768px) {
    .mooc-function {
        padding-right: 0;
    }

}

.extension-image {
    box-shadow: var(--el-box-shadow-light);
    margin: 8px 0;
}
</style>


================================================
FILE: src/views/MoocTest.vue
================================================
<script setup lang="ts">
import { watch, ref } from "vue"
import { useApiAccess } from "@/plugins/apiAccess"
import { QuestionCard } from "@/components"
import { useRoute, useRouter } from "vue-router"
import type { homework, quiz } from "@/type/mooc"
import { ElMessage } from "element-plus"

interface question {
    data: quiz | homework
    order: number
}

const props = defineProps<{
    tid: number | string
    updateCurrentTest: Function
    updateCurrentCourse: Function
    search: string
}>()

const [apiAccess, route, router] = [useApiAccess(), useRoute(), useRouter()]
const [left, right, all] = [ref(<question[]>new Array()), ref(<question[]>new Array()), ref(<question[]>new Array())]
let [leftLength, rightLength, nextPage, totalPages] = [0, 0, 0, 0]
const loading = ref(false)
const disabled = () => loading.value || nextPage >= totalPages

const loadData = async (tid: number | string = props.tid) => {
    if (!/[0-9]+/g.test(String(tid))) {
        ElMessage.error("id错误")
        router.replace({ name: "Home" })
    }
    loading.value = true
    const res = (await apiAccess("getTestDetail", { tid: tid, page: nextPage++, search: props.search }, undefined)).data
    const [course, test, questionList, total] = [res.course, res.test, res.questionList, res.totalPages]
    if (nextPage === 1) {
        totalPages = total
        props.updateCurrentTest(test)
        props.updateCurrentCourse(course)
    }
    questionList.forEach((value, index) => {
        const order = (nextPage - 1) * 20 + index + 1
        all.value.push({ data: value, order: order })
        if (leftLength <= rightLength) {
            leftLength += value.toString().length
            left.value.push({ data: value, order: order })
        } else {
            rightLength += value.toString().length
            right.value.push({ data: value, order: order })
        }
    })
    loading.value = false
}
defineExpose({ loadData })

loadData()

watch(
    () => route.params,
    async (toParams) => {
        if (toParams["tid"]) {
            loadData(<string>toParams["tid"])
        }
    }
)
watch(
    () => props.search,
    () => {
        [leftLength, rightLength, nextPage, totalPages] = [0, 0, 0, 0]
        left.value = new Array()
        right.value = new Array()
        all.value = new Array()
        loadData()
    }
)

var lastScrollTop = 0;
var moocHeader = document.getElementsByClassName('mooc-main-header').item(0) as HTMLElement;
var header = document.getElementsByClassName('header').item(0) as HTMLElement;

const scrollCallback = (arg: {scrollLeft: number, scrollTop: number}) => {
    if (window.innerWidth >= 768) return
    if (!moocHeader) moocHeader = document.getElementsByClassName('mooc-main-header').item(0) as HTMLElement
    if (!header) header = document.getElementsByClassName('header').item(0) as HTMLElement
    if (arg.scrollTop > lastScrollTop) {
        moocHeader.style.display = 'none';
        header.style.display = 'none';
    } else {
        moocHeader.style.display = 'flex';
        header.style.display = 'block';
    }
    lastScrollTop = arg.scrollTop;
}
</script>

<template>
    <ElScrollbar class="test-detail" :always="true" @scroll="scrollCallback" :noresize="true">
        <div v-infinite-scroll="loadData" :infinite-scroll-disabled="disabled()" :infinite-scroll-distance="500">
            <ElRow :gutter="20" style="margin: 0 10px 20px 10px" class="hidden-md-and-down">
                <ElCol :span="12">
                    <QuestionCard v-for="question in left" :data="question.data" :order="question.order"></QuestionCard>
                </ElCol>
                <ElCol :span="12">
                    <QuestionCard
                        v-for="question in right"
                        :data="question.data"
                        :order="question.order"
                    ></QuestionCard>
                </ElCol>
            </ElRow>
            <ElCol class="hidden-lg-and-up question-card-small">
                <QuestionCard v-for="question in all" :data="question.data" :order="question.order"></QuestionCard>
            </ElCol>
        </div>
    </ElScrollbar>
</template>

<style scoped>
.test-detail {
    padding-right: 6px;
}

.question-card-small {
    margin: 0 20px 20px 20px
}

@media only screen and (max-width: 768px) {
    .question-card-small {
        margin: 0 0;
    }
}
</style>


================================================
FILE: src/views/MoocView.vue
================================================
<script setup lang="ts">
import { watch, type Ref } from "vue"
import type { course, test } from "@/type/mooc"
import { ref, computed } from "vue"
import { MoocAside } from "@/views"
import { useRoute } from "vue-router"
import { Menu, Search, ArrowLeftBold } from "@element-plus/icons-vue"
import router from "@/router"

const currentCourse: Ref<course> = ref(<course>new Object())
const currentTest: Ref<test> = ref(<test>new Object())
const route = useRoute()
const aside = ref()
const quizQuery = ref("")

const mainHeaderTitle = computed(() => {
    let ret = ""
    if (currentCourse.value && currentCourse.value.name) {
        ret += currentCourse.value.name
    }
    if (currentTest.value && currentTest.value.name) {
        ret += ret ? " - " : ""
        ret += currentTest.value.name
    }
    return ret
})
const emptyContent = computed(() => !(currentCourse.value.id || currentTest.value.id))

const updateCurrentCourse = (course: course) => (currentCourse.value = course)
const updateCurrentTest = (test: test) => (currentTest.value = test)
const setAllAnswer = () => {
    const questions = <HTMLCollection>document.getElementsByClassName("question-card-header")
    for (const item of questions) {
        ; (<HTMLElement>item).click()
    }
}
const onCourseBack = () => router.push({ path: `/mooc/course/${currentCourse.value.id}` })
const defaultColor = "var(--el-color-primary-dark-2)"
const hoverColor = "var(--el-color-primary)"
const color = [defaultColor, hoverColor]
const [menuBtnColor, backBtnColor] = [ref(0), ref(0)]

watch(
    () => route.params,
    (toParams) => {
        if (toParams["cid"] === undefined && toParams["tid"] === undefined) {
            updateCurrentCourse(<course>new Object())
            updateCurrentTest(<test>new Object())
        }
        quizQuery.value = ""
    }
)
</script>

<template>
    <ElContainer>
        <ElAside class="mooc-aside" width="355px" :class="emptyContent ? '' : 'hidden-lg-only hidden-sm-and-down'">
            <Suspense>
                <MoocAside :current-course="currentCourse" ref="aside"></MoocAside>
            </Suspense>
        </ElAside>
        <ElMain class="mooc-main">
            <ElContainer>
                <ElHeader class="mooc-main-header">
                    <ElIcon :color="color[menuBtnColor % 2]" :size="22" @click="aside?.showDrawer"
                        @mouseenter="menuBtnColor++" @mouseleave="menuBtnColor++"
                        :style="emptyContent ? 'display: none;' : ''" class="hidden-xl-only hidden-md-only icon-btn">
                        <Menu></Menu>
                    </ElIcon>
                    <ElIcon :color="color[backBtnColor % 2]" :size="20" @click="onCourseBack"
                        v-if="currentTest && currentTest.name" @mouseenter="backBtnColor++" @mouseleave="backBtnColor++"
                        class="icon-btn">
                        <ArrowLeftBold />
                    </ElIcon>
                    <span style="flex: 1" class="mooc-header-title">
                        <span :class="{ 'hidden-xs-only': currentTest && currentTest.name }"
                            v-if="currentCourse && currentCourse.name">{{ currentCourse.name }}</span>
                        <template v-if="currentTest && currentTest.name">
                            <span class="hidden-xs-only"> - </span>
                            <span>{{ currentTest.name }}</span>
                        </template>
                    </span>
                    <template v-if="currentTest && currentTest.objective">
                        <ElInput v-model="quizQuery" :suffix-icon="Search" placeholder="按题目搜索,图片以“【图片】”代替"
                            style="width: 400px" class="hidden-xs-only"></ElInput>
                        <ElButton class="display-answer hidden-xs-only" type="primary" round plain @click="setAllAnswer"
                            style="margin-left: 16px">显示答案</ElButton>
                    </template>
                </ElHeader>
                <div class="hidden-sm-and-up small-function-bar" v-if="currentTest && currentTest.objective">
                    <ElInput v-model="quizQuery" :suffix-icon="Search" placeholder="按题目搜索,图片以“【图片】”代替" style="width: 400px"
                        size="small"></ElInput>
                    <ElButton class="display-answer" type="primary" round plain @click="setAllAnswer"
                        style="margin-left: 16px" size="small">显示答案</ElButton>
                </div>
                <div class="split-line hidden-sm-and-up"></div>
                <ElMain class="mooc-main-body" style="height: calc(100vh - 100px)">
                    <RouterView :update-current-test="updateCurrentTest" :update-current-course="updateCurrentCourse"
                        :search="quizQuery"></RouterView>
                </ElMain>
            </ElContainer>
        </ElMain>
    </ElContainer>
</template>

<style scoped>
.mooc-aside {
    border-right: 1px solid var(--el-border-color-light);
    overflow: hidden;
    background-color: var(--el-bg-color);
}

.mooc-main {
    padding: 0;
}

.mooc-main-header {
    height: 50px;
    display: flex;
    align-items: center;
    padding: 0 20px;
    background-color: var(--el-bg-color);
    font-weight: 700;
    font-size: 18px;
    color: var(--el-text-color-primary);
}

@media only screen and (max-width: 768px) {
    .mooc-main-header {
        height: 40px;
        padding: 0 16px;
        font-size: 16px;
    }
}

.mooc-header-title {
    flex: 1;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.small-function-bar {
    display: flex;
    align-items: center;
    padding: 0 16px;
    background-color: white;
    height: 42px;
}

.split-line {
    border-bottom: 1px solid var(--el-border-color-extra-light);
    box-shadow: 0 2px 4px rgba(0, 0, 0, .08);
}

.mooc-main-header .display-answer {
    transition: all var(--el-transition-duration);
}

.mooc-main-body {
    padding: 0;
    background-color: var(--el-fill-color-light);
    overflow: hidden;
}

.icon-btn {
    cursor: pointer;
    padding-right: 8px;
}
</style>


================================================
FILE: src/views/VideoView.vue
================================================
<script setup lang="ts">
import { ref, computed } from "vue"
const apiList = [
    { name: "JSONPlayer【有弹幕】", url: "https://jx.777jiexi.com/player/?url=" },
    { name: "CKPlayer", url: "https://www.ckplayer.vip/jiexi/?url=" },
    { name: "盘古", url: "https://www.pangujiexi.cc/jiexi.php?url=" },
    { name: "老板", url: "https://vip.laobandq.com/jiexi.php?url=" },
    { name: "OK", url: "https://okjx.cc/?url=" },
    { name: "8090", url: "https://www.8090.la/8090/?url=" },
    { name: "维多", url: "http://jx.ivito.cn/?url=" },
    { name: "CKMOV", url: "https://www.ckmov.vip/api.php?url=" },
    { name: "BL解析", url: "https://svip.bljiex.cc/?v=" },
    { name: "1717", url: "https://www.1717yun.com/1717yun/?url=" },
    { name: "M3U8", url: "https://dmjx.m3u8.tv/?url=" },
    { name: "云解析", url: "http://jx.ppflv.com/?url=" },
    { name: "虾米解析", url: "https://jx.xmflv.com/?url=" },
    { name: "JYPlayer", url: "https://jx.playerjy.com/?url=" }
]
const [apiUrl, videoUrl] = [ref(localStorage.getItem("GinsVideo-apiUrl")), ref("")]
const src = computed(() => apiUrl.value + videoUrl.value)
const setApiUrl = (url: string) => {
    localStorage.setItem("GinsVideo-apiUrl", url)
    apiUrl.value = url
}
</script>

<template>
    <ElContainer>
        <ElAside class="video-aside" width="305px">
            <ElScrollbar class="api-list">
                <div
                    class="api-item"
                    :class="{ 'is-selected': apiUrl === api.url }"
                    v-for="api in apiList"
                    @click="setApiUrl(api.url)"
                >
                    {{ api.name }}
                </div>
            </ElScrollbar>
        </ElAside>
        <ElMain class="video-main">
            <ElInput v-model="videoUrl" class="video-input" size="large" placeholder="将视频地址复制到这里"></ElInput>
            <iframe
                v-if="apiUrl && videoUrl"
                :src="src"
                height="675px"
                frameborder="0"
                allowfullscreen="true"
            ></iframe>
            <ElEmpty v-else class="video-mask" description="您还未选择接口或输入视频地址"></ElEmpty>
            <div class="disclaimers">
                <span>本系统只为内部交流学习,不以盈利为目的</span>
                <span>所有资源均来源第三方资源,并不提供影片资源存储,录制、上传相关视频等,视频版权归属其合法持有人所有,本站不对使用者的行为负担任何法律责任</span>
                <span>如果有因为本站而导致您的权益受到损害,请与我们联系,我们将理性对待,协助你解决相关问题</span>
            </div>
        </ElMain>
    </ElContainer>
</template>

<style scoped>
.video-aside {
    border-right: 1px solid var(--el-border-color-light);
    overflow: hidden;
    background-color: var(--el-bg-color);
    padding: 8px;
}
.video-main {
    display: flex;
    flex-direction: column;
    background-color: var(--el-bg-color);
}
.video-input {
    margin-bottom: 20px;
}
.video-mask {
    padding: 0;
    height: 675px;
    background-color: var(--el-empty-fill-color-3);
}
.api-item {
    padding: 8px 16px;
    margin-bottom: 8px;
    font-size: 16px;
    line-height: 1.5;
    border-radius: 8px;
    background-color: var(--el-color-primary-light-8);
    color: var(--el-menu-text-color);
    transition: all 0.2s;
    cursor: pointer;
}
.api-item:not(.is-selected):hover,
.api-item.is-selected {
    background-color: var(--el-color-primary-light-5);
}
.api-item:active {
    transform: scale(0.98);
}
.disclaimers {
    flex: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: flex-end;
    margin-top: 20px;
    line-height: 1.5;
    font-size: 12px;
    color: var(--el-text-color-secondary);
}
</style>


================================================
FILE: src/views/index.ts
================================================
import HomeView from "./HomeView.vue"
import MoocView from "./MoocView.vue"
import MoocHeader from "./MoocHeader.vue"
import MoocAside from "./MoocAside.vue"
import MoocCourseDetail from "./MoocCourseDetail.vue"
import MoocTest from "./MoocTest.vue"
import VideoView from "./VideoView.vue"

export { HomeView, MoocView, MoocHeader, MoocAside, MoocCourseDetail, MoocTest, VideoView }

================================================
FILE: tsconfig.config.json
================================================
{
    "extends": "@vue/tsconfig/tsconfig.node.json",
    "include": [
        "vite.config.*",
        "vitest.config.*",
        "cypress.config.*",
        "playwright.config.*"
    ],
    "compilerOptions": {
        "composite": true,
        "types": [
            "node"
        ]
    }
}

================================================
FILE: tsconfig.json
================================================
{
    "extends": "@vue/tsconfig/tsconfig.web.json",
    "include": [
        "env.d.ts",
        "src/**/*",
        "src/**/*.vue"
    ],
    "compilerOptions": {
        "baseUrl": ".",
        "paths": {
            "@/*": [
                "./src/*"
            ]
        },
        "types": ["element-plus/global"]
    },
    "references": [
        {
            "path": "./tsconfig.config.json"
        }
    ]
}

================================================
FILE: vite.config.ts
================================================
import { fileURLToPath, URL } from "node:url"

import { defineConfig } from "vite"
import vue from "@vitejs/plugin-vue"

// https://vitejs.dev/config/
export default defineConfig({
    plugins: [ vue() ],
    resolve: {
        alias: {
            "@": fileURLToPath(new URL("./src", import.meta.url))
        }
    }
})
Download .txt
gitextract_klorelqm/

├── .eslintrc.cjs
├── .gitignore
├── .prettierrc.json
├── .vscode/
│   └── extensions.json
├── README.md
├── env.d.ts
├── extension/
│   ├── .eslintrc.cjs
│   ├── .prettierrc.json
│   ├── README.md
│   ├── env.d.ts
│   ├── index.html
│   ├── package.json
│   ├── release/
│   │   ├── content-scripts/
│   │   │   └── index-ad710f80.js
│   │   └── manifest.json
│   ├── src/
│   │   ├── main.ts
│   │   ├── newExam.ts
│   │   ├── plugins/
│   │   │   ├── apiAccess.ts
│   │   │   ├── mooc.ts
│   │   │   ├── react.ts
│   │   │   └── tool.ts
│   │   └── type/
│   │       ├── api.ts
│   │       └── mooc.ts
│   ├── tsconfig.config.json
│   ├── tsconfig.json
│   └── vite.config.ts
├── index.html
├── package.json
├── public/
│   ├── background.html
│   ├── css/
│   │   ├── main.css
│   │   └── noscript.css
│   ├── guess.java
│   └── sass/
│       ├── libs/
│       │   ├── _breakpoints.scss
│       │   ├── _functions.scss
│       │   ├── _mixins.scss
│       │   ├── _vars.scss
│       │   └── _vendor.scss
│       ├── main.scss
│       └── noscript.scss
├── src/
│   ├── App.vue
│   ├── components/
│   │   ├── CourseCard.vue
│   │   ├── QuestionCard.vue
│   │   ├── icon/
│   │   │   ├── Extension.vue
│   │   │   ├── Github.vue
│   │   │   └── index.ts
│   │   ├── index.ts
│   │   └── question/
│   │       ├── Completion.vue
│   │       ├── Homework.vue
│   │       ├── MultipleChoice.vue
│   │       ├── OnlineJudge.vue
│   │       ├── SingleChoice.vue
│   │       └── index.ts
│   ├── main.ts
│   ├── plugins/
│   │   ├── apiAccess.ts
│   │   └── tool.ts
│   ├── router/
│   │   └── index.ts
│   ├── type/
│   │   ├── api.ts
│   │   ├── globleProperties.ts
│   │   └── mooc.ts
│   └── views/
│       ├── BlogView.vue
│       ├── HomeView.vue
│       ├── MoocAside.vue
│       ├── MoocCourseDetail.vue
│       ├── MoocHeader.vue
│       ├── MoocTest.vue
│       ├── MoocView.vue
│       ├── VideoView.vue
│       └── index.ts
├── tsconfig.config.json
├── tsconfig.json
└── vite.config.ts
Download .txt
SYMBOL INDEX (144 symbols across 10 files)

FILE: extension/release/content-scripts/index-ad710f80.js
  function n (line 1) | function n(s){const o={};return s.integrity&&(o.integrity=s.integrity),s...
  function r (line 1) | function r(s){if(s.ep)return;s.ep=!0;const o=n(s);fetch(s.href,o)}
  class dt (line 1) | class dt extends Array{constructor(){super();k(this,"id");k(this,"node")...
    method constructor (line 1) | constructor(){super();k(this,"id");k(this,"node");this.node=document.c...
    method add (line 1) | add(n){const r=new ft(this,n);return super.push(r),r}
  class ft (line 1) | class ft{constructor(t,n){k(this,"id");k(this,"node");k(this,"parent");k...
    method constructor (line 1) | constructor(t,n){k(this,"id");k(this,"node");k(this,"parent");k(this,"...
    method get (line 1) | get(){return this.value}
    method set (line 1) | set(t){if(this.value!==t){const n=this.value;this.value=t,this.node.di...
    method addEventListenr (line 1) | addEventListenr(t,n){this.node.addEventListener(t,n)}
  function je (line 1) | function je(e,t){return function(){return e.apply(t,arguments)}}
  function ht (line 1) | function ht(e){return e!==null&&!$(e)&&e.constructor!==null&&!$(e.constr...
  function mt (line 1) | function mt(e){let t;return typeof ArrayBuffer<"u"&&ArrayBuffer.isView?t...
  function z (line 1) | function z(e,t,{allOwnKeys:n=!1}={}){if(e===null||typeof e>"u")return;le...
  function qe (line 1) | function qe(e,t){t=t.toLowerCase();const n=Object.keys(e);let r=n.length...
  function le (line 1) | function le(){const{caseless:e}=ve(this)&&this||{},t={},n=(r,s)=>{const ...
  function $t (line 1) | function $t(e){return!!(e&&S(e.append)&&e[Symbol.toStringTag]==="FormDat...
  function m (line 1) | function m(e,t,n,r,s){Error.call(this),Error.captureStackTrace?Error.cap...
  function ue (line 1) | function ue(e){return a.isPlainObject(e)||a.isArray(e)}
  function Je (line 1) | function Je(e){return a.endsWith(e,"[]")?e.slice(0,-2):e}
  function xe (line 1) | function xe(e,t,n){return e?e.concat(t).map(function(s,o){return s=Je(s)...
  function Kt (line 1) | function Kt(e){return a.isArray(e)&&!e.some(ue)}
  function re (line 1) | function re(e,t,n){if(!a.isObject(e))throw new TypeError("target must be...
  function Oe (line 1) | function Oe(e){const t={"!":"%21","'":"%27","(":"%28",")":"%29","~":"%7E...
  function me (line 1) | function me(e,t){this._pairs=[],e&&re(e,this,t)}
  function Qt (line 1) | function Qt(e){return encodeURIComponent(e).replace(/%3A/gi,":").replace...
  function Ge (line 1) | function Ge(e,t,n){if(!t)return e;const r=n&&n.encode||Qt,s=n&&n.seriali...
  class Xt (line 1) | class Xt{constructor(){this.handlers=[]}use(t,n,r){return this.handlers....
    method constructor (line 1) | constructor(){this.handlers=[]}
    method use (line 1) | use(t,n,r){return this.handlers.push({fulfilled:t,rejected:n,synchrono...
    method eject (line 1) | eject(t){this.handlers[t]&&(this.handlers[t]=null)}
    method clear (line 1) | clear(){this.handlers&&(this.handlers=[])}
    method forEach (line 1) | forEach(t){a.forEach(this.handlers,function(r){r!==null&&t(r)})}
  function on (line 1) | function on(e,t){return re(e,new x.classes.URLSearchParams,Object.assign...
  function an (line 1) | function an(e){return a.matchAll(/\w+|\[(\w*)]/g,e).map(t=>t[0]==="[]"?"...
  function cn (line 1) | function cn(e){const t={},n=Object.keys(e);let r;const s=n.length;let o;...
  function Qe (line 1) | function Qe(e){function t(n,r,s,o){let i=n[o++];if(i==="__proto__")retur...
  function ln (line 1) | function ln(e,t,n){if(a.isString(e))try{return(t||JSON.parse)(e),a.trim(...
  function H (line 2) | function H(e){return e&&String(e).trim().toLowerCase()}
  function G (line 2) | function G(e){return e===!1||e==null?e:a.isArray(e)?e.map(G):String(e)}
  function fn (line 2) | function fn(e){const t=Object.create(null),n=/([^\s,;=]+)\s*(?:=\s*([^,;...
  function ie (line 2) | function ie(e,t,n,r,s){if(a.isFunction(r))return r.call(this,t,n);if(s&&...
  function hn (line 2) | function hn(e){return e.trim().toLowerCase().replace(/([a-z\d])(\w*)/g,(...
  function mn (line 2) | function mn(e,t){const n=a.toCamelCase(" "+t);["get","set","has"].forEac...
  method constructor (line 2) | constructor(t){t&&this.set(t)}
  method set (line 2) | set(t,n,r){const s=this;function o(c,d,f){const u=H(d);if(!u)throw new E...
  method get (line 2) | get(t,n){if(t=H(t),t){const r=a.findKey(this,t);if(r){const s=this[r];if...
  method has (line 2) | has(t,n){if(t=H(t),t){const r=a.findKey(this,t);return!!(r&&this[r]!==vo...
  method delete (line 2) | delete(t,n){const r=this;let s=!1;function o(i){if(i=H(i),i){const c=a.f...
  method clear (line 2) | clear(t){const n=Object.keys(this);let r=n.length,s=!1;for(;r--;){const ...
  method normalize (line 2) | normalize(t){const n=this,r={};return a.forEach(this,(s,o)=>{const i=a.f...
  method concat (line 2) | concat(...t){return this.constructor.concat(this,...t)}
  method toJSON (line 2) | toJSON(t){const n=Object.create(null);return a.forEach(this,(r,s)=>{r!=n...
  method [Symbol.iterator] (line 2) | [Symbol.iterator](){return Object.entries(this.toJSON())[Symbol.iterator...
  method toString (line 2) | toString(){return Object.entries(this.toJSON()).map(([t,n])=>t+": "+n).j...
  method [Symbol.toStringTag] (line 3) | get[Symbol.toStringTag](){return"AxiosHeaders"}
  method from (line 3) | static from(t){return t instanceof this?t:new this(t)}
  method concat (line 3) | static concat(t,...n){const r=new this(t);return n.forEach(s=>r.set(s)),r}
  method accessor (line 3) | static accessor(t){const r=(this[Ae]=this[Ae]={accessors:{}}).accessors,...
  method set (line 3) | set(r){this[n]=r}
  function ae (line 3) | function ae(e,t){const n=this||we,r=t||n,s=R.from(r.headers);let o=r.dat...
  function Xe (line 3) | function Xe(e){return!!(e&&e.__CANCEL__)}
  function J (line 3) | function J(e,t,n){m.call(this,e??"canceled",m.ERR_CANCELED,t,n),this.nam...
  function yn (line 3) | function yn(e,t,n){const r=n.config.validateStatus;!n.status||!r||r(n.st...
  method write (line 3) | write(e,t,n,r,s,o){const i=[e+"="+encodeURIComponent(t)];a.isNumber(n)&&...
  method read (line 3) | read(e){const t=document.cookie.match(new RegExp("(^|;\\s*)("+e+")=([^;]...
  method remove (line 3) | remove(e){this.write(e,"",Date.now()-864e5)}
  method write (line 3) | write(){}
  method read (line 3) | read(){return null}
  method remove (line 3) | remove(){}
  function En (line 3) | function En(e){return/^([a-z][a-z\d+\-.]*:)?\/\//i.test(e)}
  function gn (line 3) | function gn(e,t){return t?e.replace(/\/?\/$/,"")+"/"+t.replace(/^\/+/,""...
  function Ye (line 3) | function Ye(e,t){return e&&!En(t)?gn(e,t):t}
  function s (line 3) | function s(o){let i=o;return t&&(n.setAttribute("href",i),i=n.href),n.se...
  function Sn (line 3) | function Sn(e){const t=/^([-+\w]{1,25})(:?\/\/|:)/.exec(e);return t&&t[1...
  function xn (line 3) | function xn(e,t){e=e||10;const n=new Array(e),r=new Array(e);let s=0,o=0...
  function Re (line 3) | function Re(e,t){let n=0;const r=xn(50,250);return s=>{const o=s.loaded,...
  function f (line 3) | function f(){e.cancelToken&&e.cancelToken.unsubscribe(d),e.signal&&e.sig...
  function b (line 3) | function b(){if(!l)return;const h=R.from("getAllResponseHeaders"in l&&l....
  function ce (line 5) | function ce(e){if(e.cancelToken&&e.cancelToken.throwIfRequested(),e.sign...
  function Ce (line 5) | function Ce(e){return ce(e),e.headers=R.from(e.headers),e.data=ae.call(e...
  function _ (line 5) | function _(e,t){t=t||{};const n={};function r(f,u,l){return a.isPlainObj...
  function s (line 5) | function s(o,i){return"[Axios v"+et+"] Transitional option '"+o+"'"+i+(r...
  function Rn (line 5) | function Rn(e,t,n){if(typeof e!="object")throw new m("options must be an...
  method constructor (line 5) | constructor(t){this.defaults=t,this.interceptors={request:new Te,respons...
  method request (line 5) | async request(t,n){try{return await this._request(t,n)}catch(r){if(r ins...
  method _request (line 6) | _request(t,n){typeof t=="string"?(n=n||{},n.url=t):n=t||{},n=_(this.defa...
  method getUri (line 6) | getUri(t){t=_(this.defaults,t);const n=Ye(t.baseURL,t.url);return Ge(n,t...
  function n (line 6) | function n(r){return function(o,i,c){return this.request(_(c||{},{method...
  method constructor (line 6) | constructor(t){if(typeof t!="function")throw new TypeError("executor mus...
  method throwIfRequested (line 6) | throwIfRequested(){if(this.reason)throw this.reason}
  method subscribe (line 6) | subscribe(t){if(this.reason){t(this.reason);return}this._listeners?this....
  method unsubscribe (line 6) | unsubscribe(t){if(!this._listeners)return;const n=this._listeners.indexO...
  method source (line 6) | static source(){let t;return{token:new tt(function(s){t=s}),cancel:t}}
  function Ln (line 6) | function Ln(e){return function(n){return e.apply(null,n)}}
  function Pn (line 6) | function Pn(e){return a.isObject(e)&&e.isAxiosError===!0}
  function nt (line 6) | function nt(e){const t=new K(e),n=je(K.prototype.request,t);return a.ext...
  function _n (line 6) | async function _n(e,t,n){try{return await new Promise((r,s)=>{let o=ke[e...

FILE: extension/src/plugins/apiAccess.ts
  function apiAccess (line 17) | async function apiAccess<T extends ApiKeyType>(

FILE: extension/src/plugins/react.ts
  class CustomRefList (line 3) | class CustomRefList extends Array<CustomRef<any>> {
    method constructor (line 7) | constructor() {
    method add (line 15) | add<T>(value: T): CustomRef<T> {
  class CustomRef (line 22) | class CustomRef<T> {
    method constructor (line 28) | constructor(parent: CustomRefList, value: T) {
    method get (line 37) | get(): T {
    method set (line 41) | set(value: T): void {
    method addEventListenr (line 50) | addEventListenr(eventName: "change" | "set", callback: (ev: Event) => ...

FILE: extension/src/type/api.ts
  type RequestType (line 3) | type RequestType = {
  type Response (line 8) | type Response<T = any> = {
  type ApiKeyType (line 37) | type ApiKeyType = keyof typeof apiInfo
  type ApiResponseType (line 39) | interface ApiResponseType {
  type ApiRequestType (line 59) | interface ApiRequestType {

FILE: extension/src/type/mooc.ts
  type QuestionTypeEnumList (line 1) | enum QuestionTypeEnumList {
  type course (line 10) | interface course extends Object {
  type test (line 17) | interface test extends Object {
  type option (line 27) | interface option extends Object {
  type quiz (line 33) | interface quiz extends Object {
  type homework (line 45) | interface homework extends Object {
  type notice (line 55) | interface notice extends Object {
  type QuestionTypeEnum (line 60) | type QuestionTypeEnum = typeof QuestionTypeEnumList

FILE: public/guess.java
  class GameServiceImpl (line 13) | @Service
    method guess (line 21) | public HashMap<String, Object> guess(String token, Double guess) {
    method getState (line 58) | public HashMap<String, Object> getState(String token) {
    method refresh (line 64) | public boolean refresh(String token) {
    method init (line 76) | private void init(String key) {
    method infoBuild (line 83) | private HashMap<String, Object> infoBuild(String key) {

FILE: src/plugins/apiAccess.ts
  function apiAccess (line 16) | async function apiAccess<T extends ApiKeyType>(

FILE: src/type/api.ts
  type RequestType (line 3) | type RequestType = {
  type Response (line 8) | type Response<T = any> = {
  type courseList (line 14) | interface courseList extends Object {
  type courseDetail (line 20) | interface courseDetail extends Object {
  type testDetail (line 25) | interface testDetail extends Object {
  type ApiKeyType (line 51) | type ApiKeyType = keyof typeof apiInfo
  type ApiResponseType (line 53) | interface ApiResponseType {
  type ApiRequestType (line 60) | interface ApiRequestType {

FILE: src/type/globleProperties.ts
  type ComponentCustomProperties (line 5) | interface ComponentCustomProperties {

FILE: src/type/mooc.ts
  type QuestionTypeEnumList (line 1) | enum QuestionTypeEnumList {
  type course (line 10) | interface course extends Object {
  type test (line 17) | interface test extends Object {
  type option (line 27) | interface option extends Object {
  type quiz (line 33) | interface quiz extends Object {
  type homework (line 45) | interface homework extends Object {
  type notice (line 55) | interface notice extends Object {
  type QuestionTypeEnum (line 60) | type QuestionTypeEnum = typeof QuestionTypeEnumList
Condensed preview — 70 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (208K chars).
[
  {
    "path": ".eslintrc.cjs",
    "chars": 469,
    "preview": "/* eslint-env node */\nrequire(\"@rushstack/eslint-patch/modern-module-resolution\")\n\nmodule.exports = {\n    root: true,\n  "
  },
  {
    "path": ".gitignore",
    "chars": 314,
    "preview": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\n.DS_Stor"
  },
  {
    "path": ".prettierrc.json",
    "chars": 144,
    "preview": "{\n    \"tabWidth\": 4,\n    \"useTabs\": false,\n    \"semi\": false,\n    \"trailingComma\": \"none\",\n    \"bracketSameLine\": false,"
  },
  {
    "path": ".vscode/extensions.json",
    "chars": 75,
    "preview": "{\n  \"recommendations\": [\"Vue.volar\", \"Vue.vscode-typescript-vue-plugin\"]\n}\n"
  },
  {
    "path": "README.md",
    "chars": 891,
    "preview": "# 不支持 spoc\n\n# 反馈所需信息\n\n反馈课程更新问题,需要提新 issue,带上课程 id、课程链接和测验截止时间,我会在截止时间前修,上班太忙了\n\n# 插件简介\n\n实现对于中国大学MOOC的\n- 非在线测评题的自动答案查询,包括单"
  },
  {
    "path": "env.d.ts",
    "chars": 38,
    "preview": "/// <reference types=\"vite/client\" />\n"
  },
  {
    "path": "extension/.eslintrc.cjs",
    "chars": 347,
    "preview": "/* eslint-env node */\nrequire('@rushstack/eslint-patch/modern-module-resolution')\n\nmodule.exports = {\n    root: true,\n  "
  },
  {
    "path": "extension/.prettierrc.json",
    "chars": 144,
    "preview": "{\n    \"tabWidth\": 4,\n    \"useTabs\": false,\n    \"semi\": false,\n    \"trailingComma\": \"none\",\n    \"bracketSameLine\": false,"
  },
  {
    "path": "extension/README.md",
    "chars": 1689,
    "preview": "# GinsMooc Extention\n\nThis template should help get you started developing with Vue 3 in Vite.\n\n## Recommended IDE Setup"
  },
  {
    "path": "extension/env.d.ts",
    "chars": 38,
    "preview": "/// <reference types=\"vite/client\" />\n"
  },
  {
    "path": "extension/index.html",
    "chars": 331,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\">\n    <link rel=\"icon\" href=\"/favicon.ico\">\n    <meta"
  },
  {
    "path": "extension/package.json",
    "chars": 1116,
    "preview": "{\n    \"name\": \"ginsmooc-extention\",\n    \"version\": \"0.0.0\",\n    \"private\": true,\n    \"scripts\": {\n        \"dev\": \"vite\","
  },
  {
    "path": "extension/release/content-scripts/index-ad710f80.js",
    "chars": 40489,
    "preview": "var lt=Object.defineProperty;var ut=(e,t,n)=>t in e?lt(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n;v"
  },
  {
    "path": "extension/release/manifest.json",
    "chars": 716,
    "preview": "{\n    \"manifest_version\": 3,\n    \"name\": \"GinsMooc Extension\",\n    \"version\": \"2.2.1\",\n    \"description\": \"A Chrome exte"
  },
  {
    "path": "extension/src/main.ts",
    "chars": 7499,
    "preview": "import { CustomRefList } from \"./plugins/react\"\nimport { useApiAccess } from \"./plugins/apiAccess\"\nimport { sleep, getUr"
  },
  {
    "path": "extension/src/newExam.ts",
    "chars": 1622,
    "preview": "import { useApiAccess } from \"./plugins/apiAccess\"\nimport { sleep, getUrlParam } from \"./plugins/tool\"\n\nexport const new"
  },
  {
    "path": "extension/src/plugins/apiAccess.ts",
    "chars": 3040,
    "preview": "import type { ApiKeyType, ApiResponseType, ApiRequestType } from \"../type/api\"\nimport type { App } from \"vue\"\nimport { i"
  },
  {
    "path": "extension/src/plugins/mooc.ts",
    "chars": 4123,
    "preview": "import type { quiz, option } from \"../type/mooc\"\nimport { sleep, waitFor } from \"./tool\"\n\nconst getQuizQuestionKeys = ()"
  },
  {
    "path": "extension/src/plugins/react.ts",
    "chars": 1399,
    "preview": "import { randomString } from \"./tool\"\n\nclass CustomRefList extends Array<CustomRef<any>> {\n    id: string\n    node: HTML"
  },
  {
    "path": "extension/src/plugins/tool.ts",
    "chars": 1116,
    "preview": "const sleep = async (ms: number) => {\n    return new Promise((resolve) => {\n        setTimeout(() => {\n            resol"
  },
  {
    "path": "extension/src/type/api.ts",
    "chars": 1779,
    "preview": "import type { homework, quiz, notice } from \"./mooc\"\n\ntype RequestType = {\n    params?: Object\n    data?: Object\n}\n\ntype"
  },
  {
    "path": "extension/src/type/mooc.ts",
    "chars": 1376,
    "preview": "export enum QuestionTypeEnumList {\n    SingleChoice = \"SINGLE_CHOICE\",\n    MultipleChoice = \"MULTIPLE_CHOICE\",\n    Compl"
  },
  {
    "path": "extension/tsconfig.config.json",
    "chars": 294,
    "preview": "{\n    \"extends\": \"@vue/tsconfig/tsconfig.node.json\",\n    \"include\": [\n        \"vite.config.*\",\n        \"vitest.config.*\""
  },
  {
    "path": "extension/tsconfig.json",
    "chars": 377,
    "preview": "{\n    \"extends\": \"@vue/tsconfig/tsconfig.web.json\",\n    \"include\": [\n        \"env.d.ts\",\n        \"src/**/*\",\n        \"sr"
  },
  {
    "path": "extension/vite.config.ts",
    "chars": 371,
    "preview": "import { fileURLToPath, URL } from 'node:url'\n\nimport { defineConfig } from 'vite'\n\n// https://vitejs.dev/config/\nexport"
  },
  {
    "path": "index.html",
    "chars": 1023,
    "preview": "<!DOCTYPE html>\n<html lang=\"zh\">\n\n<head>\n    <meta charset=\"UTF-8\">\n    <link rel=\"icon\" href=\"/favicon.png\">\n    <meta "
  },
  {
    "path": "package.json",
    "chars": 1102,
    "preview": "{\n  \"name\": \"ginsmooc\",\n  \"version\": \"0.0.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"vite\",\n    \"build\": \"run-p t"
  },
  {
    "path": "public/background.html",
    "chars": 1376,
    "preview": "<!DOCTYPE html>\n<!--\n\tAerial by HTML5 UP\n\thtml5up.net | @ajlkn\n\tFree for personal and commercial use under the CCA 3.0 l"
  },
  {
    "path": "public/css/main.css",
    "chars": 22573,
    "preview": "@import url(\"https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,900\");\n@import url(\"fontawesome-all.min.css\");\n"
  },
  {
    "path": "public/css/noscript.css",
    "chars": 349,
    "preview": "/*\n\tAerial by HTML5 UP\n\thtml5up.net | @ajlkn\n\tFree for personal and commercial use under the CCA 3.0 license (html5up.ne"
  },
  {
    "path": "public/guess.java",
    "chars": 3456,
    "preview": "package top.ginnnnnn.mooc.service.implement;\n\nimport java.security.SecureRandom;\nimport java.util.HashMap;\n\nimport org.s"
  },
  {
    "path": "public/sass/libs/_breakpoints.scss",
    "chars": 4577,
    "preview": "// breakpoints.scss v1.0 | @ajlkn | MIT licensed */\n\n// Vars.\n\n\t/// Breakpoints.\n\t/// @var {list}\n\t$breakpoints: () !glo"
  },
  {
    "path": "public/sass/libs/_functions.scss",
    "chars": 1868,
    "preview": "/// Removes a specific item from a list.\n/// @author Hugo Giraudel\n/// @param {list} $list List.\n/// @param {integer} $i"
  },
  {
    "path": "public/sass/libs/_mixins.scss",
    "chars": 2141,
    "preview": "/// Makes an element's :before pseudoelement a FontAwesome icon.\n/// @param {string} $content Optional content value to "
  },
  {
    "path": "public/sass/libs/_vars.scss",
    "chars": 511,
    "preview": "// Misc.\n\t$misc: (\n\t\tbg:\t\t\t\t\t#348cb2 url(\"images/bg.jpg\") bottom left,\n\t\tbg-width:\t\t\t1500px\n\t);\n\n// Duration.\n\t$duration"
  },
  {
    "path": "public/sass/libs/_vendor.scss",
    "chars": 7355,
    "preview": "// vendor.scss v1.0 | @ajlkn | MIT licensed */\n\n// Vars.\n\n\t/// Vendor prefixes.\n\t/// @var {list}\n\t$vendor-prefixes: (\n\t\t"
  },
  {
    "path": "public/sass/main.scss",
    "chars": 8749,
    "preview": "@import 'libs/vars';\n@import 'libs/functions';\n@import 'libs/mixins';\n@import 'libs/vendor';\n@import 'libs/breakpoints';"
  },
  {
    "path": "public/sass/noscript.scss",
    "chars": 474,
    "preview": "@import 'libs/vars';\n@import 'libs/functions';\n@import 'libs/mixins';\n@import 'libs/vendor';\n@import 'libs/breakpoints';"
  },
  {
    "path": "src/App.vue",
    "chars": 4336,
    "preview": "<script setup lang=\"ts\">\nimport { ref } from \"vue\"\nimport { useDark } from \"@vueuse/core\"\nimport {  Moon, Sunny } from \""
  },
  {
    "path": "src/components/CourseCard.vue",
    "chars": 1767,
    "preview": "<script setup lang=\"ts\">\nimport type { course } from \"@/type/mooc\"\nimport { ElImage } from \"element-plus\"\n\nconst props ="
  },
  {
    "path": "src/components/QuestionCard.vue",
    "chars": 3976,
    "preview": "<script setup lang=\"ts\">\nimport type { quiz, homework, option } from \"@/type/mooc\"\nimport { QuestionTypeEnumList } from "
  },
  {
    "path": "src/components/icon/Extension.vue",
    "chars": 822,
    "preview": "<template>\n    <svg viewBox=\"0 0 1024 1024\" width=\"200\" height=\"200\">\n        <path\n            d=\"M873.984 470.016q43.9"
  },
  {
    "path": "src/components/icon/Github.vue",
    "chars": 873,
    "preview": "<template>\n    <svg viewBox=\"0 0 24 24\" width=\"1.2em\" height=\"1.2em\">\n        <path\n            fill=\"currentColor\"\n    "
  },
  {
    "path": "src/components/icon/index.ts",
    "chars": 103,
    "preview": "import Github from \"./Github.vue\"\nimport Extension from \"./Extension.vue\"\n\nexport { Github, Extension }"
  },
  {
    "path": "src/components/index.ts",
    "chars": 122,
    "preview": "import CourseCard from \"./CourseCard.vue\"\nimport QuestionCard from \"./QuestionCard.vue\"\n\nexport {CourseCard, QuestionCar"
  },
  {
    "path": "src/components/question/Completion.vue",
    "chars": 1829,
    "preview": "<script setup lang=\"ts\">\nimport type { quiz } from \"@/type/mooc\"\nimport { ref } from \"vue\"\n\nconst props = defineProps<{\n"
  },
  {
    "path": "src/components/question/Homework.vue",
    "chars": 679,
    "preview": "<script setup lang=\"ts\">\nimport type { homework } from \"@/type/mooc\"\n\nconst props = defineProps<{\n    data: homework\n   "
  },
  {
    "path": "src/components/question/MultipleChoice.vue",
    "chars": 2386,
    "preview": "<script setup lang=\"ts\">\nimport type { quiz, option } from \"@/type/mooc\"\nimport { ref } from \"vue\"\n\nconst props = define"
  },
  {
    "path": "src/components/question/OnlineJudge.vue",
    "chars": 1337,
    "preview": "<script setup lang=\"ts\">\nimport type { homework } from \"@/type/mooc\"\nimport { computed } from \"vue\"\n\nconst props = defin"
  },
  {
    "path": "src/components/question/SingleChoice.vue",
    "chars": 2022,
    "preview": "<script setup lang=\"ts\">\nimport type { option, quiz } from \"@/type/mooc\"\nimport { ref } from \"vue\"\n\nconst props = define"
  },
  {
    "path": "src/components/question/index.ts",
    "chars": 295,
    "preview": "import SingleChoice from \"./SingleChoice.vue\"\nimport MultipleChoice from \"./MultipleChoice.vue\"\nimport Completion from \""
  },
  {
    "path": "src/main.ts",
    "chars": 519,
    "preview": "import { createApp } from \"vue\"\nimport ElementPlus from 'element-plus'\nimport router from \"./router\"\nimport App from \"./"
  },
  {
    "path": "src/plugins/apiAccess.ts",
    "chars": 3061,
    "preview": "import type { ApiKeyType, ApiResponseType, ApiRequestType } from \"../type/api\"\nimport type { App } from \"vue\"\nimport { i"
  },
  {
    "path": "src/plugins/tool.ts",
    "chars": 170,
    "preview": "const sleep = async (ms: number) => {\n    return new Promise((resolve) => {\n        setTimeout(() => {\n            resol"
  },
  {
    "path": "src/router/index.ts",
    "chars": 1365,
    "preview": "import { createRouter, createWebHistory } from \"vue-router\"\nimport { HomeView, MoocView, MoocHeader, MoocCourseDetail, M"
  },
  {
    "path": "src/type/api.ts",
    "chars": 1698,
    "preview": "import type { course, homework, notice, quiz, test } from \"./mooc\"\n\ntype RequestType = {\n    params?: Object\n    data?: "
  },
  {
    "path": "src/type/globleProperties.ts",
    "chars": 194,
    "preview": "import { useApiAccess } from \"@/plugins/apiAccess\"\n\nconst apiAccess = useApiAccess()\ndeclare module 'vue' {\n    interfac"
  },
  {
    "path": "src/type/mooc.ts",
    "chars": 1376,
    "preview": "export enum QuestionTypeEnumList {\n    SingleChoice = \"SINGLE_CHOICE\",\n    MultipleChoice = \"MULTIPLE_CHOICE\",\n    Compl"
  },
  {
    "path": "src/views/BlogView.vue",
    "chars": 240,
    "preview": "<template>\n    <iframe class=\"background\" src=\"background.html\" allowtransparency=\"true\"></iframe>\n</template>\n\n<style s"
  },
  {
    "path": "src/views/HomeView.vue",
    "chars": 1178,
    "preview": "<script setup lang=\"ts\">\nimport BlogView from \"@/views/BlogView.vue\"\nimport { ref } from \"vue\"\n\nconst disabled = ref(tru"
  },
  {
    "path": "src/views/MoocAside.vue",
    "chars": 9091,
    "preview": "<script setup lang=\"ts\">\nimport type { course } from \"@/type/mooc\"\nimport type { courseList } from \"@/type/api\"\nimport {"
  },
  {
    "path": "src/views/MoocCourseDetail.vue",
    "chars": 3952,
    "preview": "<script setup lang=\"ts\">\nimport type { course, test } from \"@/type/mooc\"\nimport { watch, ref } from \"vue\"\nimport { useRo"
  },
  {
    "path": "src/views/MoocHeader.vue",
    "chars": 3860,
    "preview": "<script setup lang=\"ts\">\nimport { ref } from \"vue\"\nimport { Extension } from \"@/components/icon\"\nimport { useApiAccess }"
  },
  {
    "path": "src/views/MoocTest.vue",
    "chars": 4385,
    "preview": "<script setup lang=\"ts\">\nimport { watch, ref } from \"vue\"\nimport { useApiAccess } from \"@/plugins/apiAccess\"\nimport { Qu"
  },
  {
    "path": "src/views/MoocView.vue",
    "chars": 6120,
    "preview": "<script setup lang=\"ts\">\nimport { watch, type Ref } from \"vue\"\nimport type { course, test } from \"@/type/mooc\"\nimport { "
  },
  {
    "path": "src/views/VideoView.vue",
    "chars": 3559,
    "preview": "<script setup lang=\"ts\">\nimport { ref, computed } from \"vue\"\nconst apiList = [\n    { name: \"JSONPlayer【有弹幕】\", url: \"http"
  },
  {
    "path": "src/views/index.ts",
    "chars": 382,
    "preview": "import HomeView from \"./HomeView.vue\"\nimport MoocView from \"./MoocView.vue\"\nimport MoocHeader from \"./MoocHeader.vue\"\nim"
  },
  {
    "path": "tsconfig.config.json",
    "chars": 294,
    "preview": "{\n    \"extends\": \"@vue/tsconfig/tsconfig.node.json\",\n    \"include\": [\n        \"vite.config.*\",\n        \"vitest.config.*\""
  },
  {
    "path": "tsconfig.json",
    "chars": 419,
    "preview": "{\n    \"extends\": \"@vue/tsconfig/tsconfig.web.json\",\n    \"include\": [\n        \"env.d.ts\",\n        \"src/**/*\",\n        \"sr"
  },
  {
    "path": "vite.config.ts",
    "chars": 322,
    "preview": "import { fileURLToPath, URL } from \"node:url\"\n\nimport { defineConfig } from \"vite\"\nimport vue from \"@vitejs/plugin-vue\"\n"
  }
]

About this extraction

This page contains the full source code of the ginnnnnncc/GinsMooc GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 70 files (183.7 KB), approximately 58.0k tokens, and a symbol index with 144 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!