├── .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-lock.json ├── package.json ├── release │ ├── content-scripts │ │ └── index-68d6334d.js │ ├── icons │ │ ├── favicon128.png │ │ ├── favicon16.png │ │ ├── favicon32.png │ │ └── favicon48.png │ └── 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-lock.json ├── package.json ├── public ├── Arcaea_2-1.png ├── Arcaea_Story_Last2_epilogue_cg.jpg ├── Arcaea_Story_Last2_epilogue_cg_part.png ├── Last1_epilogue.png ├── Last1_epilogue_part.png ├── background.html ├── css │ ├── fontawesome-all.min.css │ ├── images │ │ ├── bg.jpg │ │ ├── ie │ │ │ ├── footer.png │ │ │ └── footer.svg │ │ ├── overlay-pattern.png │ │ └── overlay.svg │ ├── main.css │ └── noscript.css ├── download │ └── GinsMoocExtension_v2.0.2.zip ├── extension-auto-evaluate-1.png ├── extension-auto-evaluate-2.png ├── extension-completion.png ├── extension-developer-mode.png ├── extension-homework.png ├── extension-load-decompression.png ├── extension-multiple-choice.png ├── extension-single-choice.png ├── extension-updating.png ├── favicon.png ├── guess.java ├── headicon.png ├── new-course-help.png ├── question.png ├── sass │ ├── libs │ │ ├── _breakpoints.scss │ │ ├── _functions.scss │ │ ├── _mixins.scss │ │ ├── _vars.scss │ │ └── _vendor.scss │ ├── main.scss │ └── noscript.scss └── webfonts │ ├── fa-brands-400.eot │ ├── fa-brands-400.svg │ ├── fa-brands-400.ttf │ ├── fa-brands-400.woff │ ├── fa-brands-400.woff2 │ ├── fa-regular-400.eot │ ├── fa-regular-400.svg │ ├── fa-regular-400.ttf │ ├── fa-regular-400.woff │ ├── fa-regular-400.woff2 │ ├── fa-solid-900.eot │ ├── fa-solid-900.svg │ ├── fa-solid-900.ttf │ ├── fa-solid-900.woff │ └── fa-solid-900.woff2 ├── 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 /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | require("@rushstack/eslint-patch/modern-module-resolution") 3 | 4 | module.exports = { 5 | root: true, 6 | extends: [ 7 | "plugin:vue/vue3-essential", 8 | "eslint:recommended", 9 | "@vue/eslint-config-typescript", 10 | "@vue/eslint-config-prettier" 11 | ], 12 | parserOptions: { 13 | ecmaVersion: "latest" 14 | }, 15 | rules: { 16 | "no-unused-vars": ["off", { "vars": "all", "args": "after-used", "ignoreRestSiblings": false }] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | .DS_Store 12 | dist 13 | dist-ssr 14 | coverage 15 | *.local 16 | 17 | /cypress/videos/ 18 | /cypress/screenshots/ 19 | 20 | # Editor directories and files 21 | **/.vscode/* 22 | !.vscode/extensions.json 23 | .idea 24 | *.suo 25 | *.ntvs* 26 | *.njsproj 27 | *.sln 28 | *.sw? 29 | **/*.pem 30 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4, 3 | "useTabs": false, 4 | "semi": false, 5 | "trailingComma": "none", 6 | "bracketSameLine": false, 7 | "printWidth": 120 8 | } -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"] 3 | } 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 插件简介 2 | 3 | 实现对于中国大学MOOC的 4 | - 非在线测评题的自动答案查询,包括单选题、多选题、判断题、填空题、简答题,支持测验与作业及考试 5 | - 互评阶段的自动评分、自动点评 6 | 7 | 下载地址:[Github release v2.2.0](https://github.com/ginnnnnn666/GinsMooc/releases/tag/v2.2.0) 8 | 9 | 在线使用:[GinsMooc](https://ginnnnnn.top/mooc/) 10 | 11 | # 功能介绍 12 | 13 | 在测试的准备页面,将会自动检查是否准备就绪,若为否将自动更新课程 14 | 15 | ![](/public/extension-updating.png) 16 | 17 | 进入测验后,将显示“获取答案”按钮,点击即可 18 | 19 | ![](/public/extension-single-choice.png) 20 | ![](/public/extension-multiple-choice.png) 21 | ![](/public/extension-completion.png) 22 | ![](/public/extension-homework.png) 23 | 24 | 作业的互评阶段支持自动评分、自动点评 25 | 26 | ![](/public/extension-auto-evaluate-1.png) 27 | ![](/public/extension-auto-evaluate-2.png) 28 | 29 | 30 | # 安装介绍 31 | 32 | 下载安装包后,将其解压至文件夹内 33 | 34 | 在浏览器地址栏中输入`edge://extensions`(谷歌浏览器为`chrome://extensions`) 35 | 36 | 打开开发者模式 37 | 38 | ![](/public/extension-developer-mode.png) 39 | 40 | 点击“加载解压缩的扩展”,选择刚刚解压到的文件夹,即可开始使用 41 | 42 | ![](/public/extension-load-decompression.png) 43 | -------------------------------------------------------------------------------- /env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /extension/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | require('@rushstack/eslint-patch/modern-module-resolution') 3 | 4 | module.exports = { 5 | root: true, 6 | 'extends': [ 7 | 'plugin:vue/vue3-essential', 8 | 'eslint:recommended', 9 | '@vue/eslint-config-typescript', 10 | '@vue/eslint-config-prettier' 11 | ], 12 | parserOptions: { 13 | ecmaVersion: 'latest' 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /extension/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 4, 3 | "useTabs": false, 4 | "semi": false, 5 | "trailingComma": "none", 6 | "bracketSameLine": false, 7 | "printWidth": 120 8 | } -------------------------------------------------------------------------------- /extension/README.md: -------------------------------------------------------------------------------- 1 | # GinsMooc Extention 2 | 3 | This template should help get you started developing with Vue 3 in Vite. 4 | 5 | ## Recommended IDE Setup 6 | 7 | [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). 8 | 9 | ## Type Support for `.vue` Imports in TS 10 | 11 | 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. 12 | 13 | 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: 14 | 15 | 1. Disable the built-in TypeScript Extension 16 | 1) Run `Extensions: Show Built-in Extensions` from VSCode's command palette 17 | 2) Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)` 18 | 2. Reload the VSCode window by running `Developer: Reload Window` from the command palette. 19 | 20 | ## Customize configuration 21 | 22 | See [Vite Configuration Reference](https://vitejs.dev/config/). 23 | 24 | ## Project Setup 25 | 26 | ```sh 27 | npm install 28 | ``` 29 | 30 | ### Compile and Hot-Reload for Development 31 | 32 | ```sh 33 | npm run dev 34 | ``` 35 | 36 | ### Type-Check, Compile and Minify for Production 37 | 38 | ```sh 39 | npm run build 40 | ``` 41 | 42 | ### Lint with [ESLint](https://eslint.org/) 43 | 44 | ```sh 45 | npm run lint 46 | ``` 47 | -------------------------------------------------------------------------------- /extension/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /extension/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite App 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /extension/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ginsmooc-extention", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "run-p type-check build-only", 8 | "preview": "vite preview", 9 | "build-only": "vite build", 10 | "type-check": "vue-tsc --noEmit", 11 | "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore" 12 | }, 13 | "dependencies": { 14 | "axios": "^1.3.2", 15 | "element-plus": "^2.2.29", 16 | "jssha": "^3.3.0", 17 | "vue": "^3.2.45", 18 | "vue-router": "^4.1.6" 19 | }, 20 | "devDependencies": { 21 | "@rushstack/eslint-patch": "^1.1.4", 22 | "@types/chrome": "^0.0.212", 23 | "@types/node": "^18.11.12", 24 | "@vitejs/plugin-vue": "^4.0.0", 25 | "@vue/eslint-config-prettier": "^7.0.0", 26 | "@vue/eslint-config-typescript": "^11.0.0", 27 | "@vue/tsconfig": "^0.1.3", 28 | "eslint": "^8.22.0", 29 | "eslint-plugin-vue": "^9.3.0", 30 | "npm-run-all": "^4.1.5", 31 | "prettier": "^2.7.1", 32 | "typescript": "~4.7.4", 33 | "vite": "^4.0.0", 34 | "vue-tsc": "^1.0.12" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /extension/release/content-scripts/index-68d6334d.js: -------------------------------------------------------------------------------- 1 | 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)}),Ee=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;rt=>{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)},gt=T("Date"),Et=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;r0;)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(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:gt,isFile:Et,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,g){return!a.isUndefined(g[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,g){let E=p;if(p&&!g&&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,"[]"))&&(E=a.toArray(p)))return h=Je(h),E.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(g,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(E,C){(!(a.isUndefined(E)||E===null)&&s.call(t,E,a.isString(C)?C.trim():C,h,w))===!0&&b(E,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=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(` 2 | `).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(` 3 | `)}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 gn(e){return/^([a-z][a-z\d+\-.]*:)?\/\//i.test(e)}function En(e,t){return t?e.replace(/\/?\/$/,"")+"/"+t.replace(/^\/+/,""):e}function Ye(e,t){return e&&!gn(t)?En(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{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,...g]=u?u.split(";").map(E=>E.trim()).filter(Boolean):[];o.setContentType([h||"multipart/form-data",...g].join("; "))}}let l=new XMLHttpRequest;if(e.auth){const h=e.auth.username||"",g=e.auth.password?unescape(encodeURIComponent(e.auth.password)):"";o.set("Authorization","Basic "+btoa(h+":"+g))}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()),E={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()},E),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 g=e.timeout?"timeout of "+e.timeout+"ms exceeded":"timeout exceeded";const E=e.transitional||Ke;e.timeoutErrorMessage&&(g=e.timeoutErrorMessage),r(new m(g,E.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(g,E){l.setRequestHeader(E,g)}),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`adapter ${c} `+(d===!1?"is not supported by the environment":"is not available in the build"));let i=t?o.length>1?`since : 4 | `+o.map(Ne).join(` 5 | `):" "+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",ge={};["object","boolean","number","function","string","symbol"].forEach((e,t)=>{ge[e]=function(r){return typeof r===e||"a"+(t<1?"n ":" ")+e}});const Pe={};ge.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:ge},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+=` 6 | `+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{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{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{var n,r;const t=document.getElementsByClassName("f-richEditorText j-richTxt f-fl");for(let s=0;s{var t,n,r,s;const e=document.getElementsByClassName("u-questionItem u-analysisQuestion analysisMode");for(let o=0;o{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;or.style.display==="none"),ot(),await Fe(1e3),console.log(new Date),n.click(),await Ee(()=>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());console.log(e),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=` 7 | input.gin-answer:not(:checked) + label, #GinsMooc, .gin-answer-item { 8 | background-color: #d9ecff; 9 | } 10 | .learnPageContentLeft { 11 | background: rgb(240, 242, 245); 12 | } 13 | #GinsMooc { 14 | margin-bottom: 12px !important; 15 | } 16 | .gin-function { 17 | display: flex; 18 | align-items: center; 19 | } 20 | .gin-function .u-btn { 21 | margin-right: 16px; 22 | } 23 | .gin-state-tips { 24 | font-size: 14px; 25 | } 26 | `;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.0"},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}}); 27 | -------------------------------------------------------------------------------- /extension/release/icons/favicon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ginnnnnncc/GinsMooc/0ee1a57ec764cddcb19d5554e1aedfdf0ca5518b/extension/release/icons/favicon128.png -------------------------------------------------------------------------------- /extension/release/icons/favicon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ginnnnnncc/GinsMooc/0ee1a57ec764cddcb19d5554e1aedfdf0ca5518b/extension/release/icons/favicon16.png -------------------------------------------------------------------------------- /extension/release/icons/favicon32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ginnnnnncc/GinsMooc/0ee1a57ec764cddcb19d5554e1aedfdf0ca5518b/extension/release/icons/favicon32.png -------------------------------------------------------------------------------- /extension/release/icons/favicon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ginnnnnncc/GinsMooc/0ee1a57ec764cddcb19d5554e1aedfdf0ca5518b/extension/release/icons/favicon48.png -------------------------------------------------------------------------------- /extension/release/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "GinsMooc Extension", 4 | "version": "2.2.0", 5 | "description": "A Chrome extension to get the mooc answers and evaluate auto automatically.", 6 | "icons": { 7 | "16": "icons/favicon16.png", 8 | "32": "icons/favicon32.png", 9 | "48": "icons/favicon48.png", 10 | "128": "icons/favicon128.png" 11 | }, 12 | "author": { "email": "ginnnnnn@qq.com" }, 13 | "homepage_url": "https://ginnnnnn.top/mooc", 14 | "content_scripts": [ 15 | { 16 | "matches": ["https://www.icourse163.org/*learn/*?tid=*", "https://www.icourse163.org/mooc/main/newExam*"], 17 | "js": ["content-scripts/index-68d6334d.js"], 18 | "run_at": "document_end" 19 | } 20 | ] 21 | } -------------------------------------------------------------------------------- /extension/src/main.ts: -------------------------------------------------------------------------------- 1 | import { CustomRefList } from "./plugins/react" 2 | import { useApiAccess } from "./plugins/apiAccess" 3 | import { sleep, getUrlParam } from "./plugins/tool" 4 | import { getQuizQuestionKeys, setQuizAnswer, setHomeworkAnswer, autoEvaluate, batchEvaluate } from "./plugins/mooc" 5 | import { newExamHandle } from "./newExam" 6 | 7 | const apiAccess = useApiAccess() 8 | const refList = new CustomRefList() 9 | const testType = refList.add(<"quiz" | "homework" | undefined>undefined) 10 | const newCourseState = refList.add(-1) 11 | let testId: string | null = null 12 | const [analysis, prepare, examlist] = [refList.add(false), refList.add(false), refList.add(false)] 13 | 14 | if (location.href.indexOf("newExam") !== -1) { 15 | newExamHandle() 16 | } 17 | 18 | const getAnswer = async () => { 19 | if (stateTips.innerText === "正在获取答案,请稍后...") { 20 | return 21 | } 22 | stateTips.innerText = "正在获取答案,请稍后..." 23 | if (testType.get() === "quiz") { 24 | const answers = await apiAccess("selectQustion", { tid: testId as string }, getQuizQuestionKeys()) 25 | console.log(answers) 26 | setQuizAnswer(answers.data.choiceAns as number[], answers.data.completionAns as Object) 27 | } else if (testType.get() === "homework") { 28 | const answers = await apiAccess("selectQustion", { tid: testId as string }, {}) 29 | setHomeworkAnswer(answers.data.homeworkAns as Object) 30 | } 31 | stateTips.innerText = "" 32 | } 33 | 34 | const styleNode = document.createElement("style") 35 | styleNode.innerText = ` 36 | input.gin-answer:not(:checked) + label, #GinsMooc, .gin-answer-item { 37 | background-color: #d9ecff; 38 | } 39 | .learnPageContentLeft { 40 | background: rgb(240, 242, 245); 41 | } 42 | #GinsMooc { 43 | margin-bottom: 12px !important; 44 | } 45 | .gin-function { 46 | display: flex; 47 | align-items: center; 48 | } 49 | .gin-function .u-btn { 50 | margin-right: 16px; 51 | } 52 | .gin-state-tips { 53 | font-size: 14px; 54 | } 55 | ` 56 | document.head.append(styleNode) 57 | 58 | const wrapperNode = document.createElement("div") 59 | wrapperNode.id = "GinsMooc" 60 | wrapperNode.classList.add("m-learnbox") 61 | document.querySelector(".learnPageContentLeft")?.prepend(wrapperNode) 62 | 63 | if (location.href.indexOf("/spoc") !== -1) { 64 | const spocTipsNode = document.createElement("div") 65 | spocTipsNode.innerHTML = "当前课程为 SPOC 课程,可能无法获取答案。SPOC 课程可能会有关联的对外课程,你可以尝试搜索并加入,题目大概率是一样的" 66 | wrapperNode.prepend() 67 | } 68 | 69 | const setNotice = async () => { 70 | const notice = (await apiAccess("getNotice", { version: "v2.2.0" }, undefined)).data 71 | console.log(notice) 72 | if ( 73 | !localStorage 74 | .getItem("Gins-ignore-notice") 75 | ?.split(",") 76 | .find((item) => Number.parseInt(item) === notice.id) 77 | ) { 78 | const noticeNode = document.createElement("div") 79 | noticeNode.innerHTML = notice.content 80 | const closeBtn = document.createElement("a") 81 | closeBtn.innerText = "不再提醒" 82 | closeBtn.onclick = () => { 83 | const origin = localStorage.getItem("Gins-ignore-notice") 84 | localStorage.setItem("Gins-ignore-notice", origin ? `${origin},${notice.id}` : `${notice.id}`) 85 | noticeNode.remove() 86 | closeBtn.remove() 87 | } 88 | closeBtn.style.marginLeft = "16px" 89 | noticeNode.append(closeBtn) 90 | wrapperNode.prepend(noticeNode) 91 | } 92 | } 93 | setNotice() 94 | 95 | const functionNode = document.createElement("div") 96 | functionNode.classList.add("gin-function") 97 | wrapperNode.append(functionNode) 98 | 99 | const getAnswerBtn = document.createElement("button") 100 | getAnswerBtn.classList.add("u-btn", "u-btn-default", "f-dn") 101 | getAnswerBtn.onclick = getAnswer 102 | getAnswerBtn.innerText = "获取答案" 103 | functionNode.append(getAnswerBtn) 104 | 105 | const evaluateBtn = document.createElement("button") 106 | evaluateBtn.classList.add("u-btn", "u-btn-default", "f-dn") 107 | evaluateBtn.onclick = () => { 108 | autoEvaluate() 109 | window.scroll({ top: document.documentElement.scrollHeight, behavior: "smooth" }) 110 | } 111 | evaluateBtn.innerText = "一键互评" 112 | functionNode.append(evaluateBtn) 113 | 114 | const batchEvaluateBtn = document.createElement("button") 115 | batchEvaluateBtn.classList.add("u-btn", "u-btn-default", "f-dn") 116 | batchEvaluateBtn.onclick = () => { 117 | batchEvaluate(5, (finish: number, total: number) => { 118 | if (finish >= total) { 119 | stateTips.innerText = `已完成 ${total} 次互评` 120 | } else { 121 | stateTips.innerText = `自动互评中(${finish} / ${total})` 122 | } 123 | }) 124 | } 125 | batchEvaluateBtn.innerText = "自动互评" 126 | functionNode.append(batchEvaluateBtn) 127 | 128 | 129 | const stateTips = document.createElement("div") 130 | stateTips.classList.add("gin-state-tips") 131 | functionNode.append(stateTips) 132 | 133 | window.addEventListener("hashchange", () => { 134 | testId = getUrlParam("id") 135 | if (location.hash.indexOf("quiz") !== -1 || location.hash.indexOf("examObject") !== -1) { 136 | testType.set("quiz") 137 | } else if (location.hash.indexOf("hw") !== -1 || location.hash.indexOf("examSubjective") !== -1) { 138 | testType.set("homework") 139 | } else { 140 | testType.set(undefined) 141 | } 142 | }) 143 | 144 | window.setInterval(() => { 145 | examlist.set(location.hash.indexOf("examlist") !== -1) 146 | const prepareNode = document.querySelector(".j-prepare.prepare") 147 | prepare.set( 148 | (prepareNode && !prepareNode.classList.contains("f-dn")) || 149 | document.querySelector(".j-homework-paper") !== null || 150 | examlist.get() 151 | ) 152 | analysis.set(document.querySelector(".u-questionItem.u-analysisQuestion.analysisMode") !== null) 153 | }, 100); 154 | 155 | const onTestChange = async () => { 156 | console.log("onTestChange", testType.get(), prepare.get(), examlist.get(), testId) 157 | if ((testType.get() === "quiz" && !prepare.get()) || testType.get() === "homework") { 158 | getAnswerBtn.classList.remove("f-dn") 159 | } else { 160 | getAnswerBtn.classList.add("f-dn") 161 | } 162 | 163 | newCourseState.set(-2) 164 | if (prepare.get() && testId) { 165 | const res = await apiAccess("checkTestExist", { tid: testId, type: "isExisting" }, undefined) 166 | if (res.data.existing) { 167 | newCourseState.set(-1) 168 | return 169 | } else { 170 | const eventSource = new EventSource(`https://ginnnnnn.top/api/mooc/course/refresh/${getUrlParam("tid")}`) 171 | eventSource.onmessage = (event) => { 172 | console.log(event.data) 173 | const state = JSON.parse(event.data) 174 | if (state && state.total > 0) { 175 | newCourseState.set(Math.round((state.finished / state.total) * 100)) 176 | } 177 | if (newCourseState.value === 100 || state.status === 400) { 178 | eventSource.close() 179 | if (state.msg) { 180 | newCourseState.set(-1) 181 | } 182 | } 183 | } 184 | } 185 | } else if (!examlist.get()) { 186 | newCourseState.set(-1) 187 | return 188 | } 189 | } 190 | 191 | const onModeChange = () => { 192 | console.log("onModeChange", analysis.get()) 193 | if (analysis.get()) { 194 | evaluateBtn.classList.remove("f-dn") 195 | batchEvaluateBtn.classList.remove("f-dn") 196 | } else { 197 | evaluateBtn.classList.add("f-dn") 198 | batchEvaluateBtn.classList.add("f-dn") 199 | } 200 | } 201 | 202 | analysis.addEventListenr("change", onModeChange) 203 | prepare.addEventListenr("change", onTestChange) 204 | testType.addEventListenr("change", onTestChange) 205 | newCourseState.addEventListenr("set", () => { 206 | switch (newCourseState.get()) { 207 | case -2: 208 | stateTips.innerText = "正在检查课程..." 209 | break 210 | case -1: 211 | stateTips.innerText = prepare.get() ? "已准备就绪" : "" 212 | break 213 | default: 214 | stateTips.innerText = `正在更新课程...${newCourseState.get()}%` 215 | break 216 | } 217 | }) 218 | -------------------------------------------------------------------------------- /extension/src/newExam.ts: -------------------------------------------------------------------------------- 1 | import { useApiAccess } from "./plugins/apiAccess" 2 | import { sleep, getUrlParam } from "./plugins/tool" 3 | 4 | export const newExamHandle = async () => { 5 | const apiAccess = useApiAccess() 6 | let form = document.getElementById("app")?.getElementsByTagName("form").item(0) 7 | 8 | while (!form) { 9 | await sleep(1000) 10 | form = document.getElementById("app")?.getElementsByTagName("form").item(0) 11 | } 12 | 13 | const getAnswer = async () => { 14 | const info = await apiAccess( 15 | "getNewExamInfo", 16 | { csrfKey: document.cookie.match(/NTESSTUDYSI=([a-z0-9]+);/)![1] }, 17 | { answerformId: getUrlParam("aid")!, examId: getUrlParam("eid")! } 18 | ) 19 | let oidList: Array = [] 20 | for (let question of info.result.questions) { 21 | for (let option of question.optionDtos) { 22 | oidList.push(option.id) 23 | } 24 | } 25 | const answers = await apiAccess("selectQustion", { tid: info.result.tid }, { oidList: oidList }) 26 | const optionElements = document.querySelectorAll( 27 | ".ant-checkbox-group>div, .ant-radio-group>div" 28 | ) as NodeListOf 29 | // console.log(optionElements) 30 | for (let id of answers.data.choiceAns!) { 31 | optionElements[oidList.indexOf(id)].classList.add("gin-answer-item") 32 | } 33 | } 34 | 35 | const getAnswerBtn = document.createElement("button") 36 | getAnswerBtn.className = "ant-btn ant-btn-primary" 37 | getAnswerBtn.setAttribute("style", "margin-bottom: 16px") 38 | getAnswerBtn.onclick = getAnswer 39 | getAnswerBtn.innerText = "获取答案" 40 | form?.before(getAnswerBtn) 41 | } 42 | -------------------------------------------------------------------------------- /extension/src/plugins/apiAccess.ts: -------------------------------------------------------------------------------- 1 | import type { ApiKeyType, ApiResponseType, ApiRequestType } from "../type/api" 2 | import type { App } from "vue" 3 | import { isAxiosError } from "axios" 4 | import apiInfo from "../type/api" 5 | import axios from "axios" 6 | 7 | const baseUrl = "https://ginnnnnn.top/api" 8 | 9 | async function apiAccess(api: T): Promise 10 | async function apiAccess( 11 | api: T, 12 | params: ApiRequestType[T]["params"], 13 | data: ApiRequestType[T]["data"] 14 | ): Promise 15 | 16 | /** 函数重载 */ 17 | async function apiAccess( 18 | api: T, 19 | params?: ApiRequestType[T]["params"], 20 | data?: ApiRequestType[T]["data"] 21 | ) { 22 | /** 错误处理,主要catch 404,调用者不再需要try-catch */ 23 | try { 24 | return await new Promise((resolve, reject) => { 25 | /** 查询参数转动态路由参数 */ 26 | let url = apiInfo[api].url 27 | if (params) { 28 | for (const [key, val] of Object.entries(params)) { 29 | const reg = RegExp(`(/):${key}(/)?`, "g") 30 | if (reg.test(url)) { 31 | url = url.replaceAll(reg, `$1${val}$2`) 32 | Reflect.deleteProperty(params, key) 33 | } 34 | } 35 | } 36 | if (url.indexOf("http") != 0) { 37 | url = `${baseUrl}${url}` 38 | } 39 | /** 将对象转为json字符串 */ 40 | if (data) { 41 | for (const [key, val] of Object.entries(data)) { 42 | if (typeof val === "object") { 43 | Reflect.set(data, key, JSON.stringify(val)) 44 | } 45 | } 46 | } 47 | /** 异步发送请求 */ 48 | axios({ 49 | url: url, 50 | method: apiInfo[api].method, 51 | params: params || {}, 52 | data: data || {}, 53 | headers: { "Content-Type": "application/x-www-form-urlencoded" } 54 | }) 55 | .then((res) => { 56 | let message = "", 57 | success = false 58 | if (res.status !== 200 || !res.data) { 59 | message = "请求出错" 60 | } else if (res.data.msg) { 61 | message = res.data.msg 62 | if (res.data.status === 200) { 63 | success = true 64 | } 65 | } 66 | if (message) { 67 | console.log(message) 68 | } 69 | resolve(res.data) 70 | }) 71 | .catch((error) => { 72 | let message = error 73 | if (isAxiosError(error)) { 74 | message = error.message 75 | } 76 | console.log(message) 77 | reject(error) 78 | }) 79 | }) 80 | } catch { 81 | return {} 82 | } 83 | } 84 | 85 | export const useApiAccess = () => apiAccess 86 | 87 | export default { 88 | install: (app: App) => { 89 | app.config.globalProperties.$apiAccess = apiAccess 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /extension/src/plugins/mooc.ts: -------------------------------------------------------------------------------- 1 | import type { quiz, option } from "../type/mooc" 2 | import { sleep, waitFor } from "./tool" 3 | 4 | const getQuizQuestionKeys = () => { 5 | const [oidList, titleList] = [new Array(), new Array()] 6 | const choices = document.querySelectorAll('input[id^="op_"]') 7 | for (let i = 0; i < choices.length; i++) { 8 | const choice = choices.item(i) as HTMLInputElement 9 | oidList.push(Number.parseInt(choice.id.split("_")[2].slice(0, -13))) 10 | } 11 | const completions = document.getElementsByClassName("m-FillBlank examMode u-questionItem") 12 | for (let i = 0; i < completions.length; i++) { 13 | const questionNode = completions.item(i) as HTMLDivElement 14 | const titleNode = questionNode.querySelector(".j-richTxt") as HTMLDivElement 15 | titleList.push(titleNode.innerText) 16 | } 17 | return { oidList, titleList } 18 | } 19 | 20 | const setQuizAnswer = (choiceAns: number[], completionAns: Object) => { 21 | for (const id of choiceAns) { 22 | const node = document.querySelector(`input[id*="_${id}"]`) as HTMLInputElement 23 | node.classList.add("gin-answer") 24 | } 25 | const completions = document.getElementsByClassName("m-FillBlank examMode u-questionItem") 26 | for (let i = 0; i < completions.length; i++) { 27 | const questionNode = completions.item(i) as HTMLDivElement 28 | const titleNode = questionNode.querySelector(".j-richTxt") as HTMLDivElement 29 | const question = Reflect.get(completionAns, titleNode.innerText) as quiz 30 | 31 | const newTitleNode = document.createElement("div") 32 | newTitleNode.innerHTML = question.title 33 | titleNode.appendChild(newTitleNode) 34 | 35 | const answerList = (question.stdAnswer).split("##%_YZPRLFH_%##") 36 | const answerListNode = document.createElement("div") 37 | for (let j = 0; j < answerList.length; j++) { 38 | const answerNode = document.createElement("span") 39 | answerNode.classList.add("gin-answer-item") 40 | answerNode.innerHTML = answerList[j] 41 | answerListNode.append(answerNode) 42 | if (j !== answerList.length - 1) { 43 | answerListNode.append(" / ") 44 | } 45 | } 46 | titleNode.append(answerListNode) 47 | } 48 | } 49 | 50 | const setHomeworkAnswer = (homeworkAns: Object) => { 51 | const homeworks = document.getElementsByClassName("f-richEditorText j-richTxt f-fl") 52 | for (let i = 0; i < homeworks.length; i++) { 53 | const answerNode = document.createElement("div") 54 | answerNode.classList.add('gin-answer-item') 55 | answerNode.innerHTML = Reflect.get(homeworkAns, `${i}`)?.answer as string 56 | homeworks.item(i)?.append(answerNode) 57 | } 58 | } 59 | 60 | const autoEvaluate = () => { 61 | const analysis = document.getElementsByClassName("u-questionItem u-analysisQuestion analysisMode") 62 | for (let i = 0; i < analysis.length; i++) { 63 | const radioGroup = analysis.item(i)?.getElementsByClassName("s") as HTMLCollection 64 | for (let j = 0; j < radioGroup.length; j++) { 65 | const radio = radioGroup.item(j)?.lastElementChild?.querySelector("input") as HTMLInputElement 66 | radio.checked = true 67 | } 68 | const textarea = analysis.item(i)?.querySelector("textarea") as HTMLTextAreaElement 69 | textarea.value = "666" 70 | } 71 | } 72 | 73 | const batchEvaluate = async (times: number, updateCaller: (finish: number, total: number) => any) => { 74 | const submitBtn = document.querySelector(".u-homework-evaAction .bottombtnwrap .j-submitbtn") as HTMLDivElement 75 | const xlinfo = document.querySelector(".u-homework-evaAction .xlinfo") as HTMLDivElement 76 | const nextBtn = document.querySelector(".u-homework-evaAction .xlinfo .j-gotonext") as HTMLDivElement 77 | for (let i = 0; i < times; i++) { 78 | await waitFor(() => xlinfo.style.display === 'none') 79 | autoEvaluate() 80 | await sleep(1000) 81 | console.log(new Date()) 82 | submitBtn.click() 83 | await waitFor(() => xlinfo.style.display !== 'none') 84 | updateCaller(i + 1, times) 85 | nextBtn.click() 86 | } 87 | } 88 | 89 | export { getQuizQuestionKeys, setHomeworkAnswer, setQuizAnswer, autoEvaluate, batchEvaluate } 90 | -------------------------------------------------------------------------------- /extension/src/plugins/react.ts: -------------------------------------------------------------------------------- 1 | import { randomString } from "./tool" 2 | 3 | class CustomRefList extends Array> { 4 | id: string 5 | node: HTMLElement 6 | 7 | constructor() { 8 | super() 9 | this.node = document.createElement("gin") 10 | this.id = randomString(8) 11 | this.node.id = `gin-auto-${this.id}` 12 | document.body.appendChild(this.node) 13 | } 14 | 15 | add(value: T): CustomRef { 16 | const add = new CustomRef(this, value) 17 | super.push(add) 18 | return add 19 | } 20 | } 21 | 22 | class CustomRef { 23 | id: string 24 | node: HTMLElement 25 | parent: CustomRefList 26 | value: T 27 | 28 | constructor(parent: CustomRefList, value: T) { 29 | this.parent = parent 30 | this.node = document.createElement("gin") 31 | this.id = randomString(8) 32 | this.node.id = this.id 33 | this.parent.node.appendChild(this.node) 34 | this.value = value 35 | } 36 | 37 | get(): T { 38 | return this.value 39 | } 40 | 41 | set(value: T): void { 42 | if (this.value !== value) { 43 | const oldValue = this.value 44 | this.value = value 45 | this.node.dispatchEvent(new CustomEvent("change", { detail: { oldValue, newValue: this.value } })) 46 | } 47 | this.node.dispatchEvent(new CustomEvent("set")) 48 | } 49 | 50 | addEventListenr(eventName: "change" | "set", callback: (ev: Event) => void): void { 51 | this.node.addEventListener(eventName, callback) 52 | } 53 | } 54 | 55 | export { CustomRefList } -------------------------------------------------------------------------------- /extension/src/plugins/tool.ts: -------------------------------------------------------------------------------- 1 | const sleep = async (ms: number) => { 2 | return new Promise((resolve) => { 3 | setTimeout(() => { 4 | resolve('') 5 | }, ms) 6 | }) 7 | } 8 | 9 | const waitFor = async (checker: any) => { 10 | return new Promise((resolve) => { 11 | const checkDisplay = setInterval(() => { 12 | console.log('check wait for') 13 | if (checker()) { 14 | clearInterval(checkDisplay); 15 | resolve(''); 16 | } 17 | }, 50); 18 | }); 19 | } 20 | 21 | const getUrlParam = (key: string) => { 22 | const reg = new RegExp('(^|&)' + key + '=([^&]*)(&|$)') 23 | const result = window.location.search.substring(1).match(reg) 24 | || window.location.hash.substring((window.location.hash.search(/\?/)) + 1).match(reg) 25 | return result ? decodeURIComponent(result[2]) : null 26 | } 27 | 28 | const randomString = (length: number) => { 29 | const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890' 30 | let result = '' 31 | for (let i = 0; i < length; i++) { 32 | result += chars[Math.floor(Math.random() * chars.length)] 33 | } 34 | return result 35 | } 36 | 37 | export { sleep, getUrlParam, randomString, waitFor } -------------------------------------------------------------------------------- /extension/src/type/api.ts: -------------------------------------------------------------------------------- 1 | import type { homework, quiz, notice } from "./mooc" 2 | 3 | type RequestType = { 4 | params?: Object 5 | data?: Object 6 | } 7 | 8 | type Response = { 9 | status: number 10 | data: T 11 | msg: string 12 | } 13 | 14 | const apiInfo = { 15 | checkTestExist: { 16 | url: "/mooc/test/:tid", 17 | method: "GET" 18 | }, 19 | selectQustion: { 20 | url: "/mooc/test/:tid", 21 | method: "POST" 22 | }, 23 | getAnnouncement: { 24 | url: "/mooc/announcement", 25 | method: "GET" 26 | }, 27 | getNewExamInfo: { 28 | url: "https://www.icourse163.org/mm-tiku/web/j/mocExamBean.getPaper.rpc", 29 | method: "POST" 30 | }, 31 | getNotice: { 32 | url: "/mooc/notice/extension", 33 | method: "GET" 34 | } 35 | } 36 | 37 | export type ApiKeyType = keyof typeof apiInfo 38 | 39 | export interface ApiResponseType { 40 | checkTestExist: Response<{ 41 | existing: boolean 42 | }> 43 | selectQustion: Response<{ 44 | choiceAns?: number[] 45 | completionAns?: Object 46 | homeworkAns?: Object 47 | }> 48 | getAnnouncement: Response 49 | getNotice: Response 50 | getNewExamInfo: { 51 | code: number 52 | result: { 53 | tid: number 54 | questions: { optionDtos: { id: number }[] }[] 55 | } 56 | } 57 | } 58 | 59 | export interface ApiRequestType { 60 | checkTestExist: RequestType & { 61 | params: { tid: number | string; type: "isExisting" } 62 | } 63 | selectQustion: RequestType & { 64 | params: { tid: number | string } 65 | data: { 66 | oidList?: number[] 67 | titleList?: string[] 68 | } 69 | } 70 | getNotice: RequestType & { 71 | params: { version: string } 72 | } 73 | getNewExamInfo: RequestType & { 74 | params: { csrfKey: string } 75 | data: { 76 | answerformId: number | string 77 | examId: number | string 78 | } 79 | } 80 | } 81 | 82 | export default apiInfo 83 | -------------------------------------------------------------------------------- /extension/src/type/mooc.ts: -------------------------------------------------------------------------------- 1 | export enum QuestionTypeEnumList { 2 | SingleChoice = "SINGLE_CHOICE", 3 | MultipleChoice = "MULTIPLE_CHOICE", 4 | Completion = "COMPLETION", 5 | Judge = "JUDGEMENT", 6 | Homework = "HOMEWORK", 7 | OnlineJudge = "ONLINE_JUDGE" 8 | } 9 | 10 | interface course extends Object { 11 | id: number 12 | name: string 13 | school: string 14 | imageUrl: string 15 | } 16 | 17 | interface test extends Object { 18 | id: number 19 | name: string 20 | objective: boolean 21 | releaseTime: string 22 | deadline: string 23 | chapterId: number 24 | chapterName: string 25 | } 26 | 27 | interface option extends Object { 28 | id: number 29 | content: string 30 | answer: boolean 31 | } 32 | 33 | interface quiz extends Object { 34 | id: number 35 | type: 36 | | QuestionTypeEnumList.SingleChoice 37 | | QuestionTypeEnumList.MultipleChoice 38 | | QuestionTypeEnumList.Completion 39 | | QuestionTypeEnumList.Judge 40 | title: string 41 | stdAnswer: string | null 42 | optionList: option[] | null 43 | } 44 | 45 | interface homework extends Object { 46 | id: number 47 | type: QuestionTypeEnumList.Homework | QuestionTypeEnumList.OnlineJudge 48 | title: string 49 | answer: string | null 50 | description: string | null 51 | memoryLimit: number | null 52 | timeLimit: number | null 53 | } 54 | 55 | interface notice extends Object { 56 | id: number 57 | content: string 58 | } 59 | 60 | export type QuestionTypeEnum = typeof QuestionTypeEnumList 61 | export type { course, test, option, quiz, homework, notice } 62 | -------------------------------------------------------------------------------- /extension/tsconfig.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.node.json", 3 | "include": [ 4 | "vite.config.*", 5 | "vitest.config.*", 6 | "cypress.config.*", 7 | "playwright.config.*" 8 | ], 9 | "compilerOptions": { 10 | "composite": true, 11 | "types": [ 12 | "node" 13 | ] 14 | } 15 | } -------------------------------------------------------------------------------- /extension/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.web.json", 3 | "include": [ 4 | "env.d.ts", 5 | "src/**/*", 6 | "src/**/*.vue" 7 | ], 8 | "compilerOptions": { 9 | "baseUrl": ".", 10 | "paths": { 11 | "@/*": [ 12 | "./src/*" 13 | ] 14 | } 15 | }, 16 | "references": [ 17 | { 18 | "path": "./tsconfig.config.json" 19 | } 20 | ] 21 | } -------------------------------------------------------------------------------- /extension/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath, URL } from 'node:url' 2 | 3 | import { defineConfig } from 'vite' 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | plugins: [], 8 | resolve: { 9 | alias: { 10 | '@': fileURLToPath(new URL('./src', import.meta.url)) 11 | } 12 | }, 13 | build: { 14 | "outDir": "./release/content-scripts", 15 | "assetsDir": "./" 16 | } 17 | }) 18 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Gins 11 | 14 | 23 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ginsmooc", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "run-p type-check build-only", 8 | "preview": "vite preview", 9 | "build-only": "vite build", 10 | "type-check": "vue-tsc --noEmit", 11 | "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore" 12 | }, 13 | "dependencies": { 14 | "@element-plus/icons-vue": "^2.1.0", 15 | "@jridgewell/sourcemap-codec": "^1.4.14", 16 | "@vueuse/core": "^9.11.1", 17 | "axios": "^1.2.3", 18 | "d3-cloud": "^1.2.5", 19 | "element-plus": "^2.2.28", 20 | "fuzzysort": "^2.0.4", 21 | "vue": "^3.2.45", 22 | "vue-router": "^4.1.6" 23 | }, 24 | "devDependencies": { 25 | "@rushstack/eslint-patch": "^1.1.4", 26 | "@types/node": "^18.15.5", 27 | "@vitejs/plugin-vue": "^4.0.0", 28 | "@vue/eslint-config-prettier": "^7.0.0", 29 | "@vue/eslint-config-typescript": "^11.0.0", 30 | "@vue/tsconfig": "^0.1.3", 31 | "eslint": "^8.22.0", 32 | "eslint-plugin-vue": "^9.3.0", 33 | "npm-run-all": "^4.1.5", 34 | "prettier": "^2.7.1", 35 | "typescript": "~4.7.4", 36 | "vite": "^4.0.0", 37 | "vue-tsc": "^1.0.12" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /public/Arcaea_2-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ginnnnnncc/GinsMooc/0ee1a57ec764cddcb19d5554e1aedfdf0ca5518b/public/Arcaea_2-1.png -------------------------------------------------------------------------------- /public/Arcaea_Story_Last2_epilogue_cg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ginnnnnncc/GinsMooc/0ee1a57ec764cddcb19d5554e1aedfdf0ca5518b/public/Arcaea_Story_Last2_epilogue_cg.jpg -------------------------------------------------------------------------------- /public/Arcaea_Story_Last2_epilogue_cg_part.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ginnnnnncc/GinsMooc/0ee1a57ec764cddcb19d5554e1aedfdf0ca5518b/public/Arcaea_Story_Last2_epilogue_cg_part.png -------------------------------------------------------------------------------- /public/Last1_epilogue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ginnnnnncc/GinsMooc/0ee1a57ec764cddcb19d5554e1aedfdf0ca5518b/public/Last1_epilogue.png -------------------------------------------------------------------------------- /public/Last1_epilogue_part.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ginnnnnncc/GinsMooc/0ee1a57ec764cddcb19d5554e1aedfdf0ca5518b/public/Last1_epilogue_part.png -------------------------------------------------------------------------------- /public/background.html: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 |
17 |
18 |
19 | 20 | 24 |
25 |
26 | 37 | 38 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /public/css/images/bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ginnnnnncc/GinsMooc/0ee1a57ec764cddcb19d5554e1aedfdf0ca5518b/public/css/images/bg.jpg -------------------------------------------------------------------------------- /public/css/images/ie/footer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ginnnnnncc/GinsMooc/0ee1a57ec764cddcb19d5554e1aedfdf0ca5518b/public/css/images/ie/footer.png -------------------------------------------------------------------------------- /public/css/images/ie/footer.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /public/css/images/overlay-pattern.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ginnnnnncc/GinsMooc/0ee1a57ec764cddcb19d5554e1aedfdf0ca5518b/public/css/images/overlay-pattern.png -------------------------------------------------------------------------------- /public/css/images/overlay.svg: -------------------------------------------------------------------------------- 1 | 2 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /public/css/main.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,900"); 2 | @import url("fontawesome-all.min.css"); 3 | 4 | /* 5 | Aerial by HTML5 UP 6 | html5up.net | @ajlkn 7 | Free for personal and commercial use under the CCA 3.0 license (html5up.net/license) 8 | */ 9 | 10 | html, body, div, span, applet, object, 11 | iframe, h1, h2, h3, h4, h5, h6, p, blockquote, 12 | pre, a, abbr, acronym, address, big, cite, 13 | code, del, dfn, em, img, ins, kbd, q, s, samp, 14 | small, strike, strong, sub, sup, tt, var, b, 15 | u, i, center, dl, dt, dd, ol, ul, li, fieldset, 16 | form, label, legend, table, caption, tbody, 17 | tfoot, thead, tr, th, td, article, aside, 18 | canvas, details, embed, figure, figcaption, 19 | footer, header, hgroup, menu, nav, output, ruby, 20 | section, summary, time, mark, audio, video { 21 | margin: 0; 22 | padding: 0; 23 | border: 0; 24 | font-size: 100%; 25 | font: inherit; 26 | vertical-align: baseline;} 27 | 28 | article, aside, details, figcaption, figure, 29 | footer, header, hgroup, menu, nav, section { 30 | display: block;} 31 | 32 | body { 33 | line-height: 1; 34 | } 35 | 36 | ol, ul { 37 | list-style: none; 38 | } 39 | 40 | blockquote, q { 41 | quotes: none; 42 | } 43 | 44 | blockquote:before, blockquote:after, q:before, q:after { 45 | content: ''; 46 | content: none; 47 | } 48 | 49 | table { 50 | border-collapse: collapse; 51 | border-spacing: 0; 52 | } 53 | 54 | body { 55 | -webkit-text-size-adjust: none; 56 | } 57 | 58 | mark { 59 | background-color: transparent; 60 | color: inherit; 61 | } 62 | 63 | input::-moz-focus-inner { 64 | border: 0; 65 | padding: 0; 66 | } 67 | 68 | input, select, textarea { 69 | -moz-appearance: none; 70 | -webkit-appearance: none; 71 | -ms-appearance: none; 72 | appearance: none; 73 | } 74 | 75 | /* Basic */ 76 | 77 | html { 78 | box-sizing: border-box; 79 | } 80 | 81 | *, *:before, *:after { 82 | box-sizing: inherit; 83 | } 84 | 85 | body { 86 | background: #fff; 87 | overflow: hidden; 88 | } 89 | 90 | body.is-preload *, body.is-preload *:before, body.is-preload *:after { 91 | -moz-animation: none !important; 92 | -webkit-animation: none !important; 93 | -ms-animation: none !important; 94 | animation: none !important; 95 | -moz-transition: none !important; 96 | -webkit-transition: none !important; 97 | -ms-transition: none !important; 98 | transition: none !important; 99 | } 100 | 101 | body, input, select, textarea { 102 | color: #fff; 103 | font-family: 'Source Sans Pro', sans-serif; 104 | font-size: 15pt; 105 | font-weight: 300 !important; 106 | letter-spacing: -0.025em; 107 | line-height: 1.75em; 108 | } 109 | 110 | a { 111 | -moz-transition: border-color 0.2s ease-in-out; 112 | -webkit-transition: border-color 0.2s ease-in-out; 113 | -ms-transition: border-color 0.2s ease-in-out; 114 | transition: border-color 0.2s ease-in-out; 115 | border-bottom: dotted 1px; 116 | color: inherit; 117 | outline: 0; 118 | text-decoration: none; 119 | } 120 | 121 | a:hover { 122 | border-color: transparent; 123 | } 124 | 125 | /* Icon */ 126 | 127 | .icon { 128 | text-decoration: none; 129 | position: relative; 130 | } 131 | 132 | .icon:before { 133 | -moz-osx-font-smoothing: grayscale; 134 | -webkit-font-smoothing: antialiased; 135 | display: inline-block; 136 | font-style: normal; 137 | font-variant: normal; 138 | text-rendering: auto; 139 | line-height: 1; 140 | text-transform: none !important; 141 | font-family: 'Font Awesome 5 Free'; 142 | font-weight: 400; 143 | } 144 | 145 | .icon > .label { 146 | display: none; 147 | } 148 | 149 | .icon.solid:before { 150 | font-weight: 900; 151 | } 152 | 153 | .icon.brands:before { 154 | font-family: 'Font Awesome 5 Brands'; 155 | } 156 | 157 | /* Wrapper */ 158 | 159 | @-moz-keyframes wrapper { 160 | 0% { 161 | opacity: 0; 162 | } 163 | 164 | 100% { 165 | opacity: 1; 166 | } 167 | } 168 | 169 | @-webkit-keyframes wrapper { 170 | 0% { 171 | opacity: 0; 172 | } 173 | 174 | 100% { 175 | opacity: 1; 176 | } 177 | } 178 | 179 | @-ms-keyframes wrapper { 180 | 0% { 181 | opacity: 0; 182 | } 183 | 184 | 100% { 185 | opacity: 1; 186 | } 187 | } 188 | 189 | @keyframes wrapper { 190 | 0% { 191 | opacity: 0; 192 | } 193 | 194 | 100% { 195 | opacity: 1; 196 | } 197 | } 198 | 199 | #wrapper { 200 | -moz-animation: wrapper 3s forwards; 201 | -webkit-animation: wrapper 3s forwards; 202 | -ms-animation: wrapper 3s forwards; 203 | animation: wrapper 3s forwards; 204 | height: 100%; 205 | left: 0; 206 | opacity: 0; 207 | position: fixed; 208 | top: 0; 209 | width: 100%; 210 | } 211 | 212 | /* BG */ 213 | 214 | #bg { 215 | -moz-animation: bg 60s linear infinite; 216 | -webkit-animation: bg 60s linear infinite; 217 | -ms-animation: bg 60s linear infinite; 218 | animation: bg 60s linear infinite; 219 | -moz-backface-visibility: hidden; 220 | -webkit-backface-visibility: hidden; 221 | -ms-backface-visibility: hidden; 222 | backface-visibility: hidden; 223 | -moz-transform: translate3d(0,0,0); 224 | -webkit-transform: translate3d(0,0,0); 225 | -ms-transform: translate3d(0,0,0); 226 | transform: translate3d(0,0,0); 227 | /* Set your background with this */ 228 | 229 | background: #348cb2 url("https://gins-1255964181.cos.ap-beijing.myqcloud.com/bg.jpg") bottom left; 230 | background-repeat: repeat-x; 231 | height: 100%; 232 | left: 0; 233 | opacity: 1; 234 | position: fixed; 235 | top: 0; 236 | } 237 | 238 | @-moz-keyframes bg { 239 | 0% { 240 | -moz-transform: translate3d(0,0,0); 241 | -webkit-transform: translate3d(0,0,0); 242 | -ms-transform: translate3d(0,0,0); 243 | transform: translate3d(0,0,0); 244 | } 245 | 246 | 100% { 247 | -moz-transform: translate3d(-2250px,0,0); 248 | -webkit-transform: translate3d(-2250px,0,0); 249 | -ms-transform: translate3d(-2250px,0,0); 250 | transform: translate3d(-2250px,0,0); 251 | } 252 | } 253 | 254 | @-webkit-keyframes bg { 255 | 0% { 256 | -moz-transform: translate3d(0,0,0); 257 | -webkit-transform: translate3d(0,0,0); 258 | -ms-transform: translate3d(0,0,0); 259 | transform: translate3d(0,0,0); 260 | } 261 | 262 | 100% { 263 | -moz-transform: translate3d(-2250px,0,0); 264 | -webkit-transform: translate3d(-2250px,0,0); 265 | -ms-transform: translate3d(-2250px,0,0); 266 | transform: translate3d(-2250px,0,0); 267 | } 268 | } 269 | 270 | @-ms-keyframes bg { 271 | 0% { 272 | -moz-transform: translate3d(0,0,0); 273 | -webkit-transform: translate3d(0,0,0); 274 | -ms-transform: translate3d(0,0,0); 275 | transform: translate3d(0,0,0); 276 | } 277 | 278 | 100% { 279 | -moz-transform: translate3d(-2250px,0,0); 280 | -webkit-transform: translate3d(-2250px,0,0); 281 | -ms-transform: translate3d(-2250px,0,0); 282 | transform: translate3d(-2250px,0,0); 283 | } 284 | } 285 | 286 | @keyframes bg { 287 | 0% { 288 | -moz-transform: translate3d(0,0,0); 289 | -webkit-transform: translate3d(0,0,0); 290 | -ms-transform: translate3d(0,0,0); 291 | transform: translate3d(0,0,0); 292 | } 293 | 294 | 100% { 295 | -moz-transform: translate3d(-2250px,0,0); 296 | -webkit-transform: translate3d(-2250px,0,0); 297 | -ms-transform: translate3d(-2250px,0,0); 298 | transform: translate3d(-2250px,0,0); 299 | } 300 | } 301 | 302 | #bg { 303 | background-size: auto 100%; 304 | width: 6750px; 305 | } 306 | 307 | /* Overlay */ 308 | 309 | @-moz-keyframes overlay { 310 | 0% { 311 | opacity: 0; 312 | } 313 | 314 | 100% { 315 | opacity: 1; 316 | } 317 | } 318 | 319 | @-webkit-keyframes overlay { 320 | 0% { 321 | opacity: 0; 322 | } 323 | 324 | 100% { 325 | opacity: 1; 326 | } 327 | } 328 | 329 | @-ms-keyframes overlay { 330 | 0% { 331 | opacity: 0; 332 | } 333 | 334 | 100% { 335 | opacity: 1; 336 | } 337 | } 338 | 339 | @keyframes overlay { 340 | 0% { 341 | opacity: 0; 342 | } 343 | 344 | 100% { 345 | opacity: 1; 346 | } 347 | } 348 | 349 | #overlay { 350 | -moz-animation: overlay 1.5s 1.5s forwards; 351 | -webkit-animation: overlay 1.5s 1.5s forwards; 352 | -ms-animation: overlay 1.5s 1.5s forwards; 353 | animation: overlay 1.5s 1.5s forwards; 354 | background-attachment: fixed, fixed; 355 | background-image: url("images/overlay-pattern.png"), url("images/overlay.svg"); 356 | background-position: top left, center center; 357 | background-repeat: repeat, no-repeat; 358 | background-size: auto, cover; 359 | height: 100%; 360 | left: 0; 361 | opacity: 0; 362 | position: fixed; 363 | top: 0; 364 | width: 100%; 365 | } 366 | 367 | /* Main */ 368 | 369 | #main { 370 | height: 100%; 371 | left: 0; 372 | position: fixed; 373 | text-align: center; 374 | top: 0; 375 | width: 100%; 376 | } 377 | 378 | #main:before { 379 | content: ''; 380 | display: inline-block; 381 | height: 100%; 382 | margin-right: 0; 383 | vertical-align: middle; 384 | width: 1px; 385 | } 386 | 387 | /* Header */ 388 | 389 | @-moz-keyframes header { 390 | 0% { 391 | -moz-transform: translate3d(0,1em,0); 392 | -webkit-transform: translate3d(0,1em,0); 393 | -ms-transform: translate3d(0,1em,0); 394 | transform: translate3d(0,1em,0); 395 | opacity: 0; 396 | } 397 | 398 | 100% { 399 | -moz-transform: translate3d(0,0,0); 400 | -webkit-transform: translate3d(0,0,0); 401 | -ms-transform: translate3d(0,0,0); 402 | transform: translate3d(0,0,0); 403 | opacity: 1; 404 | } 405 | } 406 | 407 | @-webkit-keyframes header { 408 | 0% { 409 | -moz-transform: translate3d(0,1em,0); 410 | -webkit-transform: translate3d(0,1em,0); 411 | -ms-transform: translate3d(0,1em,0); 412 | transform: translate3d(0,1em,0); 413 | opacity: 0; 414 | } 415 | 416 | 100% { 417 | -moz-transform: translate3d(0,0,0); 418 | -webkit-transform: translate3d(0,0,0); 419 | -ms-transform: translate3d(0,0,0); 420 | transform: translate3d(0,0,0); 421 | opacity: 1; 422 | } 423 | } 424 | 425 | @-ms-keyframes header { 426 | 0% { 427 | -moz-transform: translate3d(0,1em,0); 428 | -webkit-transform: translate3d(0,1em,0); 429 | -ms-transform: translate3d(0,1em,0); 430 | transform: translate3d(0,1em,0); 431 | opacity: 0; 432 | } 433 | 434 | 100% { 435 | -moz-transform: translate3d(0,0,0); 436 | -webkit-transform: translate3d(0,0,0); 437 | -ms-transform: translate3d(0,0,0); 438 | transform: translate3d(0,0,0); 439 | opacity: 1; 440 | } 441 | } 442 | 443 | @keyframes header { 444 | 0% { 445 | -moz-transform: translate3d(0,1em,0); 446 | -webkit-transform: translate3d(0,1em,0); 447 | -ms-transform: translate3d(0,1em,0); 448 | transform: translate3d(0,1em,0); 449 | opacity: 0; 450 | } 451 | 452 | 100% { 453 | -moz-transform: translate3d(0,0,0); 454 | -webkit-transform: translate3d(0,0,0); 455 | -ms-transform: translate3d(0,0,0); 456 | transform: translate3d(0,0,0); 457 | opacity: 1; 458 | } 459 | } 460 | 461 | @-moz-keyframes nav-icons { 462 | 0% { 463 | -moz-transform: translate3d(0,1em,0); 464 | -webkit-transform: translate3d(0,1em,0); 465 | -ms-transform: translate3d(0,1em,0); 466 | transform: translate3d(0,1em,0); 467 | opacity: 0; 468 | } 469 | 470 | 100% { 471 | -moz-transform: translate3d(0,0,0); 472 | -webkit-transform: translate3d(0,0,0); 473 | -ms-transform: translate3d(0,0,0); 474 | transform: translate3d(0,0,0); 475 | opacity: 1; 476 | } 477 | } 478 | 479 | @-webkit-keyframes nav-icons { 480 | 0% { 481 | -moz-transform: translate3d(0,1em,0); 482 | -webkit-transform: translate3d(0,1em,0); 483 | -ms-transform: translate3d(0,1em,0); 484 | transform: translate3d(0,1em,0); 485 | opacity: 0; 486 | } 487 | 488 | 100% { 489 | -moz-transform: translate3d(0,0,0); 490 | -webkit-transform: translate3d(0,0,0); 491 | -ms-transform: translate3d(0,0,0); 492 | transform: translate3d(0,0,0); 493 | opacity: 1; 494 | } 495 | } 496 | 497 | @-ms-keyframes nav-icons { 498 | 0% { 499 | -moz-transform: translate3d(0,1em,0); 500 | -webkit-transform: translate3d(0,1em,0); 501 | -ms-transform: translate3d(0,1em,0); 502 | transform: translate3d(0,1em,0); 503 | opacity: 0; 504 | } 505 | 506 | 100% { 507 | -moz-transform: translate3d(0,0,0); 508 | -webkit-transform: translate3d(0,0,0); 509 | -ms-transform: translate3d(0,0,0); 510 | transform: translate3d(0,0,0); 511 | opacity: 1; 512 | } 513 | } 514 | 515 | @keyframes nav-icons { 516 | 0% { 517 | -moz-transform: translate3d(0,1em,0); 518 | -webkit-transform: translate3d(0,1em,0); 519 | -ms-transform: translate3d(0,1em,0); 520 | transform: translate3d(0,1em,0); 521 | opacity: 0; 522 | } 523 | 524 | 100% { 525 | -moz-transform: translate3d(0,0,0); 526 | -webkit-transform: translate3d(0,0,0); 527 | -ms-transform: translate3d(0,0,0); 528 | transform: translate3d(0,0,0); 529 | opacity: 1; 530 | } 531 | } 532 | 533 | #header { 534 | -moz-animation: header 1s 2.25s forwards; 535 | -webkit-animation: header 1s 2.25s forwards; 536 | -ms-animation: header 1s 2.25s forwards; 537 | animation: header 1s 2.25s forwards; 538 | -moz-backface-visibility: hidden; 539 | -webkit-backface-visibility: hidden; 540 | -ms-backface-visibility: hidden; 541 | backface-visibility: hidden; 542 | -moz-transform: translate3d(0,0,0); 543 | -webkit-transform: translate3d(0,0,0); 544 | -ms-transform: translate3d(0,0,0); 545 | transform: translate3d(0,0,0); 546 | cursor: default; 547 | display: inline-block; 548 | opacity: 0; 549 | position: relative; 550 | text-align: center; 551 | top: -1em; 552 | vertical-align: middle; 553 | width: 90%; 554 | } 555 | 556 | #header h1 { 557 | font-size: 4.35em; 558 | font-weight: 900; 559 | letter-spacing: -0.035em; 560 | line-height: 1em; 561 | } 562 | 563 | #header p { 564 | font-size: 1.25em; 565 | margin: 0.75em 0 0.25em 0; 566 | opacity: 0.75; 567 | } 568 | 569 | #header nav { 570 | margin: 1.5em 0 0 0; 571 | } 572 | 573 | #header nav li { 574 | -moz-animation: nav-icons 0.5s ease-in-out forwards; 575 | -webkit-animation: nav-icons 0.5s ease-in-out forwards; 576 | -ms-animation: nav-icons 0.5s ease-in-out forwards; 577 | animation: nav-icons 0.5s ease-in-out forwards; 578 | -moz-backface-visibility: hidden; 579 | -webkit-backface-visibility: hidden; 580 | -ms-backface-visibility: hidden; 581 | backface-visibility: hidden; 582 | -moz-transform: translate3d(0,0,0); 583 | -webkit-transform: translate3d(0,0,0); 584 | -ms-transform: translate3d(0,0,0); 585 | transform: translate3d(0,0,0); 586 | display: inline-block; 587 | height: 5.35em; 588 | line-height: 5.885em; 589 | opacity: 0; 590 | position: relative; 591 | top: 0; 592 | width: 5.35em; 593 | } 594 | 595 | #header nav li:nth-child(1) { 596 | -moz-animation-delay: 2.5s; 597 | -webkit-animation-delay: 2.5s; 598 | -ms-animation-delay: 2.5s; 599 | animation-delay: 2.5s; 600 | } 601 | 602 | #header nav li:nth-child(2) { 603 | -moz-animation-delay: 2.75s; 604 | -webkit-animation-delay: 2.75s; 605 | -ms-animation-delay: 2.75s; 606 | animation-delay: 2.75s; 607 | } 608 | 609 | #header nav li:nth-child(3) { 610 | -moz-animation-delay: 3s; 611 | -webkit-animation-delay: 3s; 612 | -ms-animation-delay: 3s; 613 | animation-delay: 3s; 614 | } 615 | 616 | #header nav li:nth-child(4) { 617 | -moz-animation-delay: 3.25s; 618 | -webkit-animation-delay: 3.25s; 619 | -ms-animation-delay: 3.25s; 620 | animation-delay: 3.25s; 621 | } 622 | 623 | #header nav li:nth-child(5) { 624 | -moz-animation-delay: 3.5s; 625 | -webkit-animation-delay: 3.5s; 626 | -ms-animation-delay: 3.5s; 627 | animation-delay: 3.5s; 628 | } 629 | 630 | #header nav li:nth-child(6) { 631 | -moz-animation-delay: 3.75s; 632 | -webkit-animation-delay: 3.75s; 633 | -ms-animation-delay: 3.75s; 634 | animation-delay: 3.75s; 635 | } 636 | 637 | #header nav li:nth-child(7) { 638 | -moz-animation-delay: 4s; 639 | -webkit-animation-delay: 4s; 640 | -ms-animation-delay: 4s; 641 | animation-delay: 4s; 642 | } 643 | 644 | #header nav li:nth-child(8) { 645 | -moz-animation-delay: 4.25s; 646 | -webkit-animation-delay: 4.25s; 647 | -ms-animation-delay: 4.25s; 648 | animation-delay: 4.25s; 649 | } 650 | 651 | #header nav li:nth-child(9) { 652 | -moz-animation-delay: 4.5s; 653 | -webkit-animation-delay: 4.5s; 654 | -ms-animation-delay: 4.5s; 655 | animation-delay: 4.5s; 656 | } 657 | 658 | #header nav li:nth-child(10) { 659 | -moz-animation-delay: 4.75s; 660 | -webkit-animation-delay: 4.75s; 661 | -ms-animation-delay: 4.75s; 662 | animation-delay: 4.75s; 663 | } 664 | 665 | #header nav a { 666 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 667 | -webkit-touch-callout: none; 668 | border: 0; 669 | display: inline-block; 670 | } 671 | 672 | #header nav a:before { 673 | -moz-transition: all 0.2s ease-in-out; 674 | -webkit-transition: all 0.2s ease-in-out; 675 | -ms-transition: all 0.2s ease-in-out; 676 | transition: all 0.2s ease-in-out; 677 | border-radius: 100%; 678 | border: solid 1px #fff; 679 | display: block; 680 | font-size: 1.75em; 681 | height: 2.5em; 682 | line-height: 2.5em; 683 | position: relative; 684 | text-align: center; 685 | top: 0; 686 | width: 2.5em; 687 | } 688 | 689 | #header nav a:hover { 690 | font-size: 1.1em; 691 | } 692 | 693 | #header nav a:hover:before { 694 | background-color: rgba(255, 255, 255, 0.175); 695 | color: #fff; 696 | } 697 | 698 | #header nav a:active { 699 | font-size: 0.95em; 700 | background: none; 701 | } 702 | 703 | #header nav a:active:before { 704 | background-color: rgba(255, 255, 255, 0.35); 705 | color: #fff; 706 | } 707 | 708 | #header nav a span { 709 | display: none; 710 | } 711 | 712 | /* Footer */ 713 | 714 | #footer { 715 | background-image: -moz-linear-gradient(top, rgba(0,0,0,0), rgba(0,0,0,0.5) 75%); 716 | background-image: -webkit-linear-gradient(top, rgba(0,0,0,0), rgba(0,0,0,0.5) 75%); 717 | background-image: -ms-linear-gradient(top, rgba(0,0,0,0), rgba(0,0,0,0.5) 75%); 718 | background-image: linear-gradient(top, rgba(0,0,0,0), rgba(0,0,0,0.5) 75%); 719 | bottom: 0; 720 | cursor: default; 721 | height: 6em; 722 | left: 0; 723 | line-height: 8em; 724 | position: absolute; 725 | text-align: center; 726 | width: 100%; 727 | } 728 | 729 | /* Wide */ 730 | 731 | @media screen and (max-width: 1680px) { 732 | 733 | /* Basic */ 734 | 735 | body, input, select, textarea { 736 | font-size: 13pt; 737 | } 738 | 739 | /* BG */ 740 | 741 | @-moz-keyframes bg { 742 | 0% { 743 | -moz-transform: translate3d(0,0,0); 744 | -webkit-transform: translate3d(0,0,0); 745 | -ms-transform: translate3d(0,0,0); 746 | transform: translate3d(0,0,0); 747 | } 748 | 749 | 100% { 750 | -moz-transform: translate3d(-1500px,0,0); 751 | -webkit-transform: translate3d(-1500px,0,0); 752 | -ms-transform: translate3d(-1500px,0,0); 753 | transform: translate3d(-1500px,0,0); 754 | } 755 | 756 | } 757 | 758 | @-webkit-keyframes bg { 759 | 0% { 760 | -moz-transform: translate3d(0,0,0); 761 | -webkit-transform: translate3d(0,0,0); 762 | -ms-transform: translate3d(0,0,0); 763 | transform: translate3d(0,0,0); 764 | } 765 | 766 | 100% { 767 | -moz-transform: translate3d(-1500px,0,0); 768 | -webkit-transform: translate3d(-1500px,0,0); 769 | -ms-transform: translate3d(-1500px,0,0); 770 | transform: translate3d(-1500px,0,0); 771 | } 772 | } 773 | 774 | @-ms-keyframes bg { 775 | 0% { 776 | -moz-transform: translate3d(0,0,0); 777 | -webkit-transform: translate3d(0,0,0); 778 | -ms-transform: translate3d(0,0,0); 779 | transform: translate3d(0,0,0); 780 | } 781 | 782 | 100% { 783 | -moz-transform: translate3d(-1500px,0,0); 784 | -webkit-transform: translate3d(-1500px,0,0); 785 | -ms-transform: translate3d(-1500px,0,0); 786 | transform: translate3d(-1500px,0,0); 787 | } 788 | } 789 | 790 | @keyframes bg { 791 | 0% { 792 | -moz-transform: translate3d(0,0,0); 793 | -webkit-transform: translate3d(0,0,0); 794 | -ms-transform: translate3d(0,0,0); 795 | transform: translate3d(0,0,0); 796 | } 797 | 798 | 100% { 799 | -moz-transform: translate3d(-1500px,0,0); 800 | -webkit-transform: translate3d(-1500px,0,0); 801 | -ms-transform: translate3d(-1500px,0,0); 802 | transform: translate3d(-1500px,0,0); 803 | } 804 | } 805 | 806 | #bg { 807 | background-size: 1500px auto; 808 | width: 4500px; 809 | } } 810 | 811 | /* Normal */ 812 | 813 | @media screen and (max-width: 1280px) { 814 | 815 | /* Basic */ 816 | 817 | body, input, select, textarea { 818 | font-size: 12pt; 819 | } 820 | 821 | /* BG */ 822 | 823 | @-moz-keyframes bg { 824 | 0% { 825 | -moz-transform: translate3d(0,0,0); 826 | -webkit-transform: translate3d(0,0,0); 827 | -ms-transform: translate3d(0,0,0); 828 | transform: translate3d(0,0,0); 829 | } 830 | 831 | 100% { 832 | -moz-transform: translate3d(-750px,0,0); 833 | -webkit-transform: translate3d(-750px,0,0); 834 | -ms-transform: translate3d(-750px,0,0); 835 | transform: translate3d(-750px,0,0); 836 | } 837 | 838 | } 839 | 840 | @-webkit-keyframes bg { 841 | 0% { 842 | -moz-transform: translate3d(0,0,0); 843 | -webkit-transform: translate3d(0,0,0); 844 | -ms-transform: translate3d(0,0,0); 845 | transform: translate3d(0,0,0); 846 | } 847 | 848 | 100% { 849 | -moz-transform: translate3d(-750px,0,0); 850 | -webkit-transform: translate3d(-750px,0,0); 851 | -ms-transform: translate3d(-750px,0,0); 852 | transform: translate3d(-750px,0,0); 853 | } 854 | } 855 | 856 | @-ms-keyframes bg { 857 | 0% { 858 | -moz-transform: translate3d(0,0,0); 859 | -webkit-transform: translate3d(0,0,0); 860 | -ms-transform: translate3d(0,0,0); 861 | transform: translate3d(0,0,0); 862 | } 863 | 864 | 100% { 865 | -moz-transform: translate3d(-750px,0,0); 866 | -webkit-transform: translate3d(-750px,0,0); 867 | -ms-transform: translate3d(-750px,0,0); 868 | transform: translate3d(-750px,0,0); 869 | } 870 | } 871 | 872 | @keyframes bg { 873 | 0% { 874 | -moz-transform: translate3d(0,0,0); 875 | -webkit-transform: translate3d(0,0,0); 876 | -ms-transform: translate3d(0,0,0); 877 | transform: translate3d(0,0,0); 878 | } 879 | 880 | 100% { 881 | -moz-transform: translate3d(-750px,0,0); 882 | -webkit-transform: translate3d(-750px,0,0); 883 | -ms-transform: translate3d(-750px,0,0); 884 | transform: translate3d(-750px,0,0); 885 | } 886 | } 887 | 888 | #bg { 889 | background-size: 750px auto; 890 | width: 2250px; 891 | } } 892 | 893 | /* Mobile */ 894 | 895 | @media screen and (max-width: 736px) { 896 | 897 | /* Basic */ 898 | 899 | body { 900 | min-width: 320px; 901 | } 902 | 903 | body, input, select, textarea { 904 | font-size: 11pt; 905 | } 906 | 907 | /* BG */ 908 | 909 | @-moz-keyframes bg { 910 | 0% { 911 | -moz-transform: translate3d(0,0,0); 912 | -webkit-transform: translate3d(0,0,0); 913 | -ms-transform: translate3d(0,0,0); 914 | transform: translate3d(0,0,0); 915 | } 916 | 917 | 100% { 918 | -moz-transform: translate3d(-300px,0,0); 919 | -webkit-transform: translate3d(-300px,0,0); 920 | -ms-transform: translate3d(-300px,0,0); 921 | transform: translate3d(-300px,0,0); 922 | } 923 | 924 | } 925 | 926 | @-webkit-keyframes bg { 927 | 0% { 928 | -moz-transform: translate3d(0,0,0); 929 | -webkit-transform: translate3d(0,0,0); 930 | -ms-transform: translate3d(0,0,0); 931 | transform: translate3d(0,0,0); 932 | } 933 | 934 | 100% { 935 | -moz-transform: translate3d(-300px,0,0); 936 | -webkit-transform: translate3d(-300px,0,0); 937 | -ms-transform: translate3d(-300px,0,0); 938 | transform: translate3d(-300px,0,0); 939 | } 940 | } 941 | 942 | @-ms-keyframes bg { 943 | 0% { 944 | -moz-transform: translate3d(0,0,0); 945 | -webkit-transform: translate3d(0,0,0); 946 | -ms-transform: translate3d(0,0,0); 947 | transform: translate3d(0,0,0); 948 | } 949 | 950 | 100% { 951 | -moz-transform: translate3d(-300px,0,0); 952 | -webkit-transform: translate3d(-300px,0,0); 953 | -ms-transform: translate3d(-300px,0,0); 954 | transform: translate3d(-300px,0,0); 955 | } 956 | } 957 | 958 | @keyframes bg { 959 | 0% { 960 | -moz-transform: translate3d(0,0,0); 961 | -webkit-transform: translate3d(0,0,0); 962 | -ms-transform: translate3d(0,0,0); 963 | transform: translate3d(0,0,0); 964 | } 965 | 966 | 100% { 967 | -moz-transform: translate3d(-300px,0,0); 968 | -webkit-transform: translate3d(-300px,0,0); 969 | -ms-transform: translate3d(-300px,0,0); 970 | transform: translate3d(-300px,0,0); 971 | } 972 | } 973 | 974 | #bg { 975 | background-size: 300px auto; 976 | width: 900px; 977 | } 978 | 979 | /* Header */ 980 | 981 | #header h1 { 982 | font-size: 2.5em; 983 | } 984 | 985 | #header p { 986 | font-size: 1em; 987 | } 988 | 989 | #header nav { 990 | font-size: 1em; 991 | } 992 | 993 | #header nav a:hover { 994 | font-size: 1em; 995 | } 996 | 997 | #header nav a:active { 998 | font-size: 1em; 999 | } } 1000 | 1001 | /* Mobile (Portrait) */ 1002 | 1003 | @media screen and (max-width: 480px) { 1004 | 1005 | /* BG */ 1006 | 1007 | @-moz-keyframes bg { 1008 | 0% { 1009 | -moz-transform: translate3d(0,0,0); 1010 | -webkit-transform: translate3d(0,0,0); 1011 | -ms-transform: translate3d(0,0,0); 1012 | transform: translate3d(0,0,0); 1013 | } 1014 | 1015 | 100% { 1016 | -moz-transform: translate3d(-412.5px,0,0); 1017 | -webkit-transform: translate3d(-412.5px,0,0); 1018 | -ms-transform: translate3d(-412.5px,0,0); 1019 | transform: translate3d(-412.5px,0,0); 1020 | } 1021 | 1022 | } 1023 | 1024 | @-webkit-keyframes bg { 1025 | 0% { 1026 | -moz-transform: translate3d(0,0,0); 1027 | -webkit-transform: translate3d(0,0,0); 1028 | -ms-transform: translate3d(0,0,0); 1029 | transform: translate3d(0,0,0); 1030 | } 1031 | 1032 | 100% { 1033 | -moz-transform: translate3d(-412.5px,0,0); 1034 | -webkit-transform: translate3d(-412.5px,0,0); 1035 | -ms-transform: translate3d(-412.5px,0,0); 1036 | transform: translate3d(-412.5px,0,0); 1037 | } 1038 | } 1039 | 1040 | @-ms-keyframes bg { 1041 | 0% { 1042 | -moz-transform: translate3d(0,0,0); 1043 | -webkit-transform: translate3d(0,0,0); 1044 | -ms-transform: translate3d(0,0,0); 1045 | transform: translate3d(0,0,0); 1046 | } 1047 | 1048 | 100% { 1049 | -moz-transform: translate3d(-412.5px,0,0); 1050 | -webkit-transform: translate3d(-412.5px,0,0); 1051 | -ms-transform: translate3d(-412.5px,0,0); 1052 | transform: translate3d(-412.5px,0,0); 1053 | } 1054 | } 1055 | 1056 | @keyframes bg { 1057 | 0% { 1058 | -moz-transform: translate3d(0,0,0); 1059 | -webkit-transform: translate3d(0,0,0); 1060 | -ms-transform: translate3d(0,0,0); 1061 | transform: translate3d(0,0,0); 1062 | } 1063 | 1064 | 100% { 1065 | -moz-transform: translate3d(-412.5px,0,0); 1066 | -webkit-transform: translate3d(-412.5px,0,0); 1067 | -ms-transform: translate3d(-412.5px,0,0); 1068 | transform: translate3d(-412.5px,0,0); 1069 | } 1070 | } 1071 | 1072 | #bg { 1073 | background-size: 412.5px auto; 1074 | width: 1237.5px; 1075 | } 1076 | 1077 | /* Header */ 1078 | 1079 | #header nav { 1080 | padding: 0 1em; 1081 | } } -------------------------------------------------------------------------------- /public/css/noscript.css: -------------------------------------------------------------------------------- 1 | /* 2 | Aerial by HTML5 UP 3 | html5up.net | @ajlkn 4 | Free for personal and commercial use under the CCA 3.0 license (html5up.net/license) 5 | */ 6 | 7 | /* Wrapper */ 8 | 9 | #wrapper { 10 | opacity: 1 !important; 11 | } 12 | 13 | /* Overlay */ 14 | 15 | #overlay { 16 | opacity: 1 !important; 17 | } 18 | 19 | /* Header */ 20 | 21 | #header { 22 | opacity: 1 !important; 23 | } 24 | 25 | #header nav li { 26 | opacity: 1 !important; 27 | } -------------------------------------------------------------------------------- /public/download/GinsMoocExtension_v2.0.2.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ginnnnnncc/GinsMooc/0ee1a57ec764cddcb19d5554e1aedfdf0ca5518b/public/download/GinsMoocExtension_v2.0.2.zip -------------------------------------------------------------------------------- /public/extension-auto-evaluate-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ginnnnnncc/GinsMooc/0ee1a57ec764cddcb19d5554e1aedfdf0ca5518b/public/extension-auto-evaluate-1.png -------------------------------------------------------------------------------- /public/extension-auto-evaluate-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ginnnnnncc/GinsMooc/0ee1a57ec764cddcb19d5554e1aedfdf0ca5518b/public/extension-auto-evaluate-2.png -------------------------------------------------------------------------------- /public/extension-completion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ginnnnnncc/GinsMooc/0ee1a57ec764cddcb19d5554e1aedfdf0ca5518b/public/extension-completion.png -------------------------------------------------------------------------------- /public/extension-developer-mode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ginnnnnncc/GinsMooc/0ee1a57ec764cddcb19d5554e1aedfdf0ca5518b/public/extension-developer-mode.png -------------------------------------------------------------------------------- /public/extension-homework.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ginnnnnncc/GinsMooc/0ee1a57ec764cddcb19d5554e1aedfdf0ca5518b/public/extension-homework.png -------------------------------------------------------------------------------- /public/extension-load-decompression.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ginnnnnncc/GinsMooc/0ee1a57ec764cddcb19d5554e1aedfdf0ca5518b/public/extension-load-decompression.png -------------------------------------------------------------------------------- /public/extension-multiple-choice.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ginnnnnncc/GinsMooc/0ee1a57ec764cddcb19d5554e1aedfdf0ca5518b/public/extension-multiple-choice.png -------------------------------------------------------------------------------- /public/extension-single-choice.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ginnnnnncc/GinsMooc/0ee1a57ec764cddcb19d5554e1aedfdf0ca5518b/public/extension-single-choice.png -------------------------------------------------------------------------------- /public/extension-updating.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ginnnnnncc/GinsMooc/0ee1a57ec764cddcb19d5554e1aedfdf0ca5518b/public/extension-updating.png -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ginnnnnncc/GinsMooc/0ee1a57ec764cddcb19d5554e1aedfdf0ca5518b/public/favicon.png -------------------------------------------------------------------------------- /public/guess.java: -------------------------------------------------------------------------------- 1 | package top.ginnnnnn.mooc.service.implement; 2 | 3 | import java.security.SecureRandom; 4 | import java.util.HashMap; 5 | 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.data.redis.core.HashOperations; 8 | import org.springframework.data.redis.core.StringRedisTemplate; 9 | import org.springframework.stereotype.Service; 10 | 11 | import top.ginnnnnn.mooc.service.GameService; 12 | 13 | @Service 14 | public class GameServiceImpl implements GameService { 15 | 16 | private static final SecureRandom RNG = new SecureRandom(); 17 | private static final String GUESS_GAME_PREFIX = "GinsGame-guess-"; 18 | @Autowired 19 | private StringRedisTemplate stringRedisTemplate; 20 | 21 | public HashMap guess(String token, Double guess) { 22 | final String key = GUESS_GAME_PREFIX + token; 23 | init(key); 24 | HashOperations hashOperations = stringRedisTemplate.opsForHash(); 25 | 26 | Double number = Double.valueOf((String)hashOperations.get(key, "number")); 27 | boolean isLess = number > guess + 1e-6 / 2; 28 | boolean isMore = number < guess - 1e-6 / 2; 29 | 30 | boolean isPassed = !isLess && !isMore; 31 | boolean isTalented = isPassed && !hashOperations.hasKey(key, "previous"); 32 | 33 | HashMap state = new HashMap<>(5); 34 | state.put("less", isLess); 35 | state.put("more", isMore); 36 | 37 | hashOperations.putIfAbsent(key, "previous", "0"); 38 | hashOperations.increment(key, "previous", 1); 39 | if (isPassed) { 40 | hashOperations.put(key, "number", String.format("%f", RNG.nextInt(1, 1000000) * 1e-6)); 41 | hashOperations.delete(key, "previous"); 42 | hashOperations.increment(key, "passed", 1); 43 | } 44 | if (isTalented) { 45 | hashOperations.increment(key, "talented", 1); 46 | state.put("reward", Boolean.TRUE); 47 | } else { 48 | state.put("reward", Boolean.FALSE); 49 | } 50 | 51 | HashMap ret = new HashMap<>(3); 52 | ret.put("info", infoBuild(key)); 53 | ret.put("state", state); 54 | return ret; 55 | 56 | } 57 | 58 | public HashMap getState(String token) { 59 | final String key = GUESS_GAME_PREFIX + token; 60 | init(key); 61 | return infoBuild(key); 62 | } 63 | 64 | public boolean refresh(String token) { 65 | final String key = GUESS_GAME_PREFIX + token; 66 | init(key); 67 | HashOperations hashOperations = stringRedisTemplate.opsForHash(); 68 | 69 | hashOperations.put(key, "passed", "0"); 70 | hashOperations.put(key, "talented", "0"); 71 | hashOperations.put(key, "number", String.format("%f", RNG.nextInt(1, 1000000) * 1e-6)); 72 | hashOperations.delete(key, "previous"); 73 | return true; 74 | } 75 | 76 | private void init(String key) { 77 | HashOperations hashOperations = stringRedisTemplate.opsForHash(); 78 | hashOperations.putIfAbsent(key, "passed", "0"); 79 | hashOperations.putIfAbsent(key, "talented", "0"); 80 | hashOperations.putIfAbsent(key, "number", String.format("%f", RNG.nextInt(1, 1000000) * 1e-6)); 81 | } 82 | 83 | private HashMap infoBuild(String key) { 84 | HashOperations hashOperations = stringRedisTemplate.opsForHash(); 85 | HashMap info = new HashMap<>(3); 86 | info.put("passed", hashOperations.get(key, "passed")); 87 | info.put("talented", hashOperations.get(key, "talented")); 88 | return info; 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /public/headicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ginnnnnncc/GinsMooc/0ee1a57ec764cddcb19d5554e1aedfdf0ca5518b/public/headicon.png -------------------------------------------------------------------------------- /public/new-course-help.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ginnnnnncc/GinsMooc/0ee1a57ec764cddcb19d5554e1aedfdf0ca5518b/public/new-course-help.png -------------------------------------------------------------------------------- /public/question.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ginnnnnncc/GinsMooc/0ee1a57ec764cddcb19d5554e1aedfdf0ca5518b/public/question.png -------------------------------------------------------------------------------- /public/sass/libs/_breakpoints.scss: -------------------------------------------------------------------------------- 1 | // breakpoints.scss v1.0 | @ajlkn | MIT licensed */ 2 | 3 | // Vars. 4 | 5 | /// Breakpoints. 6 | /// @var {list} 7 | $breakpoints: () !global; 8 | 9 | // Mixins. 10 | 11 | /// Sets breakpoints. 12 | /// @param {map} $x Breakpoints. 13 | @mixin breakpoints($x: ()) { 14 | $breakpoints: $x !global; 15 | } 16 | 17 | /// Wraps @content in a @media block targeting a specific orientation. 18 | /// @param {string} $orientation Orientation. 19 | @mixin orientation($orientation) { 20 | @media screen and (orientation: #{$orientation}) { 21 | @content; 22 | } 23 | } 24 | 25 | /// Wraps @content in a @media block using a given query. 26 | /// @param {string} $query Query. 27 | @mixin breakpoint($query: null) { 28 | 29 | $breakpoint: null; 30 | $op: null; 31 | $media: null; 32 | 33 | // Determine operator, breakpoint. 34 | 35 | // Greater than or equal. 36 | @if (str-slice($query, 0, 2) == '>=') { 37 | 38 | $op: 'gte'; 39 | $breakpoint: str-slice($query, 3); 40 | 41 | } 42 | 43 | // Less than or equal. 44 | @elseif (str-slice($query, 0, 2) == '<=') { 45 | 46 | $op: 'lte'; 47 | $breakpoint: str-slice($query, 3); 48 | 49 | } 50 | 51 | // Greater than. 52 | @elseif (str-slice($query, 0, 1) == '>') { 53 | 54 | $op: 'gt'; 55 | $breakpoint: str-slice($query, 2); 56 | 57 | } 58 | 59 | // Less than. 60 | @elseif (str-slice($query, 0, 1) == '<') { 61 | 62 | $op: 'lt'; 63 | $breakpoint: str-slice($query, 2); 64 | 65 | } 66 | 67 | // Not. 68 | @elseif (str-slice($query, 0, 1) == '!') { 69 | 70 | $op: 'not'; 71 | $breakpoint: str-slice($query, 2); 72 | 73 | } 74 | 75 | // Equal. 76 | @else { 77 | 78 | $op: 'eq'; 79 | $breakpoint: $query; 80 | 81 | } 82 | 83 | // Build media. 84 | @if ($breakpoint and map-has-key($breakpoints, $breakpoint)) { 85 | 86 | $a: map-get($breakpoints, $breakpoint); 87 | 88 | // Range. 89 | @if (type-of($a) == 'list') { 90 | 91 | $x: nth($a, 1); 92 | $y: nth($a, 2); 93 | 94 | // Max only. 95 | @if ($x == null) { 96 | 97 | // Greater than or equal (>= 0 / anything) 98 | @if ($op == 'gte') { 99 | $media: 'screen'; 100 | } 101 | 102 | // Less than or equal (<= y) 103 | @elseif ($op == 'lte') { 104 | $media: 'screen and (max-width: ' + $y + ')'; 105 | } 106 | 107 | // Greater than (> y) 108 | @elseif ($op == 'gt') { 109 | $media: 'screen and (min-width: ' + ($y + 1) + ')'; 110 | } 111 | 112 | // Less than (< 0 / invalid) 113 | @elseif ($op == 'lt') { 114 | $media: 'screen and (max-width: -1px)'; 115 | } 116 | 117 | // Not (> y) 118 | @elseif ($op == 'not') { 119 | $media: 'screen and (min-width: ' + ($y + 1) + ')'; 120 | } 121 | 122 | // Equal (<= y) 123 | @else { 124 | $media: 'screen and (max-width: ' + $y + ')'; 125 | } 126 | 127 | } 128 | 129 | // Min only. 130 | @else if ($y == null) { 131 | 132 | // Greater than or equal (>= x) 133 | @if ($op == 'gte') { 134 | $media: 'screen and (min-width: ' + $x + ')'; 135 | } 136 | 137 | // Less than or equal (<= inf / anything) 138 | @elseif ($op == 'lte') { 139 | $media: 'screen'; 140 | } 141 | 142 | // Greater than (> inf / invalid) 143 | @elseif ($op == 'gt') { 144 | $media: 'screen and (max-width: -1px)'; 145 | } 146 | 147 | // Less than (< x) 148 | @elseif ($op == 'lt') { 149 | $media: 'screen and (max-width: ' + ($x - 1) + ')'; 150 | } 151 | 152 | // Not (< x) 153 | @elseif ($op == 'not') { 154 | $media: 'screen and (max-width: ' + ($x - 1) + ')'; 155 | } 156 | 157 | // Equal (>= x) 158 | @else { 159 | $media: 'screen and (min-width: ' + $x + ')'; 160 | } 161 | 162 | } 163 | 164 | // Min and max. 165 | @else { 166 | 167 | // Greater than or equal (>= x) 168 | @if ($op == 'gte') { 169 | $media: 'screen and (min-width: ' + $x + ')'; 170 | } 171 | 172 | // Less than or equal (<= y) 173 | @elseif ($op == 'lte') { 174 | $media: 'screen and (max-width: ' + $y + ')'; 175 | } 176 | 177 | // Greater than (> y) 178 | @elseif ($op == 'gt') { 179 | $media: 'screen and (min-width: ' + ($y + 1) + ')'; 180 | } 181 | 182 | // Less than (< x) 183 | @elseif ($op == 'lt') { 184 | $media: 'screen and (max-width: ' + ($x - 1) + ')'; 185 | } 186 | 187 | // Not (< x and > y) 188 | @elseif ($op == 'not') { 189 | $media: 'screen and (max-width: ' + ($x - 1) + '), screen and (min-width: ' + ($y + 1) + ')'; 190 | } 191 | 192 | // Equal (>= x and <= y) 193 | @else { 194 | $media: 'screen and (min-width: ' + $x + ') and (max-width: ' + $y + ')'; 195 | } 196 | 197 | } 198 | 199 | } 200 | 201 | // String. 202 | @else { 203 | 204 | // Missing a media type? Prefix with "screen". 205 | @if (str-slice($a, 0, 1) == '(') { 206 | $media: 'screen and ' + $a; 207 | } 208 | 209 | // Otherwise, use as-is. 210 | @else { 211 | $media: $a; 212 | } 213 | 214 | } 215 | 216 | } 217 | 218 | // Output. 219 | @media #{$media} { 220 | @content; 221 | } 222 | 223 | } -------------------------------------------------------------------------------- /public/sass/libs/_functions.scss: -------------------------------------------------------------------------------- 1 | /// Removes a specific item from a list. 2 | /// @author Hugo Giraudel 3 | /// @param {list} $list List. 4 | /// @param {integer} $index Index. 5 | /// @return {list} Updated list. 6 | @function remove-nth($list, $index) { 7 | 8 | $result: null; 9 | 10 | @if type-of($index) != number { 11 | @warn "$index: #{quote($index)} is not a number for `remove-nth`."; 12 | } 13 | @else if $index == 0 { 14 | @warn "List index 0 must be a non-zero integer for `remove-nth`."; 15 | } 16 | @else if abs($index) > length($list) { 17 | @warn "List index is #{$index} but list is only #{length($list)} item long for `remove-nth`."; 18 | } 19 | @else { 20 | 21 | $result: (); 22 | $index: if($index < 0, length($list) + $index + 1, $index); 23 | 24 | @for $i from 1 through length($list) { 25 | 26 | @if $i != $index { 27 | $result: append($result, nth($list, $i)); 28 | } 29 | 30 | } 31 | 32 | } 33 | 34 | @return $result; 35 | 36 | } 37 | 38 | /// Gets a value from a map. 39 | /// @author Hugo Giraudel 40 | /// @param {map} $map Map. 41 | /// @param {string} $keys Key(s). 42 | /// @return {string} Value. 43 | @function val($map, $keys...) { 44 | 45 | @if nth($keys, 1) == null { 46 | $keys: remove-nth($keys, 1); 47 | } 48 | 49 | @each $key in $keys { 50 | $map: map-get($map, $key); 51 | } 52 | 53 | @return $map; 54 | 55 | } 56 | 57 | /// Gets a duration value. 58 | /// @param {string} $keys Key(s). 59 | /// @return {string} Value. 60 | @function _duration($keys...) { 61 | @return val($duration, $keys...); 62 | } 63 | 64 | /// Gets a font value. 65 | /// @param {string} $keys Key(s). 66 | /// @return {string} Value. 67 | @function _font($keys...) { 68 | @return val($font, $keys...); 69 | } 70 | 71 | /// Gets a misc value. 72 | /// @param {string} $keys Key(s). 73 | /// @return {string} Value. 74 | @function _misc($keys...) { 75 | @return val($misc, $keys...); 76 | } 77 | 78 | /// Gets a palette value. 79 | /// @param {string} $keys Key(s). 80 | /// @return {string} Value. 81 | @function _palette($keys...) { 82 | @return val($palette, $keys...); 83 | } 84 | 85 | /// Gets a size value. 86 | /// @param {string} $keys Key(s). 87 | /// @return {string} Value. 88 | @function _size($keys...) { 89 | @return val($size, $keys...); 90 | } -------------------------------------------------------------------------------- /public/sass/libs/_mixins.scss: -------------------------------------------------------------------------------- 1 | /// Makes an element's :before pseudoelement a FontAwesome icon. 2 | /// @param {string} $content Optional content value to use. 3 | /// @param {string} $category Optional category to use. 4 | /// @param {string} $where Optional pseudoelement to target (before or after). 5 | @mixin icon($content: false, $category: regular, $where: before) { 6 | 7 | text-decoration: none; 8 | 9 | &:#{$where} { 10 | 11 | @if $content { 12 | content: $content; 13 | } 14 | 15 | -moz-osx-font-smoothing: grayscale; 16 | -webkit-font-smoothing: antialiased; 17 | display: inline-block; 18 | font-style: normal; 19 | font-variant: normal; 20 | text-rendering: auto; 21 | line-height: 1; 22 | text-transform: none !important; 23 | 24 | @if ($category == brands) { 25 | font-family: 'Font Awesome 5 Brands'; 26 | } 27 | @elseif ($category == solid) { 28 | font-family: 'Font Awesome 5 Free'; 29 | font-weight: 900; 30 | } 31 | @else { 32 | font-family: 'Font Awesome 5 Free'; 33 | font-weight: 400; 34 | } 35 | 36 | } 37 | 38 | } 39 | 40 | /// Applies padding to an element, taking the current element-margin value into account. 41 | /// @param {mixed} $tb Top/bottom padding. 42 | /// @param {mixed} $lr Left/right padding. 43 | /// @param {list} $pad Optional extra padding (in the following order top, right, bottom, left) 44 | /// @param {bool} $important If true, adds !important. 45 | @mixin padding($tb, $lr, $pad: (0,0,0,0), $important: null) { 46 | 47 | @if $important { 48 | $important: '!important'; 49 | } 50 | 51 | $x: 0.1em; 52 | 53 | @if unit(_size(element-margin)) == 'rem' { 54 | $x: 0.1rem; 55 | } 56 | 57 | padding: ($tb + nth($pad,1)) ($lr + nth($pad,2)) max($x, $tb - _size(element-margin) + nth($pad,3)) ($lr + nth($pad,4)) #{$important}; 58 | 59 | } 60 | 61 | /// Encodes a SVG data URL so IE doesn't choke (via codepen.io/jakob-e/pen/YXXBrp). 62 | /// @param {string} $svg SVG data URL. 63 | /// @return {string} Encoded SVG data URL. 64 | @function svg-url($svg) { 65 | 66 | $svg: str-replace($svg, '"', '\''); 67 | $svg: str-replace($svg, '%', '%25'); 68 | $svg: str-replace($svg, '<', '%3C'); 69 | $svg: str-replace($svg, '>', '%3E'); 70 | $svg: str-replace($svg, '&', '%26'); 71 | $svg: str-replace($svg, '#', '%23'); 72 | $svg: str-replace($svg, '{', '%7B'); 73 | $svg: str-replace($svg, '}', '%7D'); 74 | $svg: str-replace($svg, ';', '%3B'); 75 | 76 | @return url("data:image/svg+xml;charset=utf8,#{$svg}"); 77 | 78 | } -------------------------------------------------------------------------------- /public/sass/libs/_vars.scss: -------------------------------------------------------------------------------- 1 | // Misc. 2 | $misc: ( 3 | bg: #348cb2 url("images/bg.jpg") bottom left, 4 | bg-width: 1500px 5 | ); 6 | 7 | // Duration. 8 | $duration: ( 9 | bg: 60s, 10 | wrapper: 3s, 11 | overlay: 1.5s, 12 | header: 1s, 13 | nav-icons: 0.5s 14 | ); 15 | 16 | // Size. 17 | $size: ( 18 | nav-icon-wrapper: 5.35em, 19 | nav-icon: 1.75em 20 | ); 21 | 22 | // Font. 23 | $font: ( 24 | ); 25 | 26 | // Palette. 27 | $palette: ( 28 | bg: #fff, 29 | fg: #fff, 30 | 31 | nav-icon: ( 32 | hover-bg: rgba(255,255,255,0.175), 33 | hover-fg: #fff, 34 | active-bg: rgba(255,255,255,0.35), 35 | active-fg: #fff 36 | ) 37 | ); -------------------------------------------------------------------------------- /public/sass/libs/_vendor.scss: -------------------------------------------------------------------------------- 1 | // vendor.scss v1.0 | @ajlkn | MIT licensed */ 2 | 3 | // Vars. 4 | 5 | /// Vendor prefixes. 6 | /// @var {list} 7 | $vendor-prefixes: ( 8 | '-moz-', 9 | '-webkit-', 10 | '-ms-', 11 | '' 12 | ); 13 | 14 | /// Properties that should be vendorized. 15 | /// Data via caniuse.com, github.com/postcss/autoprefixer, and developer.mozilla.org 16 | /// @var {list} 17 | $vendor-properties: ( 18 | 19 | // Animation. 20 | 'animation', 21 | 'animation-delay', 22 | 'animation-direction', 23 | 'animation-duration', 24 | 'animation-fill-mode', 25 | 'animation-iteration-count', 26 | 'animation-name', 27 | 'animation-play-state', 28 | 'animation-timing-function', 29 | 30 | // Appearance. 31 | 'appearance', 32 | 33 | // Backdrop filter. 34 | 'backdrop-filter', 35 | 36 | // Background image options. 37 | 'background-clip', 38 | 'background-origin', 39 | 'background-size', 40 | 41 | // Box sizing. 42 | 'box-sizing', 43 | 44 | // Clip path. 45 | 'clip-path', 46 | 47 | // Filter effects. 48 | 'filter', 49 | 50 | // Flexbox. 51 | 'align-content', 52 | 'align-items', 53 | 'align-self', 54 | 'flex', 55 | 'flex-basis', 56 | 'flex-direction', 57 | 'flex-flow', 58 | 'flex-grow', 59 | 'flex-shrink', 60 | 'flex-wrap', 61 | 'justify-content', 62 | 'order', 63 | 64 | // Font feature. 65 | 'font-feature-settings', 66 | 'font-language-override', 67 | 'font-variant-ligatures', 68 | 69 | // Font kerning. 70 | 'font-kerning', 71 | 72 | // Fragmented borders and backgrounds. 73 | 'box-decoration-break', 74 | 75 | // Grid layout. 76 | 'grid-column', 77 | 'grid-column-align', 78 | 'grid-column-end', 79 | 'grid-column-start', 80 | 'grid-row', 81 | 'grid-row-align', 82 | 'grid-row-end', 83 | 'grid-row-start', 84 | 'grid-template-columns', 85 | 'grid-template-rows', 86 | 87 | // Hyphens. 88 | 'hyphens', 89 | 'word-break', 90 | 91 | // Masks. 92 | 'mask', 93 | 'mask-border', 94 | 'mask-border-outset', 95 | 'mask-border-repeat', 96 | 'mask-border-slice', 97 | 'mask-border-source', 98 | 'mask-border-width', 99 | 'mask-clip', 100 | 'mask-composite', 101 | 'mask-image', 102 | 'mask-origin', 103 | 'mask-position', 104 | 'mask-repeat', 105 | 'mask-size', 106 | 107 | // Multicolumn. 108 | 'break-after', 109 | 'break-before', 110 | 'break-inside', 111 | 'column-count', 112 | 'column-fill', 113 | 'column-gap', 114 | 'column-rule', 115 | 'column-rule-color', 116 | 'column-rule-style', 117 | 'column-rule-width', 118 | 'column-span', 119 | 'column-width', 120 | 'columns', 121 | 122 | // Object fit. 123 | 'object-fit', 124 | 'object-position', 125 | 126 | // Regions. 127 | 'flow-from', 128 | 'flow-into', 129 | 'region-fragment', 130 | 131 | // Scroll snap points. 132 | 'scroll-snap-coordinate', 133 | 'scroll-snap-destination', 134 | 'scroll-snap-points-x', 135 | 'scroll-snap-points-y', 136 | 'scroll-snap-type', 137 | 138 | // Shapes. 139 | 'shape-image-threshold', 140 | 'shape-margin', 141 | 'shape-outside', 142 | 143 | // Tab size. 144 | 'tab-size', 145 | 146 | // Text align last. 147 | 'text-align-last', 148 | 149 | // Text decoration. 150 | 'text-decoration-color', 151 | 'text-decoration-line', 152 | 'text-decoration-skip', 153 | 'text-decoration-style', 154 | 155 | // Text emphasis. 156 | 'text-emphasis', 157 | 'text-emphasis-color', 158 | 'text-emphasis-position', 159 | 'text-emphasis-style', 160 | 161 | // Text size adjust. 162 | 'text-size-adjust', 163 | 164 | // Text spacing. 165 | 'text-spacing', 166 | 167 | // Transform. 168 | 'transform', 169 | 'transform-origin', 170 | 171 | // Transform 3D. 172 | 'backface-visibility', 173 | 'perspective', 174 | 'perspective-origin', 175 | 'transform-style', 176 | 177 | // Transition. 178 | 'transition', 179 | 'transition-delay', 180 | 'transition-duration', 181 | 'transition-property', 182 | 'transition-timing-function', 183 | 184 | // Unicode bidi. 185 | 'unicode-bidi', 186 | 187 | // User select. 188 | 'user-select', 189 | 190 | // Writing mode. 191 | 'writing-mode', 192 | 193 | ); 194 | 195 | /// Values that should be vendorized. 196 | /// Data via caniuse.com, github.com/postcss/autoprefixer, and developer.mozilla.org 197 | /// @var {list} 198 | $vendor-values: ( 199 | 200 | // Cross fade. 201 | 'cross-fade', 202 | 203 | // Element function. 204 | 'element', 205 | 206 | // Filter function. 207 | 'filter', 208 | 209 | // Flexbox. 210 | 'flex', 211 | 'inline-flex', 212 | 213 | // Grab cursors. 214 | 'grab', 215 | 'grabbing', 216 | 217 | // Gradients. 218 | 'linear-gradient', 219 | 'repeating-linear-gradient', 220 | 'radial-gradient', 221 | 'repeating-radial-gradient', 222 | 223 | // Grid layout. 224 | 'grid', 225 | 'inline-grid', 226 | 227 | // Image set. 228 | 'image-set', 229 | 230 | // Intrinsic width. 231 | 'max-content', 232 | 'min-content', 233 | 'fit-content', 234 | 'fill', 235 | 'fill-available', 236 | 'stretch', 237 | 238 | // Sticky position. 239 | 'sticky', 240 | 241 | // Transform. 242 | 'transform', 243 | 244 | // Zoom cursors. 245 | 'zoom-in', 246 | 'zoom-out', 247 | 248 | ); 249 | 250 | // Functions. 251 | 252 | /// Removes a specific item from a list. 253 | /// @author Hugo Giraudel 254 | /// @param {list} $list List. 255 | /// @param {integer} $index Index. 256 | /// @return {list} Updated list. 257 | @function remove-nth($list, $index) { 258 | 259 | $result: null; 260 | 261 | @if type-of($index) != number { 262 | @warn "$index: #{quote($index)} is not a number for `remove-nth`."; 263 | } 264 | @else if $index == 0 { 265 | @warn "List index 0 must be a non-zero integer for `remove-nth`."; 266 | } 267 | @else if abs($index) > length($list) { 268 | @warn "List index is #{$index} but list is only #{length($list)} item long for `remove-nth`."; 269 | } 270 | @else { 271 | 272 | $result: (); 273 | $index: if($index < 0, length($list) + $index + 1, $index); 274 | 275 | @for $i from 1 through length($list) { 276 | 277 | @if $i != $index { 278 | $result: append($result, nth($list, $i)); 279 | } 280 | 281 | } 282 | 283 | } 284 | 285 | @return $result; 286 | 287 | } 288 | 289 | /// Replaces a substring within another string. 290 | /// @author Hugo Giraudel 291 | /// @param {string} $string String. 292 | /// @param {string} $search Substring. 293 | /// @param {string} $replace Replacement. 294 | /// @return {string} Updated string. 295 | @function str-replace($string, $search, $replace: '') { 296 | 297 | $index: str-index($string, $search); 298 | 299 | @if $index { 300 | @return str-slice($string, 1, $index - 1) + $replace + str-replace(str-slice($string, $index + str-length($search)), $search, $replace); 301 | } 302 | 303 | @return $string; 304 | 305 | } 306 | 307 | /// Replaces a substring within each string in a list. 308 | /// @param {list} $strings List of strings. 309 | /// @param {string} $search Substring. 310 | /// @param {string} $replace Replacement. 311 | /// @return {list} Updated list of strings. 312 | @function str-replace-all($strings, $search, $replace: '') { 313 | 314 | @each $string in $strings { 315 | $strings: set-nth($strings, index($strings, $string), str-replace($string, $search, $replace)); 316 | } 317 | 318 | @return $strings; 319 | 320 | } 321 | 322 | // Mixins. 323 | 324 | /// Wraps @content in vendorized keyframe blocks. 325 | /// @param {string} $name Name. 326 | @mixin keyframes($name) { 327 | 328 | @-moz-keyframes #{$name} { @content; } 329 | @-webkit-keyframes #{$name} { @content; } 330 | @-ms-keyframes #{$name} { @content; } 331 | @keyframes #{$name} { @content; } 332 | 333 | } 334 | 335 | /// Vendorizes a declaration's property and/or value(s). 336 | /// @param {string} $property Property. 337 | /// @param {mixed} $value String/list of value(s). 338 | @mixin vendor($property, $value) { 339 | 340 | // Determine if property should expand. 341 | $expandProperty: index($vendor-properties, $property); 342 | 343 | // Determine if value should expand (and if so, add '-prefix-' placeholder). 344 | $expandValue: false; 345 | 346 | @each $x in $value { 347 | @each $y in $vendor-values { 348 | @if $y == str-slice($x, 1, str-length($y)) { 349 | 350 | $value: set-nth($value, index($value, $x), '-prefix-' + $x); 351 | $expandValue: true; 352 | 353 | } 354 | } 355 | } 356 | 357 | // Expand property? 358 | @if $expandProperty { 359 | @each $vendor in $vendor-prefixes { 360 | #{$vendor}#{$property}: #{str-replace-all($value, '-prefix-', $vendor)}; 361 | } 362 | } 363 | 364 | // Expand just the value? 365 | @elseif $expandValue { 366 | @each $vendor in $vendor-prefixes { 367 | #{$property}: #{str-replace-all($value, '-prefix-', $vendor)}; 368 | } 369 | } 370 | 371 | // Neither? Treat them as a normal declaration. 372 | @else { 373 | #{$property}: #{$value}; 374 | } 375 | 376 | } -------------------------------------------------------------------------------- /public/sass/main.scss: -------------------------------------------------------------------------------- 1 | @import 'libs/vars'; 2 | @import 'libs/functions'; 3 | @import 'libs/mixins'; 4 | @import 'libs/vendor'; 5 | @import 'libs/breakpoints'; 6 | @import url("https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,900"); 7 | @import url("fontawesome-all.min.css"); 8 | 9 | /* 10 | Aerial by HTML5 UP 11 | html5up.net | @ajlkn 12 | Free for personal and commercial use under the CCA 3.0 license (html5up.net/license) 13 | */ 14 | 15 | // Breakpoints. 16 | 17 | @include breakpoints(( 18 | wide: ( 1281px, 1680px ), 19 | normal: ( 737px, 1280px ), 20 | mobile: ( 481px, 736px ), 21 | mobilep: ( null, 480px ) 22 | )); 23 | 24 | // Mixins. 25 | 26 | @mixin bg($width) { 27 | @include keyframes('bg') { 28 | 0% { @include vendor('transform', 'translate3d(0,0,0)'); } 29 | 100% { @include vendor('transform', 'translate3d(#{$width * -1},0,0)'); } 30 | } 31 | 32 | #bg { 33 | background-size: $width auto; 34 | width: ($width * 3); 35 | } 36 | } 37 | 38 | $delay-wrapper: _duration(wrapper) - 1s; 39 | $delay-overlay: $delay-wrapper - 0.5s; 40 | $delay-header: $delay-overlay + _duration(overlay) - 0.75s; 41 | $delay-nav-icons: $delay-header + _duration(header) - 1s; 42 | $delay-nav-icon: 0.25s; 43 | 44 | // Reset. 45 | // Based on meyerweb.com/eric/tools/css/reset (v2.0 | 20110126 | License: public domain) 46 | 47 | html, body, div, span, applet, object, 48 | iframe, h1, h2, h3, h4, h5, h6, p, blockquote, 49 | pre, a, abbr, acronym, address, big, cite, 50 | code, del, dfn, em, img, ins, kbd, q, s, samp, 51 | small, strike, strong, sub, sup, tt, var, b, 52 | u, i, center, dl, dt, dd, ol, ul, li, fieldset, 53 | form, label, legend, table, caption, tbody, 54 | tfoot, thead, tr, th, td, article, aside, 55 | canvas, details, embed, figure, figcaption, 56 | footer, header, hgroup, menu, nav, output, ruby, 57 | section, summary, time, mark, audio, video { 58 | margin: 0; 59 | padding: 0; 60 | border: 0; 61 | font-size: 100%; 62 | font: inherit; 63 | vertical-align: baseline; 64 | } 65 | 66 | article, aside, details, figcaption, figure, 67 | footer, header, hgroup, menu, nav, section { 68 | display: block; 69 | } 70 | 71 | body { 72 | line-height: 1; 73 | } 74 | 75 | ol, ul { 76 | list-style:none; 77 | } 78 | 79 | blockquote, q { 80 | quotes: none; 81 | 82 | &:before, 83 | &:after { 84 | content: ''; 85 | content: none; 86 | } 87 | } 88 | 89 | table { 90 | border-collapse: collapse; 91 | border-spacing: 0; 92 | } 93 | 94 | body { 95 | -webkit-text-size-adjust: none; 96 | } 97 | 98 | mark { 99 | background-color: transparent; 100 | color: inherit; 101 | } 102 | 103 | input::-moz-focus-inner { 104 | border: 0; 105 | padding: 0; 106 | } 107 | 108 | input, select, textarea { 109 | -moz-appearance: none; 110 | -webkit-appearance: none; 111 | -ms-appearance: none; 112 | appearance: none; 113 | } 114 | 115 | /* Basic */ 116 | 117 | // Set box model to border-box. 118 | // Based on css-tricks.com/inheriting-box-sizing-probably-slightly-better-best-practice 119 | html { 120 | box-sizing: border-box; 121 | } 122 | 123 | *, *:before, *:after { 124 | box-sizing: inherit; 125 | } 126 | 127 | body { 128 | background: _palette(bg); 129 | overflow: hidden; 130 | 131 | // Stops initial animations until page loads. 132 | &.is-preload { 133 | *, *:before, *:after { 134 | @include vendor('animation', 'none !important'); 135 | @include vendor('transition', 'none !important'); 136 | } 137 | } 138 | 139 | } 140 | 141 | body, input, select, textarea { 142 | color: _palette(fg); 143 | font-family: 'Source Sans Pro', sans-serif; 144 | font-size: 15pt; 145 | font-weight: 300 !important; 146 | letter-spacing: -0.025em; 147 | line-height: 1.75em; 148 | } 149 | 150 | a { 151 | @include vendor('transition', 'border-color 0.2s ease-in-out'); 152 | border-bottom: dotted 1px; 153 | color: inherit; 154 | outline: 0; 155 | text-decoration: none; 156 | 157 | &:hover { 158 | border-color: transparent; 159 | } 160 | } 161 | 162 | /* Icon */ 163 | 164 | .icon { 165 | @include icon; 166 | position: relative; 167 | 168 | > .label { 169 | display: none; 170 | } 171 | 172 | &.solid { 173 | &:before { 174 | font-weight: 900; 175 | } 176 | } 177 | 178 | &.brands { 179 | &:before { 180 | font-family: 'Font Awesome 5 Brands'; 181 | } 182 | } 183 | } 184 | 185 | /* Wrapper */ 186 | 187 | @include keyframes('wrapper') { 188 | 0% { opacity: 0; } 189 | 100% { opacity: 1; } 190 | } 191 | 192 | #wrapper { 193 | @include vendor('animation', 'wrapper #{_duration(wrapper)} forwards'); 194 | height: 100%; 195 | left: 0; 196 | opacity: 0; 197 | position: fixed; 198 | top: 0; 199 | width: 100%; 200 | } 201 | 202 | /* BG */ 203 | 204 | #bg { 205 | @include vendor('animation', 'bg #{_duration(bg)} linear infinite'); 206 | @include vendor('backface-visibility', 'hidden'); 207 | @include vendor('transform', 'translate3d(0,0,0)'); 208 | 209 | /* Set your background with this */ 210 | background: _misc(bg); 211 | 212 | background-repeat: repeat-x; 213 | height: 100%; 214 | left: 0; 215 | opacity: 1; 216 | position: fixed; 217 | top: 0; 218 | } 219 | 220 | @include bg(_misc(bg-width) * 1.5); 221 | 222 | /* Overlay */ 223 | 224 | @include keyframes('overlay') { 225 | 0% { opacity: 0; } 226 | 100% { opacity: 1; } 227 | } 228 | 229 | #overlay { 230 | @include vendor('animation', 'overlay #{_duration(overlay)} #{$delay-overlay} forwards'); 231 | background-attachment: fixed, fixed; 232 | background-image: url('images/overlay-pattern.png'), url('images/overlay.svg'); 233 | background-position: top left, center center; 234 | background-repeat: repeat, no-repeat; 235 | background-size: auto, cover; 236 | height: 100%; 237 | left: 0; 238 | opacity: 0; 239 | position: fixed; 240 | top: 0; 241 | width: 100%; 242 | } 243 | 244 | /* Main */ 245 | 246 | #main { 247 | height: 100%; 248 | left: 0; 249 | position: fixed; 250 | text-align: center; 251 | top: 0; 252 | width: 100%; 253 | 254 | &:before { 255 | content: ''; 256 | display: inline-block; 257 | height: 100%; 258 | margin-right: 0; 259 | vertical-align: middle; 260 | width: 1px; 261 | } 262 | } 263 | 264 | /* Header */ 265 | 266 | @include keyframes('header') { 267 | 0% { @include vendor('transform', 'translate3d(0,1em,0)'); opacity: 0; } 268 | 100% { @include vendor('transform', 'translate3d(0,0,0)'); opacity: 1; } 269 | } 270 | 271 | @include keyframes('nav-icons') { 272 | 0% { @include vendor('transform', 'translate3d(0,1em,0)'); opacity: 0; } 273 | 100% { @include vendor('transform', 'translate3d(0,0,0)'); opacity: 1; } 274 | } 275 | 276 | #header { 277 | @include vendor('animation', 'header #{_duration(header)} #{$delay-header} forwards'); 278 | @include vendor('backface-visibility', 'hidden'); 279 | @include vendor('transform', 'translate3d(0,0,0)'); 280 | cursor: default; 281 | display: inline-block; 282 | opacity: 0; 283 | position: relative; 284 | text-align: center; 285 | top: -1em; 286 | vertical-align: middle; 287 | width: 90%; 288 | 289 | h1 { 290 | font-size: 4.35em; 291 | font-weight: 900; 292 | letter-spacing: -0.035em; 293 | line-height: 1em; 294 | } 295 | 296 | p { 297 | font-size: 1.25em; 298 | margin: 0.75em 0 0.25em 0; 299 | opacity: 0.75; 300 | } 301 | 302 | nav { 303 | margin: 1.5em 0 0 0; 304 | 305 | li { 306 | @include vendor('animation', 'nav-icons #{_duration(nav-icons)} ease-in-out forwards'); 307 | @include vendor('backface-visibility', 'hidden'); 308 | @include vendor('transform', 'translate3d(0,0,0)'); 309 | display: inline-block; 310 | height: _size(nav-icon-wrapper); 311 | line-height: _size(nav-icon-wrapper) * 1.1; 312 | opacity: 0; 313 | position: relative; 314 | top: 0; 315 | width: _size(nav-icon-wrapper); 316 | 317 | @for $x from 1 through 10 { 318 | &:nth-child(#{$x}) { 319 | @include vendor('animation-delay', ($delay-nav-icons + ($x * $delay-nav-icon)) + ''); 320 | } 321 | } 322 | } 323 | 324 | a { 325 | -webkit-tap-highlight-color: rgba(0,0,0,0); 326 | -webkit-touch-callout: none; 327 | border: 0; 328 | display: inline-block; 329 | 330 | &:before { 331 | @include vendor('transition', 'all 0.2s ease-in-out'); 332 | border-radius: 100%; 333 | border: solid 1px _palette(fg); 334 | display: block; 335 | font-size: _size(nav-icon); 336 | height: 2.5em; 337 | line-height: 2.5em; 338 | position: relative; 339 | text-align: center; 340 | top: 0; 341 | width: 2.5em; 342 | } 343 | 344 | &:hover { 345 | font-size: 1.1em; 346 | 347 | &:before { 348 | background-color: _palette(nav-icon, hover-bg); 349 | color: _palette(nav-icon, hover-fg); 350 | } 351 | } 352 | 353 | &:active { 354 | font-size: 0.95em; 355 | background: none; 356 | 357 | &:before { 358 | background-color: _palette(nav-icon, active-bg); 359 | color: _palette(nav-icon, active-fg); 360 | } 361 | } 362 | 363 | span { 364 | display: none; 365 | } 366 | } 367 | } 368 | } 369 | 370 | /* Footer */ 371 | 372 | #footer { 373 | @include vendor('background-image', 'linear-gradient(top, rgba(0,0,0,0), rgba(0,0,0,0.5) 75%)'); 374 | bottom: 0; 375 | cursor: default; 376 | height: 6em; 377 | left: 0; 378 | line-height: 8em; 379 | position: absolute; 380 | text-align: center; 381 | width: 100%; 382 | } 383 | 384 | /* Wide */ 385 | 386 | @include breakpoint('<=wide') { 387 | 388 | /* Basic */ 389 | 390 | body, input, select, textarea { 391 | font-size: 13pt; 392 | } 393 | 394 | /* BG */ 395 | 396 | @include bg(_misc(bg-width)); 397 | 398 | } 399 | 400 | /* Normal */ 401 | 402 | @include breakpoint('<=normal') { 403 | 404 | /* Basic */ 405 | 406 | body, input, select, textarea { 407 | font-size: 12pt; 408 | } 409 | 410 | /* BG */ 411 | 412 | @include bg(_misc(bg-width) * 0.5); 413 | 414 | } 415 | 416 | /* Mobile */ 417 | 418 | @include breakpoint('<=mobile') { 419 | 420 | /* Basic */ 421 | 422 | body { 423 | min-width: 320px; 424 | } 425 | 426 | body, input, select, textarea { 427 | font-size: 11pt; 428 | } 429 | 430 | /* BG */ 431 | 432 | @include bg(_misc(bg-width) * 0.2); 433 | 434 | /* Header */ 435 | 436 | #header { 437 | h1 { 438 | font-size: 2.5em; 439 | } 440 | 441 | p { 442 | font-size: 1em; 443 | } 444 | 445 | nav { 446 | font-size: 1em; 447 | 448 | a { 449 | &:hover { 450 | font-size: 1em; 451 | } 452 | 453 | &:active { 454 | font-size: 1em; 455 | } 456 | } 457 | } 458 | } 459 | 460 | } 461 | 462 | /* Mobile (Portrait) */ 463 | 464 | @include breakpoint('<=mobilep') { 465 | 466 | /* BG */ 467 | 468 | @include bg(_misc(bg-width) * 0.275); 469 | 470 | /* Header */ 471 | 472 | #header { 473 | nav { 474 | padding: 0 1em; 475 | } 476 | } 477 | 478 | } -------------------------------------------------------------------------------- /public/sass/noscript.scss: -------------------------------------------------------------------------------- 1 | @import 'libs/vars'; 2 | @import 'libs/functions'; 3 | @import 'libs/mixins'; 4 | @import 'libs/vendor'; 5 | @import 'libs/breakpoints'; 6 | 7 | /* 8 | Aerial by HTML5 UP 9 | html5up.net | @ajlkn 10 | Free for personal and commercial use under the CCA 3.0 license (html5up.net/license) 11 | */ 12 | 13 | /* Wrapper */ 14 | 15 | #wrapper { 16 | opacity: 1 !important; 17 | } 18 | 19 | /* Overlay */ 20 | 21 | #overlay { 22 | opacity: 1 !important; 23 | } 24 | 25 | /* Header */ 26 | 27 | #header { 28 | opacity: 1 !important; 29 | 30 | nav { 31 | li { 32 | opacity: 1 !important; 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /public/webfonts/fa-brands-400.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ginnnnnncc/GinsMooc/0ee1a57ec764cddcb19d5554e1aedfdf0ca5518b/public/webfonts/fa-brands-400.eot -------------------------------------------------------------------------------- /public/webfonts/fa-brands-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ginnnnnncc/GinsMooc/0ee1a57ec764cddcb19d5554e1aedfdf0ca5518b/public/webfonts/fa-brands-400.ttf -------------------------------------------------------------------------------- /public/webfonts/fa-brands-400.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ginnnnnncc/GinsMooc/0ee1a57ec764cddcb19d5554e1aedfdf0ca5518b/public/webfonts/fa-brands-400.woff -------------------------------------------------------------------------------- /public/webfonts/fa-brands-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ginnnnnncc/GinsMooc/0ee1a57ec764cddcb19d5554e1aedfdf0ca5518b/public/webfonts/fa-brands-400.woff2 -------------------------------------------------------------------------------- /public/webfonts/fa-regular-400.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ginnnnnncc/GinsMooc/0ee1a57ec764cddcb19d5554e1aedfdf0ca5518b/public/webfonts/fa-regular-400.eot -------------------------------------------------------------------------------- /public/webfonts/fa-regular-400.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ginnnnnncc/GinsMooc/0ee1a57ec764cddcb19d5554e1aedfdf0ca5518b/public/webfonts/fa-regular-400.ttf -------------------------------------------------------------------------------- /public/webfonts/fa-regular-400.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ginnnnnncc/GinsMooc/0ee1a57ec764cddcb19d5554e1aedfdf0ca5518b/public/webfonts/fa-regular-400.woff -------------------------------------------------------------------------------- /public/webfonts/fa-regular-400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ginnnnnncc/GinsMooc/0ee1a57ec764cddcb19d5554e1aedfdf0ca5518b/public/webfonts/fa-regular-400.woff2 -------------------------------------------------------------------------------- /public/webfonts/fa-solid-900.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ginnnnnncc/GinsMooc/0ee1a57ec764cddcb19d5554e1aedfdf0ca5518b/public/webfonts/fa-solid-900.eot -------------------------------------------------------------------------------- /public/webfonts/fa-solid-900.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ginnnnnncc/GinsMooc/0ee1a57ec764cddcb19d5554e1aedfdf0ca5518b/public/webfonts/fa-solid-900.ttf -------------------------------------------------------------------------------- /public/webfonts/fa-solid-900.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ginnnnnncc/GinsMooc/0ee1a57ec764cddcb19d5554e1aedfdf0ca5518b/public/webfonts/fa-solid-900.woff -------------------------------------------------------------------------------- /public/webfonts/fa-solid-900.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ginnnnnncc/GinsMooc/0ee1a57ec764cddcb19d5554e1aedfdf0ca5518b/public/webfonts/fa-solid-900.woff2 -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 60 | 61 | 129 | 130 | 175 | -------------------------------------------------------------------------------- /src/components/CourseCard.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 23 | 24 | 82 | -------------------------------------------------------------------------------- /src/components/QuestionCard.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 37 | 38 | 139 | -------------------------------------------------------------------------------- /src/components/icon/Extension.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /src/components/icon/Github.vue: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /src/components/icon/index.ts: -------------------------------------------------------------------------------- 1 | import Github from "./Github.vue" 2 | import Extension from "./Extension.vue" 3 | 4 | export { Github, Extension } -------------------------------------------------------------------------------- /src/components/index.ts: -------------------------------------------------------------------------------- 1 | import CourseCard from "./CourseCard.vue" 2 | import QuestionCard from "./QuestionCard.vue" 3 | 4 | export {CourseCard, QuestionCard} -------------------------------------------------------------------------------- /src/components/question/Completion.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 55 | 56 | 67 | -------------------------------------------------------------------------------- /src/components/question/Homework.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/components/question/MultipleChoice.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 60 | 61 | 75 | -------------------------------------------------------------------------------- /src/components/question/OnlineJudge.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 38 | 39 | 41 | -------------------------------------------------------------------------------- /src/components/question/SingleChoice.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 52 | 53 | 70 | -------------------------------------------------------------------------------- /src/components/question/index.ts: -------------------------------------------------------------------------------- 1 | import SingleChoice from "./SingleChoice.vue" 2 | import MultipleChoice from "./MultipleChoice.vue" 3 | import Completion from "./Completion.vue" 4 | import Homework from "./Homework.vue" 5 | import OnlineJudge from "./OnlineJudge.vue" 6 | 7 | export { SingleChoice, MultipleChoice, Completion, Homework, OnlineJudge } -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from "vue" 2 | import ElementPlus from 'element-plus' 3 | import router from "./router" 4 | import App from "./App.vue" 5 | import * as ElementPlusIconsVue from '@element-plus/icons-vue' 6 | import 'element-plus/dist/index.css' 7 | import 'element-plus/theme-chalk/dark/css-vars.css' 8 | import 'element-plus/theme-chalk/display.css' 9 | 10 | const app = createApp(App) 11 | 12 | app.use(ElementPlus) 13 | for (const [key, component] of Object.entries(ElementPlusIconsVue)) { 14 | app.component(key, component) 15 | } 16 | 17 | app.use(router) 18 | 19 | app.mount("body") 20 | -------------------------------------------------------------------------------- /src/plugins/apiAccess.ts: -------------------------------------------------------------------------------- 1 | import type { ApiKeyType, ApiResponseType, ApiRequestType } from "../type/api" 2 | import type { App } from "vue" 3 | import { isAxiosError } from "axios" 4 | import { ElMessage } from "element-plus" 5 | import apiInfo from "../type/api" 6 | import axios from "axios" 7 | 8 | async function apiAccess(api: T): Promise 9 | async function apiAccess( 10 | api: T, 11 | params: ApiRequestType[T]["params"], 12 | data: ApiRequestType[T]["data"] 13 | ): Promise 14 | 15 | /** 函数重载 */ 16 | async function apiAccess( 17 | api: T, 18 | params?: ApiRequestType[T]["params"], 19 | data?: ApiRequestType[T]["data"] 20 | ) { 21 | /** 错误处理,主要catch 404,调用者不再需要try-catch */ 22 | try { 23 | return await new Promise((resolve, reject) => { 24 | /** 查询参数转动态路由参数 */ 25 | let url = apiInfo[api].url 26 | if (params) { 27 | for (const [key, val] of Object.entries(params)) { 28 | const reg = RegExp(`(/):${key}(/)?`, "g") 29 | if (reg.test(url)) { 30 | url = url.replaceAll(reg, `$1${val}$2`) 31 | Reflect.deleteProperty(params, key) 32 | } 33 | } 34 | } 35 | /** 将对象转为json字符串 */ 36 | if (data) { 37 | for (const [key, val] of Object.entries(data)) { 38 | if (typeof val === "object") { 39 | Reflect.set(data, key, JSON.stringify(val)) 40 | } 41 | } 42 | } 43 | /** 异步发送请求 */ 44 | let urlPrefix = "https://ginnnnnn.top/api" 45 | axios({ 46 | url: urlPrefix + url, 47 | method: apiInfo[api].method, 48 | params: params || {}, 49 | data: data || {}, 50 | headers: { "Content-Type": "application/x-www-form-urlencoded" } 51 | }) 52 | .then((res) => { 53 | let message = "", 54 | success = false 55 | if (res.status !== 200 || !res.data) { 56 | message = "请求出错" 57 | } else if (res.data.msg) { 58 | message = res.data.msg 59 | if (res.data.status === 200) { 60 | success = true 61 | } 62 | } 63 | if (message) { 64 | ElMessage({ message, type: success ? "success" : "error" }) 65 | } 66 | resolve(res.data) 67 | }) 68 | .catch((error) => { 69 | let message = error 70 | if (isAxiosError(error)) { 71 | message = error.message 72 | } 73 | ElMessage({ message, type: "error" }) 74 | reject(error) 75 | }) 76 | }) 77 | } catch { 78 | return {} 79 | } 80 | } 81 | 82 | export const useApiAccess = () => apiAccess 83 | 84 | export default { 85 | install: (app: App) => { 86 | app.config.globalProperties.$apiAccess = apiAccess 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/plugins/tool.ts: -------------------------------------------------------------------------------- 1 | const sleep = async (ms: number) => { 2 | return new Promise((resolve) => { 3 | setTimeout(() => { 4 | resolve(''); 5 | }, ms) 6 | }) 7 | } 8 | 9 | export { sleep } -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHistory } from "vue-router" 2 | import { HomeView, MoocView, MoocHeader, MoocCourseDetail, MoocTest, VideoView } from "@/views" 3 | import { ElMessage } from "element-plus" 4 | 5 | const router = createRouter({ 6 | history: createWebHistory(import.meta.env.BASE_URL), 7 | routes: [ 8 | { 9 | path: "/", 10 | name: "Home", 11 | meta: { title: "Gins" }, 12 | component: HomeView 13 | }, 14 | { 15 | path: "/ginnnnnn/video", 16 | name: "Video", 17 | meta: { title: "GinsVideo" }, 18 | component: VideoView 19 | }, 20 | { 21 | path: "/mooc", 22 | name: "Mooc", 23 | components: { 24 | default: MoocView, 25 | header: MoocHeader 26 | }, 27 | meta: { title: "GinsMooc" }, 28 | children: [ 29 | { path: "course/:cid", name: "MoocCourse", component: MoocCourseDetail, props: true }, 30 | { path: "test/:tid", name: "MoocTest", component: MoocTest, props: true } 31 | ] 32 | } 33 | ] 34 | }) 35 | 36 | router.beforeEach((to, from) => { 37 | if (!to.name) { 38 | ElMessage.error("路由错误") 39 | return { name: "Home" } 40 | } 41 | if (to.meta.title) { 42 | document.title = to.meta.title 43 | } 44 | return true 45 | }) 46 | 47 | export default router 48 | 49 | const checkMoocItemId = (id: string) => /[0-9]/.test(id) -------------------------------------------------------------------------------- /src/type/api.ts: -------------------------------------------------------------------------------- 1 | import type { course, homework, notice, quiz, test } from "./mooc" 2 | 3 | type RequestType = { 4 | params?: Object 5 | data?: Object 6 | } 7 | 8 | type Response = { 9 | status: number 10 | data: T 11 | msg: string 12 | } 13 | 14 | export interface courseList extends Object { 15 | courseList: course[] 16 | totalPages: number 17 | currentPage: number 18 | } 19 | 20 | export interface courseDetail extends Object { 21 | course: course 22 | testList: test[] 23 | } 24 | 25 | export interface testDetail extends Object { 26 | course: course 27 | test: test 28 | questionList: quiz[] | homework[] 29 | totalPages: number 30 | } 31 | 32 | const apiInfo = { 33 | getCourseList: { 34 | url: "/mooc/course", 35 | method: "GET" 36 | }, 37 | getCourseDetail: { 38 | url: "/mooc/course/:cid", 39 | method: "GET" 40 | }, 41 | getTestDetail: { 42 | url: "/mooc/test/:tid", 43 | method: "GET" 44 | }, 45 | getNotice: { 46 | url: "/mooc/notice/website", 47 | method: "GET" 48 | } 49 | } 50 | 51 | export type ApiKeyType = keyof typeof apiInfo 52 | 53 | export interface ApiResponseType { 54 | getCourseList: Response 55 | getCourseDetail: Response 56 | getTestDetail: Response 57 | getNotice: Response 58 | } 59 | 60 | export interface ApiRequestType { 61 | getCourseList: RequestType & { 62 | params: { 63 | page?: number | string 64 | search?: string 65 | cid?: number | string 66 | } 67 | } 68 | getCourseDetail: RequestType & { 69 | params: { cid: number | string } 70 | } 71 | getTestDetail: RequestType & { 72 | params: { 73 | tid: number | string 74 | page: number | string 75 | search?: string 76 | } 77 | } 78 | getNotice: RequestType & { 79 | params: { version: string } 80 | } 81 | } 82 | 83 | export default apiInfo 84 | -------------------------------------------------------------------------------- /src/type/globleProperties.ts: -------------------------------------------------------------------------------- 1 | import { useApiAccess } from "@/plugins/apiAccess" 2 | 3 | const apiAccess = useApiAccess() 4 | declare module 'vue' { 5 | interface ComponentCustomProperties { 6 | $apiAccess: typeof apiAccess 7 | } 8 | } -------------------------------------------------------------------------------- /src/type/mooc.ts: -------------------------------------------------------------------------------- 1 | export enum QuestionTypeEnumList { 2 | SingleChoice = "SINGLE_CHOICE", 3 | MultipleChoice = "MULTIPLE_CHOICE", 4 | Completion = "COMPLETION", 5 | Judge = "JUDGEMENT", 6 | Homework = "HOMEWORK", 7 | OnlineJudge = "ONLINE_JUDGE" 8 | } 9 | 10 | interface course extends Object { 11 | id: number 12 | name: string 13 | school: string 14 | imageUrl: string 15 | } 16 | 17 | interface test extends Object { 18 | id: number 19 | name: string 20 | objective: boolean 21 | releaseTime: string 22 | deadline: string 23 | chapterId: number 24 | chapterName: string 25 | } 26 | 27 | interface option extends Object { 28 | id: number 29 | content: string 30 | answer: boolean 31 | } 32 | 33 | interface quiz extends Object { 34 | id: number 35 | type: 36 | | QuestionTypeEnumList.SingleChoice 37 | | QuestionTypeEnumList.MultipleChoice 38 | | QuestionTypeEnumList.Completion 39 | | QuestionTypeEnumList.Judge 40 | title: string 41 | stdAnswer: string | null 42 | optionList: option[] | null 43 | } 44 | 45 | interface homework extends Object { 46 | id: number 47 | type: QuestionTypeEnumList.Homework | QuestionTypeEnumList.OnlineJudge 48 | title: string 49 | answer: string | null 50 | description: string | null 51 | memoryLimit: number | null 52 | timeLimit: number | null 53 | } 54 | 55 | interface notice extends Object { 56 | id: number 57 | content: string 58 | } 59 | 60 | export type QuestionTypeEnum = typeof QuestionTypeEnumList 61 | export type { course, test, option, quiz, homework, notice } 62 | -------------------------------------------------------------------------------- /src/views/BlogView.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /src/views/HomeView.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 24 | 25 | 56 | -------------------------------------------------------------------------------- /src/views/MoocAside.vue: -------------------------------------------------------------------------------- 1 | 153 | 154 | 211 | 212 | 243 | 244 | 265 | -------------------------------------------------------------------------------- /src/views/MoocCourseDetail.vue: -------------------------------------------------------------------------------- 1 | 52 | 53 | 68 | 69 | 153 | -------------------------------------------------------------------------------- /src/views/MoocHeader.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 72 | 73 | 107 | -------------------------------------------------------------------------------- /src/views/MoocTest.vue: -------------------------------------------------------------------------------- 1 | 94 | 95 | 116 | 117 | 132 | -------------------------------------------------------------------------------- /src/views/MoocView.vue: -------------------------------------------------------------------------------- 1 | 54 | 55 | 105 | 106 | 171 | -------------------------------------------------------------------------------- /src/views/VideoView.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 59 | 60 | 110 | -------------------------------------------------------------------------------- /src/views/index.ts: -------------------------------------------------------------------------------- 1 | import HomeView from "./HomeView.vue" 2 | import MoocView from "./MoocView.vue" 3 | import MoocHeader from "./MoocHeader.vue" 4 | import MoocAside from "./MoocAside.vue" 5 | import MoocCourseDetail from "./MoocCourseDetail.vue" 6 | import MoocTest from "./MoocTest.vue" 7 | import VideoView from "./VideoView.vue" 8 | 9 | export { HomeView, MoocView, MoocHeader, MoocAside, MoocCourseDetail, MoocTest, VideoView } -------------------------------------------------------------------------------- /tsconfig.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.node.json", 3 | "include": [ 4 | "vite.config.*", 5 | "vitest.config.*", 6 | "cypress.config.*", 7 | "playwright.config.*" 8 | ], 9 | "compilerOptions": { 10 | "composite": true, 11 | "types": [ 12 | "node" 13 | ] 14 | } 15 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.web.json", 3 | "include": [ 4 | "env.d.ts", 5 | "src/**/*", 6 | "src/**/*.vue" 7 | ], 8 | "compilerOptions": { 9 | "baseUrl": ".", 10 | "paths": { 11 | "@/*": [ 12 | "./src/*" 13 | ] 14 | }, 15 | "types": ["element-plus/global"] 16 | }, 17 | "references": [ 18 | { 19 | "path": "./tsconfig.config.json" 20 | } 21 | ] 22 | } -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath, URL } from "node:url" 2 | 3 | import { defineConfig } from "vite" 4 | import vue from "@vitejs/plugin-vue" 5 | 6 | // https://vitejs.dev/config/ 7 | export default defineConfig({ 8 | plugins: [ vue() ], 9 | resolve: { 10 | alias: { 11 | "@": fileURLToPath(new URL("./src", import.meta.url)) 12 | } 13 | } 14 | }) 15 | --------------------------------------------------------------------------------