>>=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 |
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 ``; // 图片支持
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 | 
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 |
--------------------------------------------------------------------------------