From b43a9e174a1ada329f5567c080ef57e329cb7126 Mon Sep 17 00:00:00 2001 From: undefined Date: Mon, 24 May 2021 13:56:22 +0800 Subject: [PATCH] ui: download single problem --- packages/ui-default/build/config/webpack.js | 15 +--- .../components/zipDownloader/index.js | 85 +++++++++++++++++++ packages/ui-default/package.json | 2 +- .../ui-default/pages/problem_detail.page.jsx | 39 +++------ .../ui-default/pages/problem_files.page.js | 57 ++----------- .../ui-default/pages/problem_main.page.js | 73 +--------------- .../partials/problem_sidebar_normal.html | 18 +--- 7 files changed, 112 insertions(+), 177 deletions(-) create mode 100644 packages/ui-default/components/zipDownloader/index.js diff --git a/packages/ui-default/build/config/webpack.js b/packages/ui-default/build/config/webpack.js index 95ab22f1f..1848886ca 100644 --- a/packages/ui-default/build/config/webpack.js +++ b/packages/ui-default/build/config/webpack.js @@ -2,7 +2,7 @@ import { dirname } from 'path'; import webpack from 'webpack'; import ExtractCssPlugin from 'mini-css-extract-plugin'; -// import { languagesArr } from 'monaco-editor-webpack-plugin/out/languages'; +import MonacoWebpackPlugin from 'monaco-editor-webpack-plugin'; import FriendlyErrorsPlugin from 'friendly-errors-webpack-plugin'; import OptimizeCssAssetsPlugin from 'optimize-css-assets-webpack-plugin'; import CopyWebpackPlugin from 'copy-webpack-plugin'; @@ -13,17 +13,6 @@ import mapWebpackUrlPrefix from '../utils/mapWebpackUrlPrefix'; import StaticManifestPlugin from '../plugins/manifest.webpack'; import root from '../utils/root'; -// const yamlLang = languagesArr.find((t) => t.label === 'yaml'); -// yamlLang.entry = [ -// yamlLang.entry, -// require.resolve('@undefined-moe/monaco-yaml/lib/esm/monaco.contribution'), -// ]; -// yamlLang.worker = { -// id: 'vs/language/yaml/yamlWorker', -// entry: require.resolve('@undefined-moe/monaco-yaml/lib/esm/yaml.worker.js'), -// }; -const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin'); - const beautifyOutputUrl = mapWebpackUrlPrefix([ { prefix: 'node_modules/katex/dist/', replace: './katex/' }, { prefix: 'misc/.iconfont', replace: './ui/iconfont' }, @@ -182,7 +171,7 @@ export default function (env = {}) { new webpack.LoaderOptionsPlugin({ minimize: true }), new webpack.HashedModuleIdsPlugin(), ] - : [new webpack.NamedModulesPlugin()], + : [], new webpack.LoaderOptionsPlugin({ options: { context: root(), diff --git a/packages/ui-default/components/zipDownloader/index.js b/packages/ui-default/components/zipDownloader/index.js new file mode 100644 index 000000000..ffb8ba375 --- /dev/null +++ b/packages/ui-default/components/zipDownloader/index.js @@ -0,0 +1,85 @@ +import { dump } from 'js-yaml'; +import { createZipStream } from 'vj/utils/zip'; +import pipeStream from 'vj/utils/pipeStream'; +import i18n from 'vj/utils/i18n'; +import request from 'vj/utils/request'; +import Notification from 'vj/components/notification'; + +let isBeforeUnloadTriggeredByLibrary = !window.isSecureContext; +function onBeforeUnload(e) { + if (isBeforeUnloadTriggeredByLibrary) { + isBeforeUnloadTriggeredByLibrary = false; + return; + } + e.returnValue = ''; +} + +export default async function download(name, targets) { + const streamsaver = await import('streamsaver'); + if (window.location.protocol === 'https:' + || window.location.protocol === 'chrome-extension:' + || window.location.hostname === 'localhost') { + streamsaver.mitm = '/streamsaver/mitm.html'; + } + if (!window.WritableStream) { + window.WritableStream = (await import('web-streams-polyfill/dist/ponyfill.es6')).WritableStream; + streamsaver.WritableStream = window.WritableStream; + } + const fileStream = streamsaver.createWriteStream(name); + let i = 0; + const zipStream = createZipStream({ + // eslint-disable-next-line consistent-return + async pull(ctrl) { + if (i === targets.length) return ctrl.close(); + try { + if (targets[i].url) { + const response = await fetch(targets[i].url); + if (!response.ok) throw response.statusText; + ctrl.enqueue({ + name: targets[i].filename, + stream: () => response.body, + }); + } else { + ctrl.enqueue({ + name: targets[i].filename, + stream: () => new Blob([targets[i].content]).stream(), + }); + } + } catch (e) { + // eslint-disable-next-line no-use-before-define + stopDownload(); + Notification.error(i18n('Download Error', [targets[i].filename, e.toString()])); + } + i++; + }, + }); + const abortCallbackReceiver = {}; + function stopDownload() { abortCallbackReceiver.abort(); } + window.addEventListener('unload', stopDownload); + window.addEventListener('beforeunload', onBeforeUnload); + await pipeStream(zipStream, fileStream, abortCallbackReceiver); + window.removeEventListener('unload', stopDownload); + window.removeEventListener('beforeunload', onBeforeUnload); +} + +export async function downloadProblemSet(pids) { + const targets = []; + for (const pid of pids) { + const { pdoc } = await request.get(`/d/${UiContext.domainId}/p/${pid}`); + targets.push({ filename: `/d/${UiContext.domainId}/p/${pid}/problem.yaml`, content: dump(pdoc) }); + let { links } = await request.post( + `/d/${UiContext.domainId}/p/${pid}/files`, + { operation: 'get_links', files: pdoc.data.map((i) => i.name), type: 'testdata' } + ); + for (const filename of Object.keys(links)) { + targets.push({ filename: `/d/${UiContext.domainId}/p/${pid}/testdata/${filename}`, url: links[filename] }); + } + ({ links } = await request.post(`/d/${UiContext.domainId}/p/${pid}/files`, { + operation: 'get_links', files: pdoc.additional_file.map((i) => i.name), type: 'additional_file', + })); + for (const filename of Object.keys(links)) { + targets.push({ filename: `/d/${UiContext.domainId}/p/${pid}/additional_file/${filename}`, url: links[filename] }); + } + } + await download('Export.zip', targets); +} diff --git a/packages/ui-default/package.json b/packages/ui-default/package.json index 1bee61d45..ee16d9197 100644 --- a/packages/ui-default/package.json +++ b/packages/ui-default/package.json @@ -1,6 +1,6 @@ { "name": "@hydrooj/ui-default", - "version": "4.8.45", + "version": "4.8.46", "author": "undefined ", "license": "AGPL-3.0", "main": "hydro.js", diff --git a/packages/ui-default/pages/problem_detail.page.jsx b/packages/ui-default/pages/problem_detail.page.jsx index 4b0a64343..e30e170d3 100644 --- a/packages/ui-default/pages/problem_detail.page.jsx +++ b/packages/ui-default/pages/problem_detail.page.jsx @@ -5,6 +5,7 @@ import { NamedPage } from 'vj/misc/Page'; import Navigation from 'vj/components/navigation'; import Notification from 'vj/components/notification/index'; import { ActionDialog } from 'vj/components/dialog'; +import { downloadProblemSet } from 'vj/components/zipDownloader'; import loadReactRedux from 'vj/utils/loadReactRedux'; import delay from 'vj/utils/delay'; import i18n from 'vj/utils/i18n'; @@ -129,41 +130,25 @@ const page = new NamedPage(['problem_detail', 'contest_detail_problem', 'homewor return this; }; - async function handleClickCopyProblem() { - const action = await copyProblemDialog.clear().open(); - if (action !== 'ok') return; - const target = copyProblemDialog.$dom.find('[name="target"]').val(); - try { - await request.post(UiContext.postCopyUrl, { - operation: 'send', - target, - pids: [UiContext.problemNumId], - }); - Notification.send(i18n('Problem Sended.')); - } catch (error) { - Notification.error(error.message); - } + async function handleClickDownloadProblem() { + await downloadProblemSet([UiContext.problemNumId]); } async function scratchpadFadeIn() { await $('#scratchpad') - .transition({ - opacity: 1, - }, { - duration: 200, - easing: 'easeOutCubic', - }) + .transition( + { opacity: 1 }, + { duration: 200, easing: 'easeOutCubic' } + ) .promise(); } async function scratchpadFadeOut() { await $('#scratchpad') - .transition({ - opacity: 0, - }, { - duration: 200, - easing: 'easeOutCubic', - }) + .transition( + { opacity: 0 }, + { duration: 200, easing: 'easeOutCubic' } + ) .promise(); } @@ -247,7 +232,7 @@ const page = new NamedPage(['problem_detail', 'contest_detail_problem', 'homewor $(ev.currentTarget).hide(); $('[name="problem-sidebar__categories"]').show(); }); - $('[name="problem-sidebar__send-to"]').on('click', handleClickCopyProblem); + $('[name="problem-sidebar__download').on('click', handleClickDownloadProblem); loadSubjective(); if (pagename === 'contest_detail_problem') return; diff --git a/packages/ui-default/pages/problem_files.page.js b/packages/ui-default/pages/problem_files.page.js index c38e90794..56c96507b 100644 --- a/packages/ui-default/pages/problem_files.page.js +++ b/packages/ui-default/pages/problem_files.page.js @@ -1,65 +1,22 @@ -import { WritableStream } from 'web-streams-polyfill/dist/ponyfill.es6'; -import * as streamsaver from 'streamsaver'; import _ from 'lodash'; -import { createZipStream } from 'vj/utils/zip'; import { NamedPage } from 'vj/misc/Page'; import Notification from 'vj/components/notification'; import { ConfirmDialog, ActionDialog, Dialog } from 'vj/components/dialog/index'; +import download from 'vj/components/zipDownloader'; import request from 'vj/utils/request'; import pjax from 'vj/utils/pjax'; -import pipeStream from 'vj/utils/pipeStream'; import tpl from 'vj/utils/tpl'; import i18n from 'vj/utils/i18n'; -// Firefox have no WritableStream -if (!window.WritableStream) streamsaver.WritableStream = WritableStream; -if (window.location.protocol === 'https:' - || window.location.protocol === 'chrome-extension:' - || window.location.hostname === 'localhost') { - streamsaver.mitm = '/streamsaver/mitm.html'; -} - -let isBeforeUnloadTriggeredByLibrary = !window.isSecureContext; -function onBeforeUnload(e) { - if (isBeforeUnloadTriggeredByLibrary) { - isBeforeUnloadTriggeredByLibrary = false; - return; - } - e.returnValue = ''; -} - async function downloadProblemFilesAsArchive(type, files) { const { links, pdoc } = await request.post('', { operation: 'get_links', files, type }); - const fileStream = streamsaver.createWriteStream(`${pdoc.docId} ${pdoc.title}.zip`); - let i = 0; const targets = []; - for (const filename of Object.keys(links)) targets.push({ filename, downloadUrl: links[filename] }); - const zipStream = createZipStream({ - // eslint-disable-next-line consistent-return - async pull(ctrl) { - if (i === targets.length) return ctrl.close(); - try { - const response = await fetch(targets[i].downloadUrl); - if (!response.ok) throw response.statusText; - ctrl.enqueue({ - name: targets[i].filename, - stream: () => response.body, - }); - } catch (e) { - // eslint-disable-next-line no-use-before-define - stopDownload(); - Notification.error(i18n('problem_files.download_as_archive_error', [targets[i].filename, e.toString()])); - } - i++; - }, - }); - const abortCallbackReceiver = {}; - function stopDownload() { abortCallbackReceiver.abort(); } - window.addEventListener('unload', stopDownload); - window.addEventListener('beforeunload', onBeforeUnload); - await pipeStream(zipStream, fileStream, abortCallbackReceiver); - window.removeEventListener('unload', stopDownload); - window.removeEventListener('beforeunload', onBeforeUnload); + for (const filename of Object.keys(links)) targets.push({ filename, url: links[filename] }); + await download(`${pdoc.docId} ${pdoc.title}.zip`, targets); +} + +function onBeforeUnload(e) { + e.returnValue = ''; } const page = new NamedPage('problem_files', () => { diff --git a/packages/ui-default/pages/problem_main.page.js b/packages/ui-default/pages/problem_main.page.js index 80cf2707b..ce34da832 100644 --- a/packages/ui-default/pages/problem_main.page.js +++ b/packages/ui-default/pages/problem_main.page.js @@ -1,39 +1,19 @@ -import { WritableStream } from 'web-streams-polyfill/dist/ponyfill.es6'; import _ from 'lodash'; -import { dump } from 'js-yaml'; -import * as streamsaver from 'streamsaver'; import { NamedPage } from 'vj/misc/Page'; import Notification from 'vj/components/notification'; import { ConfirmDialog } from 'vj/components/dialog'; import Dropdown from 'vj/components/dropdown/Dropdown'; -import { createZipStream } from 'vj/utils/zip'; +import { downloadProblemSet } from 'vj/components/zipDownloader'; import pjax from 'vj/utils/pjax'; import substitute from 'vj/utils/substitute'; import request from 'vj/utils/request'; import tpl from 'vj/utils/tpl'; -import pipeStream from 'vj/utils/pipeStream'; import delay from 'vj/utils/delay'; import i18n from 'vj/utils/i18n'; const categories = {}; const dirtyCategories = []; const selections = []; -// Firefox have no WritableStream -if (!window.WritableStream) streamsaver.WritableStream = WritableStream; -if (window.location.protocol === 'https:' - || window.location.protocol === 'chrome-extension:' - || window.location.hostname === 'localhost') { - streamsaver.mitm = '/streamsaver/mitm.html'; -} - -let isBeforeUnloadTriggeredByLibrary = !window.isSecureContext; -function onBeforeUnload(e) { - if (isBeforeUnloadTriggeredByLibrary) { - isBeforeUnloadTriggeredByLibrary = false; - return; - } - e.returnValue = ''; -} function setDomSelected($dom, selected) { if (selected) { @@ -224,56 +204,7 @@ async function handleOperation(operation) { async function handleDownload() { const pids = ensureAndGetSelectedPids(); - const fileStream = streamsaver.createWriteStream('Export.zip'); - const targets = []; - for (const pid of pids) { - const { pdoc } = await request.get(`p/${pid}`); - targets.push({ filename: `${pid}/problem.yaml`, content: dump(pdoc) }); - let { links } = await request.post(`p/${pid}/files`, { operation: 'get_links', files: pdoc.data.map((i) => i.name), type: 'testdata' }); - for (const filename of Object.keys(links)) { - targets.push({ filename: `${pid}/testdata/${filename}`, url: links[filename] }); - } - ({ links } = await request.post(`p/${pid}/files`, { - operation: 'get_links', files: pdoc.additional_file.map((i) => i.name), type: 'additional_file', - })); - for (const filename of Object.keys(links)) { - targets.push({ filename: `${pid}/additional_file/${filename}`, url: links[filename] }); - } - } - let i = 0; - const zipStream = createZipStream({ - // eslint-disable-next-line consistent-return - async pull(ctrl) { - if (i === targets.length) return ctrl.close(); - try { - if (targets[i].url) { - const response = await fetch(targets[i].url); - if (!response.ok) throw response.statusText; - ctrl.enqueue({ - name: targets[i].filename, - stream: () => response.body, - }); - } else { - ctrl.enqueue({ - name: targets[i].filename, - stream: () => new Blob([targets[i].content], { type: 'application/json' }).stream(), - }); - } - } catch (e) { - // eslint-disable-next-line no-use-before-define - stopDownload(); - Notification.error(i18n('problem_files.download_as_archive_error', [targets[i].filename, e.toString()])); - } - i++; - }, - }); - const abortCallbackReceiver = {}; - function stopDownload() { abortCallbackReceiver.abort(); } - window.addEventListener('unload', stopDownload); - window.addEventListener('beforeunload', onBeforeUnload); - await pipeStream(zipStream, fileStream, abortCallbackReceiver); - window.removeEventListener('unload', stopDownload); - window.removeEventListener('beforeunload', onBeforeUnload); + await downloadProblemSet(pids); } const page = new NamedPage(['problem_main', 'problem_category'], () => { diff --git a/packages/ui-default/templates/partials/problem_sidebar_normal.html b/packages/ui-default/templates/partials/problem_sidebar_normal.html index d205b00bd..84a5f5b6b 100644 --- a/packages/ui-default/templates/partials/problem_sidebar_normal.html +++ b/packages/ui-default/templates/partials/problem_sidebar_normal.html @@ -85,21 +85,9 @@ {% endif %} {% if page_name == 'problem_detail' and (handler.user.own(pdoc) or handler.user.hasPriv(PRIV.PRIV_READ_PROBLEM_DATA) or handler.user.hasPerm(perm.PERM_READ_PROBLEM_DATA)) %} - - + {% endif %}