core&ui: support pasting image in editor

This commit is contained in:
undefined committed 2022-03-01 21:40:09 +08:00
1 parent 6a289f788d
commit 8223b45d23
13 files changed
+131 -39

No files matched your search

+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "hydrooj",
"version": "3.5.5",
"version": "3.5.6",
"bin": "bin/hydrooj.js",
"main": "src/loader",
"module": "src/loader",
+13 -1
View File
@@ -790,8 +790,20 @@ export class ProblemCreateHandler extends Handler {
domainId: string, title: string, content: string, pid: string,
hidden = false, difficulty = 0, tag: string[] = [], assign: string[] = [],
) {
if (pid && await problem.get(domainId, pid)) throw new BadRequestError('invalid pid');
if (pid && await problem.get(domainId, pid)) throw new BadRequestError('ProblemID already exists');
const docId = await problem.add(domainId, pid, title, content, this.user._id, tag ?? [], hidden, assign);
const files = new Set(Array.from(content.matchAll(/file:\/\/([a-zA-Z0-9_-]+\.[a-zA-Z0-9]+)/g)).map((i) => i[1]));
const tasks = [];
for (const file of files) {
if (this.user._files.find((i) => i.name === file)) {
tasks.push(
storage.rename(`user/${this.user._id}/${file}`, `problem/${domainId}/${docId}/additional_file/${file}`)
.then(() => problem.addAdditionalFile(domainId, docId, file, '', true)),
user.setById(this.user._id, { _files: this.user._files.filter((i) => i.name !== file) }),
);
}
}
await Promise.all(tasks);
if (difficulty) await problem.edit(domainId, docId, { difficulty });
this.response.body = { pid: pid || docId };
this.response.redirect = this.url('problem_files', { pid: pid || docId });
+2 -2
View File
@@ -264,10 +264,10 @@ export class ProblemModel {
await bus.emit('problem/delTestdata', domainId, pid, names);
}
static async addAdditionalFile(domainId: string, pid: number, name: string, f: Readable | Buffer | string) {
static async addAdditionalFile(domainId: string, pid: number, name: string, f: Readable | Buffer | string, skipUpload = false) {
const [[, fileinfo]] = await Promise.all([
document.getSub(domainId, document.TYPE_PROBLEM, pid, 'additional_file', name),
storage.put(`problem/${domainId}/${pid}/additional_file/${name}`, f),
skipUpload || storage.put(`problem/${domainId}/${pid}/additional_file/${name}`, f),
]);
const meta = await storage.getMeta(`problem/${domainId}/${pid}/additional_file/${name}`);
const payload = { name, ...pick(meta, ['size', 'lastModified', 'etag']) };
+7 -2
View File
@@ -108,8 +108,13 @@ class StorageService {
accessKey: this.opts.accessKey,
secretKey: this.opts.secretKey,
});
const exists = await this.client.bucketExists(this.opts.bucket);
if (!exists) await this.client.makeBucket(this.opts.bucket, this.opts.region);
try {
const exists = await this.client.bucketExists(this.opts.bucket);
if (!exists) await this.client.makeBucket(this.opts.bucket, this.opts.region);
} catch (e) {
// Some platform doesn't support bucketExists & makeBucket API.
// Ignore this error.
}
this.replaceWithAlternativeUrlFor = {
user: parseAlternativeEndpointUrl(this.opts.endPointForUser),
judge: parseAlternativeEndpointUrl(this.opts.endPointForJudge),
+1
View File
@@ -686,6 +686,7 @@ const scripts: UpgradeScript[] = [
async function _59_60() {
const langs = await system.get('hydrooj.langs');
await system.set('hydrooj.langs', langs.replace(/\$\{dir\}/g, '/w').replace(/\$\{name\}/g, 'foo'));
return true;
},
];
+1 -3
View File
@@ -140,9 +140,7 @@ env.addGlobal('set', (obj, key, val) => {
else Object.assign(obj, key);
return '';
});
env.addGlobal('findSubModule', (prefix) => {
filter(Object.keys(global.Hydro.ui.template), (n) => n.startsWith(prefix));
});
env.addGlobal('findSubModule', (prefix) => Object.keys(global.Hydro.ui.template).filter((n) => n.startsWith(prefix)));
async function render(name, state) {
// eslint-disable-next-line no-return-await
@@ -2,7 +2,9 @@ import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
import { EditorAction, registerEditorAction } from 'monaco-editor/esm/vs/editor/browser/editorExtensions';
import { IQuickInputService } from 'monaco-editor/esm/vs/platform/quickinput/common/quickInput';
import list from 'monaco-themes/themes/themelist.json';
import { nanoid } from 'nanoid';
import i18n from 'vj/utils/i18n';
import request from 'vj/utils/request';
import './monaco.styl';
export default monaco;
@@ -59,6 +61,72 @@ class ChangeThemeAction extends EditorAction {
registerEditorAction(ChangeThemeAction);
const pagename = document.documentElement.getAttribute('data-page');
const isProblemPage = ['problem_create', 'problem_edit'].includes(pagename);
const isProblemEdit = pagename === 'problem_edit';
function handlePasteEvent(editor: monaco.editor.IStandaloneCodeEditor) {
window.addEventListener('paste', (ev: ClipboardEvent) => {
if (!editor.hasTextFocus()) return;
const selection = editor.getSelection();
const { items } = ev.clipboardData;
let wrapper = ['', ''];
let ext;
const matches = items[0].type.match(/^image\/(png|jpg|jpeg|gif)$/i);
if (matches) {
wrapper = ['![image](', ')'];
[, ext] = matches;
} else if (items[0].type === 'application/x-zip-compressed') {
wrapper = ['[file](', ')'];
ext = 'zip';
}
if (!ext) return;
const filename = `${nanoid()}.${ext}`;
const data = new FormData();
data.append('filename', filename);
data.append('file', items[0].getAsFile());
data.append('operation', 'upload_file');
if (isProblemEdit) data.append('type', 'additional_file');
let range: monaco.Range = null;
editor.executeEdits('', [{
range: new monaco.Range(selection.endLineNumber, selection.endColumn, selection.endLineNumber, selection.endColumn),
text: `![image](${i18n('Preparing Upload...')}) `,
}], (inverseOp) => { range = inverseOp[0].range; return null; });
editor.setPosition(editor.getSelection().getEndPosition());
function updateText(text: string) {
const pos = editor.getSelection();
const rangeBefore = range;
editor.executeEdits('', [{ range, text: `${wrapper[0]}${text}${wrapper[1]} ` }], (inverseOp) => {
range = inverseOp[0].range;
if (pos.endLineNumber !== pos.startLineNumber || pos.endLineNumber !== range.endLineNumber) return null;
const delta = rangeBefore.endColumn - range.endColumn;
editor.setPosition(new monaco.Position(pos.endLineNumber, pos.endColumn - delta));
return null;
});
}
let progress = 0;
request.postFile(isProblemEdit ? './file' : '/file', data, {
xhr() {
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener('loadstart', () => updateText(i18n('Uploading...')));
xhr.upload.addEventListener('progress', (e) => {
if (!e.lengthComputable) return;
const percentComplete = Math.round((e.loaded / e.total) * 100);
if (percentComplete === progress) return;
progress = percentComplete;
updateText(`${i18n('Uploading...')} ${percentComplete}%`);
}, false);
return xhr;
},
})
.then(() => updateText(`${isProblemPage ? 'file://' : `/file/${UserContext._id}/`}${filename}`))
.catch((e) => {
console.error(e);
updateText(`${i18n('Upload Failed')}: ${e.message}`);
});
});
}
export function registerAction(
editor: monaco.editor.IStandaloneCodeEditor,
model: monaco.editor.IModel,
@@ -92,5 +160,6 @@ export function registerAction(
if (model.getLanguageId() === 'markdown') {
const suggestWidget = (editor.getContribution('editor.contrib.suggestController') as any).widget?.value;
if (suggestWidget?._setDetailsVisible) suggestWidget._setDetailsVisible(true);
handlePasteEvent(editor);
}
}
@@ -92,6 +92,7 @@ export default class Tab extends DOMAttachedObject {
.removeClass('active');
this.$content
.css('x', '0');
$newTab.trigger('vjContentShow');
// 4. Finalize
this.currentIndex = idx;
+7
View File
@@ -52,6 +52,8 @@ About test data: 关于测试数据
about: 关于
About: 关于
AC Message: 通过消息
Accept: 通过
Accepted Problem: 通过的题目
Accepted Ratio: 通过率
Accepted: 已通过
Access Control: 访问控制
@@ -574,6 +576,7 @@ Problem Settings: 题目设置
Problem Solution List: 题解列表
Problem Solution: 题解列表
Problem Tags Visibility: 题目标签可见性
Problem Tags: 题目标签
problem_create: 创建题目
problem_edit: 编辑题目
problem_import: 导入题目
@@ -825,8 +828,12 @@ User ID: 用户 ID
User will no longer be allowed to join the domain after expiration.: 过期后,用户将不能加入此域。
user_detail: 用户
user_login: 登录
Uploading...: 正在上传...
Upload Failed: 上传失败
Preparing Upload...: 准备上传...
user_logout: 登出
user_lostpass: 忘记密码
user_register_with_code: 注册
user_register: 注册
User-defined role: 用户定义的角色
User: 用户
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@hydrooj/ui-default",
"version": "4.32.10",
"version": "4.32.11",
"author": "undefined <i@undefined.moe>",
"license": "AGPL-3.0",
"main": "hydro.js",
+24 -25
View File
@@ -99,35 +99,34 @@
{% endif %}
</div>
</li>
{% if pdocs.length %}
<li class="section__tab-item">
<h1 class="section__tab-title">{{ _('Accepted Problems') }}</h1>
<div class="section__tab-main">
<div class="typo section__body no-media">
{% set _mark = Math.ceil(pdocs.length/4) %}
{% macro col(pdocs) %}
<div class="medium-3 columns"><ul>
{% for pdoc in pdocs %}
<li>{{ problem.render_problem_title(pdoc, show_tags=false, small=true) }}</li>
{% endfor %}
</ul></div>
{% endmacro %}
<div class="row">
{{ col(pdocs.slice(0, _mark)) }}
{{ col(pdocs.slice(_mark, _mark*2)) }}
{{ col(pdocs.slice(_mark*2, _mark*3)) }}
{{ col(pdocs.slice(_mark*3, pdocs.length - 1)) }}
</div>
</div>
</div>
</li>
{% endif %}
{% for page in findSubModule('user_detail/') %}{% include page %}{% endfor %}
</ul>
</div>
</div>
</div>
{% if pdocs.length %}
<div class="section">
<div class="section__header">
<h2 class="section__title">{{ _('Accepted Problems') }}</h2>
</div>
<div class="typo section__body no-media">
{% set _mark = Math.ceil(pdocs.length/4) %}
{% macro col(pdocs) %}
<div class="medium-3 columns">
<ul>
{% for pdoc in pdocs %}
<li>{{ problem.render_problem_title(pdoc, show_tags=false, small=true) }}</li>
{% endfor %}
</ul>
</div>
{% endmacro %}
<div class="row">
{{ col(pdocs.slice(0, _mark)) }}
{{ col(pdocs.slice(_mark, _mark*2)) }}
{{ col(pdocs.slice(_mark*2, _mark*3)) }}
{{ col(pdocs.slice(_mark*3, pdocs.length - 1)) }}
</div>
</div>
</div>
{% endif %}
</div>
<div class="medium-3 columns">
{% if isSelfProfile %}
+2 -2
View File
@@ -21,8 +21,6 @@ const request = {
reject(err);
} else if (jqXHR.readyState === 0) {
reject(new Error(i18n('Network error')));
} else if (errorThrown instanceof Error) {
reject(errorThrown);
} else if (typeof jqXHR.responseJSON === 'object' && jqXHR.responseJSON.error) {
const { error } = jqXHR.responseJSON;
if (error.params) {
@@ -32,6 +30,8 @@ const request = {
err.params = error.params;
reject(err);
} else reject(new Error(jqXHR.responseJSON.error.message));
} else if (errorThrown instanceof Error) {
reject(errorThrown);
} else {
reject(new Error(textStatus));
}
+2 -2
View File
@@ -28,7 +28,7 @@ declare global {
interface SetConstructor {
isSuperset: (set: Set<any>, subset: Set<any>) => boolean;
intersection: <T>(setA: Set<T> | Array<T>, setB: Set<T> | Array<T>) => Set<T>;
union: <T>(setA: Set<T>, setB: Set<T>) => Set<T>;
union: <T>(setA: Set<T> | Array<T>, setB: Set<T> | Array<T>) => Set<T>;
}
}
@@ -124,7 +124,7 @@ Set.isSuperset = function isSuperset(set, subset) {
return true;
};
Set.union = function Union<T>(setA: Set<T>, setB: Set<T>) {
Set.union = function Union<T>(setA: Set<T> | Array<T>, setB: Set<T> | Array<T>) {
const union = new Set(setA);
for (const elem of setB) union.add(elem);
return union;