From 388adbeddece0b8aed7ef4b4dd3076f24fe69f58 Mon Sep 17 00:00:00 2001 From: panda Date: Mon, 22 Aug 2022 03:27:11 +0800 Subject: [PATCH] ui: add data-preview (#412) Co-authored-by: undefined --- .../backendlib/markdown-it-media.ts | 2 +- .../components/preview/preview.page.ts | 165 ++++++++++++++++++ packages/ui-default/locales/zh.yaml | 2 + packages/ui-default/pages/files.page.js | 11 -- .../ui-default/pages/problem_files.page.js | 99 +---------- .../ui-default/templates/partials/files.html | 2 +- .../problem_files-additional_file.html | 2 +- .../partials/problem_files-testdata.html | 2 +- .../problem_sidebar_additional_file.html | 2 +- .../partials/problem_sidebar_testdata.html | 2 +- 10 files changed, 177 insertions(+), 112 deletions(-) create mode 100644 packages/ui-default/components/preview/preview.page.ts diff --git a/packages/ui-default/backendlib/markdown-it-media.ts b/packages/ui-default/backendlib/markdown-it-media.ts index 8e311715d..3c7efffca 100644 --- a/packages/ui-default/backendlib/markdown-it-media.ts +++ b/packages/ui-default/backendlib/markdown-it-media.ts @@ -105,7 +105,7 @@ export function Media(md: MarkdownIt) { if (service === 'msoffice') { return `\ + scrolling="no" border="0" frameborder="no" framespacing="0" width="100%" style="min-height:500px" ${allowFullScreen}> `; } if (service === 'pdf') { diff --git a/packages/ui-default/components/preview/preview.page.ts b/packages/ui-default/components/preview/preview.page.ts new file mode 100644 index 000000000..e8ff18280 --- /dev/null +++ b/packages/ui-default/components/preview/preview.page.ts @@ -0,0 +1,165 @@ +import Clipboard from 'clipboard'; +import $ from 'jquery'; +import { nanoid } from 'nanoid'; +import { ActionDialog, InfoDialog } from 'vj/components/dialog/index'; +import Notification from 'vj/components/notification'; +import { AutoloadPage } from 'vj/misc/Page'; +import i18n from 'vj/utils/i18n'; +import pjax from 'vj/utils/pjax'; +import request from 'vj/utils/request'; +import tpl from 'vj/utils/tpl'; + +async function startEdit(filename, value) { + const { default: Editor } = await import('vj/components/editor/index'); + const promise = new ActionDialog({ + $body: tpl` +
+ +
`, + width: `${window.innerWidth - 200}px`, + height: `${window.innerHeight - 100}px`, + cancelByEsc: false, + }).open(); + const languages = [ + ['yaml', ['yaml', 'yml']], + ['cpp', ['c', 'cc', 'cpp', 'h', 'hpp']], + ['json', ['json']], + ['plain', ['in', 'out', 'ans']], + ]; + const language = languages.filter((i) => i[1].includes(filename.split('.').pop()))[0]?.[0] || 'auto'; + const editor = new Editor($('[name="fileContent"]'), { + value, + autoResize: false, + autoLayout: false, + language: (language as string), + model: `hydro://problem/file/${filename}`, + }); + const action = await promise; + value = (editor.value() as string).replace(/\r\n/g, '\n'); + editor.destory(); + if (action !== 'ok') return null; + return value; +} + +const dialogAction = (id) => [ + tpl``, + tpl``, + tpl``, +]; + +function bindCopyLink(id, src: string) { + const url = !(window.location.href.endsWith('file') || window.location.href.endsWith('files')) || window.location.href.match('contest/.*/file') + ? `file://${src.substring(src.lastIndexOf('/') + 1)}` : src; + const clip = new Clipboard(`#copy-${id}`, { text: () => `${url}` }); + clip.on('success', () => Notification.success(i18n(`${url.startsWith('file://') ? 'Reference' : 'Download'} link copied to clipboard!`))); + clip.on('error', () => Notification.error(i18n('Copy failed :('))); +} + +async function previewImage(link) { + const id = nanoid(); + const dialog = new InfoDialog({ + $body: tpl`
`, + $action: dialogAction(id), + }); + bindCopyLink(id, link); + const action = await dialog.open(); + if (action === 'download') window.open(link); +} + +async function previewPDF(link) { + const uuidURL = URL.createObjectURL(new Blob()); + const uuid = uuidURL.toString(); + URL.revokeObjectURL(uuidURL); + const id = nanoid(); + const dialog = new InfoDialog({ + $body: tpl` +
+ + + + + + +
`, + width: `${window.innerWidth - 200}px`, + height: `${window.innerHeight - 100}px`, + $action: dialogAction(id), + }); + bindCopyLink(id, link); + const action = await dialog.open(); + if (action === 'download') window.open(link); +} + +async function previewOffice(link, src) { + const id = nanoid(); + const dialog = new InfoDialog({ + $body: tpl` +
+ +
`, + width: `${window.innerWidth - 200}px`, + height: `${window.innerHeight - 100}px`, + $action: dialogAction(id), + }); + bindCopyLink(id, link); + const action = await dialog.open(); + if (action === 'download') window.open(link); +} + +export async function previewFile(ev, type = '') { + if (ev?.metaKey || ev?.ctrlKey || ev?.shiftKey) return null; + if (ev) ev.preventDefault(); + const filename = ev + ? ev.currentTarget.closest('[data-filename]').getAttribute('data-filename') + // eslint-disable-next-line no-alert + : prompt('Filename'); + if (!filename) return null; + const filesize = ev + ? +ev.currentTarget.closest('[data-size]').getAttribute('data-size') + : 0; + let content = ''; + if (ev) { + const link = $(ev.currentTarget).find('a').attr('href'); + if (!link) return null; + if (!type) type = ev.currentTarget.getAttribute('data-preview'); + const ext = filename.split('.').pop(); + if (['zip', 'rar', '7z'].includes(ext) || filesize > 8 * 1024 * 1024) { + const action = await new ActionDialog({ + $body: tpl.typoMsg(i18n('Cannot preview this file. Download now?')), + }).open(); + if (action === 'ok') window.open(link); + return null; + } + if (['png', 'jpeg', 'jpg', 'gif', 'webp', 'bmp'].includes(ext)) return previewImage(link); + if (ext === 'pdf') return previewPDF(`${link}${link.includes('?') ? '&' : '?'}noDisposition=1`); + Notification.info(i18n('Loading file...')); + try { + const { url } = await request.get(link); + if (/^(doc|xls|ppt)x?$/.test(ext)) return previewOffice(link, url); + content = await request.get(url, undefined, { dataType: 'text' }); + } catch (e) { + Notification.error(i18n('Failed to load file: {0}', e.message)); + throw e; + } + } else Notification.info(i18n('Loading editor...')); + const val = await startEdit(filename, content); + if (typeof val !== 'string') return null; + Notification.info(i18n('Saving file...')); + const data = new FormData(); + data.append('filename', filename); + data.append('file', new Blob([val], { type: 'text/plain' })); + if (type) data.append('type', type); + data.append('operation', 'upload_file'); + const postUrl = !window.location.href.endsWith('/files') + ? `${window.location.href.substring(0, window.location.href.lastIndexOf('/'))}/files` : ''; + await request.postFile(postUrl, data); + Notification.success(i18n('File saved.')); + return pjax.request({ push: false }); +} + +const dataPreviewPage = new AutoloadPage('dataPreview', () => { + $(document).on('click', '[data-preview]', previewFile); +}); + +export default dataPreviewPage; diff --git a/packages/ui-default/locales/zh.yaml b/packages/ui-default/locales/zh.yaml index cbac82a9d..2077006f5 100644 --- a/packages/ui-default/locales/zh.yaml +++ b/packages/ui-default/locales/zh.yaml @@ -168,6 +168,7 @@ Continue: 继续 Contributed Problems: 贡献的题目 Contributions: 贡献 Copy Email: 复制电子邮件 +Copy Link: 复制链接 Copy from: 复制自 Copy QQ Number: 复制QQ号 Copy Selected: 复制选中 @@ -650,6 +651,7 @@ Record {0} not found.: 记录 {0} 不存在。 record_detail: 记录详情 record_main: 评测记录 Records: 评测记录 +Reference link copied to clipboard!: 引用链接已复制到剪贴板。 Refresh Records: 刷新评测记录 Refresh: 刷新 Registered at: 注册于 diff --git a/packages/ui-default/pages/files.page.js b/packages/ui-default/pages/files.page.js index 67140ab7c..0688ebac1 100644 --- a/packages/ui-default/pages/files.page.js +++ b/packages/ui-default/pages/files.page.js @@ -1,4 +1,3 @@ -import Clipboard from 'clipboard'; import $ from 'jquery'; import _ from 'lodash'; import { ConfirmDialog, Dialog } from 'vj/components/dialog/index'; @@ -154,16 +153,6 @@ function handleDrop(ev) { } const page = new NamedPage(['home_files', 'contest_files'], () => { - const clip = new Clipboard('.files .col--name', { - text: (trigger) => new URL(trigger.children[0].href, window.location.href).toString(), - }); - clip.on('success', () => { - Notification.success(i18n('Download link copied to clipboard!'), 1000); - }); - clip.on('error', () => { - Notification.error(i18n('Copy failed :(')); - }); - $(document).on('click', '.files .col--name', (ev) => ev.preventDefault()); $(document).on('click', '[name="upload_file"]', () => handleClickUpload()); $(document).on('click', '[name="remove_selected"]', () => handleClickRemoveSelected()); $(document).on('dragover', '.files', (ev) => handleDragOver(ev)); diff --git a/packages/ui-default/pages/problem_files.page.js b/packages/ui-default/pages/problem_files.page.js index c27cac623..5f8538852 100644 --- a/packages/ui-default/pages/problem_files.page.js +++ b/packages/ui-default/pages/problem_files.page.js @@ -1,11 +1,9 @@ import $ from 'jquery'; import _ from 'lodash'; -import { - ActionDialog, ConfirmDialog, Dialog, - InfoDialog, -} from 'vj/components/dialog/index'; +import { ConfirmDialog, Dialog } from 'vj/components/dialog/index'; import createHint from 'vj/components/hint'; import Notification from 'vj/components/notification'; +import { previewFile } from 'vj/components/preview/preview.page'; import download from 'vj/components/zipDownloader'; import { NamedPage } from 'vj/misc/Page'; import i18n from 'vj/utils/i18n'; @@ -172,100 +170,11 @@ const page = new NamedPage('problem_files', () => { handleClickUpload(type, files); } - async function startEdit(filename, value) { - const { default: Editor } = await import('vj/components/editor/index'); - const promise = new ActionDialog({ - $body: tpl` -
- -
`, - width: `${window.innerWidth - 200}px`, - height: `${window.innerHeight - 100}px`, - cancelByEsc: false, - }).open(); - const languages = [ - ['yaml', ['yaml', 'yml']], - ['cpp', ['c', 'cc', 'cpp', 'h', 'hpp']], - ['json', ['json']], - ['plain', ['in', 'out', 'ans']], - ]; - const language = languages.filter((i) => i[1].includes(filename.split('.').pop()))[0]?.[0] || 'auto'; - const editor = new Editor($('[name="fileContent"]'), { - value, - autoResize: false, - autoLayout: false, - language, - model: `hydro://problem/file/${filename}`, - }); - const action = await promise; - value = editor.value().replace(/\r\n/g, '\n'); - editor.destory(); - if (action !== 'ok') return null; - return value; - } - /** - * @param {string} type - * @param {JQuery.ClickEvent} ev - */ - async function handleEdit(type, ev) { - if (ev?.metaKey || ev?.ctrlKey || ev?.shiftKey) return; - if (ev) ev.preventDefault(); - const filename = ev - ? ev.currentTarget.closest('[data-filename]').getAttribute('data-filename') - // eslint-disable-next-line no-alert - : prompt('Filename'); - if (!filename) return; - const filesize = ev - ? +ev.currentTarget.closest('[data-size]').getAttribute('data-size') - : 0; - let content = ''; - if (ev) { - const link = $(ev.currentTarget).find('a').attr('href'); - if (!link) return; - const ext = filename.split('.').pop(); - if (['png', 'jpeg', 'jpg', 'gif', 'webp', 'bmp'].includes(ext)) { - await new InfoDialog({ - $body: tpl`
`, - }).open(); - return; - } - if (['zip', 'rar', '7z'].includes(ext) || filesize > 8 * 1024 * 1024) { - const action = await new ActionDialog({ - $body: tpl.typoMsg(i18n('Cannot preview this file. Download now?')), - }).open(); - if (action === 'ok') window.open(link); - return; - } - Notification.info(i18n('Loading file...')); - try { - const res = await request.get(link); - content = await request.get(res.url, undefined, { dataType: 'text' }); - } catch (e) { - window.captureException?.(e); - Notification.error(i18n('Failed to load file: {0}', e.message)); - throw e; - } - } else Notification.info(i18n('Loading editor...')); - const val = await startEdit(filename, content); - if (typeof val !== 'string') return; - Notification.info(i18n('Saving file...')); - const data = new FormData(); - data.append('filename', filename); - data.append('file', new Blob([val], { type: 'text/plain' })); - data.append('type', type); - data.append('operation', 'upload_file'); - await request.postFile('', data); - Notification.success(i18n('File saved.')); - await pjax.request({ push: false }); - } - if ($('[name="upload_testdata"]').length) { - $(document).on('click', '.problem-files-testdata .col--name', (ev) => handleEdit('testdata', ev)); - $(document).on('click', '.problem-files-additional_file .col--name', (ev) => handleEdit('additional_file', ev)); $(document).on('click', '[name="upload_testdata"]', () => handleClickUpload('testdata')); $(document).on('click', '[name="upload_file"]', () => handleClickUpload('additional_file')); - $(document).on('click', '[name="create_testdata"]', () => handleEdit('testdata')); - $(document).on('click', '[name="create_file"]', () => handleEdit('additional_file')); + $(document).on('click', '[name="create_testdata"]', () => previewFile(undefined, 'testdata')); + $(document).on('click', '[name="create_file"]', () => previewFile(undefined, 'additional_file')); $(document).on('click', '[name="remove_selected_testdata"]', () => handleClickRemoveSelected('testdata')); $(document).on('click', '[name="remove_selected_file"]', () => handleClickRemoveSelected('additional_file')); } diff --git a/packages/ui-default/templates/partials/files.html b/packages/ui-default/templates/partials/files.html index cfc33f40a..b2cb16a43 100644 --- a/packages/ui-default/templates/partials/files.html +++ b/packages/ui-default/templates/partials/files.html @@ -27,7 +27,7 @@ - + {{ file.name }} {{ size(file.size) }} diff --git a/packages/ui-default/templates/partials/problem_files-additional_file.html b/packages/ui-default/templates/partials/problem_files-additional_file.html index d57bbc238..a3feae9d5 100644 --- a/packages/ui-default/templates/partials/problem_files-additional_file.html +++ b/packages/ui-default/templates/partials/problem_files-additional_file.html @@ -26,7 +26,7 @@ - + {{ file.name }} {{ size(file.size) }} diff --git a/packages/ui-default/templates/partials/problem_files-testdata.html b/packages/ui-default/templates/partials/problem_files-testdata.html index 9a947d3e6..42002e6d0 100644 --- a/packages/ui-default/templates/partials/problem_files-testdata.html +++ b/packages/ui-default/templates/partials/problem_files-testdata.html @@ -30,7 +30,7 @@ - + {% if handler.user.own(pdoc) or handler.user.hasPriv(PRIV.PRIV_READ_PROBLEM_DATA) or handler.user.hasPerm(perm.PERM_READ_PROBLEM_DATA) %} {{ file.name }} {% else %} diff --git a/packages/ui-default/templates/partials/problem_sidebar_additional_file.html b/packages/ui-default/templates/partials/problem_sidebar_additional_file.html index 4d4ef2413..6194a1301 100644 --- a/packages/ui-default/templates/partials/problem_sidebar_additional_file.html +++ b/packages/ui-default/templates/partials/problem_sidebar_additional_file.html @@ -32,7 +32,7 @@ {%- for file in additional_file -%} - + {{ file.name }} {{ size(file.size) }} diff --git a/packages/ui-default/templates/partials/problem_sidebar_testdata.html b/packages/ui-default/templates/partials/problem_sidebar_testdata.html index 62f80d6c8..9316d66d5 100644 --- a/packages/ui-default/templates/partials/problem_sidebar_testdata.html +++ b/packages/ui-default/templates/partials/problem_sidebar_testdata.html @@ -33,7 +33,7 @@ {%- for file in testdata -%} - {{ file.name }} + {{ file.name }} {{ size(file.size) }}