mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-17 09:36:08 +08:00
core&ui: support pasting image in editor
This commit is contained in:
1 parent
6a289f788d
commit
8223b45d23
13 files changed
+131
-39
No files matched your search
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "hydrooj",
|
||||
"version": "3.5.5",
|
||||
"version": "3.5.6",
|
||||
"bin": "bin/hydrooj.js",
|
||||
"main": "src/loader",
|
||||
"module": "src/loader",
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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']) };
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -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 = [''];
|
||||
[, 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: `}) `,
|
||||
}], (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;
|
||||
|
||||
@@ -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,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",
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user