diff --git a/packages/ui-default/components/scratchpad/reducers/editor.ts b/packages/ui-default/components/scratchpad/reducers/editor.ts index 41736b675..22ce18946 100644 --- a/packages/ui-default/components/scratchpad/reducers/editor.ts +++ b/packages/ui-default/components/scratchpad/reducers/editor.ts @@ -1,6 +1,7 @@ let cacheKey = `${UserContext._id}/${UiContext.pdoc.domainId}/${UiContext.pdoc.docId}`; if (UiContext.tdoc?._id && UiContext.tdoc.rule !== 'homework') cacheKey += `@${UiContext.tdoc._id}`; +// TODO switch to indexeddb export default function reducer(state = { lang: localStorage.getItem(`${cacheKey}#lang`) || UiContext.codeLang, code: localStorage.getItem(cacheKey) || UiContext.codeTemplate, diff --git a/packages/ui-default/components/socket/index.ts b/packages/ui-default/components/socket/index.ts index b741602e8..981e78aa8 100644 --- a/packages/ui-default/components/socket/index.ts +++ b/packages/ui-default/components/socket/index.ts @@ -54,6 +54,10 @@ export default class Sock { onclose: (code: number, reason: string) => void; onopen: (sock: ReconnectingWebSocket) => void; + on(event: 'message' | 'close' | 'open', callback: (...args: any[]) => void) { + this[`on${event}`] = callback; + } + send(data) { this.sock.send(data); } diff --git a/packages/ui-default/package.json b/packages/ui-default/package.json index 133a41467..b14ae8125 100644 --- a/packages/ui-default/package.json +++ b/packages/ui-default/package.json @@ -14,8 +14,8 @@ "cli": false }, "devDependencies": { - "@blueprintjs/core": "^5.10.3", - "@cordisjs/core": "^3.17.3", + "@blueprintjs/core": "^5.10.4", + "@cordisjs/core": "3.17.3", "@fontsource/dm-mono": "^5.0.20", "@fontsource/fira-code": "^5.0.18", "@fontsource/inconsolata": "^5.0.18", @@ -31,6 +31,7 @@ "@svgr/webpack": "^8.1.0", "@types/gulp-if": "^0.0.34", "@types/jquery": "^3.5.30", + "@types/jquery.transit": "^0.9.33", "@types/json-schema": "^7.0.15", "@types/katex": "^0.16.7", "@types/markdown-it": "^14.1.1", @@ -40,7 +41,7 @@ "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", "@types/redux-logger": "^3.0.13", - "@types/uuid": "^10", + "@types/uuid": "^10.0.0", "@types/webpack-env": "^1.18.5", "@vscode/codicons": "^0.0.36", "ajv": "^8.16.0", @@ -69,12 +70,13 @@ "gulp-if": "^3.0.0", "gulp-plumber": "^1.2.1", "gulp-svgmin": "^4.1.0", + "idb": "^8.0.0", "jquery": "^3.7.1", "jquery-scroll-lock": "^3.1.3", "jquery.easing": "^1.4.1", "jquery.transit": "^0.9.12", "matchmedia-polyfill": "^0.3.2", - "md-editor-rt": "^4.17.0", + "md-editor-rt": "^4.17.1", "mini-css-extract-plugin": "^2.9.0", "moment": "^2.30.1", "monaco-editor": "0.50.0", @@ -135,7 +137,7 @@ "fs-extra": "^11.2.0", "js-yaml": "^4.1.0", "jsesc": "^3.0.2", - "katex": "^0.16.10", + "katex": "^0.16.11", "lodash": "^4.17.21", "markdown-it": "^14.1.0", "markdown-it-anchor": "^9.0.1", diff --git a/packages/ui-default/pages/contest_scoreboard.page.ts b/packages/ui-default/pages/contest_scoreboard.page.ts index e8c2459be..e8d1160eb 100644 --- a/packages/ui-default/pages/contest_scoreboard.page.ts +++ b/packages/ui-default/pages/contest_scoreboard.page.ts @@ -1,17 +1,19 @@ import $ from 'jquery'; import { NamedPage } from 'vj/misc/Page'; import { pjax } from 'vj/utils'; +import { openDB } from 'vj/utils/db'; -const page = new NamedPage(['contest_scoreboard', 'homework_scoreboard'], () => { +const page = new NamedPage(['contest_scoreboard', 'homework_scoreboard'], async () => { + const db = await openDB; const { tdoc } = UiContext; - const key = `scoreboard-star/${tdoc.domainId}/${tdoc.docId}`; - const read = () => JSON.parse(localStorage.getItem(key) || '[]'); - const write = (data) => localStorage.setItem(key, JSON.stringify(data)); + const id = `${tdoc.domainId}/${tdoc.docId}`; + const read = async () => (await db.get('scoreboard-star', id)).value || []; + const write = (data) => db.put('scoreboard-star', { id, data }); $(`.star.user--${UserContext._id}`).closest('tr').addClass('star-highlight'); - read().forEach((uid) => $(`.star.user--${uid}`).addClass('activated').closest('tr').addClass('star-highlight')); - $('.star').on('click', (e) => { - const star = read(); + (await read()).forEach((uid) => $(`.star.user--${uid}`).addClass('activated').closest('tr').addClass('star-highlight')); + $('.star').on('click', async (e) => { + const star = await read(); const $target = $(e.currentTarget); const uid = $target.data('uid'); if (star.includes(uid)) { @@ -24,14 +26,14 @@ const page = new NamedPage(['contest_scoreboard', 'homework_scoreboard'], () => write(star); }); - $('.select.filter').on('change', (e) => { - const val = $(e.target).val(); + async function update() { + const val = $($('.select.filter')).val(); if (val === 'all') { $('.data-table tbody tr').show(); - read().forEach((uid) => $(`.star.user--${uid}`).closest('tr').addClass('star-highlight')); + (await read()).forEach((uid) => $(`.star.user--${uid}`).closest('tr').addClass('star-highlight')); } else if (val === 'star') { $('.data-table tbody tr').hide(); - read().forEach((uid) => $(`.star.user--${uid}`).closest('tr').show().removeClass('star-highlight')); + (await read()).forEach((uid) => $(`.star.user--${uid}`).closest('tr').show().removeClass('star-highlight')); } else if (val === 'rank') { $('.data-table tbody tr').show(); $('.rank--unrank').closest('tr').hide(); @@ -41,13 +43,17 @@ const page = new NamedPage(['contest_scoreboard', 'homework_scoreboard'], () => if (!uids?.length) return; uids.forEach((uid) => $(`.user--${uid}`).closest('tr').show()); } - }); + } + $('.select.filter').on('change', update); const beginAt = new Date(UiContext.tdoc.beginAt).getTime(); const endAt = new Date(UiContext.tdoc.endAt).getTime(); - function updateScoreboard() { + async function updateScoreboard() { const now = Date.now(); - if (beginAt <= now && now <= endAt) pjax.request({ url: UiContext.scoreboardUrl || '', push: false }); + if (beginAt <= now && now <= endAt) { + await pjax.request({ url: UiContext.scoreboardUrl || '', push: false }); + await update(); + } } setInterval(updateScoreboard, 180000); diff --git a/packages/ui-default/pages/problem_detail.page.jsx b/packages/ui-default/pages/problem_detail.page.tsx similarity index 89% rename from packages/ui-default/pages/problem_detail.page.jsx rename to packages/ui-default/pages/problem_detail.page.tsx index 980b267e1..ecda3f84f 100644 --- a/packages/ui-default/pages/problem_detail.page.jsx +++ b/packages/ui-default/pages/problem_detail.page.tsx @@ -9,15 +9,14 @@ import { NamedPage } from 'vj/misc/Page'; import { delay, i18n, loadReactRedux, pjax, request, tpl, } from 'vj/utils'; +import { openDB } from 'vj/utils/db'; class ProblemPageExtender { - constructor() { - this.isExtended = false; - this.inProgress = false; - this.$content = $('.problem-content-container'); - this.$contentBound = this.$content.closest('.section'); - this.$scratchpadContainer = $('.scratchpad-container'); - } + isExtended = false; + inProgress = false; + $content = $('.problem-content-container'); + $contentBound = this.$content.closest('.section'); + $scratchpadContainer = $('.scratchpad-container'); async extend() { if (this.inProgress) return; @@ -28,6 +27,7 @@ class ProblemPageExtender { .get(0) .getBoundingClientRect(); + // @ts-ignore this.$content.transition({ opacity: 0 }, { duration: 100 }); await delay(100); @@ -41,6 +41,7 @@ class ProblemPageExtender { }) .show() .transition({ + // @ts-ignore left: 0, top: 0, width: '100%', @@ -77,6 +78,7 @@ class ProblemPageExtender { await this.$scratchpadContainer .transition({ + // @ts-ignore left: bound.left, top: bound.top, width: bound.width, @@ -88,6 +90,7 @@ class ProblemPageExtender { .promise(); this.$scratchpadContainer.hide(); + // @ts-ignore this.$content.transition({ opacity: 1 }, { duration: 100 }); window.document.body.style.overflow = 'scroll'; @@ -112,6 +115,7 @@ const page = new NamedPage(['problem_detail', 'contest_detail_problem', 'homewor } async function scratchpadFadeIn() { + // @ts-ignore await $('#scratchpad') .transition( { opacity: 1 }, @@ -121,6 +125,7 @@ const page = new NamedPage(['problem_detail', 'contest_detail_problem', 'homewor } async function scratchpadFadeOut() { + // @ts-ignore await $('#scratchpad') .transition( { opacity: 0 }, @@ -138,6 +143,7 @@ const page = new NamedPage(['problem_detail', 'contest_detail_problem', 'homewor const { default: ScratchpadReducer } = await import('../components/scratchpad/reducers'); const { Provider, store } = await loadReactRedux(ScratchpadReducer); + // @ts-ignore window.store = store; const sock = new WebSocket(UiContext.ws_prefix + UiContext.pretestConnUrl); sock.onmessage = (message) => { @@ -237,22 +243,25 @@ const page = new NamedPage(['problem_detail', 'contest_detail_problem', 'homewor [, setUpdate] = React.useState(0); return
{pids.map((i) => - {i} - {ans[i] && } + {i} + {ans[i] && } )}
; } - function saveAns() { - localStorage.setItem(`${cacheKey}#objective`, JSON.stringify(ans)); - setUpdate?.((i) => i + 1); + const db = await openDB; + async function saveAns() { + await db.put('solutions', { + id: `${cacheKey}#objective`, + value: JSON.stringify(ans), + }); } - function clearAns() { - localStorage.removeItem(`${cacheKey}#objective`); + async function clearAns() { + await db.delete('solutions', `${cacheKey}#objective`); window.location.reload(); } - function loadAns() { - const saved = localStorage.getItem(`${cacheKey}#objective`); + async function loadAns() { + const saved = await db.get('solutions', `${cacheKey}#objective`); if (!saved) return; const isValidOption = (v) => v.length === 1 && v.charCodeAt(0) >= 65 && v.charCodeAt(0) <= 90; Object.assign(ans, JSON.parse(saved)); @@ -261,27 +270,28 @@ const page = new NamedPage(['problem_detail', 'contest_detail_problem', 'homewor for (const v of val) { if (isValidOption(v)) $(`.objective_${id} input[value="${v}"]`).prop('checked', true); } - } else { - $(`.objective_${id} input[type=text], .objective_${id} textarea`).val(val); + } else if (val) { + $(`.objective_${id} input[type=text], .objective_${id} textarea`).val(val.toString()); if (isValidOption(val)) $(`.objective_${id}.radiobox [value="${val}"]`).prop('checked', true); } } + setUpdate((v) => v + 1); } if (cnt) { - loadAns(); + await loadAns(); $('.problem-content .typo').append(document.getElementsByClassName('nav__item--round').length ? `` : ``); - $('.objective-input[type!=checkbox]').on('input', (e) => { + $('.objective-input[type!=checkbox]').on('input', (e: JQuery.TriggeredEvent) => { ans[e.target.name] = e.target.value; saveAns(); }); - $('input.objective-input[type=checkbox]').on('input', (e) => { + $('input.objective-input[type=checkbox]').on('input', (e: JQuery.TriggeredEvent) => { if (e.target.checked) { ans[e.target.name] ||= []; ans[e.target.name].push(e.target.value); - ans[e.target.name] = [...new Set(ans[e.target.name])].sort((a, b) => a.charCodeAt(0) - b.charCodeAt(0)); + ans[e.target.name] = [...new Set(ans[e.target.name])].sort((a: string, b: string) => a.charCodeAt(0) - b.charCodeAt(0)); } else { ans[e.target.name] = ans[e.target.name].filter((v) => v !== e.target.value); } @@ -310,7 +320,7 @@ const page = new NamedPage(['problem_detail', 'contest_detail_problem', 'homewor const result = await new ConfirmDialog({ $body: tpl.typoMsg('All changes will be lost. Are you sure to clear all answers?'), }).open(); - if (result === 'yes') clearAns(); + if (result === 'yes') await clearAns(); }); } const ele = document.createElement('div'); diff --git a/packages/ui-default/utils/db.ts b/packages/ui-default/utils/db.ts new file mode 100644 index 000000000..9ebb172f0 --- /dev/null +++ b/packages/ui-default/utils/db.ts @@ -0,0 +1,37 @@ +import { openDB as _open } from 'idb'; +import { InfoDialog } from 'vj/components/dialog'; +import { tpl } from 'vj/utils'; + +export const openDB = _open('hydro', 1, { + upgrade(db, oldVersion) { + if (oldVersion < 1) { + const solutionStore = db.createObjectStore('solutions', { keyPath: 'id' }); + db.createObjectStore('scoreboard-star', { keyPath: 'id' }); + const keys = Object.keys(localStorage); + for (const key of keys) { + if (key.endsWith('#objective')) { + const value = localStorage.getItem(key); + if (value) solutionStore.put({ id: key, value: JSON.parse(value) }); + localStorage.removeItem(key); + } + if (key.startsWith('scoreboard-star/')) localStorage.removeItem(key); + if (/^0\.\d+$/.test(key)) localStorage.removeItem(key); + } + } + }, + blocked(currentVersion, blockedVersion) { + console.error('IDB Blocked by version', blockedVersion, 'want', currentVersion); + new InfoDialog({ + $body: tpl.typoMsg('Some other opened tabs locked the database. Please close them.'), + }).open(); + }, + blocking(currentVersion, blockedVersion) { + console.error('IDB Blocking version', blockedVersion); + new InfoDialog({ + $body: tpl.typoMsg('Please close or refresh this tab to perform the upgrade.'), + }).open(); + }, + terminated() { + console.error('IDB Terminated'); + }, +});