diff --git a/package.json b/package.json index 17a9db3..92996d2 100644 --- a/package.json +++ b/package.json @@ -20,8 +20,10 @@ "homepage": "https://github.com/memset0/CPAssistant.js#readme", "dependencies": { "@types/node": "^20.3.0", + "file-saver": "^2.0.5", "h": "^1.0.0", "less": "^4.1.3", + "papaparse": "^5.4.1", "query-string": "^8.1.0", "typescript": "^5.1.3", "vite": "^4.3.9", diff --git a/src/modules/vjudge/features/DownloadRanklist.ts b/src/modules/vjudge/features/DownloadRanklist.ts new file mode 100644 index 0000000..d7a01b6 --- /dev/null +++ b/src/modules/vjudge/features/DownloadRanklist.ts @@ -0,0 +1,87 @@ +import * as Papa from 'papaparse'; +import saveAs from 'file-saver'; +import Module from '../../../types/module'; +import Feature from '../../../types/feature'; + +export default class ForkContest extends Feature { + run() {} + + registerPlugins() { + this.plugin('hdu', function (this: Feature) { + this.on('/contest/rank', (_, params) => { + const contestId = params.cid; + const csvLink = `/contest/rank?cid=${contestId}&export=csv`; + + const $downloadButton = document.createElement('button'); + $downloadButton.innerText = 'Download Ranklist (VJ)'; + $downloadButton.setAttribute('href', '#'); + $downloadButton.onclick = async () => { + $downloadButton.setAttribute('disabled', ''); + + const response = await fetch(csvLink); + if (!response.ok) { + const errorMessage = `Request failed (code: ${response.status})!`; + alert(errorMessage); + throw new Error(errorMessage); + } + + const csvPlain = await response.text(); + const csvData = Papa.parse(csvPlain).data.slice(1); + + const parse = (pattern: string): string => { + let [acTime, penalty] = pattern.split(' '); + if (pattern == '') { + acTime = '--'; + penalty = '--'; + } else { + if (acTime.startsWith('(')) { + penalty = acTime; + acTime = '--'; + } else { + const [hour, minute, _second] = acTime.split(':'); + acTime = String(Number(hour) * 60 + Number(minute)); + } + if (penalty !== undefined && penalty.startsWith('(')) { + penalty = penalty.slice(2, -1); + } else { + penalty = '--'; + } + if (acTime != '--' && penalty != '--') { + penalty = String(Number(penalty) + 1); // The accepted submission counted. + } + } + return acTime + ' # ' + penalty; + }; + + const data = []; + for (const source of csvData) { + const parsed = [source[1]]; + for (let i = 4; i < source.length; i++) { + parsed.push(parse(source[i])); + } + data.push(parsed); + } + + let blob = new Blob( + [ + data.map((row) => row.join(',')).join('\n'), // + ], + { type: 'text/csv;charset=utf-8;' } + ); + saveAs(blob, 'output.csv'); + + $downloadButton.removeAttribute('disabled'); + }; + + const $actionBar = document.querySelector('.page-card-heading-actions')!; + $actionBar.appendChild($downloadButton); + }); + }); + } + + constructor(module: Module, name: string) { + super(module, name); + + this.registerPlugins(); + } +} diff --git a/src/modules/vjudge/features/ForkContest.ts b/src/modules/vjudge/features/ForkContest.ts index fb17446..f030ecc 100644 --- a/src/modules/vjudge/features/ForkContest.ts +++ b/src/modules/vjudge/features/ForkContest.ts @@ -61,8 +61,6 @@ export default class ForkContest extends Feature { } this.plugin('codeforces', function (this: Feature) { - this.log('setup'); - function setup(situation: string, roundId: string) { const $menu = document.getElementsByClassName('second-level-menu')[0]; const $menuList = $menu.children[0]; diff --git a/src/modules/vjudge/main.ts b/src/modules/vjudge/main.ts index 8da0956..834aed0 100644 --- a/src/modules/vjudge/main.ts +++ b/src/modules/vjudge/main.ts @@ -2,6 +2,7 @@ import config from '../../config'; import App from '../../app'; import Module from '../../types/module'; import AcceptedCounter from './features/AcceptedCounter'; +import DownloadRanklist from './features/DownloadRanklist'; import ForkContest from './features/ForkContest'; export default class ModuleVjudge extends Module { @@ -11,6 +12,7 @@ export default class ModuleVjudge extends Module { super(app, 'vjudge', config.match.vjudge); this.register(new AcceptedCounter(this, 'accepted-counter')); + this.register(new DownloadRanklist(this, 'download-ranklist')); this.register(new ForkContest(this, 'fork-contest')); } } diff --git a/src/types/feature.ts b/src/types/feature.ts index 3a548f6..6511094 100644 --- a/src/types/feature.ts +++ b/src/types/feature.ts @@ -33,7 +33,7 @@ export default class Feature { } } - on(match: string | Array, func: (args: Dict) => void): boolean { + on(match: string | Array, func: (args: Dict, params: Dict) => void): boolean { if (match instanceof Array) { let ok = false; for (const singleMatch of match) { @@ -75,7 +75,7 @@ export default class Feature { } } - func(args); + func(args, Object.fromEntries(new URLSearchParams(location.search))); return true; } diff --git a/yarn.lock b/yarn.lock index fff09cd..1dd97f5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -255,6 +255,11 @@ esbuild@^0.17.5: "@esbuild/win32-ia32" "0.17.19" "@esbuild/win32-x64" "0.17.19" +file-saver@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/file-saver/-/file-saver-2.0.5.tgz#d61cfe2ce059f414d899e9dd6d4107ee25670c38" + integrity sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA== + filter-obj@^5.1.0: version "5.1.0" resolved "https://registry.npmmirror.com/filter-obj/-/filter-obj-5.1.0.tgz" @@ -402,6 +407,11 @@ open@^8.4.1: is-docker "^2.1.1" is-wsl "^2.2.0" +papaparse@^5.4.1: + version "5.4.1" + resolved "https://registry.yarnpkg.com/papaparse/-/papaparse-5.4.1.tgz#f45c0f871853578bd3a30f92d96fdcfb6ebea127" + integrity sha512-HipMsgJkZu8br23pW15uvo6sib6wne/4woLZPlFf3rpDyMe9ywEXUsuD7+6K9PRkJlVT51j/sCOYDKGGS3ZJrw== + parse-node-version@^1.0.1: version "1.0.1" resolved "https://registry.npmmirror.com/parse-node-version/-/parse-node-version-1.0.1.tgz"