├── .github └── workflows │ └── ci.yml ├── .gitignore ├── LICENSE ├── README.md ├── build.js ├── lib └── jszip.min.js ├── package-lock.json ├── package.json ├── src ├── app.js ├── panel.jsx ├── panel.less ├── plugins │ ├── better-pintia │ │ ├── README.md │ │ ├── index.js │ │ └── style.less │ ├── better-video-player │ │ ├── README.md │ │ ├── index.js │ │ └── style.less │ ├── builtin-video-pages │ │ ├── index.js │ │ └── style.less │ ├── copy-with-timestamp │ │ ├── README.md │ │ └── index.js │ ├── example-plugin │ │ └── index.js │ ├── focus-mode │ │ ├── README.md │ │ ├── index.js │ │ ├── xzzd.less │ │ └── zykt.less │ ├── picture-in-picture │ │ ├── README.md │ │ ├── index.js │ │ └── style.less │ ├── ppt-downloader │ │ ├── README.md │ │ └── index.js │ ├── replay-parser │ │ ├── README.md │ │ └── index.js │ └── score-finder │ │ ├── README.md │ │ ├── index.js │ │ └── style.less └── utils │ ├── browser.js │ ├── checker.js │ ├── dom.js │ ├── global.js │ ├── logger.js │ └── vue.js ├── userscript.json └── webpack.config.js /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | workflow_dispatch: 9 | 10 | permissions: 11 | contents: read 12 | pages: write 13 | id-token: write 14 | 15 | jobs: 16 | build: 17 | runs-on: ubuntu-latest 18 | 19 | strategy: 20 | matrix: 21 | node-version: [18.x] 22 | 23 | steps: 24 | - uses: actions/checkout@v4 25 | 26 | - name: Setup Pages 27 | uses: actions/configure-pages@v4 28 | 29 | - name: Use Node.js ${{ matrix.node-version }} 30 | uses: actions/setup-node@v3 31 | with: 32 | node-version: ${{ matrix.node-version }} 33 | 34 | - name: Build 35 | run: | 36 | npm ci 37 | npm run build 38 | 39 | - name: Copy lib 40 | run: | 41 | cp -r lib/ dist/ 42 | 43 | - name: Upload artifact 44 | uses: actions/upload-pages-artifact@v3 45 | with: 46 | path: 'dist/' 47 | 48 | - name: Deploy to GitHub Pages 49 | id: deployment 50 | uses: actions/deploy-pages@v4 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | lerna-debug.log* 10 | .pnpm-debug.log* 11 | 12 | # Diagnostic reports (https://nodejs.org/api/report.html) 13 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 14 | 15 | # Runtime data 16 | pids 17 | *.pid 18 | *.seed 19 | *.pid.lock 20 | 21 | # Directory for instrumented libs generated by jscoverage/JSCover 22 | lib-cov 23 | 24 | # Coverage directory used by tools like istanbul 25 | coverage 26 | *.lcov 27 | 28 | # nyc test coverage 29 | .nyc_output 30 | 31 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 32 | .grunt 33 | 34 | # Bower dependency directory (https://bower.io/) 35 | bower_components 36 | 37 | # node-waf configuration 38 | .lock-wscript 39 | 40 | # Compiled binary addons (https://nodejs.org/api/addons.html) 41 | build/Release 42 | 43 | # Dependency directories 44 | node_modules/ 45 | jspm_packages/ 46 | 47 | # Snowpack dependency directory (https://snowpack.dev/) 48 | web_modules/ 49 | 50 | # TypeScript cache 51 | *.tsbuildinfo 52 | 53 | # Optional npm cache directory 54 | .npm 55 | 56 | # Optional eslint cache 57 | .eslintcache 58 | 59 | # Optional stylelint cache 60 | .stylelintcache 61 | 62 | # Microbundle cache 63 | .rpt2_cache/ 64 | .rts2_cache_cjs/ 65 | .rts2_cache_es/ 66 | .rts2_cache_umd/ 67 | 68 | # Optional REPL history 69 | .node_repl_history 70 | 71 | # Output of 'npm pack' 72 | *.tgz 73 | 74 | # Yarn Integrity file 75 | .yarn-integrity 76 | 77 | # dotenv environment variable files 78 | .env 79 | .env.development.local 80 | .env.test.local 81 | .env.production.local 82 | .env.local 83 | 84 | # parcel-bundler cache (https://parceljs.org/) 85 | .cache 86 | .parcel-cache 87 | 88 | # Next.js build output 89 | .next 90 | out 91 | 92 | # Nuxt.js build / generate output 93 | .nuxt 94 | dist 95 | 96 | # Gatsby files 97 | .cache/ 98 | # Comment in the public line in if your project uses Gatsby and not Next.js 99 | # https://nextjs.org/blog/next-9-1#public-directory-support 100 | # public 101 | 102 | # vuepress build output 103 | .vuepress/dist 104 | 105 | # vuepress v2.x temp and cache directory 106 | .temp 107 | .cache 108 | 109 | # Docusaurus cache and generated files 110 | .docusaurus 111 | 112 | # Serverless directories 113 | .serverless/ 114 | 115 | # FuseBox cache 116 | .fusebox/ 117 | 118 | # DynamoDB Local files 119 | .dynamodb/ 120 | 121 | # TernJS port file 122 | .tern-port 123 | 124 | # Stores VSCode versions used for testing VSCode extensions 125 | .vscode-test 126 | 127 | # yarn v2 128 | .yarn/cache 129 | .yarn/unplugged 130 | .yarn/build-state.yml 131 | .yarn/install-state.gz 132 | .pnp.* 133 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Memento mori. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Learning-at-ZJU-Helper 2 | 3 | 学在浙大/智云课堂的辅助脚本,请配合 Tampermonkey 等脚本加载器使用。[**`通过 Greasy Fork 安装`**](https://greasyfork.org/zh-CN/scripts/488869-%E5%AD%A6%E5%9C%A8%E6%B5%99%E5%A4%A7-%E6%99%BA%E4%BA%91%E8%AF%BE%E5%A0%82-%E8%BE%85%E5%8A%A9%E8%84%9A%E6%9C%AC)。 4 | 5 | 目前正在开发中,欢迎关注... 6 | 7 | - 良好封装的代码,您可以在 `./src/plugins/` 中编写并贡献你的插件,并获得通过上下文提供的已有工具链。参见 [示例插件](https://github.com/memset0/Learning-at-ZJU-Helper/tree/master/src/plugins/example-plugin)。 8 | - 由于我没有在北教/蒙民伟楼的课程,对北教智云的支持可能会不完善,欢迎同学们补充。 9 | 10 | 11 | 12 | ## 功能列表 13 | 14 | ### 更好的 PTA [`better-pintia`](https://github.com/memset0/Learning-at-ZJU-Helper/tree/master/src/plugins/better-pintia) 15 | 16 | PTA 助手,提供以下功能: 17 | 18 | - 复制题面 _beta_,可用于题目纠错等场景。 19 | 20 | 21 | 22 | ### 更好的视频播放器 [`better-video-player`](https://github.com/memset0/Learning-at-ZJU-Helper/tree/master/src/plugins/better-video-player) 23 | 24 | 为网课的视频播放器添加以下功能: 25 | 26 | - 网页全屏 27 | 28 | 29 | 30 | ### 带时间戳的地址复制(精准空降) [`copy-with-timestamp`](https://github.com/memset0/Learning-at-ZJU-Helper/tree/master/src/plugins/copy-with-timestamp) 31 | 32 | 复制带时间戳的视频地址,这样再次打开时就会自动跳转到对应位置。 33 | 34 | 35 | 36 | ### 专注模式 [`focus-mode`](https://github.com/memset0/Learning-at-ZJU-Helper/tree/master/src/plugins/focus-mode) 37 | 38 | 屏蔽掉无用的网页组件,使你可以专注于课堂本身。开启后智云课堂将不会显示推荐的课程、收藏点赞等无用功能。 39 | 40 | 如果需要使用被屏蔽的组件,到设置中关闭本功能即可。 41 | 42 | 43 | 44 | ### 画中画模式 [`picture-in-picture`](https://github.com/memset0/Learning-at-ZJU-Helper/tree/master/src/plugins/picture-in-picture) 45 | 46 | > 感谢 [@Trzeth](https://github.com/Trzeth) 贡献。 47 | 48 | 允许智云课堂的视频或 PPT 兼容浏览器画中画(PIP)功能。通过点击 按钮进入。 49 | 50 | 51 | 52 | ### 课件下载 [`ppt-downloader`](https://github.com/memset0/Learning-at-ZJU-Helper/tree/master/src/plugins/ppt-downloader) 53 | 54 | 下载智云课堂自动生成的课件,支持配置最小间隔时间,还支持多种下载方式: 55 | 56 | - **导出为 PDF**:将所有课件导出为 PDF,会调用浏览器自带的打印对话框,也可以直接通过打印机打印。[点我下载示例文件](https://pan.memset0.cn/Share/2024/03/03/%E4%BD%BF%E7%94%A8%E8%84%9A%E6%9C%AC%E5%AF%BC%E5%87%BA%E7%9A%84%E8%AF%BE%E4%BB%B6%EF%BC%88%E9%AB%98%E7%BA%A7%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95%E5%88%86%E6%9E%902024-02-26%E7%AC%AC3-5%E8%8A%82%EF%BC%89.pdf)。 57 | (由于浏览器性能限制,当图片数量过多时导出速度较慢。如果你有更好的解决方案,请联系我。) 58 | 59 | - **打包下载**:将所有课件添加到压缩包中,示例如下: 60 | ![](https://static.memset0.cn/img/v6/2024/03/03/uEUzlIZR.png) 61 | 62 | 63 | 64 | ### 视频链接解析 [`replay-parser`](https://github.com/memset0/Learning-at-ZJU-Helper/tree/master/src/plugins/replay-parser) 65 | 66 | 添加视频解析按钮,点击后自动复制视频连接到剪贴板,可以直接下载。直播也能使用,但需要在流媒体播放器中打开。 67 | 68 | 69 | 70 | ### 成绩嗅探 [`score-finder`](https://github.com/memset0/Learning-at-ZJU-Helper/tree/master/src/plugins/score-finder) 71 | 72 | 通过 API 查询学在浙大中已被登记但尚未公开的成绩。 73 | 74 | 75 | 76 | > 以上功能介绍基于版本 2.0.4 生成,在最新版中可能发生改变,请参见 [项目仓库](https://github.com/memset0/Learning-at-ZJU-Helper)。 77 | 78 | -------------------------------------------------------------------------------- /build.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const beautify = require('js-beautify/js').js; 4 | 5 | const packageJSON = JSON.parse(fs.readFileSync(path.resolve(__dirname, 'package.json'), 'utf8')); 6 | 7 | if (!fs.existsSync(path.join(__dirname, 'dist'))) { 8 | fs.mkdirSync(path.join(__dirname, 'dist')); 9 | } 10 | 11 | fs.writeFileSync(path.join(__dirname, 'dist', 'user.dev.js'), generateHeader(true)); 12 | if (fs.existsSync(path.join(__dirname, 'dist', 'bundle.js'))) { 13 | const userjs = generateHeader(false) + fs.readFileSync(path.join(__dirname, 'dist', 'bundle.js'), 'utf-8').toString(); 14 | fs.writeFileSync(path.join(__dirname, 'dist', 'user.js'), userjs); 15 | fs.writeFileSync(path.join(__dirname, 'dist', 'user.format.js'), beautify(userjs, { indent_with_tabs: true })); 16 | } 17 | 18 | updateReadme(packageJSON.version); 19 | 20 | function generateHeader(devlopment = false) { 21 | const data = { 22 | ...JSON.parse(fs.readFileSync(path.resolve(__dirname, 'userscript.json'), 'utf8')), 23 | version: packageJSON.version, 24 | author: packageJSON.author, 25 | license: packageJSON.license, 26 | }; 27 | 28 | if (devlopment) { 29 | if (!Object.keys(data).includes('require')) { 30 | data.require = []; 31 | } 32 | data.name = '[DEV] ' + data.name; 33 | data.require.push('file:///' + path.resolve(__dirname, 'dist', 'bundle.js').replace(/\\/g, '/')); 34 | } 35 | 36 | const spaces = (key) => { 37 | const number = 16 - key.length; 38 | return ' '.repeat(number); 39 | }; 40 | 41 | let header = ''; 42 | header += '// ==UserScript==\n'; 43 | for (const key in data) { 44 | const value = data[key]; 45 | if (value instanceof Array) { 46 | value.forEach((v) => { 47 | header += `// @${key}${spaces(key)}${v}\n`; 48 | }); 49 | } else { 50 | header += `// @${key}${spaces(key)}${value}\n`; 51 | } 52 | } 53 | header += '// ==/UserScript==\n\n\n'; 54 | 55 | return header; 56 | } 57 | 58 | function updateReadme(version) { 59 | const SEPARATOR = ''; 60 | 61 | let [readme, _] = fs.readFileSync(path.join(__dirname, 'README.md'), 'utf-8').toString().split(SEPARATOR); 62 | readme += SEPARATOR + '\n\n'; 63 | 64 | readme += `## 功能列表\n\n`; 65 | 66 | const plugins = fs.readdirSync(path.join(__dirname, 'src/plugins')); 67 | for (const plugin of plugins) { 68 | if (fs.existsSync(path.join(__dirname, 'src/plugins', plugin, 'README.md'))) { 69 | for (const line of fs.readFileSync(path.join(__dirname, 'src/plugins', plugin, 'README.md'), 'utf-8').toString().split('\n')) { 70 | if (line.startsWith('## ')) { 71 | const title = line.slice(3).trim(); 72 | readme += `### ${title} [\`${plugin}\`](https://github.com/memset0/Learning-at-ZJU-Helper/tree/master/src/plugins/${plugin})\n`; 73 | } else { 74 | readme += line + '\n'; 75 | } 76 | } 77 | readme += '\n\n'; 78 | } 79 | } 80 | 81 | readme += `> 以上功能介绍基于版本 ${version} 生成,在最新版中可能发生改变,请参见 [项目仓库](https://github.com/memset0/Learning-at-ZJU-Helper)。\n\n`; 82 | 83 | // console.log(readme); 84 | fs.writeFileSync(path.join(__dirname, 'README.md'), readme); 85 | fs.writeFileSync(path.join(__dirname, 'dist/README.md'), readme); 86 | } 87 | -------------------------------------------------------------------------------- /lib/jszip.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | 3 | JSZip v3.9.1 - A JavaScript class for generating and reading zip files 4 | 5 | 6 | (c) 2009-2016 Stuart Knightley 7 | Dual licenced under the MIT license or GPLv3. See https://raw.github.com/Stuk/jszip/master/LICENSE.markdown. 8 | 9 | JSZip uses the library pako released under the MIT license : 10 | https://github.com/nodeca/pako/blob/master/LICENSE 11 | */ 12 | 13 | !function(t){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=t();else if("function"==typeof define&&define.amd)define([],t);else{("undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this).JSZip=t()}}(function(){return function s(a,o,h){function u(r,t){if(!o[r]){if(!a[r]){var e="function"==typeof require&&require;if(!t&&e)return e(r,!0);if(l)return l(r,!0);var i=new Error("Cannot find module '"+r+"'");throw i.code="MODULE_NOT_FOUND",i}var n=o[r]={exports:{}};a[r][0].call(n.exports,function(t){var e=a[r][1][t];return u(e||t)},n,n.exports,s,a,o,h)}return o[r].exports}for(var l="function"==typeof require&&require,t=0;t>2,s=(3&e)<<4|r>>4,a=1>6:64,o=2>4,r=(15&n)<<4|(s=p.indexOf(t.charAt(o++)))>>2,i=(3&s)<<6|(a=p.indexOf(t.charAt(o++))),l[h++]=e,64!==s&&(l[h++]=r),64!==a&&(l[h++]=i);return l}},{"./support":30,"./utils":32}],2:[function(t,e,r){"use strict";var i=t("./external"),n=t("./stream/DataWorker"),s=t("./stream/Crc32Probe"),a=t("./stream/DataLengthProbe");function o(t,e,r,i,n){this.compressedSize=t,this.uncompressedSize=e,this.crc32=r,this.compression=i,this.compressedContent=n}o.prototype={getContentWorker:function(){var t=new n(i.Promise.resolve(this.compressedContent)).pipe(this.compression.uncompressWorker()).pipe(new a("data_length")),e=this;return t.on("end",function(){if(this.streamInfo.data_length!==e.uncompressedSize)throw new Error("Bug : uncompressed data size mismatch")}),t},getCompressedWorker:function(){return new n(i.Promise.resolve(this.compressedContent)).withStreamInfo("compressedSize",this.compressedSize).withStreamInfo("uncompressedSize",this.uncompressedSize).withStreamInfo("crc32",this.crc32).withStreamInfo("compression",this.compression)}},o.createWorkerFrom=function(t,e,r){return t.pipe(new s).pipe(new a("uncompressedSize")).pipe(e.compressWorker(r)).pipe(new a("compressedSize")).withStreamInfo("compression",e)},e.exports=o},{"./external":6,"./stream/Crc32Probe":25,"./stream/DataLengthProbe":26,"./stream/DataWorker":27}],3:[function(t,e,r){"use strict";var i=t("./stream/GenericWorker");r.STORE={magic:"\0\0",compressWorker:function(t){return new i("STORE compression")},uncompressWorker:function(){return new i("STORE decompression")}},r.DEFLATE=t("./flate")},{"./flate":7,"./stream/GenericWorker":28}],4:[function(t,e,r){"use strict";var i=t("./utils");var o=function(){for(var t,e=[],r=0;r<256;r++){t=r;for(var i=0;i<8;i++)t=1&t?3988292384^t>>>1:t>>>1;e[r]=t}return e}();e.exports=function(t,e){return void 0!==t&&t.length?"string"!==i.getTypeOf(t)?function(t,e,r,i){var n=o,s=i+r;t^=-1;for(var a=i;a>>8^n[255&(t^e[a])];return-1^t}(0|e,t,t.length,0):function(t,e,r,i){var n=o,s=i+r;t^=-1;for(var a=i;a>>8^n[255&(t^e.charCodeAt(a))];return-1^t}(0|e,t,t.length,0):0}},{"./utils":32}],5:[function(t,e,r){"use strict";r.base64=!1,r.binary=!1,r.dir=!1,r.createFolders=!0,r.date=null,r.compression=null,r.compressionOptions=null,r.comment=null,r.unixPermissions=null,r.dosPermissions=null},{}],6:[function(t,e,r){"use strict";var i=null;i="undefined"!=typeof Promise?Promise:t("lie"),e.exports={Promise:i}},{lie:37}],7:[function(t,e,r){"use strict";var i="undefined"!=typeof Uint8Array&&"undefined"!=typeof Uint16Array&&"undefined"!=typeof Uint32Array,n=t("pako"),s=t("./utils"),a=t("./stream/GenericWorker"),o=i?"uint8array":"array";function h(t,e){a.call(this,"FlateWorker/"+t),this._pako=null,this._pakoAction=t,this._pakoOptions=e,this.meta={}}r.magic="\b\0",s.inherits(h,a),h.prototype.processChunk=function(t){this.meta=t.meta,null===this._pako&&this._createPako(),this._pako.push(s.transformTo(o,t.data),!1)},h.prototype.flush=function(){a.prototype.flush.call(this),null===this._pako&&this._createPako(),this._pako.push([],!0)},h.prototype.cleanUp=function(){a.prototype.cleanUp.call(this),this._pako=null},h.prototype._createPako=function(){this._pako=new n[this._pakoAction]({raw:!0,level:this._pakoOptions.level||-1});var e=this;this._pako.onData=function(t){e.push({data:t,meta:e.meta})}},r.compressWorker=function(t){return new h("Deflate",t)},r.uncompressWorker=function(){return new h("Inflate",{})}},{"./stream/GenericWorker":28,"./utils":32,pako:38}],8:[function(t,e,r){"use strict";function A(t,e){var r,i="";for(r=0;r>>=8;return i}function i(t,e,r,i,n,s){var a,o,h=t.file,u=t.compression,l=s!==O.utf8encode,f=I.transformTo("string",s(h.name)),d=I.transformTo("string",O.utf8encode(h.name)),c=h.comment,p=I.transformTo("string",s(c)),m=I.transformTo("string",O.utf8encode(c)),_=d.length!==h.name.length,g=m.length!==c.length,b="",v="",y="",w=h.dir,k=h.date,x={crc32:0,compressedSize:0,uncompressedSize:0};e&&!r||(x.crc32=t.crc32,x.compressedSize=t.compressedSize,x.uncompressedSize=t.uncompressedSize);var S=0;e&&(S|=8),l||!_&&!g||(S|=2048);var z=0,C=0;w&&(z|=16),"UNIX"===n?(C=798,z|=function(t,e){var r=t;return t||(r=e?16893:33204),(65535&r)<<16}(h.unixPermissions,w)):(C=20,z|=function(t){return 63&(t||0)}(h.dosPermissions)),a=k.getUTCHours(),a<<=6,a|=k.getUTCMinutes(),a<<=5,a|=k.getUTCSeconds()/2,o=k.getUTCFullYear()-1980,o<<=4,o|=k.getUTCMonth()+1,o<<=5,o|=k.getUTCDate(),_&&(v=A(1,1)+A(B(f),4)+d,b+="up"+A(v.length,2)+v),g&&(y=A(1,1)+A(B(p),4)+m,b+="uc"+A(y.length,2)+y);var E="";return E+="\n\0",E+=A(S,2),E+=u.magic,E+=A(a,2),E+=A(o,2),E+=A(x.crc32,4),E+=A(x.compressedSize,4),E+=A(x.uncompressedSize,4),E+=A(f.length,2),E+=A(b.length,2),{fileRecord:R.LOCAL_FILE_HEADER+E+f+b,dirRecord:R.CENTRAL_FILE_HEADER+A(C,2)+E+A(p.length,2)+"\0\0\0\0"+A(z,4)+A(i,4)+f+b+p}}var I=t("../utils"),n=t("../stream/GenericWorker"),O=t("../utf8"),B=t("../crc32"),R=t("../signature");function s(t,e,r,i){n.call(this,"ZipFileWorker"),this.bytesWritten=0,this.zipComment=e,this.zipPlatform=r,this.encodeFileName=i,this.streamFiles=t,this.accumulate=!1,this.contentBuffer=[],this.dirRecords=[],this.currentSourceOffset=0,this.entriesCount=0,this.currentFile=null,this._sources=[]}I.inherits(s,n),s.prototype.push=function(t){var e=t.meta.percent||0,r=this.entriesCount,i=this._sources.length;this.accumulate?this.contentBuffer.push(t):(this.bytesWritten+=t.data.length,n.prototype.push.call(this,{data:t.data,meta:{currentFile:this.currentFile,percent:r?(e+100*(r-i-1))/r:100}}))},s.prototype.openedSource=function(t){this.currentSourceOffset=this.bytesWritten,this.currentFile=t.file.name;var e=this.streamFiles&&!t.file.dir;if(e){var r=i(t,e,!1,this.currentSourceOffset,this.zipPlatform,this.encodeFileName);this.push({data:r.fileRecord,meta:{percent:0}})}else this.accumulate=!0},s.prototype.closedSource=function(t){this.accumulate=!1;var e=this.streamFiles&&!t.file.dir,r=i(t,e,!0,this.currentSourceOffset,this.zipPlatform,this.encodeFileName);if(this.dirRecords.push(r.dirRecord),e)this.push({data:function(t){return R.DATA_DESCRIPTOR+A(t.crc32,4)+A(t.compressedSize,4)+A(t.uncompressedSize,4)}(t),meta:{percent:100}});else for(this.push({data:r.fileRecord,meta:{percent:0}});this.contentBuffer.length;)this.push(this.contentBuffer.shift());this.currentFile=null},s.prototype.flush=function(){for(var t=this.bytesWritten,e=0;e=this.index;e--)r=(r<<8)+this.byteAt(e);return this.index+=t,r},readString:function(t){return i.transformTo("string",this.readData(t))},readData:function(t){},lastIndexOfSignature:function(t){},readAndCheckSignature:function(t){},readDate:function(){var t=this.readInt(4);return new Date(Date.UTC(1980+(t>>25&127),(t>>21&15)-1,t>>16&31,t>>11&31,t>>5&63,(31&t)<<1))}},e.exports=n},{"../utils":32}],19:[function(t,e,r){"use strict";var i=t("./Uint8ArrayReader");function n(t){i.call(this,t)}t("../utils").inherits(n,i),n.prototype.readData=function(t){this.checkOffset(t);var e=this.data.slice(this.zero+this.index,this.zero+this.index+t);return this.index+=t,e},e.exports=n},{"../utils":32,"./Uint8ArrayReader":21}],20:[function(t,e,r){"use strict";var i=t("./DataReader");function n(t){i.call(this,t)}t("../utils").inherits(n,i),n.prototype.byteAt=function(t){return this.data.charCodeAt(this.zero+t)},n.prototype.lastIndexOfSignature=function(t){return this.data.lastIndexOf(t)-this.zero},n.prototype.readAndCheckSignature=function(t){return t===this.readData(4)},n.prototype.readData=function(t){this.checkOffset(t);var e=this.data.slice(this.zero+this.index,this.zero+this.index+t);return this.index+=t,e},e.exports=n},{"../utils":32,"./DataReader":18}],21:[function(t,e,r){"use strict";var i=t("./ArrayReader");function n(t){i.call(this,t)}t("../utils").inherits(n,i),n.prototype.readData=function(t){if(this.checkOffset(t),0===t)return new Uint8Array(0);var e=this.data.subarray(this.zero+this.index,this.zero+this.index+t);return this.index+=t,e},e.exports=n},{"../utils":32,"./ArrayReader":17}],22:[function(t,e,r){"use strict";var i=t("../utils"),n=t("../support"),s=t("./ArrayReader"),a=t("./StringReader"),o=t("./NodeBufferReader"),h=t("./Uint8ArrayReader");e.exports=function(t){var e=i.getTypeOf(t);return i.checkSupport(e),"string"!==e||n.uint8array?"nodebuffer"===e?new o(t):n.uint8array?new h(i.transformTo("uint8array",t)):new s(i.transformTo("array",t)):new a(t)}},{"../support":30,"../utils":32,"./ArrayReader":17,"./NodeBufferReader":19,"./StringReader":20,"./Uint8ArrayReader":21}],23:[function(t,e,r){"use strict";r.LOCAL_FILE_HEADER="PK",r.CENTRAL_FILE_HEADER="PK",r.CENTRAL_DIRECTORY_END="PK",r.ZIP64_CENTRAL_DIRECTORY_LOCATOR="PK",r.ZIP64_CENTRAL_DIRECTORY_END="PK",r.DATA_DESCRIPTOR="PK\b"},{}],24:[function(t,e,r){"use strict";var i=t("./GenericWorker"),n=t("../utils");function s(t){i.call(this,"ConvertWorker to "+t),this.destType=t}n.inherits(s,i),s.prototype.processChunk=function(t){this.push({data:n.transformTo(this.destType,t.data),meta:t.meta})},e.exports=s},{"../utils":32,"./GenericWorker":28}],25:[function(t,e,r){"use strict";var i=t("./GenericWorker"),n=t("../crc32");function s(){i.call(this,"Crc32Probe"),this.withStreamInfo("crc32",0)}t("../utils").inherits(s,i),s.prototype.processChunk=function(t){this.streamInfo.crc32=n(t.data,this.streamInfo.crc32||0),this.push(t)},e.exports=s},{"../crc32":4,"../utils":32,"./GenericWorker":28}],26:[function(t,e,r){"use strict";var i=t("../utils"),n=t("./GenericWorker");function s(t){n.call(this,"DataLengthProbe for "+t),this.propName=t,this.withStreamInfo(t,0)}i.inherits(s,n),s.prototype.processChunk=function(t){if(t){var e=this.streamInfo[this.propName]||0;this.streamInfo[this.propName]=e+t.data.length}n.prototype.processChunk.call(this,t)},e.exports=s},{"../utils":32,"./GenericWorker":28}],27:[function(t,e,r){"use strict";var i=t("../utils"),n=t("./GenericWorker");function s(t){n.call(this,"DataWorker");var e=this;this.dataIsReady=!1,this.index=0,this.max=0,this.data=null,this.type="",this._tickScheduled=!1,t.then(function(t){e.dataIsReady=!0,e.data=t,e.max=t&&t.length||0,e.type=i.getTypeOf(t),e.isPaused||e._tickAndRepeat()},function(t){e.error(t)})}i.inherits(s,n),s.prototype.cleanUp=function(){n.prototype.cleanUp.call(this),this.data=null},s.prototype.resume=function(){return!!n.prototype.resume.call(this)&&(!this._tickScheduled&&this.dataIsReady&&(this._tickScheduled=!0,i.delay(this._tickAndRepeat,[],this)),!0)},s.prototype._tickAndRepeat=function(){this._tickScheduled=!1,this.isPaused||this.isFinished||(this._tick(),this.isFinished||(i.delay(this._tickAndRepeat,[],this),this._tickScheduled=!0))},s.prototype._tick=function(){if(this.isPaused||this.isFinished)return!1;var t=null,e=Math.min(this.max,this.index+16384);if(this.index>=this.max)return this.end();switch(this.type){case"string":t=this.data.substring(this.index,e);break;case"uint8array":t=this.data.subarray(this.index,e);break;case"array":case"nodebuffer":t=this.data.slice(this.index,e)}return this.index=e,this.push({data:t,meta:{percent:this.max?this.index/this.max*100:0}})},e.exports=s},{"../utils":32,"./GenericWorker":28}],28:[function(t,e,r){"use strict";function i(t){this.name=t||"default",this.streamInfo={},this.generatedError=null,this.extraStreamInfo={},this.isPaused=!0,this.isFinished=!1,this.isLocked=!1,this._listeners={data:[],end:[],error:[]},this.previous=null}i.prototype={push:function(t){this.emit("data",t)},end:function(){if(this.isFinished)return!1;this.flush();try{this.emit("end"),this.cleanUp(),this.isFinished=!0}catch(t){this.emit("error",t)}return!0},error:function(t){return!this.isFinished&&(this.isPaused?this.generatedError=t:(this.isFinished=!0,this.emit("error",t),this.previous&&this.previous.error(t),this.cleanUp()),!0)},on:function(t,e){return this._listeners[t].push(e),this},cleanUp:function(){this.streamInfo=this.generatedError=this.extraStreamInfo=null,this._listeners=[]},emit:function(t,e){if(this._listeners[t])for(var r=0;r "+t:t}},e.exports=i},{}],29:[function(t,e,r){"use strict";var h=t("../utils"),n=t("./ConvertWorker"),s=t("./GenericWorker"),u=t("../base64"),i=t("../support"),a=t("../external"),o=null;if(i.nodestream)try{o=t("../nodejs/NodejsStreamOutputAdapter")}catch(t){}function l(t,o){return new a.Promise(function(e,r){var i=[],n=t._internalType,s=t._outputType,a=t._mimeType;t.on("data",function(t,e){i.push(t),o&&o(e)}).on("error",function(t){i=[],r(t)}).on("end",function(){try{var t=function(t,e,r){switch(t){case"blob":return h.newBlob(h.transformTo("arraybuffer",e),r);case"base64":return u.encode(e);default:return h.transformTo(t,e)}}(s,function(t,e){var r,i=0,n=null,s=0;for(r=0;r>>6:(r<65536?e[s++]=224|r>>>12:(e[s++]=240|r>>>18,e[s++]=128|r>>>12&63),e[s++]=128|r>>>6&63),e[s++]=128|63&r);return e}(t)},s.utf8decode=function(t){return h.nodebuffer?o.transformTo("nodebuffer",t).toString("utf-8"):function(t){var e,r,i,n,s=t.length,a=new Array(2*s);for(e=r=0;e>10&1023,a[r++]=56320|1023&i)}return a.length!==r&&(a.subarray?a=a.subarray(0,r):a.length=r),o.applyFromCharCode(a)}(t=o.transformTo(h.uint8array?"uint8array":"array",t))},o.inherits(a,i),a.prototype.processChunk=function(t){var e=o.transformTo(h.uint8array?"uint8array":"array",t.data);if(this.leftOver&&this.leftOver.length){if(h.uint8array){var r=e;(e=new Uint8Array(r.length+this.leftOver.length)).set(this.leftOver,0),e.set(r,this.leftOver.length)}else e=this.leftOver.concat(e);this.leftOver=null}var i=function(t,e){var r;for((e=e||t.length)>t.length&&(e=t.length),r=e-1;0<=r&&128==(192&t[r]);)r--;return r<0?e:0===r?e:r+u[t[r]]>e?r:e}(e),n=e;i!==e.length&&(h.uint8array?(n=e.subarray(0,i),this.leftOver=e.subarray(i,e.length)):(n=e.slice(0,i),this.leftOver=e.slice(i,e.length))),this.push({data:s.utf8decode(n),meta:t.meta})},a.prototype.flush=function(){this.leftOver&&this.leftOver.length&&(this.push({data:s.utf8decode(this.leftOver),meta:{}}),this.leftOver=null)},s.Utf8DecodeWorker=a,o.inherits(l,i),l.prototype.processChunk=function(t){this.push({data:s.utf8encode(t.data),meta:t.meta})},s.Utf8EncodeWorker=l},{"./nodejsUtils":14,"./stream/GenericWorker":28,"./support":30,"./utils":32}],32:[function(t,e,a){"use strict";var o=t("./support"),h=t("./base64"),r=t("./nodejsUtils"),i=t("set-immediate-shim"),u=t("./external");function n(t){return t}function l(t,e){for(var r=0;r>8;this.dir=!!(16&this.externalFileAttributes),0==t&&(this.dosPermissions=63&this.externalFileAttributes),3==t&&(this.unixPermissions=this.externalFileAttributes>>16&65535),this.dir||"/"!==this.fileNameStr.slice(-1)||(this.dir=!0)},parseZIP64ExtraField:function(t){if(this.extraFields[1]){var e=i(this.extraFields[1].value);this.uncompressedSize===s.MAX_VALUE_32BITS&&(this.uncompressedSize=e.readInt(8)),this.compressedSize===s.MAX_VALUE_32BITS&&(this.compressedSize=e.readInt(8)),this.localHeaderOffset===s.MAX_VALUE_32BITS&&(this.localHeaderOffset=e.readInt(8)),this.diskNumberStart===s.MAX_VALUE_32BITS&&(this.diskNumberStart=e.readInt(4))}},readExtraFields:function(t){var e,r,i,n=t.index+this.extraFieldsLength;for(this.extraFields||(this.extraFields={});t.index+4>>6:(r<65536?e[s++]=224|r>>>12:(e[s++]=240|r>>>18,e[s++]=128|r>>>12&63),e[s++]=128|r>>>6&63),e[s++]=128|63&r);return e},r.buf2binstring=function(t){return l(t,t.length)},r.binstring2buf=function(t){for(var e=new h.Buf8(t.length),r=0,i=e.length;r>10&1023,o[i++]=56320|1023&n)}return l(o,i)},r.utf8border=function(t,e){var r;for((e=e||t.length)>t.length&&(e=t.length),r=e-1;0<=r&&128==(192&t[r]);)r--;return r<0?e:0===r?e:r+u[t[r]]>e?r:e}},{"./common":41}],43:[function(t,e,r){"use strict";e.exports=function(t,e,r,i){for(var n=65535&t|0,s=t>>>16&65535|0,a=0;0!==r;){for(r-=a=2e3>>1:t>>>1;e[r]=t}return e}();e.exports=function(t,e,r,i){var n=o,s=i+r;t^=-1;for(var a=i;a>>8^n[255&(t^e[a])];return-1^t}},{}],46:[function(t,e,r){"use strict";var h,d=t("../utils/common"),u=t("./trees"),c=t("./adler32"),p=t("./crc32"),i=t("./messages"),l=0,f=4,m=0,_=-2,g=-1,b=4,n=2,v=8,y=9,s=286,a=30,o=19,w=2*s+1,k=15,x=3,S=258,z=S+x+1,C=42,E=113,A=1,I=2,O=3,B=4;function R(t,e){return t.msg=i[e],e}function T(t){return(t<<1)-(4t.avail_out&&(r=t.avail_out),0!==r&&(d.arraySet(t.output,e.pending_buf,e.pending_out,r,t.next_out),t.next_out+=r,e.pending_out+=r,t.total_out+=r,t.avail_out-=r,e.pending-=r,0===e.pending&&(e.pending_out=0))}function N(t,e){u._tr_flush_block(t,0<=t.block_start?t.block_start:-1,t.strstart-t.block_start,e),t.block_start=t.strstart,F(t.strm)}function U(t,e){t.pending_buf[t.pending++]=e}function P(t,e){t.pending_buf[t.pending++]=e>>>8&255,t.pending_buf[t.pending++]=255&e}function L(t,e){var r,i,n=t.max_chain_length,s=t.strstart,a=t.prev_length,o=t.nice_match,h=t.strstart>t.w_size-z?t.strstart-(t.w_size-z):0,u=t.window,l=t.w_mask,f=t.prev,d=t.strstart+S,c=u[s+a-1],p=u[s+a];t.prev_length>=t.good_match&&(n>>=2),o>t.lookahead&&(o=t.lookahead);do{if(u[(r=e)+a]===p&&u[r+a-1]===c&&u[r]===u[s]&&u[++r]===u[s+1]){s+=2,r++;do{}while(u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&u[++s]===u[++r]&&sh&&0!=--n);return a<=t.lookahead?a:t.lookahead}function j(t){var e,r,i,n,s,a,o,h,u,l,f=t.w_size;do{if(n=t.window_size-t.lookahead-t.strstart,t.strstart>=f+(f-z)){for(d.arraySet(t.window,t.window,f,f,0),t.match_start-=f,t.strstart-=f,t.block_start-=f,e=r=t.hash_size;i=t.head[--e],t.head[e]=f<=i?i-f:0,--r;);for(e=r=f;i=t.prev[--e],t.prev[e]=f<=i?i-f:0,--r;);n+=f}if(0===t.strm.avail_in)break;if(a=t.strm,o=t.window,h=t.strstart+t.lookahead,u=n,l=void 0,l=a.avail_in,u=x)for(s=t.strstart-t.insert,t.ins_h=t.window[s],t.ins_h=(t.ins_h<=x&&(t.ins_h=(t.ins_h<=x)if(i=u._tr_tally(t,t.strstart-t.match_start,t.match_length-x),t.lookahead-=t.match_length,t.match_length<=t.max_lazy_match&&t.lookahead>=x){for(t.match_length--;t.strstart++,t.ins_h=(t.ins_h<=x&&(t.ins_h=(t.ins_h<=x&&t.match_length<=t.prev_length){for(n=t.strstart+t.lookahead-x,i=u._tr_tally(t,t.strstart-1-t.prev_match,t.prev_length-x),t.lookahead-=t.prev_length-1,t.prev_length-=2;++t.strstart<=n&&(t.ins_h=(t.ins_h<t.pending_buf_size-5&&(r=t.pending_buf_size-5);;){if(t.lookahead<=1){if(j(t),0===t.lookahead&&e===l)return A;if(0===t.lookahead)break}t.strstart+=t.lookahead,t.lookahead=0;var i=t.block_start+r;if((0===t.strstart||t.strstart>=i)&&(t.lookahead=t.strstart-i,t.strstart=i,N(t,!1),0===t.strm.avail_out))return A;if(t.strstart-t.block_start>=t.w_size-z&&(N(t,!1),0===t.strm.avail_out))return A}return t.insert=0,e===f?(N(t,!0),0===t.strm.avail_out?O:B):(t.strstart>t.block_start&&(N(t,!1),t.strm.avail_out),A)}),new M(4,4,8,4,Z),new M(4,5,16,8,Z),new M(4,6,32,32,Z),new M(4,4,16,16,W),new M(8,16,32,32,W),new M(8,16,128,128,W),new M(8,32,128,256,W),new M(32,128,258,1024,W),new M(32,258,258,4096,W)],r.deflateInit=function(t,e){return Y(t,e,v,15,8,0)},r.deflateInit2=Y,r.deflateReset=K,r.deflateResetKeep=G,r.deflateSetHeader=function(t,e){return t&&t.state?2!==t.state.wrap?_:(t.state.gzhead=e,m):_},r.deflate=function(t,e){var r,i,n,s;if(!t||!t.state||5>8&255),U(i,i.gzhead.time>>16&255),U(i,i.gzhead.time>>24&255),U(i,9===i.level?2:2<=i.strategy||i.level<2?4:0),U(i,255&i.gzhead.os),i.gzhead.extra&&i.gzhead.extra.length&&(U(i,255&i.gzhead.extra.length),U(i,i.gzhead.extra.length>>8&255)),i.gzhead.hcrc&&(t.adler=p(t.adler,i.pending_buf,i.pending,0)),i.gzindex=0,i.status=69):(U(i,0),U(i,0),U(i,0),U(i,0),U(i,0),U(i,9===i.level?2:2<=i.strategy||i.level<2?4:0),U(i,3),i.status=E);else{var a=v+(i.w_bits-8<<4)<<8;a|=(2<=i.strategy||i.level<2?0:i.level<6?1:6===i.level?2:3)<<6,0!==i.strstart&&(a|=32),a+=31-a%31,i.status=E,P(i,a),0!==i.strstart&&(P(i,t.adler>>>16),P(i,65535&t.adler)),t.adler=1}if(69===i.status)if(i.gzhead.extra){for(n=i.pending;i.gzindex<(65535&i.gzhead.extra.length)&&(i.pending!==i.pending_buf_size||(i.gzhead.hcrc&&i.pending>n&&(t.adler=p(t.adler,i.pending_buf,i.pending-n,n)),F(t),n=i.pending,i.pending!==i.pending_buf_size));)U(i,255&i.gzhead.extra[i.gzindex]),i.gzindex++;i.gzhead.hcrc&&i.pending>n&&(t.adler=p(t.adler,i.pending_buf,i.pending-n,n)),i.gzindex===i.gzhead.extra.length&&(i.gzindex=0,i.status=73)}else i.status=73;if(73===i.status)if(i.gzhead.name){n=i.pending;do{if(i.pending===i.pending_buf_size&&(i.gzhead.hcrc&&i.pending>n&&(t.adler=p(t.adler,i.pending_buf,i.pending-n,n)),F(t),n=i.pending,i.pending===i.pending_buf_size)){s=1;break}s=i.gzindexn&&(t.adler=p(t.adler,i.pending_buf,i.pending-n,n)),0===s&&(i.gzindex=0,i.status=91)}else i.status=91;if(91===i.status)if(i.gzhead.comment){n=i.pending;do{if(i.pending===i.pending_buf_size&&(i.gzhead.hcrc&&i.pending>n&&(t.adler=p(t.adler,i.pending_buf,i.pending-n,n)),F(t),n=i.pending,i.pending===i.pending_buf_size)){s=1;break}s=i.gzindexn&&(t.adler=p(t.adler,i.pending_buf,i.pending-n,n)),0===s&&(i.status=103)}else i.status=103;if(103===i.status&&(i.gzhead.hcrc?(i.pending+2>i.pending_buf_size&&F(t),i.pending+2<=i.pending_buf_size&&(U(i,255&t.adler),U(i,t.adler>>8&255),t.adler=0,i.status=E)):i.status=E),0!==i.pending){if(F(t),0===t.avail_out)return i.last_flush=-1,m}else if(0===t.avail_in&&T(e)<=T(r)&&e!==f)return R(t,-5);if(666===i.status&&0!==t.avail_in)return R(t,-5);if(0!==t.avail_in||0!==i.lookahead||e!==l&&666!==i.status){var o=2===i.strategy?function(t,e){for(var r;;){if(0===t.lookahead&&(j(t),0===t.lookahead)){if(e===l)return A;break}if(t.match_length=0,r=u._tr_tally(t,0,t.window[t.strstart]),t.lookahead--,t.strstart++,r&&(N(t,!1),0===t.strm.avail_out))return A}return t.insert=0,e===f?(N(t,!0),0===t.strm.avail_out?O:B):t.last_lit&&(N(t,!1),0===t.strm.avail_out)?A:I}(i,e):3===i.strategy?function(t,e){for(var r,i,n,s,a=t.window;;){if(t.lookahead<=S){if(j(t),t.lookahead<=S&&e===l)return A;if(0===t.lookahead)break}if(t.match_length=0,t.lookahead>=x&&0t.lookahead&&(t.match_length=t.lookahead)}if(t.match_length>=x?(r=u._tr_tally(t,1,t.match_length-x),t.lookahead-=t.match_length,t.strstart+=t.match_length,t.match_length=0):(r=u._tr_tally(t,0,t.window[t.strstart]),t.lookahead--,t.strstart++),r&&(N(t,!1),0===t.strm.avail_out))return A}return t.insert=0,e===f?(N(t,!0),0===t.strm.avail_out?O:B):t.last_lit&&(N(t,!1),0===t.strm.avail_out)?A:I}(i,e):h[i.level].func(i,e);if(o!==O&&o!==B||(i.status=666),o===A||o===O)return 0===t.avail_out&&(i.last_flush=-1),m;if(o===I&&(1===e?u._tr_align(i):5!==e&&(u._tr_stored_block(i,0,0,!1),3===e&&(D(i.head),0===i.lookahead&&(i.strstart=0,i.block_start=0,i.insert=0))),F(t),0===t.avail_out))return i.last_flush=-1,m}return e!==f?m:i.wrap<=0?1:(2===i.wrap?(U(i,255&t.adler),U(i,t.adler>>8&255),U(i,t.adler>>16&255),U(i,t.adler>>24&255),U(i,255&t.total_in),U(i,t.total_in>>8&255),U(i,t.total_in>>16&255),U(i,t.total_in>>24&255)):(P(i,t.adler>>>16),P(i,65535&t.adler)),F(t),0=r.w_size&&(0===s&&(D(r.head),r.strstart=0,r.block_start=0,r.insert=0),u=new d.Buf8(r.w_size),d.arraySet(u,e,l-r.w_size,r.w_size,0),e=u,l=r.w_size),a=t.avail_in,o=t.next_in,h=t.input,t.avail_in=l,t.next_in=0,t.input=e,j(r);r.lookahead>=x;){for(i=r.strstart,n=r.lookahead-(x-1);r.ins_h=(r.ins_h<>>=y=v>>>24,p-=y,0===(y=v>>>16&255))C[s++]=65535&v;else{if(!(16&y)){if(0==(64&y)){v=m[(65535&v)+(c&(1<>>=y,p-=y),p<15&&(c+=z[i++]<>>=y=v>>>24,p-=y,!(16&(y=v>>>16&255))){if(0==(64&y)){v=_[(65535&v)+(c&(1<>>=y,p-=y,(y=s-a)>3,c&=(1<<(p-=w<<3))-1,t.next_in=i,t.next_out=s,t.avail_in=i>>24&255)+(t>>>8&65280)+((65280&t)<<8)+((255&t)<<24)}function s(){this.mode=0,this.last=!1,this.wrap=0,this.havedict=!1,this.flags=0,this.dmax=0,this.check=0,this.total=0,this.head=null,this.wbits=0,this.wsize=0,this.whave=0,this.wnext=0,this.window=null,this.hold=0,this.bits=0,this.length=0,this.offset=0,this.extra=0,this.lencode=null,this.distcode=null,this.lenbits=0,this.distbits=0,this.ncode=0,this.nlen=0,this.ndist=0,this.have=0,this.next=null,this.lens=new I.Buf16(320),this.work=new I.Buf16(288),this.lendyn=null,this.distdyn=null,this.sane=0,this.back=0,this.was=0}function a(t){var e;return t&&t.state?(e=t.state,t.total_in=t.total_out=e.total=0,t.msg="",e.wrap&&(t.adler=1&e.wrap),e.mode=P,e.last=0,e.havedict=0,e.dmax=32768,e.head=null,e.hold=0,e.bits=0,e.lencode=e.lendyn=new I.Buf32(i),e.distcode=e.distdyn=new I.Buf32(n),e.sane=1,e.back=-1,N):U}function o(t){var e;return t&&t.state?((e=t.state).wsize=0,e.whave=0,e.wnext=0,a(t)):U}function h(t,e){var r,i;return t&&t.state?(i=t.state,e<0?(r=0,e=-e):(r=1+(e>>4),e<48&&(e&=15)),e&&(e<8||15=s.wsize?(I.arraySet(s.window,e,r-s.wsize,s.wsize,0),s.wnext=0,s.whave=s.wsize):(i<(n=s.wsize-s.wnext)&&(n=i),I.arraySet(s.window,e,r-i,n,s.wnext),(i-=n)?(I.arraySet(s.window,e,r-i,i,0),s.wnext=i,s.whave=s.wsize):(s.wnext+=n,s.wnext===s.wsize&&(s.wnext=0),s.whave>>8&255,r.check=B(r.check,E,2,0),l=u=0,r.mode=2;break}if(r.flags=0,r.head&&(r.head.done=!1),!(1&r.wrap)||(((255&u)<<8)+(u>>8))%31){t.msg="incorrect header check",r.mode=30;break}if(8!=(15&u)){t.msg="unknown compression method",r.mode=30;break}if(l-=4,k=8+(15&(u>>>=4)),0===r.wbits)r.wbits=k;else if(k>r.wbits){t.msg="invalid window size",r.mode=30;break}r.dmax=1<>8&1),512&r.flags&&(E[0]=255&u,E[1]=u>>>8&255,r.check=B(r.check,E,2,0)),l=u=0,r.mode=3;case 3:for(;l<32;){if(0===o)break t;o--,u+=i[s++]<>>8&255,E[2]=u>>>16&255,E[3]=u>>>24&255,r.check=B(r.check,E,4,0)),l=u=0,r.mode=4;case 4:for(;l<16;){if(0===o)break t;o--,u+=i[s++]<>8),512&r.flags&&(E[0]=255&u,E[1]=u>>>8&255,r.check=B(r.check,E,2,0)),l=u=0,r.mode=5;case 5:if(1024&r.flags){for(;l<16;){if(0===o)break t;o--,u+=i[s++]<>>8&255,r.check=B(r.check,E,2,0)),l=u=0}else r.head&&(r.head.extra=null);r.mode=6;case 6:if(1024&r.flags&&(o<(c=r.length)&&(c=o),c&&(r.head&&(k=r.head.extra_len-r.length,r.head.extra||(r.head.extra=new Array(r.head.extra_len)),I.arraySet(r.head.extra,i,s,c,k)),512&r.flags&&(r.check=B(r.check,i,c,s)),o-=c,s+=c,r.length-=c),r.length))break t;r.length=0,r.mode=7;case 7:if(2048&r.flags){if(0===o)break t;for(c=0;k=i[s+c++],r.head&&k&&r.length<65536&&(r.head.name+=String.fromCharCode(k)),k&&c>9&1,r.head.done=!0),t.adler=r.check=0,r.mode=12;break;case 10:for(;l<32;){if(0===o)break t;o--,u+=i[s++]<>>=7&l,l-=7&l,r.mode=27;break}for(;l<3;){if(0===o)break t;o--,u+=i[s++]<>>=1)){case 0:r.mode=14;break;case 1:if(j(r),r.mode=20,6!==e)break;u>>>=2,l-=2;break t;case 2:r.mode=17;break;case 3:t.msg="invalid block type",r.mode=30}u>>>=2,l-=2;break;case 14:for(u>>>=7&l,l-=7&l;l<32;){if(0===o)break t;o--,u+=i[s++]<>>16^65535)){t.msg="invalid stored block lengths",r.mode=30;break}if(r.length=65535&u,l=u=0,r.mode=15,6===e)break t;case 15:r.mode=16;case 16:if(c=r.length){if(o>>=5,l-=5,r.ndist=1+(31&u),u>>>=5,l-=5,r.ncode=4+(15&u),u>>>=4,l-=4,286>>=3,l-=3}for(;r.have<19;)r.lens[A[r.have++]]=0;if(r.lencode=r.lendyn,r.lenbits=7,S={bits:r.lenbits},x=T(0,r.lens,0,19,r.lencode,0,r.work,S),r.lenbits=S.bits,x){t.msg="invalid code lengths set",r.mode=30;break}r.have=0,r.mode=19;case 19:for(;r.have>>16&255,b=65535&C,!((_=C>>>24)<=l);){if(0===o)break t;o--,u+=i[s++]<>>=_,l-=_,r.lens[r.have++]=b;else{if(16===b){for(z=_+2;l>>=_,l-=_,0===r.have){t.msg="invalid bit length repeat",r.mode=30;break}k=r.lens[r.have-1],c=3+(3&u),u>>>=2,l-=2}else if(17===b){for(z=_+3;l>>=_)),u>>>=3,l-=3}else{for(z=_+7;l>>=_)),u>>>=7,l-=7}if(r.have+c>r.nlen+r.ndist){t.msg="invalid bit length repeat",r.mode=30;break}for(;c--;)r.lens[r.have++]=k}}if(30===r.mode)break;if(0===r.lens[256]){t.msg="invalid code -- missing end-of-block",r.mode=30;break}if(r.lenbits=9,S={bits:r.lenbits},x=T(D,r.lens,0,r.nlen,r.lencode,0,r.work,S),r.lenbits=S.bits,x){t.msg="invalid literal/lengths set",r.mode=30;break}if(r.distbits=6,r.distcode=r.distdyn,S={bits:r.distbits},x=T(F,r.lens,r.nlen,r.ndist,r.distcode,0,r.work,S),r.distbits=S.bits,x){t.msg="invalid distances set",r.mode=30;break}if(r.mode=20,6===e)break t;case 20:r.mode=21;case 21:if(6<=o&&258<=h){t.next_out=a,t.avail_out=h,t.next_in=s,t.avail_in=o,r.hold=u,r.bits=l,R(t,d),a=t.next_out,n=t.output,h=t.avail_out,s=t.next_in,i=t.input,o=t.avail_in,u=r.hold,l=r.bits,12===r.mode&&(r.back=-1);break}for(r.back=0;g=(C=r.lencode[u&(1<>>16&255,b=65535&C,!((_=C>>>24)<=l);){if(0===o)break t;o--,u+=i[s++]<>v)])>>>16&255,b=65535&C,!(v+(_=C>>>24)<=l);){if(0===o)break t;o--,u+=i[s++]<>>=v,l-=v,r.back+=v}if(u>>>=_,l-=_,r.back+=_,r.length=b,0===g){r.mode=26;break}if(32&g){r.back=-1,r.mode=12;break}if(64&g){t.msg="invalid literal/length code",r.mode=30;break}r.extra=15&g,r.mode=22;case 22:if(r.extra){for(z=r.extra;l>>=r.extra,l-=r.extra,r.back+=r.extra}r.was=r.length,r.mode=23;case 23:for(;g=(C=r.distcode[u&(1<>>16&255,b=65535&C,!((_=C>>>24)<=l);){if(0===o)break t;o--,u+=i[s++]<>v)])>>>16&255,b=65535&C,!(v+(_=C>>>24)<=l);){if(0===o)break t;o--,u+=i[s++]<>>=v,l-=v,r.back+=v}if(u>>>=_,l-=_,r.back+=_,64&g){t.msg="invalid distance code",r.mode=30;break}r.offset=b,r.extra=15&g,r.mode=24;case 24:if(r.extra){for(z=r.extra;l>>=r.extra,l-=r.extra,r.back+=r.extra}if(r.offset>r.dmax){t.msg="invalid distance too far back",r.mode=30;break}r.mode=25;case 25:if(0===h)break t;if(c=d-h,r.offset>c){if((c=r.offset-c)>r.whave&&r.sane){t.msg="invalid distance too far back",r.mode=30;break}p=c>r.wnext?(c-=r.wnext,r.wsize-c):r.wnext-c,c>r.length&&(c=r.length),m=r.window}else m=n,p=a-r.offset,c=r.length;for(hc?(m=R[T+a[v]],A[I+a[v]]):(m=96,0),h=1<>S)+(u-=h)]=p<<24|m<<16|_|0,0!==u;);for(h=1<>=1;if(0!==h?(E&=h-1,E+=h):E=0,v++,0==--O[b]){if(b===w)break;b=e[r+a[v]]}if(k>>7)]}function U(t,e){t.pending_buf[t.pending++]=255&e,t.pending_buf[t.pending++]=e>>>8&255}function P(t,e,r){t.bi_valid>c-r?(t.bi_buf|=e<>c-t.bi_valid,t.bi_valid+=r-c):(t.bi_buf|=e<>>=1,r<<=1,0<--e;);return r>>>1}function Z(t,e,r){var i,n,s=new Array(g+1),a=0;for(i=1;i<=g;i++)s[i]=a=a+r[i-1]<<1;for(n=0;n<=e;n++){var o=t[2*n+1];0!==o&&(t[2*n]=j(s[o]++,o))}}function W(t){var e;for(e=0;e>1;1<=r;r--)G(t,s,r);for(n=h;r=t.heap[1],t.heap[1]=t.heap[t.heap_len--],G(t,s,1),i=t.heap[1],t.heap[--t.heap_max]=r,t.heap[--t.heap_max]=i,s[2*n]=s[2*r]+s[2*i],t.depth[n]=(t.depth[r]>=t.depth[i]?t.depth[r]:t.depth[i])+1,s[2*r+1]=s[2*i+1]=n,t.heap[1]=n++,G(t,s,1),2<=t.heap_len;);t.heap[--t.heap_max]=t.heap[1],function(t,e){var r,i,n,s,a,o,h=e.dyn_tree,u=e.max_code,l=e.stat_desc.static_tree,f=e.stat_desc.has_stree,d=e.stat_desc.extra_bits,c=e.stat_desc.extra_base,p=e.stat_desc.max_length,m=0;for(s=0;s<=g;s++)t.bl_count[s]=0;for(h[2*t.heap[t.heap_max]+1]=0,r=t.heap_max+1;r<_;r++)p<(s=h[2*h[2*(i=t.heap[r])+1]+1]+1)&&(s=p,m++),h[2*i+1]=s,u>=7;i>>=1)if(1&r&&0!==t.dyn_ltree[2*e])return o;if(0!==t.dyn_ltree[18]||0!==t.dyn_ltree[20]||0!==t.dyn_ltree[26])return h;for(e=32;e>>3,(s=t.static_len+3+7>>>3)<=n&&(n=s)):n=s=r+5,r+4<=n&&-1!==e?J(t,e,r,i):4===t.strategy||s===n?(P(t,2+(i?1:0),3),K(t,z,C)):(P(t,4+(i?1:0),3),function(t,e,r,i){var n;for(P(t,e-257,5),P(t,r-1,5),P(t,i-4,4),n=0;n>>8&255,t.pending_buf[t.d_buf+2*t.last_lit+1]=255&e,t.pending_buf[t.l_buf+t.last_lit]=255&r,t.last_lit++,0===e?t.dyn_ltree[2*r]++:(t.matches++,e--,t.dyn_ltree[2*(A[r]+u+1)]++,t.dyn_dtree[2*N(e)]++),t.last_lit===t.lit_bufsize-1},r._tr_align=function(t){P(t,2,3),L(t,m,z),function(t){16===t.bi_valid?(U(t,t.bi_buf),t.bi_buf=0,t.bi_valid=0):8<=t.bi_valid&&(t.pending_buf[t.pending++]=255&t.bi_buf,t.bi_buf>>=8,t.bi_valid-=8)}(t)}},{"../utils/common":41}],53:[function(t,e,r){"use strict";e.exports=function(){this.input=null,this.next_in=0,this.avail_in=0,this.total_in=0,this.output=null,this.next_out=0,this.avail_out=0,this.total_out=0,this.msg="",this.state=null,this.data_type=2,this.adler=0}},{}],54:[function(t,e,r){"use strict";e.exports="function"==typeof setImmediate?setImmediate:function(){var t=[].slice.apply(arguments);t.splice(1,0,0),setTimeout.apply(null,t)}},{}]},{},[10])(10)}); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "learning-at-zju-helper", 3 | "version": "2.0.4", 4 | "description": "学在浙大/智云课堂 辅助脚本", 5 | "main": "src/app.js", 6 | "scripts": { 7 | "build": "webpack --config webpack.config.js --mode production && node build.js", 8 | "dev": "node build.js && webpack --config webpack.config.js --mode development --watch", 9 | "version": "node build.js" 10 | }, 11 | "author": "memset0", 12 | "license": "MIT", 13 | "dependencies": { 14 | "@babel/plugin-transform-react-jsx-development": "^7.25.9", 15 | "@babel/preset-env": "^7.26.0", 16 | "@babel/preset-react": "^7.25.9", 17 | "@ui5/webcomponents": "^2.4.0", 18 | "@ui5/webcomponents-fiori": "^2.4.0", 19 | "@ui5/webcomponents-icons": "^2.4.0", 20 | "babel-loader": "^9.2.1", 21 | "clean-css-loader": "^4.2.1", 22 | "css-loader": "^7.1.2", 23 | "file-saver": "^2.0.5", 24 | "hyperapp": "^2.0.22", 25 | "hyperapp-jsx-pragma": "^1.3.0", 26 | "js-beautify": "^1.15.1", 27 | "learning-at-zju-helper": "file:", 28 | "less-loader": "^12.2.0", 29 | "less-plugin-clean-css": "^1.6.0", 30 | "loader-utils": "^3.3.1", 31 | "mini-css-extract-plugin": "^2.9.2", 32 | "style-loader": "^4.0.0", 33 | "webpack": "^5.96.1", 34 | "webpack-cli": "^5.1.4" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | import { initializePanel } from './panel'; 2 | 3 | import logger from './utils/logger'; 4 | import { isVideoPage } from './utils/checker'; 5 | import { sleep, matchRoute } from './utils/global'; 6 | import { copyToClipboard, loadUrlQuery } from './utils/browser'; 7 | 8 | class App { 9 | getNamespace() { 10 | const hostname = location.hostname; 11 | 12 | if (hostname === 'courses.zju.edu.cn') { 13 | return '学在浙大'; 14 | } 15 | if (hostname === 'classroom.zju.edu.cn' || hostname === 'livingroom.cmc.zju.edu.cn' || hostname === 'onlineroom.cmc.zju.edu.cn' || hostname === 'interactivemeta.cmc.zju.edu.cn') { 16 | return '智云课堂'; 17 | } 18 | if (hostname === 'pintia.cn') { 19 | return 'PTA'; 20 | } 21 | return null; 22 | } 23 | 24 | loadScript(link) { 25 | if (this.loadedScripts.includes(link)) { 26 | return; 27 | } 28 | this.loadedScripts.push(link); 29 | logger.debug(link, GM_getResourceText); 30 | const script = GM_getResourceText(link); 31 | if (script === null) { 32 | logger.error(`脚本 ${link} 加载失败`); 33 | } else { 34 | logger.debug(script); 35 | unsafeWindow.eval(script); 36 | } 37 | } 38 | 39 | constructor() { 40 | this.plugins = {}; 41 | const pluginLoader = require.context('./plugins', true, /\/index\.js$/); 42 | pluginLoader.keys().forEach((filename) => { 43 | const slug = filename.slice(2, -9); 44 | if (slug.startsWith('example-')) { 45 | return; // 示例插件将不会被加载 46 | } 47 | this.plugins[slug] = pluginLoader(filename); 48 | this.plugins[slug].slug = slug; 49 | }); 50 | 51 | this.loadedScripts = []; 52 | } 53 | 54 | async load() { 55 | // 初始化面板 56 | const panel = initializePanel(this.plugins); 57 | 58 | // 上下文管理 59 | const globalContext = { 60 | panel, 61 | namespace: this.getNamespace(), 62 | clipboard: { 63 | copy: copyToClipboard, 64 | }, 65 | query: loadUrlQuery(location.search), 66 | window: unsafeWindow, 67 | document: unsafeWindow.document, 68 | location: unsafeWindow.location, 69 | env: { isVideoPage: isVideoPage() }, 70 | loadScript: (link) => this.loadScript(link), 71 | extendContext: (context) => { 72 | Object.assign(globalContext, context); 73 | }, 74 | }; 75 | 76 | // 检查插件队列是否已经清空 77 | const isQueueCleaned = () => { 78 | for (const slug in this.plugins) { 79 | const plugin = this.plugins[slug]; 80 | if (!plugin.loaded) { 81 | return false; 82 | } 83 | } 84 | return true; 85 | }; 86 | 87 | logger.debug('开始加载插件', this.plugins); 88 | let retryTimes = 0; 89 | 90 | do { 91 | for (const slug in this.plugins) { 92 | const plugin = this.plugins[slug]; 93 | if (!plugin.loaded) { 94 | // 合成插件上下文 95 | const pluginContext = { 96 | ...globalContext, // 这里每次都需要重新综合一次主 globalContext,因为其可能被插件更新 97 | logger: logger.extends(plugin.slug), 98 | panelInitialize: panel.pluginInitializers[plugin.slug], 99 | }; 100 | 101 | // 检测插件前置列表 102 | if (plugin.required && plugin.required instanceof Array && plugin.required.length > 0) { 103 | let status = 'ok'; 104 | for (const required of plugin.required) { 105 | if (this.plugins[required].skipped) { 106 | status = 'skip'; 107 | break; 108 | } else if (!this.plugins[required].loaded) { 109 | status = 'wait'; 110 | break; 111 | } 112 | } 113 | if (status === 'skip') { 114 | plugin.loaded = true; 115 | plugin.skipped = true; 116 | logger.debug(`跳过加载 ${plugin.slug} 插件,因为前置插件被跳过`); 117 | continue; 118 | } else if (status === 'wait') { 119 | continue; 120 | } 121 | } 122 | 123 | // 检查该插件是否需要跳过 124 | let needSkip = false; 125 | if (!needSkip && plugin.namespace) { 126 | if (plugin.namespace instanceof Array) { 127 | if (!plugin.namespace.includes(globalContext.namespace)) { 128 | needSkip = true; 129 | } 130 | } else if (plugin.namespace !== globalContext.namespace) { 131 | needSkip = true; 132 | } 133 | } 134 | if (!needSkip && plugin.route) { 135 | if (matchRoute(plugin.route, location.pathname) === false) { 136 | needSkip = true; 137 | } 138 | } 139 | if (!needSkip && plugin.skip instanceof Function) { 140 | if (await plugin.skip(pluginContext)) { 141 | needSkip = true; 142 | } 143 | } 144 | if (needSkip) { 145 | plugin.loaded = true; 146 | plugin.skipped = true; 147 | logger.debug(`跳过加载 ${plugin.slug} 插件`); 148 | continue; 149 | } 150 | 151 | // 检查该插件是否可以加载 152 | if (plugin.check instanceof Function) { 153 | if (!(await plugin.check(pluginContext))) { 154 | continue; 155 | } 156 | } 157 | 158 | if (plugin.route) { 159 | const params = matchRoute(plugin.route, location.pathname); 160 | pluginContext.params = params; 161 | } 162 | // 进行插件加载 163 | await plugin.load(pluginContext); 164 | plugin.loaded = true; 165 | 166 | panel.pushLoadedPlugin({ 167 | slug: plugin.slug, 168 | name: plugin.name, 169 | namespace: plugin.namespace ? globalContext.namespace : null, 170 | description: plugin.description, 171 | }); 172 | } 173 | } 174 | 175 | // 等待 100ms 后进行下一轮检查,避免阻塞渲染进程 176 | await sleep(100); 177 | } while (!isQueueCleaned() && ++retryTimes < 129); 178 | 179 | if (!isQueueCleaned()) { 180 | logger.error( 181 | '插件加载失败,还有以下插件未加载:', 182 | Object.keys(this.plugins).filter((slug) => !this.plugins[slug].loaded) 183 | ); 184 | } else { 185 | logger.info('插件加载完成!'); 186 | } 187 | 188 | // 设置加载完成,不显示 busy-indicator 189 | panel.element.classList.add('zju-helper-loaded'); 190 | } 191 | 192 | safe_load() { 193 | (async () => { 194 | try { 195 | await app.load(); // 这里需要 await,否则捕获不到异常 196 | } catch (error) { 197 | logger.error(error); 198 | throw error; 199 | } 200 | })(); 201 | } 202 | } 203 | 204 | const app = new App(); 205 | app.safe_load(); 206 | -------------------------------------------------------------------------------- /src/panel.jsx: -------------------------------------------------------------------------------- 1 | import '@ui5/webcomponents/dist/Card.js'; 2 | import '@ui5/webcomponents/dist/CardHeader.js'; 3 | import '@ui5/webcomponents/dist/Tag.js'; 4 | 5 | import { app } from 'hyperapp'; 6 | 7 | import { createElement } from './utils/dom'; 8 | 9 | export function initializePanel(plugins) { 10 | require('./panel.less'); 11 | 12 | const $panel = createElement(
); 13 | function togglePanel() { 14 | $panel.classList.toggle('visible'); 15 | } 16 | function showPanel() { 17 | $panel.classList.add('visible'); 18 | } 19 | function hidePanel() { 20 | $panel.classList.remove('visible'); 21 | } 22 | 23 | const $trigger = createElement(
); 24 | 25 | // 注册panel出现和隐藏的事件 26 | $trigger.addEventListener('mouseenter', showPanel); 27 | $panel.addEventListener('mouseleave', hidePanel); 28 | // showPanel(); 29 | 30 | document.body.appendChild($trigger); 31 | document.body.appendChild($panel); 32 | 33 | const pluginInitializers = {}; 34 | Object.entries(plugins).forEach(([slug, plugin]) => { 35 | const initializer = () => { 36 | const $card = createElement( 37 | 38 | 39 | 40 | ); 41 | const $cardContent = createElement(
); 42 | const $pluginRoot = createElement(
); 43 | $cardContent.appendChild($pluginRoot); 44 | $card.appendChild($cardContent); 45 | $panel.appendChild($card); 46 | 47 | // 检测是否存在滚动条,用于实现动画效果 48 | const observer = new ResizeObserver(() => { 49 | if ($cardContent.scrollHeight > $cardContent.clientHeight) { 50 | $cardContent.classList.add('has-overflow'); 51 | } else { 52 | $cardContent.classList.remove('has-overflow'); 53 | } 54 | }); 55 | observer.observe($pluginRoot); 56 | 57 | return $pluginRoot; 58 | }; 59 | pluginInitializers[slug] = initializer; 60 | }); 61 | 62 | const $panelHeader = createElement(
); 63 | $panel.appendChild($panelHeader); 64 | function getPluginColorScheme(plugin) { 65 | if (plugin.slug.startsWith('builtin-')) { 66 | return 8; 67 | } 68 | if (plugin.namespace === '学在浙大') { 69 | return 4; 70 | } 71 | if (plugin.namespace === '智云课堂') { 72 | return 5; 73 | } 74 | if (plugin.namespace === 'PTA') { 75 | return 6; 76 | } 77 | return 10; 78 | } 79 | const panelHeaderDispatch = app({ 80 | node: $panelHeader, 81 | init: { loadedPlugins: [] }, 82 | view: ({ loadedPlugins }) => ( 83 | 84 |
85 |
学在浙大/智云课堂小助手
86 |
当前共加载 {loadedPlugins.length} 个插件
87 |
88 | {loadedPlugins.map((plugin) => ( 89 | 94 | 95 | {plugin.slug} 96 | 97 | 98 | ))} 99 |
100 |
101 |
102 | ), 103 | }); 104 | function pushLoadedPlugin(newPlugin) { 105 | panelHeaderDispatch((state) => { 106 | state.loadedPlugins.push(newPlugin); 107 | return { ...state }; 108 | }); 109 | } 110 | 111 | return { 112 | element: $panel, 113 | show: showPanel, 114 | hide: hidePanel, 115 | toggle: togglePanel, 116 | pluginInitializers, 117 | pushLoadedPlugin, 118 | }; 119 | } 120 | -------------------------------------------------------------------------------- /src/panel.less: -------------------------------------------------------------------------------- 1 | @panel-width: 18rem; 2 | @duration: 0.25s; 3 | 4 | html { 5 | font-size: 16px; 6 | } 7 | 8 | .zju-helper { 9 | &, 10 | & * { 11 | box-sizing: border-box; 12 | } 13 | 14 | position: fixed; 15 | top: 0; 16 | left: -@panel-width; // 初始状态隐藏在左侧 17 | width: @panel-width; 18 | height: 100vh; 19 | box-shadow: 2px 0 8px rgba(0, 0, 0, 0.15); 20 | transition: left @duration, opacity @duration; 21 | opacity: 0; 22 | z-index: 9999; 23 | 24 | padding: 1.5rem 1rem; 25 | 26 | // 毛玻璃效果 27 | backdrop-filter: blur(4px); 28 | background-color: rgba(255, 255, 255, 0.375); 29 | 30 | // 滚动 31 | overflow-y: auto; 32 | overflow-x: hidden; 33 | scrollbar-gutter: stable; 34 | &::-webkit-scrollbar { 35 | width: 6px; 36 | } 37 | &::-webkit-scrollbar-thumb { 38 | background-color: rgba(0, 0, 0, 0.2); 39 | border-radius: 3px; 40 | } 41 | &::-webkit-scrollbar-track { 42 | background-color: transparent; 43 | } 44 | 45 | &.visible { 46 | left: 0; 47 | opacity: 1; 48 | } 49 | } 50 | 51 | .zju-helper-busy-indicator { 52 | opacity: 1; 53 | transition: opacity @duration ease; 54 | .zju-helper.zju-helper-loaded & { 55 | opacity: 0; 56 | } 57 | } 58 | 59 | .zju-helper-trigger { 60 | position: fixed; 61 | top: 0; 62 | left: 0; 63 | width: 20px; 64 | height: 100vh; 65 | z-index: 9998; 66 | } 67 | 68 | // 侧边栏 header 69 | .zju-helper-panel-header { 70 | padding: 1rem; 71 | 72 | .zju-helper-panel-header-title { 73 | margin-top: 0.15rem; 74 | font-weight: bold; 75 | font-size: 1.025rem; 76 | } 77 | 78 | .zju-helper-loaded-plugins-slogen { 79 | margin-top: 0.5rem; 80 | font-size: 0.8rem; 81 | color: #666; 82 | } 83 | 84 | .zju-helper-loaded-plugins { 85 | margin-top: 0.25rem; 86 | } 87 | } 88 | .zju-helper-loaded-plugin-tag { 89 | zoom: 0.875; 90 | margin-right: 0.375rem; 91 | margin-top: 0.375rem; 92 | display: inline-block; 93 | } 94 | 95 | // 插件卡片 96 | .zju-helper-plugin { 97 | margin-top: 0.75rem; 98 | 99 | .zju-helper-plugin-content { 100 | max-height: 400px; 101 | overflow-y: hidden; 102 | transition: max-height (@duration * 2) cubic-bezier(0, 1, 0, 1); 103 | 104 | &.has-overflow { 105 | mask-image: linear-gradient(to bottom, black 0%, black 80%, rgba(0, 0, 0, 0.1) 100%); 106 | } 107 | } 108 | 109 | &:hover .zju-helper-plugin-content { 110 | max-height: 9999vh; 111 | transition: max-height (@duration * 4) ease-in-out; 112 | mask-image: none; 113 | -webkit-mask-image: none; 114 | } 115 | } 116 | 117 | // theme overrides 118 | *, 119 | :before, 120 | :after { 121 | --_ui5-v2-4-0_card_header_title_font_weight: bold !important; 122 | --_ui5-v2-4-0_card_header_padding: 1rem 1rem 0.5rem 1rem !important; 123 | --_ui5-v2-4-0_tl_li_margin_bottom: 0.625rem !important; 124 | --_ui5-v2-4-0_timeline_tli_indicator_before_bottom: -0.625rem !important; 125 | } 126 | -------------------------------------------------------------------------------- /src/plugins/better-pintia/README.md: -------------------------------------------------------------------------------- 1 | ## 更好的 PTA 2 | 3 | PTA 助手,提供以下功能: 4 | 5 | - 复制题面 _beta_,可用于题目纠错等场景。 6 | -------------------------------------------------------------------------------- /src/plugins/better-pintia/index.js: -------------------------------------------------------------------------------- 1 | import { sleep } from '../../utils/global'; 2 | 3 | export const name = '更好的 PTA'; 4 | export const namespace = 'PTA'; 5 | 6 | function getUrl() { 7 | return location.href + location.hash; 8 | } 9 | 10 | export function load({ logger, clipboard }) { 11 | require('./style.less'); 12 | 13 | async function render() { 14 | function getPlain($target) { 15 | const $wrapper = document.createElement('div'); 16 | $wrapper.appendChild($target.cloneNode(true)); 17 | function convert($e) { 18 | if ($e.tagName === 'LABEL') { 19 | return '- ' + $e.innerHTML + '\n\n'; // 题目选项 20 | } 21 | if ($e.className === 'pc-text-raw') { 22 | return $e.innerHTML + ' '; // 题目选项 23 | } 24 | if ($e.className === 'katex-html' || $e.tagName === 'mrow') { 25 | return ''; // 过滤掉 latex 中的重复部分 26 | } 27 | if ($e.className === 'katex') { 28 | return '$' + $e.innerHTML + '$'; // latex 支持 29 | } 30 | if ($e.tagName === 'IMG') { 31 | return `![${$e.alt || ''}](${$e.src})`; // 图片支持 32 | } 33 | if ($e.tagName === 'PRE') { 34 | return '```\n' + $e.innerHTML + '\n```\n'; // 代码 35 | } 36 | if ($e.tagName === 'P') { 37 | return $e.innerHTML + '\n\n'; // 换行支持 38 | } 39 | return $e.innerHTML; 40 | } 41 | function flatNode($e) { 42 | while ($e.children.length > 0) { 43 | flatNode($e.children[0]); 44 | } 45 | // logger.debug('flat', $e.tagName, $e.className, convert($e), $e); 46 | $e.outerHTML = convert($e); 47 | } 48 | flatNode($wrapper.children[0]); 49 | return $wrapper.innerHTML 50 | .replace(/\n{2,}/g, '\n\n') 51 | .replace(/</g, '<') 52 | .replace(/>/g, '>') 53 | .replace(/&/g, '&') 54 | .replace(/ /g, ' ') 55 | .replace(/"/g, '"') 56 | .replace(/'/g, "'"); 57 | } 58 | 59 | function createButton($target) { 60 | const $btn = document.createElement('button'); 61 | $btn.classList.add('mem-pta-btn'); 62 | $btn.innerText = '复制文本'; 63 | $btn.onclick = () => { 64 | $btn.innerText = '已复制'; 65 | setTimeout(() => { 66 | $btn.innerText = '复制文本'; 67 | }, 500); 68 | const plain = getPlain($target); 69 | clipboard.copy(plain); 70 | logger.debug('plain text:', plain); 71 | }; 72 | return $btn; 73 | } 74 | 75 | function renderUpsolvingProblem($e) { 76 | $e.children[0].appendChild(createButton($e.children[1])); 77 | } 78 | function renderExamProblem($e) { 79 | $e.children[0].children[0].appendChild(createButton($e.children[1])); 80 | } 81 | 82 | Array.from(document.querySelectorAll('.pc-x:not(.mem-pta-rendered)')).filter(($e) => { 83 | if (!$e.id) return false; 84 | logger.debug($e.id); 85 | $e.classList.add('mem-pta-rendered'); 86 | renderExamProblem($e); 87 | return true; 88 | }); 89 | Array.from(document.querySelectorAll('.p-4:not(.mem-pta-rendered)')).filter(($e) => { 90 | if (!$e.children || !$e.children.length || $e.children[0].innerText.trim() != '题目描述') return false; 91 | logger.debug($e); 92 | $e.classList.add('mem-pta-rendered'); 93 | renderUpsolvingProblem($e); 94 | return true; 95 | }); 96 | } 97 | 98 | const max_times = 20; 99 | let times = max_times; 100 | 101 | document.addEventListener( 102 | 'click', 103 | (event) => { 104 | if (times < 5) times = 5; 105 | }, 106 | true // 将第三个参数设定为 true,确保在点击已绑定 click listener 的元素上也起作用 107 | ); 108 | 109 | (async () => { 110 | let url = getUrl(); 111 | while (true) { 112 | await sleep(100); 113 | // logger.debug('tracking... times =', times); 114 | if (getUrl() !== url) { 115 | url = getUrl(); 116 | times = max_times; 117 | } 118 | if (times > 0) { 119 | --times; 120 | await render(); 121 | } 122 | } 123 | })(); 124 | } 125 | -------------------------------------------------------------------------------- /src/plugins/better-pintia/style.less: -------------------------------------------------------------------------------- 1 | .mem-pta-btn { 2 | border: none; 3 | border-radius: 4px; 4 | } 5 | -------------------------------------------------------------------------------- /src/plugins/better-video-player/README.md: -------------------------------------------------------------------------------- 1 | ## 更好的视频播放器 2 | 3 | 为网课的视频播放器添加以下功能: 4 | 5 | - 网页全屏 6 | -------------------------------------------------------------------------------- /src/plugins/better-video-player/index.js: -------------------------------------------------------------------------------- 1 | import { sleep } from '../../utils/global.js'; 2 | 3 | export const name = '更好的视频播放器'; 4 | export const required = ['builtin-video-pages']; 5 | export const namespace = '智云课堂'; 6 | 7 | function getWrapper(document) { 8 | const $wrapper = document.querySelector('.control-bottom .control-right'); 9 | if (!$wrapper || !$wrapper.children || $wrapper.children.length === 0) { 10 | return null; 11 | } 12 | return $wrapper; 13 | } 14 | 15 | export function check({ document }) { 16 | return !!getWrapper(document); 17 | } 18 | 19 | export async function load({ logger, document, elements }) { 20 | require('./style.less'); 21 | 22 | async function toggleFullscreen() { 23 | document.body.classList.toggle('mem-bvt-fullscreen'); 24 | await sleep(100); 25 | elements.playerVue.resizePlayer(); 26 | } 27 | 28 | const $wrapper = getWrapper(document); 29 | 30 | const $button = document.createElement('div'); 31 | $button.className = 'mem-bvp-btn'; 32 | $button.innerText = '网页全屏'; 33 | $button.onclick = () => toggleFullscreen(); 34 | $wrapper.insertBefore($button, $wrapper.firstChild); 35 | } 36 | -------------------------------------------------------------------------------- /src/plugins/better-video-player/style.less: -------------------------------------------------------------------------------- 1 | .content-right .mem-bvp-btn { 2 | display: none; 3 | } 4 | 5 | .mem-bvp-btn { 6 | position: relative; 7 | margin-right: 10px; 8 | order: -1; 9 | color: white; 10 | font-size: 12px; 11 | } 12 | 13 | .mem-bvp-btn:hover { 14 | color: #248ef1; /* 鼠标悬停时的字体颜色与智云保持一致 */ 15 | } 16 | 17 | .mem-bvt-fullscreen { 18 | .app-wrap { 19 | overflow: hidden !important; 20 | } 21 | 22 | .player-wrapper { 23 | position: fixed !important; 24 | top: 0; 25 | left: 0; 26 | z-index: 114514 !important; 27 | width: 100% !important; 28 | height: 100% !important; 29 | } 30 | } -------------------------------------------------------------------------------- /src/plugins/builtin-video-pages/index.js: -------------------------------------------------------------------------------- 1 | import { isVueReady } from '../../utils/vue.js'; 2 | 3 | export const name = '[builtin]视频页面前置'; 4 | export const description = '内置插件,用于处理智云课堂的视频页面的播放器及相关内容。另外,将这一插件加入到其余模块的前置列表中,可以确保这些模块在播放器加载后再进行加载。'; 5 | 6 | export function skip({ env }) { 7 | return !env.isVideoPage; 8 | } 9 | 10 | function getElements({ document }) { 11 | const $course = document.querySelector('.living-page-wrapper'); 12 | const $player = document.querySelector('#cmcPlayer_container'); 13 | const $wrapper = document.querySelector('.living-page-wrapper .operate_wrap'); 14 | 15 | if (!isVueReady($course) || !isVueReady($player) || !$wrapper) { 16 | return null; 17 | } 18 | 19 | return { 20 | course: $course, 21 | player: $player, 22 | wrapper: $wrapper, 23 | courseVue: $course.__vue__, 24 | playerVue: $player.__vue__, 25 | }; 26 | } 27 | 28 | export function check({ document }) { 29 | // 检查能否从 document 中获取到播放器组件和对应的 Vue 实例。 30 | // 直到能够获取,才结束等待并正式加载本插件。 31 | return !!getElements({ document }); 32 | } 33 | 34 | export function load({ logger, document, extendContext }) { 35 | require('./style.less'); 36 | 37 | const elements = getElements({ document }); 38 | logger.debug('视频页面元素:', elements); 39 | extendContext({ elements }); 40 | 41 | const $wrapper = elements.wrapper; 42 | const $btn_group = document.createElement('div'); 43 | $btn_group.className = 'mem-btn-group'; 44 | $wrapper.insertBefore($btn_group, $wrapper.firstChild); 45 | logger.debug('wrapper', $wrapper); 46 | 47 | function addButton(key, text, callback) { 48 | const $btn = document.createElement('button'); 49 | $btn.className = 'mem-btn mem-btn-primary'; 50 | $btn.textContent = text; 51 | $btn.style = 'display: inline-block'; 52 | $btn.setAttribute('data-key', key); 53 | 54 | $btn.onclick = () => { 55 | callback({ 56 | element: $btn, 57 | setStatus: (status) => { 58 | logger.debug('(button)' + text, 'set status:', status); 59 | if (status) { 60 | $btn.innerText = text + '(' + status + ')'; 61 | } else { 62 | $btn.innerText = text; 63 | } 64 | }, 65 | }); 66 | }; 67 | 68 | for (const $current of $btn_group.children) { 69 | // 保持 data-key 有序 70 | if (Number($current.getAttribute('data-key')) > key) { 71 | $btn_group.insertBefore($btn, $current); 72 | return $btn; 73 | } 74 | } 75 | 76 | $btn_group.appendChild($btn); 77 | return $btn; 78 | } 79 | 80 | extendContext({ addButton }); 81 | } 82 | -------------------------------------------------------------------------------- /src/plugins/builtin-video-pages/style.less: -------------------------------------------------------------------------------- 1 | .mem-btn { 2 | border: none; 3 | 4 | /* 以下按钮代码从 .operate_wrap .QRcode_span 的样式中拷贝而来 */ 5 | 6 | display: flex; 7 | margin-left: 16px; 8 | cursor: pointer; 9 | height: 32px; 10 | line-height: 32px; 11 | -webkit-box-align: center; 12 | -ms-flex-align: center; 13 | align-items: center; 14 | -webkit-box-pack: center; 15 | -ms-flex-pack: center; 16 | justify-content: center; 17 | 18 | border-radius: 4px; 19 | background-color: #f0f1f3; 20 | font-size: 14px; 21 | color: #144aea; 22 | text-align: center; 23 | position: relative; 24 | 25 | padding: 0 6px; 26 | 27 | @media screen and (max-width: 1679px) { 28 | margin-left: 11.42856px; 29 | height: 22.85712px; 30 | line-height: 22.85712px; 31 | font-size: 9.99999px; 32 | } 33 | 34 | @media screen and (min-width: 1680px) and (max-width: 1919px) { 35 | margin-left: 14px; 36 | height: 28px; 37 | line-height: 28px; 38 | font-size: 12.25px; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/plugins/copy-with-timestamp/README.md: -------------------------------------------------------------------------------- 1 | ## 带时间戳的地址复制(精准空降) 2 | 3 | 复制带时间戳的视频地址,这样再次打开时就会自动跳转到对应位置。 4 | -------------------------------------------------------------------------------- /src/plugins/copy-with-timestamp/index.js: -------------------------------------------------------------------------------- 1 | import { copyToClipboard, showMessage, loadUrlQuery, dumpUrlQuery } from '../../utils/browser.js'; 2 | 3 | export const name = '带时间戳的地址复制(精准空降)'; 4 | export const required = ['builtin-video-pages']; 5 | export const namespace = '智云课堂'; 6 | 7 | function getWrapper(document) { 8 | const $wrapper = document.querySelector('.control-bottom .control-right'); 9 | if (!$wrapper || !$wrapper.children || $wrapper.children.length === 0) { 10 | return null; 11 | } 12 | return $wrapper; 13 | } 14 | 15 | export function check({ document }) { 16 | return !!getWrapper(document); 17 | } 18 | 19 | export async function load({ logger, document, elements }) { 20 | async function copyLinkWithTimestamp() { 21 | const url = document.location.origin + document.location.pathname; 22 | const query = loadUrlQuery(document.location.search) || {}; 23 | query.ts = Math.floor(elements.playerVue.getPlayTime()); 24 | 25 | const finalUrl = url + dumpUrlQuery(query); 26 | copyToClipboard(finalUrl); 27 | showMessage('复制成功!'); 28 | } 29 | 30 | const query = loadUrlQuery(document.location.search) || {}; 31 | if (query.ts) { 32 | try { 33 | logger.info('需定位到对应时间戳'); 34 | logger.log('player', elements.playerVue); 35 | logger.log('playTime', elements.playerVue.getPlayTime()); 36 | elements.playerVue.setPlayerPlayTime(query.ts); 37 | logger.log('playTime', elements.playerVue.getPlayTime()); 38 | } catch (e) { 39 | logger.error('定位失败', e); 40 | } 41 | } 42 | 43 | const $wrapper = getWrapper(document); 44 | 45 | const $button = document.createElement('div'); 46 | $button.className = 'mem-bvp-btn'; 47 | $button.innerText = '复制地址(精准空降)'; 48 | $button.onclick = () => copyLinkWithTimestamp(); 49 | $wrapper.insertBefore($button, $wrapper.firstChild); 50 | } 51 | -------------------------------------------------------------------------------- /src/plugins/example-plugin/index.js: -------------------------------------------------------------------------------- 1 | export const name = '示例插件'; 2 | export const description = '这是一个示例插件,他不应该被加载到脚本中。'; 3 | 4 | export const required = []; // 前置要求:在列出的插件都被加载后才会加载,如果某个前置插件被跳过那么本插件也会跳过 5 | 6 | export function skip() { 7 | // 是否需要跳过加载本插件:如果返回 false 或者本函数不存在则不跳过。 8 | return false; 9 | } 10 | 11 | export function check() { 12 | // 是否可以加载本插件:如果返回 true 或者本函数不存在则可以加载,否则等待下一轮直到返回 true 为止。 13 | // 注意:如果判定该插件不应该被加载,请在 skip() 方法中进行处理,而不是在 check() 方法中一直返回 false。 14 | return true; 15 | } 16 | 17 | export function load({ logger }) { 18 | // logger 是从上下文中继承来的,可以直接使用 19 | logger.debug('示例插件已被加载。'); // 使用这种方式输出的调试信息会附带插件名称等额外信息。 20 | } 21 | -------------------------------------------------------------------------------- /src/plugins/focus-mode/README.md: -------------------------------------------------------------------------------- 1 | ## 专注模式 2 | 3 | 屏蔽掉无用的网页组件,使你可以专注于课堂本身。开启后智云课堂将不会显示推荐的课程、收藏点赞等无用功能。 4 | 5 | 如果需要使用被屏蔽的组件,到设置中关闭本功能即可。 6 | -------------------------------------------------------------------------------- /src/plugins/focus-mode/index.js: -------------------------------------------------------------------------------- 1 | export const name = '专注模式'; 2 | 3 | export function load({ logger, namespace }) { 4 | if (namespace === '学在浙大') { 5 | require('./xzzd.less'); 6 | } else if (namespace === '智云课堂') { 7 | require('./zykt.less'); 8 | } else { 9 | logger.debug('没有可以加载的样式.'); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/plugins/focus-mode/xzzd.less: -------------------------------------------------------------------------------- 1 | .footer.gtm-category.ng-scope, // 页脚 2 | __nothing__ { 3 | display: none !important; 4 | } 5 | -------------------------------------------------------------------------------- /src/plugins/focus-mode/zykt.less: -------------------------------------------------------------------------------- 1 | .custom-footer, // 页脚 2 | .feedback-wrapper, // 问题反馈 3 | .menu-content > .first-menu:nth-child(4), // 顶栏:学路(注意:在智云课堂更新时元素位置可能被改变) 4 | .menu-content > .first-menu:nth-child(5), // 顶栏:知识图谱(注意:在智云课堂更新时元素位置可能被改变) 5 | .hot-recommend-wrapper, // 视频页:猜你喜欢 6 | __nothing__ { 7 | display: none !important; 8 | } 9 | 10 | __nothing__ { 11 | opacity: 0 !important; 12 | pointer-events: none; // 避免鼠标不小心点到上面的链接 13 | cursor: default; 14 | } 15 | 16 | // ===== 避免删除页脚后页面内容直接贴底 ===== 17 | .living-page-wrapper { 18 | padding-bottom: 20.040129px; // Magic Number 19 | } 20 | 21 | // ===== 顶栏删除不好看的白线 ===== 22 | .course-filter-searchAll-custom { 23 | border-left: none !important; 24 | border-right: none !important; 25 | } 26 | 27 | // ===== 视频页:屏蔽收藏点赞按钮 ===== 28 | .operate_wrap > .collect_span, // 视频页:收藏 29 | .operate_wrap > .good_span, // 视频页:点赞 30 | .collect > .collect_span, // 视频页:收藏 31 | .collect > .good_span // 视频页:点赞 32 | { 33 | // 把这两条单独提出来是想记录一下为什么要屏蔽这两个按钮:因为看了一下智云课堂上收藏最多的课程也就一百多次,相信绝大部分人都用不到这个功能。且一门课程只能点赞一次,实在想点赞禁用本脚本后点赞也可。 */ 34 | display: none !important; 35 | } 36 | 37 | // ===== 视频页:侧边的三个按钮删除渐变背景 ===== 38 | .side_tab_wrap .side_tab { 39 | background: #f9f9f9 !important; 40 | // border: solid 1px black !important; 41 | transform: none !important; 42 | color: #a0a0a0 !important; 43 | } 44 | .side_tab_wrap .side_tab span { 45 | transform: none !important; 46 | } 47 | .side_tab_wrap .side_tab.active span { 48 | color: #144aea !important; 49 | font-weight: bold !important; 50 | } 51 | 52 | // ===== 视频页:相关信息部分屏蔽右栏 ===== 53 | .relative-info-gap, 54 | .relative-info-right { 55 | display: none; 56 | } 57 | 58 | .relative-info-left { 59 | width: 100% !important; 60 | } 61 | -------------------------------------------------------------------------------- /src/plugins/picture-in-picture/README.md: -------------------------------------------------------------------------------- 1 | ## 画中画模式 2 | 3 | > 感谢 [@Trzeth](https://github.com/Trzeth) 贡献。 4 | 5 | 允许智云课堂的视频或 PPT 兼容浏览器画中画(PIP)功能。通过点击 按钮进入。 6 | -------------------------------------------------------------------------------- /src/plugins/picture-in-picture/index.js: -------------------------------------------------------------------------------- 1 | import { sleep } from '../../utils/global.js'; 2 | 3 | export const name = '播放器画中画'; 4 | export const required = ['builtin-video-pages']; 5 | export const namespace = '智云课堂'; 6 | 7 | function getVideoWrapper(document) { 8 | const $wrapper = document.querySelector('.control-bottom .control-right'); 9 | if (!$wrapper) { 10 | return null; 11 | } 12 | if (!$wrapper.children || $wrapper.children.length === 0) { 13 | return null; 14 | } 15 | return $wrapper; 16 | } 17 | 18 | function getPPTWrapper(document) { 19 | const $wrapper = document.querySelector('.opr_lay .ppt_opr_lay'); 20 | if (!$wrapper) { 21 | return null; 22 | } 23 | if (!$wrapper.children || $wrapper.children.length === 0) { 24 | return null; 25 | } 26 | return $wrapper; 27 | } 28 | 29 | function getHook(document) { 30 | const $wrapper = document.querySelector('.change-item'); 31 | if (!$wrapper) { 32 | return null; 33 | } 34 | if (!$wrapper.children || $wrapper.children.length === 0) { 35 | return null; 36 | } 37 | return $wrapper; 38 | } 39 | 40 | function createButton() { 41 | const $button = document.createElement('div'); 42 | $button.className = 'pip-btn'; 43 | $button.innerHTML = ''; 44 | return $button; 45 | } 46 | 47 | async function openPIP() { 48 | if (documentPictureInPicture.window) { 49 | documentPictureInPicture.window.close(); 50 | return; 51 | } 52 | 53 | // Open a Picture-in-Picture window. 54 | const pipWindow = await documentPictureInPicture.requestWindow({ 55 | width: 640, 56 | height: 360, 57 | }); 58 | 59 | // Copy all style sheets. 60 | [...document.styleSheets].forEach((styleSheet) => { 61 | try { 62 | const cssRules = [...styleSheet.cssRules].map((rule) => rule.cssText).join(''); 63 | const style = document.createElement('style'); 64 | 65 | style.textContent = cssRules; 66 | pipWindow.document.head.appendChild(style); 67 | } catch (e) { 68 | const link = document.createElement('link'); 69 | 70 | link.rel = 'stylesheet'; 71 | link.type = styleSheet.type; 72 | link.media = styleSheet.media; 73 | link.href = styleSheet.href; 74 | pipWindow.document.head.appendChild(link); 75 | } 76 | }); 77 | 78 | return pipWindow; 79 | } 80 | 81 | export function check({ document }) { 82 | let PIP = false; 83 | 84 | if ('documentPictureInPicture' in window) { 85 | // The Document Picture-in-Picture API is supported. 86 | PIP = true; 87 | } else { 88 | logger.debug('PIP api not supported'); 89 | } 90 | 91 | return PIP && !!getVideoWrapper(document) && !!getPPTWrapper(document) && !!getHook(document); 92 | } 93 | 94 | export async function load({ logger, document, elements, addButton }) { 95 | require('./style.less'); 96 | 97 | const $videoWrapper = getVideoWrapper(document); 98 | const $videoBtn = createButton(); 99 | $videoBtn.onclick = () => { 100 | // Native handle 101 | document.querySelector('#cmc_player_video').requestPictureInPicture(); 102 | }; 103 | $videoWrapper.insertBefore($videoBtn, $videoWrapper.lastChild); 104 | 105 | let flag = false; 106 | let pip = null; 107 | 108 | // PPT View Handle 109 | const $hook = getHook(document); 110 | $hook.onclick = async () => { 111 | if (flag) { 112 | flag = false; 113 | return; 114 | } 115 | flag = true; 116 | 117 | await sleep(100); 118 | 119 | const $pptWrapper = getPPTWrapper(document); 120 | const $pptBtn = createButton(); 121 | 122 | $pptBtn.onclick = async () => { 123 | pip = openPIP(); 124 | 125 | // Hook vue for document query 126 | const pptVue = document.querySelector('.main_resize_con .ppt_container').__vue__; 127 | const pptCanvas = document.querySelector('#ppt_canvas'); 128 | pptVue.drawImg = function (t) { 129 | var e = pptVue, 130 | i = pptCanvas, 131 | n = new Image(); 132 | (n.crossOrigin = 'anonymous'), 133 | (n.onload = () => 134 | (function (elem) { 135 | var t = n.width, 136 | s = n.height, 137 | a = elem.offsetWidth, 138 | r = elem.offsetHeight, 139 | o = i.getContext('2d'), 140 | l = t / s, 141 | c = a / r, 142 | u = 0, 143 | d = 0; 144 | l > c ? (d = (r - (s = (t = a) / l)) / 2) : (u = (a - (t = (s = r) * l)) / 2), console.log('imgW=', t, 'imgH=', s, 'imgRatio=', l, 'csvRatio=', c, 'drawPosY=', d, 'drawPosX=', u), i.setAttribute('width', a), i.setAttribute('height', r), o.drawImage(n, u, d, t, s); 145 | var p = o.getImageData(0, 0, a, r); 146 | e.middleAry = [p]; 147 | })(i)), 148 | (n.src = t); 149 | }; 150 | 151 | // Drag is broken, just clean evt to clean error 152 | const dragWrapperVue = document.querySelector('.el-slider__button-wrapper').__vue__; 153 | const dragVue = document.querySelector('.el-slider__button').__vue__; 154 | let dragCache = {}; 155 | dragCache.onDragStart = dragWrapperVue.onDragStart; 156 | dragWrapperVue.onDragStart = function () {}; 157 | dragCache.onDragging = dragWrapperVue.onDragging; 158 | dragWrapperVue.onDragging = function () {}; 159 | dragCache.onDragEnd = dragWrapperVue.onDragEnd; 160 | dragWrapperVue.onDragEnd = function () {}; 161 | dragCache.updatePopper = dragVue.updatePopper; 162 | dragVue.updatePopper = function () {}; 163 | 164 | pip = await pip; 165 | getHook(document).style.display = 'none'; 166 | 167 | const ppt = document.querySelector('.main_resize_con').firstElementChild; 168 | pip.document.body.className = 'pip-window'; 169 | pip.document.body.append(ppt); 170 | 171 | // Redraw to resize 172 | pptVue.drawImg(pptVue.pptImgSrc); 173 | 174 | // Listen for the PiP closing event to move the video back. 175 | pip.addEventListener('pagehide', (event) => { 176 | const container = document.querySelector('.main_resize_con'); 177 | const elem = event.target.body.lastChild; 178 | 179 | // This is very strange, if you directly pip.close(), evt will fire, but elem has gone. maybe vue unmounted? 180 | if (elem) { 181 | container.append(elem); 182 | 183 | // Retrieve 184 | dragWrapperVue.onDragStart = dragCache.onDragStart; 185 | dragWrapperVue.onDragging = dragCache.onDragging; 186 | dragWrapperVue.onDragEnd = dragCache.onDragEnd; 187 | dragVue.updatePopper = dragCache.updatePopper; 188 | 189 | pptVue.drawImg = function (t) { 190 | var e = this, 191 | i = document.getElementById('ppt_canvas'), 192 | n = new Image(); 193 | (n.crossOrigin = 'anonymous'), 194 | (n.onload = function () { 195 | var t = n.width, 196 | s = n.height, 197 | a = document.getElementById('ppt').offsetWidth, 198 | r = document.getElementById('ppt').offsetHeight, 199 | o = i.getContext('2d'), 200 | l = t / s, 201 | c = a / r, 202 | u = 0, 203 | d = 0; 204 | l > c ? (d = (r - (s = (t = a) / l)) / 2) : (u = (a - (t = (s = r) * l)) / 2), console.log('imgW=', t, 'imgH=', s, 'imgRatio=', l, 'csvRatio=', c, 'drawPosY=', d, 'drawPosX=', u), i.setAttribute('width', a), i.setAttribute('height', r), o.drawImage(n, u, d, t, s); 205 | var p = o.getImageData(0, 0, a, r); 206 | e.middleAry = [p]; 207 | }), 208 | (n.src = t); 209 | }; 210 | 211 | // Redraw to resize 212 | pptVue.drawImg(pptVue.pptImgSrc); 213 | 214 | getHook(document).style.display = 'block'; 215 | } else { 216 | // create elem manually not working, maybe a racing condition? 217 | // just hidden the btn to solve it. 218 | // let e = container.createElement("div"); 219 | // e.className = "ppt_container" 220 | // e = e.createElement("div"); 221 | // e.className = "ppt_img_con"; 222 | // e = e.createElement("canvas"); 223 | // e.className = "ppt_canvas"; 224 | // e.id = "ppt_canvas"; 225 | } 226 | }); 227 | }; 228 | 229 | $pptWrapper.insertBefore($pptBtn, null); 230 | }; 231 | } 232 | -------------------------------------------------------------------------------- /src/plugins/picture-in-picture/style.less: -------------------------------------------------------------------------------- 1 | .ppt_opr_lay > .pip-btn { 2 | display: inline-block; 3 | vertical-align: middle; 4 | margin-left: 10px; 5 | margin-right: -20px; 6 | } 7 | 8 | .pip-btn { 9 | position: relative; 10 | margin-right: 10px; 11 | order: 2; 12 | opacity: 0.75; 13 | 14 | > .svg-icon { 15 | fill: white; 16 | width: 24px; 17 | height: 24px; 18 | // transition: fill 300ms; 19 | } 20 | 21 | &:hover > .svg-icon { 22 | fill: rgb(36, 142, 241); 23 | } 24 | } 25 | 26 | .pip-window { 27 | .pip-btn { 28 | display: none; 29 | } 30 | 31 | > .ppt_container { 32 | > .ppt_opr_con { 33 | display: none; 34 | position: absolute; 35 | bottom: 0; 36 | background: black; 37 | overflow-x: clip; 38 | } 39 | 40 | &:hover > .ppt_opr_con { 41 | display: block; 42 | } 43 | } 44 | 45 | .opr_lay { 46 | justify-content: start !important; 47 | } 48 | 49 | .ppt_page_btn { 50 | margin-left: 30px; 51 | } 52 | 53 | .el-slider__button-wrapper { 54 | cursor: pointer !important; 55 | 56 | > .el-slider__button { 57 | cursor: pointer !important; 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/plugins/ppt-downloader/README.md: -------------------------------------------------------------------------------- 1 | ## 课件下载 2 | 3 | 下载智云课堂自动生成的课件,支持配置最小间隔时间,还支持多种下载方式: 4 | 5 | - **导出为 PDF**:将所有课件导出为 PDF,会调用浏览器自带的打印对话框,也可以直接通过打印机打印。[点我下载示例文件](https://pan.memset0.cn/Share/2024/03/03/%E4%BD%BF%E7%94%A8%E8%84%9A%E6%9C%AC%E5%AF%BC%E5%87%BA%E7%9A%84%E8%AF%BE%E4%BB%B6%EF%BC%88%E9%AB%98%E7%BA%A7%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95%E5%88%86%E6%9E%902024-02-26%E7%AC%AC3-5%E8%8A%82%EF%BC%89.pdf)。 6 | (由于浏览器性能限制,当图片数量过多时导出速度较慢。如果你有更好的解决方案,请联系我。) 7 | 8 | - **打包下载**:将所有课件添加到压缩包中,示例如下: 9 | ![](https://static.memset0.cn/img/v6/2024/03/03/uEUzlIZR.png) 10 | -------------------------------------------------------------------------------- /src/plugins/ppt-downloader/index.js: -------------------------------------------------------------------------------- 1 | import { saveAs } from 'file-saver'; 2 | import { limitConcurrency } from '../../utils/global'; 3 | import { printToPdf } from '../../utils/browser.js'; 4 | 5 | export const name = '课件下载'; 6 | export const required = ['builtin-video-pages']; 7 | export const namespace = '智云课堂'; 8 | 9 | export const options = { 10 | 'auto-remove': true, 11 | }; 12 | 13 | function getPPTList(elements) { 14 | return Array.from(elements.courseVue.$data.pptList); 15 | } 16 | 17 | function autoRemoveFilter(pptList) { 18 | const result = []; 19 | for (let i = 0; i < pptList.length; i++) { 20 | if (i + 1 < pptList.length && pptList[i + 1].switchTime === pptList[i].switchTime) { 21 | // 删除来自同一秒钟的PPT 22 | continue; 23 | } 24 | result.push(pptList[i]); 25 | } 26 | return result; 27 | } 28 | 29 | export function check({ elements }) { 30 | return getPPTList(elements).length > 0; 31 | } 32 | 33 | export function load({ logger, elements, addButton, loadScript }, options) { 34 | let pptList = getPPTList(elements) 35 | .map((item) => { 36 | // Convert vue object to normal object 37 | return { 38 | ...item, 39 | ppt: { ...item.ppt }, 40 | }; 41 | }) 42 | .map((ppt) => { 43 | ppt.imgSrc = ppt.imgSrc.replace('http://', 'https://'); 44 | ppt.s_imgSrc = ppt.s_imgSrc.replace('http://', 'https://'); 45 | return ppt; 46 | }); 47 | logger.debug(`PPT下载(共${pptList.length}个):`, pptList[0]); 48 | 49 | if (true) { 50 | // if (options.get("auto-remove")) { 51 | pptList = autoRemoveFilter(pptList); 52 | logger.debug(`删除同一秒内的PPT后(共${pptList.length}个):`, pptList[0]); 53 | } 54 | 55 | addButton(1.1, '打包下载', async ({ setStatus }) => { 56 | setStatus('加载JSZip库'); 57 | loadScript('jszip.min.js'); 58 | const zip = new JSZip(); 59 | 60 | let counter = 0; 61 | let total = pptList.length; 62 | await limitConcurrency( 63 | pptList.map(async (ppt, index) => { 64 | const filename = `ppt-${String(index).padStart(4, '0')}-${ppt.switchTime.replace(/\:/g, '-')}.jpg`; 65 | const res = await fetch(ppt.imgSrc, { method: 'GET' }); 66 | const blob = await res.blob(); 67 | logger.debug('添加图片', filename, blob); 68 | setStatus(`正在下载(${++counter}/${total})`); 69 | zip.file(filename, blob, { binary: true }); 70 | }), 71 | 8 72 | ); 73 | 74 | setStatus('生成Zip'); 75 | logger.debug(zip); 76 | const content = await zip.generateAsync({ type: 'blob' }); 77 | logger.debug('完成生成zip', content); 78 | 79 | setStatus('完成'); 80 | saveAs(content, 'ppt.zip'); 81 | setStatus(null); 82 | }); 83 | 84 | addButton(1.2, '导出为PDF', async ({ setStatus }) => { 85 | let html = ''; 86 | let counter = 0; 87 | let total = pptList.length; 88 | const imageList = await limitConcurrency( 89 | pptList.map(async (ppt, index) => { 90 | const res = await fetch(ppt.imgSrc, { method: 'GET' }); 91 | setStatus(`正在下载(${++counter}/${total})`); 92 | const blob = await res.blob(); 93 | const blobUrl = URL.createObjectURL(blob); 94 | logger.log(index, blobUrl); 95 | return blobUrl; 96 | }), 97 | 8 98 | ); 99 | 100 | setStatus('生成PDF中'); 101 | for (const image of imageList) { 102 | html += `
`; 103 | } 104 | 105 | await printToPdf( 106 | { 107 | width: 1280, 108 | height: 720, 109 | margin: 0, 110 | }, 111 | html 112 | ); 113 | 114 | setStatus(null); 115 | }); 116 | } 117 | -------------------------------------------------------------------------------- /src/plugins/replay-parser/README.md: -------------------------------------------------------------------------------- 1 | ## 智云回放链接解析 2 | 3 | 添加视频解析按钮,点击后自动复制视频连接到剪贴板,可以直接下载。直播也能使用,但需要在流媒体播放器中打开。 4 | -------------------------------------------------------------------------------- /src/plugins/replay-parser/index.js: -------------------------------------------------------------------------------- 1 | export const name = '智云回放链接解析'; 2 | export const required = ['builtin-video-pages']; 3 | export const namespace = '智云课堂'; 4 | 5 | export function load({ logger, clipboard, elements, addButton }) { 6 | // context.elements 是在 builtin-video-pages 插件中注入的 7 | 8 | function getUrl() { 9 | try { 10 | if (elements.playerVue.liveType === 'live') { 11 | return JSON.parse(elements.playerVue.liveUrl.replace('mutli-rate: ', ''))[0].url; 12 | } else { 13 | return document.querySelector('#cmc_player_video').src; 14 | } 15 | } catch (err) { 16 | // logger.error(err); 17 | return null; 18 | } 19 | } 20 | 21 | addButton(2, '解析链接', ({ setStatus }) => { 22 | const url = getUrl(); 23 | if (!url) { 24 | alert('获取视频地址失败,请待播放器完全加载后再试。'); 25 | return; 26 | } 27 | logger.info('视频链接:', url); 28 | clipboard.copy(url); 29 | setStatus('已拷贝'); 30 | setTimeout(() => { 31 | setStatus(null); 32 | }, 500); 33 | }); 34 | } 35 | -------------------------------------------------------------------------------- /src/plugins/score-finder/README.md: -------------------------------------------------------------------------------- 1 | ## 成绩嗅探 2 | 3 | 通过 API 查询学在浙大中已被登记但尚未公开的成绩。 4 | -------------------------------------------------------------------------------- /src/plugins/score-finder/index.js: -------------------------------------------------------------------------------- 1 | import '@ui5/webcomponents-fiori/dist/Timeline.js'; 2 | import '@ui5/webcomponents-fiori/dist/TimelineItem.js'; 3 | import '@ui5/webcomponents-icons/dist/away.js'; 4 | import '@ui5/webcomponents-icons/dist/accelerated.js'; 5 | import '@ui5/webcomponents-icons/dist/document-text.js'; 6 | 7 | import { app } from 'hyperapp'; 8 | 9 | export const name = '成绩嗅探'; 10 | export const description = '通过 API 显示学在浙大中已被登记但尚未公开的成绩。'; 11 | 12 | export const namespace = '学在浙大'; 13 | export const route = '/course//'; 14 | 15 | export async function load({ params, logger, panelInitialize }) { 16 | require('./style.less'); 17 | 18 | const courseId = params.course_id; 19 | logger.debug('当前课程:', courseId); 20 | 21 | const activityReadsPromise = fetch(`https://courses.zju.edu.cn/api/course/${courseId}/activity-reads-for-user`); 22 | const homeworkActivitiesPromise = fetch(`https://courses.zju.edu.cn/api/course/${courseId}/homework-scores?fields=id,title`); 23 | const examsActivitiesPromise = fetch(`https://courses.zju.edu.cn/api/courses/${courseId}/exams`); 24 | 25 | const [activityReadsResponse, homeworkActivitiesResponse, examsActivitiesResponse] = await Promise.all((await Promise.all([activityReadsPromise, homeworkActivitiesPromise, examsActivitiesPromise])).map((response) => response.json())); 26 | if (!activityReadsResponse || !activityReadsResponse.activity_reads) { 27 | logger.warn('活动阅读数据获取失败!'); 28 | } 29 | if (!homeworkActivitiesResponse || !homeworkActivitiesResponse.homework_activities) { 30 | logger.warn('作业数据获取失败!'); 31 | } 32 | if (!examsActivitiesResponse || !examsActivitiesResponse.exams) { 33 | logger.warn('考试数据获取失败!'); 34 | } 35 | const activityReads = activityReadsResponse.activity_reads; 36 | const homeworkActivities = homeworkActivitiesResponse.homework_activities; 37 | const examsActivities = examsActivitiesResponse.exams; 38 | 39 | logger.debug('活动阅读数据:', activityReads); 40 | logger.debug('作业数据:', homeworkActivities); 41 | logger.debug('考试数据:', examsActivities); 42 | 43 | const titleMap = new Map(); 44 | homeworkActivities.forEach((homework) => titleMap.set(homework.id, homework.title)); 45 | examsActivities.forEach((exam) => titleMap.set(exam.id, exam.title)); 46 | activityReads.forEach((activityRead) => { 47 | if (titleMap.has(activityRead.activity_id)) { 48 | activityRead.title = titleMap.get(activityRead.activity_id); 49 | } else { 50 | activityRead.title = '未知活动'; 51 | } 52 | }); 53 | logger.info('合并后的活动数据:', activityReads); 54 | 55 | app({ 56 | node: panelInitialize(), 57 | view: () => { 58 | if (activityReads.length === 0) { 59 | return
没有检测到作业或考试。
; 60 | } 61 | 62 | const items = []; 63 | let meaninglessCounter = 0; 64 | 65 | for (const activity of activityReads) { 66 | let icon = null; 67 | let link = null; 68 | let content = ''; 69 | let addToItems = true; 70 | 71 | if (activity.activity_type === 'learning_activity') { 72 | icon = 'document-text'; 73 | link = `https://courses.zju.edu.cn/course/${courseId}/learning-activity#/${activity.activity_id}`; 74 | if (activity.title === '未知活动') { 75 | icon = 'accelerated'; 76 | if (Object.keys(activity.data).length === 0) { 77 | addToItems = false; 78 | meaninglessCounter += 1; 79 | } else { 80 | content = JSON.stringify(activity.data); 81 | } 82 | } else { 83 | if (activity.data.score === undefined) { 84 | content = '未评分'; 85 | } else { 86 | content = `得分:${activity.data.score}`; 87 | } 88 | } 89 | } else if (activity.activity_type === 'exam_activity') { 90 | link = `https://courses.zju.edu.cn/course/${courseId}/learning-activity#/exam/${activity.activity_id}`; 91 | icon = 'away'; 92 | content = `得分:${activity.data.score}`; 93 | } else { 94 | icon = 'accelerated'; 95 | content = '缺少数据'; 96 | } 97 | 98 | logger.debug('活动:', activity); 99 | 100 | if (addToItems) { 101 | items.push( 102 | 107 |
{content}
108 |
109 | ); 110 | } 111 | } 112 | logger.debug('活动组件:', items); 113 | 114 | return ( 115 |
116 | {items} 117 | {meaninglessCounter > 0 ?
还有 {meaninglessCounter} 条缺少数据的活动。
: null} 118 |
119 | ); 120 | }, 121 | }); 122 | } 123 | -------------------------------------------------------------------------------- /src/plugins/score-finder/style.less: -------------------------------------------------------------------------------- 1 | .score-finder-root { 2 | margin-top: -0.75rem; 3 | } 4 | .score-finder-item-content { 5 | word-break: break-all; 6 | margin-top: -0.375rem; 7 | font-size: 0.675rem; 8 | } 9 | .score-finder-meaningless-counter { 10 | padding: 0 1rem 1rem 1rem; 11 | font-size: 0.75rem; 12 | } 13 | .score-finder-no-activity { 14 | padding: 1rem; 15 | } 16 | -------------------------------------------------------------------------------- /src/utils/browser.js: -------------------------------------------------------------------------------- 1 | import logger from './logger'; 2 | 3 | export function copyToClipboard(text) { 4 | const input = document.createElement('textarea'); 5 | input.style.position = 'fixed'; 6 | input.style.opacity = 0; 7 | input.value = text; 8 | document.body.appendChild(input); 9 | input.select(); 10 | document.execCommand('copy'); 11 | document.body.removeChild(input); 12 | } 13 | 14 | export async function printToPdf(options, html) { 15 | const { width, height, margin } = options; 16 | html = '' + html; 17 | html = '' + html; 18 | html = '' + html; 19 | 20 | const { style } = options; 21 | if (style) { 22 | html += '\n\n\n\n\n\n'; 23 | } 24 | 25 | // const document = unsafeWindow.document; // seemingly needless 26 | const blob = new Blob([html], { type: 'text/html;charset=utf-8' }); 27 | const blobUrl = URL.createObjectURL(blob); 28 | logger.debug('blobUrl:', blobUrl); 29 | 30 | const $iframe = document.createElement('iframe'); 31 | $iframe.style.display = 'none'; 32 | $iframe.src = blobUrl; 33 | document.body.appendChild($iframe); 34 | $iframe.onload = () => { 35 | setTimeout(() => { 36 | $iframe.focus(); 37 | $iframe.contentWindow.print(); 38 | }, 1); 39 | }; 40 | } 41 | 42 | export function loadUrlQuery(search) { 43 | const query = {}; 44 | search 45 | .slice(1) 46 | .split('&') 47 | .forEach((item) => { 48 | const [key, value] = item.split('='); 49 | query[key] = value; 50 | }); 51 | return query; 52 | } 53 | 54 | export function dumpUrlQuery(query) { 55 | return ( 56 | '?' + 57 | Object.entries(query) 58 | .map(([key, value]) => `${key}=${value}`) 59 | .join('&') 60 | ); 61 | } 62 | 63 | export function showMessage(message) { 64 | alert(message); 65 | } 66 | -------------------------------------------------------------------------------- /src/utils/checker.js: -------------------------------------------------------------------------------- 1 | export function isVideoPage() { 2 | if (location.host === 'classroom.zju.edu.cn' && location.pathname === '/livingroom') { 3 | return true; 4 | } 5 | if (location.host === 'interactivemeta.cmc.zju.edu.cn' && location.pathname === '/' && location.hash.startsWith('#/replay?')) { 6 | return true; 7 | } 8 | return false; 9 | } 10 | -------------------------------------------------------------------------------- /src/utils/dom.js: -------------------------------------------------------------------------------- 1 | export function createHyperappElement(vnode) { 2 | const element = document.createElement(vnode.tag); 3 | 4 | for (const [key, value] of Object.entries(vnode.props || {})) { 5 | element.setAttribute(key, value); 6 | } 7 | 8 | vnode.children.forEach((child) => { 9 | if (typeof child === 'string') { 10 | element.appendChild(document.createTextNode(child)); 11 | } else { 12 | element.appendChild(createElement(child)); 13 | } 14 | }); 15 | 16 | return element; 17 | } 18 | 19 | export const createElement = createHyperappElement; -------------------------------------------------------------------------------- /src/utils/global.js: -------------------------------------------------------------------------------- 1 | export async function sleep(ms) { 2 | return new Promise((resolve) => setTimeout(resolve, ms)); 3 | } 4 | 5 | export function limitConcurrency(tasks, limit) { 6 | return new Promise((resolve, reject) => { 7 | let active = 0; 8 | let finished = 0; 9 | let started = 0; 10 | let results = []; 11 | 12 | function startNext() { 13 | console.log('!! ', finished, tasks.length); 14 | if (finished >= tasks.length) { 15 | resolve(results); 16 | return; 17 | } 18 | 19 | while (started < tasks.length && active < limit) { 20 | let currentPos = started; 21 | let p = tasks[started]; 22 | 23 | p.then((result) => { 24 | active--; 25 | finished++; 26 | results[currentPos] = result; 27 | startNext(); 28 | }).catch((err) => { 29 | reject(err); 30 | }); 31 | 32 | active++; 33 | started++; 34 | } 35 | } 36 | 37 | startNext(); 38 | }); 39 | } 40 | 41 | export function matchRoute(route, pathname) { 42 | const paramTypes = []; 43 | const paramNames = []; 44 | let paramMatch; 45 | const paramRegex = /<([^>:]+)(?::([^>]+))?>/g; 46 | while ((paramMatch = paramRegex.exec(route)) !== null) { 47 | const name = paramMatch[1]; 48 | const type = paramMatch[2] || 'string'; 49 | paramTypes.push(type); 50 | paramNames.push(name); 51 | } 52 | 53 | const pattern = route.replace(/<([^>:]+)(?::([^>]+))?>/g, '([^/]+)'); 54 | const regex = new RegExp(`^${pattern}$`); 55 | 56 | const match = pathname.match(regex); 57 | if (!match) { 58 | return false; 59 | } 60 | 61 | const result = {}; 62 | for (let i = 0; i < paramNames.length; i++) { 63 | const value = match[i + 1]; 64 | const type = paramTypes[i]; 65 | 66 | if (type === 'int') { 67 | if (!/^\d+$/.test(value)) { 68 | return false; 69 | } 70 | result[paramNames[i]] = parseInt(value); 71 | } else if (type === 'string') { 72 | result[paramNames[i]] = value; 73 | } else { 74 | return false; 75 | } 76 | } 77 | 78 | return result; 79 | } 80 | -------------------------------------------------------------------------------- /src/utils/logger.js: -------------------------------------------------------------------------------- 1 | export class Logger { 2 | log(...args) { 3 | console.log(this.prefix, ...args); 4 | } 5 | 6 | warn(...args) { 7 | console.warn(this.prefix, ...args); 8 | } 9 | 10 | error(...args) { 11 | console.error(this.prefix, ...args); 12 | } 13 | 14 | debug(...args) { 15 | console.debug(this.prefix, ...args); 16 | } 17 | 18 | info(...args) { 19 | console.info(this.prefix, ...args); 20 | } 21 | 22 | extends(name) { 23 | return new Logger(this.namespace + ':' + name); 24 | } 25 | 26 | constructor(namespace) { 27 | this.namespace = namespace; 28 | this.prefix = '[' + namespace + ']'; 29 | } 30 | } 31 | 32 | export const logger = new Logger('zju-helper'); 33 | 34 | export default logger; 35 | -------------------------------------------------------------------------------- /src/utils/vue.js: -------------------------------------------------------------------------------- 1 | export function isVueReady(element) { 2 | return element && '__vue__' in element; 3 | } 4 | -------------------------------------------------------------------------------- /userscript.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "学在浙大/智云课堂 辅助脚本", 3 | "description": "学在浙大/智云课堂 辅助脚本 by memset0", 4 | "namespace": "https://github.com/memset0/Learning-at-ZJU-Helper", 5 | "homepage": "https://github.com/memset0/Learning-at-ZJU-Helper", 6 | "supportURL": "https://github.com/memset0/Learning-at-ZJU-Helper/issues", 7 | "match": ["*://classroom.zju.edu.cn/*", "*://onlineroom.cmc.zju.edu.cn/*", "*://livingroom.cmc.zju.edu.cn/*", "*://interactivemeta.cmc.zju.edu.cn/*", "*://courses.zju.edu.cn/*", "**://pintia.cn/*"], 8 | "grant": ["unsafeWindow", "GM_setValue", "GM_getValue", "GM_addValueChangeListener", "GM_removeValueChangeListener", "GM_getResourceText"], 9 | "require": [], 10 | "resource": ["jszip.min.js https://jsd.cdn.zzko.cn/gh/memset0/Learning-at-ZJU-Helper@latest/lib/jszip.min.js"], 11 | "encoding": "utf-8", 12 | "run-at": "document-start", 13 | "downloadURL": "https://update.greasyfork.org/scripts/488869/%E5%AD%A6%E5%9C%A8%E6%B5%99%E5%A4%A7%E6%99%BA%E4%BA%91%E8%AF%BE%E5%A0%82%20%E8%BE%85%E5%8A%A9%E8%84%9A%E6%9C%AC.user.js", 14 | "updateURL": "https://update.greasyfork.org/scripts/488869/%E5%AD%A6%E5%9C%A8%E6%B5%99%E5%A4%A7%E6%99%BA%E4%BA%91%E8%AF%BE%E5%A0%82%20%E8%BE%85%E5%8A%A9%E8%84%9A%E6%9C%AC.meta.js" 15 | } 16 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | entry: './src/app.js', 3 | output: { 4 | filename: 'bundle.js', 5 | }, 6 | module: { 7 | rules: [ 8 | { 9 | test: /\.less$/, 10 | use: ['style-loader', 'css-loader', 'clean-css-loader', 'less-loader'], 11 | }, 12 | { 13 | test: /\.jsx?$/, 14 | exclude: /node_modules/, 15 | use: { 16 | loader: 'babel-loader', 17 | options: { 18 | presets: ['@babel/preset-env'], 19 | plugins: [ 20 | [ 21 | '@babel/plugin-transform-react-jsx', 22 | { 23 | runtime: 'automatic', 24 | importSource: 'hyperapp-jsx-pragma', 25 | }, 26 | ], 27 | ], 28 | }, 29 | }, 30 | }, 31 | ], 32 | }, 33 | resolve: { 34 | extensions: ['.js', '.jsx'], 35 | }, 36 | }; 37 | --------------------------------------------------------------------------------