mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-20 01:38:19 +08:00
Merge branch 'master' into monaco
This commit is contained in:
93 files changed
+1414
-736
No files matched your search
@@ -1,7 +1,7 @@
|
||||
import esbuild from 'esbuild';
|
||||
import {
|
||||
Context, fs, Handler, Logger, NotFoundError, param, SettingModel, sha1,
|
||||
size, Types, UiContextBase,
|
||||
size, SystemModel, Types, UiContextBase,
|
||||
} from 'hydrooj';
|
||||
import { debounce } from 'lodash';
|
||||
import { tmpdir } from 'os';
|
||||
@@ -46,6 +46,7 @@ const build = async (contents: string) => {
|
||||
format: 'iife' as 'iife',
|
||||
bundle: true,
|
||||
outdir: tmp,
|
||||
sourcemap: SystemModel.get('ui-default.nosourcemap') ? false : 'external',
|
||||
splitting: false,
|
||||
write: false,
|
||||
target: ['chrome65'],
|
||||
@@ -103,11 +104,11 @@ export async function buildUI() {
|
||||
}
|
||||
const entry = await build([
|
||||
`window.lazyloadMetadata = ${JSON.stringify(hashes)};`,
|
||||
`window.LANGS=${JSON.stringify(SettingModel.langs)};`,
|
||||
...entryPoints.map((i) => `import '${relative(tmp, i).replace(/\\/g, '\\\\')}';`),
|
||||
].join('\n'));
|
||||
const pages = entry.outputFiles.map((i) => i.text);
|
||||
const str = `window.LANGS=${JSON.stringify(SettingModel.langs)};${pages.join('\n')}`;
|
||||
addFile('entry.js', str);
|
||||
const pages = entry.outputFiles.filter((i) => i.path.endsWith('.js')).map((i) => i.text);
|
||||
addFile('entry.js', `window._hydroLoad=()=>{ ${pages.join('\n')} };`);
|
||||
UiContextBase.constantVersion = hashes['entry.js'];
|
||||
for (const key in vfs) {
|
||||
if (newFiles.includes(key)) continue;
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
// Process 
|
||||
// ^^^^^^^^ this size specification
|
||||
function parseNextNumber(str, pos, max) {
|
||||
const start = pos;
|
||||
const result = {
|
||||
ok: false,
|
||||
pos,
|
||||
value: '',
|
||||
};
|
||||
let code = str.charCodeAt(pos);
|
||||
while (pos < max && (code >= 0x30 /* 0 */ && code <= 0x39 /* 9 */) || code === 0x25 /* % */) {
|
||||
code = str.charCodeAt(++pos);
|
||||
}
|
||||
result.ok = true;
|
||||
result.pos = pos;
|
||||
result.value = str.slice(start, pos);
|
||||
return result;
|
||||
}
|
||||
|
||||
function parseImageSize(str, pos, max) {
|
||||
const result = {
|
||||
ok: false,
|
||||
pos: 0,
|
||||
width: '',
|
||||
height: '',
|
||||
};
|
||||
if (pos >= max) { return result; }
|
||||
let code = str.charCodeAt(pos);
|
||||
if (code !== 0x3d /* = */) { return result; }
|
||||
pos++;
|
||||
code = str.charCodeAt(pos);
|
||||
if (code !== 0x78 /* x */ && (code < 0x30 || code > 0x39) /* [0-9] */) {
|
||||
return result;
|
||||
}
|
||||
// parse width
|
||||
const resultW = parseNextNumber(str, pos, max);
|
||||
pos = resultW.pos;
|
||||
// next charactor must be 'x'
|
||||
code = str.charCodeAt(pos);
|
||||
if (code !== 0x78 /* x */) { return result; }
|
||||
pos++;
|
||||
// parse height
|
||||
const resultH = parseNextNumber(str, pos, max);
|
||||
pos = resultH.pos;
|
||||
result.width = resultW.value;
|
||||
result.height = resultH.value;
|
||||
result.pos = pos;
|
||||
result.ok = true;
|
||||
return result;
|
||||
}
|
||||
|
||||
export default function plugin(md) {
|
||||
md.inline.ruler.before('emphasis', 'image', (state, silent) => {
|
||||
let attrs;
|
||||
let code;
|
||||
let label;
|
||||
let pos;
|
||||
let ref;
|
||||
let res;
|
||||
let title;
|
||||
let width = '';
|
||||
let height = '';
|
||||
let token;
|
||||
let tokens;
|
||||
let start;
|
||||
let href = '';
|
||||
const oldPos = state.pos;
|
||||
const max = state.posMax;
|
||||
|
||||
if (state.src.charCodeAt(state.pos) !== 0x21/* ! */) { return false; }
|
||||
if (state.src.charCodeAt(state.pos + 1) !== 0x5B/* [ */) { return false; }
|
||||
|
||||
const labelStart = state.pos + 2;
|
||||
const labelEnd = md.helpers.parseLinkLabel(state, state.pos + 1, false);
|
||||
|
||||
// parser failed to find ']', so it's not a valid link
|
||||
if (labelEnd < 0) { return false; }
|
||||
|
||||
pos = labelEnd + 1;
|
||||
if (pos < max && state.src.charCodeAt(pos) === 0x28/* ( */) {
|
||||
//
|
||||
// Inline link
|
||||
//
|
||||
|
||||
// [link]( <href> "title" )
|
||||
// ^^ skipping these spaces
|
||||
pos++;
|
||||
for (; pos < max; pos++) {
|
||||
code = state.src.charCodeAt(pos);
|
||||
if (code !== 0x20 && code !== 0x0A) { break; }
|
||||
}
|
||||
if (pos >= max) { return false; }
|
||||
|
||||
// [link]( <href> "title" )
|
||||
// ^^^^^^ parsing link destination
|
||||
start = pos;
|
||||
res = md.helpers.parseLinkDestination(state.src, pos, state.posMax);
|
||||
if (res.ok) {
|
||||
href = state.md.normalizeLink(res.str);
|
||||
if (state.md.validateLink(href)) {
|
||||
pos = res.pos;
|
||||
} else {
|
||||
href = '';
|
||||
}
|
||||
}
|
||||
|
||||
// [link]( <href> "title" )
|
||||
// ^^ skipping these spaces
|
||||
start = pos;
|
||||
for (; pos < max; pos++) {
|
||||
code = state.src.charCodeAt(pos);
|
||||
if (code !== 0x20 && code !== 0x0A) { break; }
|
||||
}
|
||||
|
||||
// [link]( <href> "title" )
|
||||
// ^^^^^^^ parsing link title
|
||||
res = md.helpers.parseLinkTitle(state.src, pos, state.posMax);
|
||||
if (pos < max && start !== pos && res.ok) {
|
||||
title = res.str;
|
||||
pos = res.pos;
|
||||
|
||||
// [link]( <href> "title" )
|
||||
// ^^ skipping these spaces
|
||||
for (; pos < max; pos++) {
|
||||
code = state.src.charCodeAt(pos);
|
||||
if (code !== 0x20 && code !== 0x0A) { break; }
|
||||
}
|
||||
} else {
|
||||
title = '';
|
||||
}
|
||||
|
||||
// [link]( <href> "title" =WxH )
|
||||
// ^^^^ parsing image size
|
||||
if (pos - 1 >= 0) {
|
||||
code = state.src.charCodeAt(pos - 1);
|
||||
|
||||
// there must be at least one white spaces
|
||||
// between previous field and the size
|
||||
if (code === 0x20) {
|
||||
res = parseImageSize(state.src, pos, state.posMax);
|
||||
if (res.ok) {
|
||||
width = res.width;
|
||||
height = res.height;
|
||||
pos = res.pos;
|
||||
|
||||
// [link]( <href> "title" =WxH )
|
||||
// ^^ skipping these spaces
|
||||
for (; pos < max; pos++) {
|
||||
code = state.src.charCodeAt(pos);
|
||||
if (code !== 0x20 && code !== 0x0A) { break; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (pos >= max || state.src.charCodeAt(pos) !== 0x29/* ) */) {
|
||||
state.pos = oldPos;
|
||||
return false;
|
||||
}
|
||||
pos++;
|
||||
} else {
|
||||
//
|
||||
// Link reference
|
||||
//
|
||||
if (typeof state.env.references === 'undefined') { return false; }
|
||||
|
||||
// [foo] [bar]
|
||||
// ^^ optional whitespace (can include newlines)
|
||||
for (; pos < max; pos++) {
|
||||
code = state.src.charCodeAt(pos);
|
||||
if (code !== 0x20 && code !== 0x0A) { break; }
|
||||
}
|
||||
|
||||
if (pos < max && state.src.charCodeAt(pos) === 0x5B/* [ */) {
|
||||
start = pos + 1;
|
||||
pos = md.helpers.parseLinkLabel(state, pos);
|
||||
if (pos >= 0) {
|
||||
label = state.src.slice(start, pos++);
|
||||
} else {
|
||||
pos = labelEnd + 1;
|
||||
}
|
||||
} else {
|
||||
pos = labelEnd + 1;
|
||||
}
|
||||
|
||||
// covers label === '' and label === undefined
|
||||
// (collapsed reference link and shortcut reference link respectively)
|
||||
label ||= state.src.slice(labelStart, labelEnd);
|
||||
|
||||
ref = state.env.references[md.utils.normalizeReference(label)];
|
||||
if (!ref) {
|
||||
state.pos = oldPos;
|
||||
return false;
|
||||
}
|
||||
href = ref.href;
|
||||
title = ref.title;
|
||||
}
|
||||
|
||||
//
|
||||
// We found the end of the link, and know for a fact it's a valid link;
|
||||
// so all that's left to do is to call tokenizer.
|
||||
//
|
||||
if (!silent) {
|
||||
state.pos = labelStart;
|
||||
state.posMax = labelEnd;
|
||||
|
||||
const newState = new state.md.inline.State(
|
||||
state.src.slice(labelStart, labelEnd),
|
||||
state.md,
|
||||
state.env,
|
||||
tokens = [],
|
||||
);
|
||||
newState.md.inline.tokenize(newState);
|
||||
token = state.push('image', 'img', 0);
|
||||
token.attrs = attrs = [['src', href],
|
||||
['alt', '']];
|
||||
token.children = tokens;
|
||||
if (title) {
|
||||
attrs.push(['title', title]);
|
||||
}
|
||||
|
||||
if (width !== '') {
|
||||
attrs.push(['width', width]);
|
||||
}
|
||||
|
||||
if (height !== '') {
|
||||
attrs.push(['height', height]);
|
||||
}
|
||||
}
|
||||
|
||||
state.pos = pos;
|
||||
state.posMax = max;
|
||||
return true;
|
||||
});
|
||||
};
|
||||
+6
-5
@@ -1,7 +1,8 @@
|
||||
// https://github.com/waylonflinn/markdown-it-katex/blob/master/index.js
|
||||
const katex = require('katex');
|
||||
import { Logger } from 'hydrooj';
|
||||
import katex from 'katex';
|
||||
|
||||
const logger = new global.Hydro.Logger('katex');
|
||||
const logger = new Logger('katex');
|
||||
|
||||
function isValidDelim(state, pos) {
|
||||
const max = state.posMax;
|
||||
@@ -109,8 +110,8 @@ function block(state, start, end, silent) {
|
||||
return true;
|
||||
}
|
||||
|
||||
module.exports = function plugin(md) {
|
||||
const options = { throwOnError: false, strict: 'ignore' };
|
||||
export default function plugin(md) {
|
||||
const options = { throwOnError: false, strict: 'ignore', displayMode: false };
|
||||
const render = (latex: string, displayMode = false) => {
|
||||
options.displayMode = displayMode;
|
||||
try {
|
||||
@@ -136,4 +137,4 @@ module.exports = function plugin(md) {
|
||||
});
|
||||
md.renderer.rules.math_inline = inlineRenderer;
|
||||
md.renderer.rules.math_block = blockRenderer;
|
||||
};
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
/* eslint-disable max-len */
|
||||
/* eslint-disable prefer-destructuring */
|
||||
|
||||
import { randomUUID } from 'crypto';
|
||||
import MarkdownIt from 'markdown-it';
|
||||
import type MarkdownIt from 'markdown-it';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
const allowFullScreen = ' webkitallowfullscreen mozallowfullscreen allowfullscreen';
|
||||
|
||||
@@ -110,7 +110,7 @@ export function Media(md: MarkdownIt) {
|
||||
if (service === 'pdf') {
|
||||
if (src.startsWith('file://') || src.startsWith('./')) src += src.includes('?') ? '&noDisposition=1' : '?noDisposition=1';
|
||||
return `\
|
||||
<object classid="clsid:${randomUUID().toUpperCase()}">
|
||||
<object classid="clsid:${uuid().toUpperCase()}">
|
||||
<param name="SRC" value="${src}" >
|
||||
<embed width="100%" style="min-height: 100vh;border: none;" fullscreen="yes" src="${src}">
|
||||
<noembed></noembed>
|
||||
|
||||
@@ -23,7 +23,7 @@ const MergeCells = require('markdown-it-merge-cells');
|
||||
// @[youtube](https://youtube.com/watch?v=xxx)
|
||||
// @[pdf](https://foo.com/bar.pdf)
|
||||
const { Media } = require('./markdown-it-media');
|
||||
const Katex = require('./markdown-it-katex');
|
||||
const { default: Katex } = require('./markdown-it-katex');
|
||||
const { xssProtector } = require('./markdown-it-xss');
|
||||
|
||||
class Markdown extends MarkdownIt {
|
||||
|
||||
@@ -2,17 +2,42 @@
|
||||
import { sentryWebpackPlugin } from '@sentry/webpack-plugin';
|
||||
import { CleanWebpackPlugin } from 'clean-webpack-plugin';
|
||||
import CopyWebpackPlugin from 'copy-webpack-plugin';
|
||||
import { version as coreJsVersion } from 'core-js/package.json';
|
||||
import compat from 'core-js-compat';
|
||||
import { EsbuildPlugin } from 'esbuild-loader';
|
||||
import fs from 'fs';
|
||||
import { DuplicatesPlugin } from 'inspectpack/plugin';
|
||||
import ExtractCssPlugin from 'mini-css-extract-plugin';
|
||||
import packageJson from 'package-json';
|
||||
import { dirname } from 'path';
|
||||
import { gt } from 'semver';
|
||||
import webpack from 'webpack';
|
||||
import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
|
||||
import { WebpackManifestPlugin } from 'webpack-manifest-plugin';
|
||||
import WebpackBar from 'webpackbar';
|
||||
import { version } from '../../package.json';
|
||||
import root from '../utils/root';
|
||||
|
||||
export default function (env: { watch?: boolean, production?: boolean, measure?: boolean } = {}) {
|
||||
const {
|
||||
list,
|
||||
targets,
|
||||
} = compat({
|
||||
targets: '> 1%, chrome 70, firefox 60, safari 16, ios_saf 16, not ie 11, not op_mini all',
|
||||
modules: [
|
||||
'core-js/stable',
|
||||
],
|
||||
exclude: [],
|
||||
version: coreJsVersion,
|
||||
inverse: false,
|
||||
});
|
||||
fs.writeFileSync(root('__core-js.js'), `${list.map((i) => `import 'core-js/modules/${i}';`).join('\n')}\n`);
|
||||
|
||||
export default async function (env: { watch?: boolean, production?: boolean, measure?: boolean } = {}) {
|
||||
if (env.production) console.log(targets);
|
||||
let isNew = false;
|
||||
const { version: latest } = await packageJson('@hydrooj/ui-default', { version: 'latest' });
|
||||
if (typeof version === 'string' && gt(version, latest)) isNew = true;
|
||||
|
||||
function cssLoader() {
|
||||
return {
|
||||
loader: 'css-loader',
|
||||
@@ -61,11 +86,9 @@ export default function (env: { watch?: boolean, production?: boolean, measure?:
|
||||
// sentry requires source-map while keep it simple in dev mode
|
||||
devtool: env.production ? 'source-map' : false,
|
||||
entry: {
|
||||
hydro: './entry.js',
|
||||
polyfill: './polyfill.ts',
|
||||
[`hydro-${version}`]: './entry.js',
|
||||
'default.theme': './theme/default.js',
|
||||
'service-worker': './service-worker.ts',
|
||||
'messages-shared-worker': './components/message/worker.ts',
|
||||
},
|
||||
cache: {
|
||||
type: 'filesystem',
|
||||
@@ -78,6 +101,7 @@ export default function (env: { watch?: boolean, production?: boolean, measure?:
|
||||
output: {
|
||||
path: root('public'),
|
||||
publicPath: '/', // overwrite in entry.js
|
||||
workerPublicPath: '/',
|
||||
hashFunction: 'sha1',
|
||||
hashDigest: 'hex',
|
||||
hashDigestLength: 10,
|
||||
@@ -233,18 +257,30 @@ export default function (env: { watch?: boolean, production?: boolean, measure?:
|
||||
filename: '[name].css?[fullhash:6]',
|
||||
}),
|
||||
new WebpackManifestPlugin({}),
|
||||
new webpack.IgnorePlugin({ resourceRegExp: /(^\.\/locale$|mathjax|abcjs|vditor.+\.d\.ts)/ }),
|
||||
new webpack.IgnorePlugin({ resourceRegExp: /(^\.\/locale$)/ }),
|
||||
new CopyWebpackPlugin({
|
||||
patterns: [
|
||||
{ from: root('static') },
|
||||
{ from: root('components/navigation/nav-logo-small_dark.png'), to: 'components/navigation/nav-logo-small_dark.png' },
|
||||
{ from: root(`${dirname(require.resolve('streamsaver/package.json'))}/mitm.html`), to: 'streamsaver/mitm.html' },
|
||||
{ from: root(`${dirname(require.resolve('streamsaver/package.json'))}/sw.js`), to: 'streamsaver/sw.js' },
|
||||
{ from: root(`${dirname(require.resolve('vditor/package.json'))}/dist`), to: 'vditor/dist' },
|
||||
{ from: root(`${dirname(require.resolve('graphiql/package.json'))}/graphiql.min.css`), to: 'graphiql.min.css' },
|
||||
{ from: `${dirname(require.resolve('monaco-themes/package.json'))}/themes`, to: 'monaco/themes/' },
|
||||
],
|
||||
}),
|
||||
sentryWebpackPlugin({
|
||||
authToken: process.env.SENTRY_AUTH_TOKEN,
|
||||
org: 'hydro-dev',
|
||||
project: 'hydro-web',
|
||||
url: 'https://sentry.hydro.ac',
|
||||
sourcemaps: {
|
||||
rewriteSources: (source) => source.replace('@hydrooj/ui-default/../../node_modules/', ''),
|
||||
},
|
||||
release: (process.env.CI && isNew) ? {
|
||||
name: `hydro-web@${version}`,
|
||||
uploadLegacySourcemaps: root('public'),
|
||||
} : {},
|
||||
}),
|
||||
new webpack.DefinePlugin({
|
||||
'process.env.VERSION': JSON.stringify(require('@hydrooj/ui-default/package.json').version),
|
||||
}),
|
||||
@@ -265,6 +301,7 @@ export default function (env: { watch?: boolean, production?: boolean, measure?:
|
||||
url: 'https://sentry.hydro.ac',
|
||||
}),
|
||||
] : []),
|
||||
new webpack.NormalModuleReplacementPlugin(/core-js\/stable/, root('__core-js.js')),
|
||||
...env.measure ? [
|
||||
new BundleAnalyzerPlugin({ analyzerPort: 'auto' }),
|
||||
new DuplicatesPlugin(),
|
||||
|
||||
@@ -19,7 +19,7 @@ const argv = cac().parse();
|
||||
async function runWebpack({
|
||||
watch, production, measure, dev, https,
|
||||
}) {
|
||||
const compiler = webpack(webpackConfig({ watch, production, measure }));
|
||||
const compiler = webpack(await webpackConfig({ watch, production, measure }));
|
||||
if (dev) {
|
||||
const server = new WebpackDevServer({
|
||||
port: https ? 8001 : 8000,
|
||||
@@ -49,7 +49,10 @@ async function runWebpack({
|
||||
return;
|
||||
}
|
||||
if (argv.options.detail) console.log(stats.toString());
|
||||
if (!watch && (!stats || stats.hasErrors())) process.exitCode = 1;
|
||||
if (!watch && (!stats || stats.hasErrors())) {
|
||||
if (!argv.options.detail) console.log(stats.toString());
|
||||
process.exitCode = 1;
|
||||
}
|
||||
resolve(stats);
|
||||
}
|
||||
if (watch) compiler.watch({}, compilerCallback);
|
||||
@@ -121,22 +124,9 @@ async function main() {
|
||||
if (argv.options.gulp) await runGulp();
|
||||
else {
|
||||
await runWebpack(argv.options as any);
|
||||
if (fs.existsSync('public/hydro.js')) {
|
||||
fs.copyFileSync('public/hydro.js', `public/hydro-${pkg.version}.js`);
|
||||
}
|
||||
if (fs.existsSync('public/polyfill.js')) {
|
||||
fs.copyFileSync('public/polyfill.js', `public/polyfill-${pkg.version}.js`);
|
||||
}
|
||||
if (fs.existsSync('public/theme.css')) {
|
||||
fs.copyFileSync('public/theme.css', `public/theme-${pkg.version}.css`);
|
||||
}
|
||||
if (argv.options.production) {
|
||||
for (const f of ['echarts', 'graphviz', 'mermaid', 'mathjax']) {
|
||||
fs.removeSync(`public/vditor/dist/js/${f}`);
|
||||
}
|
||||
const files = fs.readdirSync('public');
|
||||
files.filter((i) => /(^[in]\..+|worker)\.js\.map$/.test(i)).forEach((i) => fs.removeSync(`public/${i}`));
|
||||
}
|
||||
await Promise.all(globbySync('public/**/*.map').map((i) => fs.remove(i)));
|
||||
}
|
||||
process.chdir(dir);
|
||||
|
||||
@@ -24,9 +24,8 @@ const Component = React.forwardRef<any, any>((props, ref) => {
|
||||
);
|
||||
});
|
||||
|
||||
export default class CustomSelectAutoComplete<Multi extends boolean> extends AutoComplete {
|
||||
export default class CustomSelectAutoComplete<Multi extends boolean> extends AutoComplete<CustomSelectOptions> {
|
||||
static DOMAttachKey = 'ucwCustomSelectAutoCompleteInstance';
|
||||
options: CustomSelectOptions & AutoCompleteOptions;
|
||||
|
||||
constructor($dom, options: CustomSelectOptions & AutoCompleteOptions<Multi>) {
|
||||
super($dom, {
|
||||
|
||||
@@ -27,9 +27,8 @@ const Component = React.forwardRef<any, any>((props, ref) => {
|
||||
);
|
||||
});
|
||||
|
||||
export default class FileSelectAutoComplete<Multi extends boolean> extends AutoComplete {
|
||||
export default class FileSelectAutoComplete<Multi extends boolean> extends AutoComplete<FileSelectOptions> {
|
||||
static DOMAttachKey = 'ucwFileSelectAutoCompleteInstance';
|
||||
options: FileSelectOptions & AutoCompleteOptions;
|
||||
|
||||
constructor($dom, options: FileSelectOptions & AutoCompleteOptions<Multi>) {
|
||||
super($dom, {
|
||||
|
||||
@@ -11,10 +11,7 @@ const CustomSelectAutoComplete = React.forwardRef<any, any>((props, ref) => (
|
||||
<AutoComplete<DefaultProps>
|
||||
ref={ref as any}
|
||||
fetchItems={(keys) => props.data.filter((i) => (i._id ? keys.includes(i._id) : keys.includes(i)))}
|
||||
queryItems={(query) => props.data.filter((i) => (i.name
|
||||
? i.name.toLowerCase().match(query.toLowerCase())
|
||||
: i.toString().toLowerCase().match(query.toLowerCase())
|
||||
))}
|
||||
queryItems={(query) => props.data.filter((i) => (i.name || i).toString().toLowerCase().includes(query.toLowerCase()))}
|
||||
itemText={(item) => `${item.name || item}`}
|
||||
itemKey={(item) => `${item._id?.toString() || item.name || item}`}
|
||||
renderItem={(item) => `${item.name || item}`}
|
||||
|
||||
@@ -8,6 +8,7 @@ export interface AutoCompleteOptions<Multi extends boolean = boolean> {
|
||||
defaultItems?: string;
|
||||
width?: string;
|
||||
height?: string;
|
||||
classes?: string;
|
||||
listStyle?: any;
|
||||
allowEmptyQuery?: boolean;
|
||||
freeSolo?: boolean;
|
||||
@@ -18,17 +19,17 @@ export interface AutoCompleteOptions<Multi extends boolean = boolean> {
|
||||
text?: () => string;
|
||||
}
|
||||
|
||||
export default class AutoComplete extends DOMAttachedObject {
|
||||
export default class AutoComplete<Options extends Record<string, any> = {}> extends DOMAttachedObject {
|
||||
static DOMAttachKey = 'ucwAutoCompleteInstance';
|
||||
ref = null;
|
||||
container = document.createElement('div');
|
||||
options: AutoCompleteOptions;
|
||||
options: AutoCompleteOptions & Options;
|
||||
component = ReactDOM.createRoot(this.container);
|
||||
changeListener = [
|
||||
(val) => this.$dom.val(val),
|
||||
];
|
||||
|
||||
constructor($dom, options = {}) {
|
||||
constructor($dom, options = {} as Options) {
|
||||
super($dom);
|
||||
this.options = {
|
||||
items: async () => [],
|
||||
|
||||
@@ -6,9 +6,13 @@
|
||||
|
||||
.drop.nav__dropdown
|
||||
max-height: calc(100% - 60px)
|
||||
overflow-x: hidden
|
||||
overflow-y: auto
|
||||
box-shadow: 0rem 0.125rem 0.4375rem rgba(0 0 0 , 30%);
|
||||
|
||||
.drop.nav__dropdown.drop-open
|
||||
overflow-x: auto
|
||||
|
||||
.dropdown
|
||||
.menu
|
||||
min-width: rem(150px)
|
||||
|
||||
@@ -1,12 +1,3 @@
|
||||
.vditor
|
||||
margin-bottom: rem(10px)
|
||||
|
||||
.vditor-preview__action
|
||||
display: none
|
||||
|
||||
.vditor ol
|
||||
list-style: decimal
|
||||
|
||||
.decoration-hide
|
||||
color: grey !important
|
||||
background-color: grey !important
|
||||
+103
-109
@@ -1,36 +1,11 @@
|
||||
import $ from 'jquery';
|
||||
import type { editor } from 'monaco-editor';
|
||||
import { nanoid } from 'nanoid';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import DOMAttachedObject from 'vj/components/DOMAttachedObject';
|
||||
import Notification from 'vj/components/notification';
|
||||
import { i18n, request } from 'vj/utils';
|
||||
|
||||
export const config = {
|
||||
toolbar: [
|
||||
'emoji', 'headings', 'bold', 'italic', 'strike', 'link', '|',
|
||||
'list', 'ordered-list', 'check', 'outdent', 'indent', '|',
|
||||
'quote', 'line', 'code', 'inline-code', 'table', '|',
|
||||
'upload', 'edit-mode', 'fullscreen', 'export',
|
||||
],
|
||||
mode: UserContext.preferredEditorType || 'ir',
|
||||
toolbarConfig: {
|
||||
pin: true,
|
||||
},
|
||||
cdn: `${UiContext.cdn_prefix}vditor`,
|
||||
counter: {
|
||||
enable: true,
|
||||
max: 65536,
|
||||
},
|
||||
preview: {
|
||||
math: {
|
||||
inlineDigit: true,
|
||||
},
|
||||
theme: {
|
||||
path: `${UiContext.cdn_prefix}vditor/dist/css/content-theme`,
|
||||
current: 'light',
|
||||
},
|
||||
},
|
||||
};
|
||||
import { i18n } from 'vj/utils';
|
||||
import uploadFiles from '../upload';
|
||||
|
||||
interface MonacoOptions {
|
||||
language?: string;
|
||||
@@ -43,23 +18,23 @@ interface MonacoOptions {
|
||||
hide?: string[];
|
||||
lineNumbers?: 'on' | 'off' | 'relative' | 'interval';
|
||||
}
|
||||
interface VditorOptions {
|
||||
theme?: 'classic' | 'dark'
|
||||
}
|
||||
type Options = MonacoOptions & VditorOptions;
|
||||
type Options = MonacoOptions;
|
||||
|
||||
export default class Editor extends DOMAttachedObject {
|
||||
static DOMAttachKey = 'vjEditorInstance';
|
||||
model: editor.IModel;
|
||||
editor: editor.IStandaloneCodeEditor;
|
||||
vditor: import('vditor').default;
|
||||
markdownEditor: any;
|
||||
valueCache?: string;
|
||||
setMarkdownEditorValue?: (v: string) => void;
|
||||
reactRoot?: ReactDOM.Root;
|
||||
isValid: boolean;
|
||||
|
||||
constructor($dom, public options: Options = {}) {
|
||||
super($dom);
|
||||
if (UserContext.preferredEditorType === 'monaco') this.initMonaco();
|
||||
else if (options.language && options.language !== 'markdown') this.initMonaco();
|
||||
else this.initVditor();
|
||||
else this.initMarkdownEditor();
|
||||
}
|
||||
|
||||
async initMonaco() {
|
||||
@@ -169,87 +144,102 @@ export default class Editor extends DOMAttachedObject {
|
||||
window.editor = this.editor;
|
||||
}
|
||||
|
||||
async initVditor() {
|
||||
async initMarkdownEditor() {
|
||||
const pagename = document.documentElement.getAttribute('data-page');
|
||||
const isProblemPage = ['problem_create', 'problem_edit'].includes(pagename);
|
||||
const isProblemEdit = pagename === 'problem_edit';
|
||||
const { default: Vditor } = await import('vditor');
|
||||
const that = this;
|
||||
const { $dom } = this;
|
||||
const hasFocus = $dom.is(':focus') || $dom.hasClass('autofocus');
|
||||
const origin = $dom.get(0);
|
||||
const ele = document.createElement('div');
|
||||
const value = $dom.val();
|
||||
const { onChange } = this.options;
|
||||
await new Promise((resolve) => {
|
||||
this.vditor = new Vditor(ele, {
|
||||
...config,
|
||||
upload: {
|
||||
multiple: false,
|
||||
handler: (files) => {
|
||||
let wrapper = ['', ''];
|
||||
let ext: string;
|
||||
const matches = files[0].type.match(/^image\/(png|jpg|jpeg|gif)$/i);
|
||||
if (matches) {
|
||||
wrapper = [''];
|
||||
[, ext] = matches;
|
||||
} else if (files[0].type === 'application/x-zip-compressed') {
|
||||
wrapper = ['[file](', ')'];
|
||||
ext = 'zip';
|
||||
}
|
||||
if (!ext) return i18n('No Supported file type.');
|
||||
const filename = `${nanoid()}.${ext}`;
|
||||
const data = new FormData();
|
||||
data.append('filename', filename);
|
||||
data.append('file', files[0]);
|
||||
data.append('operation', 'upload_file');
|
||||
if (isProblemEdit) data.append('type', 'additional_file');
|
||||
let progress = 0;
|
||||
request.postFile(isProblemEdit ? './files' : '/file', data, {
|
||||
xhr: () => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.upload.addEventListener('loadstart', () => this.vditor.vditor.tip.show(i18n('Uploading...'), 0));
|
||||
xhr.upload.addEventListener('progress', (e) => {
|
||||
if (!e.lengthComputable) return;
|
||||
const percentComplete = Math.round((e.loaded / e.total) * 100);
|
||||
if (percentComplete === progress) return;
|
||||
progress = percentComplete;
|
||||
this.vditor.vditor.tip.show(`${i18n('Uploading...')} ${percentComplete}%`, 0);
|
||||
}, false);
|
||||
return xhr;
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
this.vditor.insertValue(`${wrapper.join(`${isProblemPage ? 'file://' : `/file/${UserContext._id}/`}${filename}`)} `);
|
||||
this.vditor.vditor.tip.hide();
|
||||
})
|
||||
.catch((e: { message: string; }) => {
|
||||
console.error(e);
|
||||
return `${i18n('Upload Failed')}: ${e.message}`;
|
||||
});
|
||||
return null;
|
||||
},
|
||||
},
|
||||
...this.options,
|
||||
after: () => {
|
||||
const pos = $(ele).find('button[data-mode="sv"]').get();
|
||||
const button = $('<button data-mode="monaco">Monaco Editor</button>');
|
||||
button.on('click', (e) => {
|
||||
Notification.info('You can select this in PreferenceSettings');
|
||||
e.preventDefault();
|
||||
});
|
||||
button.insertAfter(pos);
|
||||
resolve(null);
|
||||
},
|
||||
input(v) {
|
||||
const { MdEditor } = await import('./mdeditor');
|
||||
|
||||
const renderCallback = (ref) => {
|
||||
this.markdownEditor = ref;
|
||||
};
|
||||
|
||||
function EditorComponent() {
|
||||
const [val, setVal] = React.useState(value);
|
||||
that.setMarkdownEditorValue = setVal;
|
||||
return <MdEditor
|
||||
className='textbox'
|
||||
autoFocus={hasFocus}
|
||||
codeTheme='github'
|
||||
codeStyleReverse={false}
|
||||
ref={renderCallback}
|
||||
modelValue={val}
|
||||
noMermaid
|
||||
noPrettier
|
||||
autoDetectCode
|
||||
toolbarsExclude={[
|
||||
// 'bold',
|
||||
// 'underline',
|
||||
// 'italic',
|
||||
// '-',
|
||||
// 'strikeThrough',
|
||||
// 'title',
|
||||
// 'sub',
|
||||
// 'sup',
|
||||
// 'quote',
|
||||
// 'unorderedList',
|
||||
// 'orderedList',
|
||||
// 'task',
|
||||
// '-',
|
||||
// 'codeRow',
|
||||
// 'code',
|
||||
// 'link',
|
||||
// 'image',
|
||||
// 'table',
|
||||
'mermaid',
|
||||
// 'katex',
|
||||
// '-',
|
||||
// 'revoke',
|
||||
// 'next',
|
||||
'save',
|
||||
// '=',
|
||||
'pageFullscreen',
|
||||
'fullscreen',
|
||||
// 'preview',
|
||||
'previewOnly',
|
||||
'htmlPreview',
|
||||
// 'catalog',
|
||||
'github',
|
||||
]}
|
||||
onChange={(v) => {
|
||||
that.valueCache = v;
|
||||
setVal(v);
|
||||
$dom.val(v);
|
||||
$dom.text(v);
|
||||
if (onChange) onChange(v);
|
||||
},
|
||||
value,
|
||||
cache: { id: Math.random().toString() },
|
||||
});
|
||||
});
|
||||
$(ele).addClass('textbox');
|
||||
onChange?.(v);
|
||||
}}
|
||||
onUploadImg={async (files, callback) => {
|
||||
let ext: string;
|
||||
const matches = files[0].type.match(/^image\/(png|jpg|jpeg|gif)$/i);
|
||||
if (matches) {
|
||||
[, ext] = matches;
|
||||
} else if (files[0].type === 'application/x-zip-compressed') {
|
||||
ext = 'zip';
|
||||
}
|
||||
if (!ext) return i18n('No Supported file type.');
|
||||
const filename = `${nanoid()}.${ext}`;
|
||||
await uploadFiles(isProblemEdit ? './files' : '/file', [files[0]], {
|
||||
type: isProblemEdit ? 'additional_file' : undefined,
|
||||
filenameCallback: () => filename,
|
||||
}).then(() => {
|
||||
callback([`${isProblemPage ? 'file://' : `/file/${UserContext._id}/`}${filename}`]);
|
||||
}).catch(() => {
|
||||
callback([]);
|
||||
});
|
||||
return null;
|
||||
}}
|
||||
/>;
|
||||
}
|
||||
|
||||
this.reactRoot = ReactDOM.createRoot(ele);
|
||||
this.reactRoot.render(<EditorComponent />);
|
||||
$dom.hide();
|
||||
origin.parentElement.appendChild(ele);
|
||||
this.isValid = true;
|
||||
@@ -258,7 +248,7 @@ export default class Editor extends DOMAttachedObject {
|
||||
|
||||
destroy() {
|
||||
this.detach();
|
||||
if (this.vditor?.destroy) this.vditor.destroy();
|
||||
if (this.reactRoot) this.reactRoot.unmount();
|
||||
else if (this.editor?.dispose) this.editor.dispose();
|
||||
}
|
||||
|
||||
@@ -268,13 +258,17 @@ export default class Editor extends DOMAttachedObject {
|
||||
|
||||
value(val?: string) {
|
||||
this.ensureValid();
|
||||
if (typeof val === 'string') return (this.editor || this.vditor).setValue(val);
|
||||
return (this.editor || this.vditor).getValue({ lineEnding: '\n', preserveBOM: false });
|
||||
if (typeof val === 'string') {
|
||||
if (this.editor) return this.editor.setValue(val);
|
||||
this.setMarkdownEditorValue?.(val);
|
||||
this.markdownEditor?.resetHistory?.();
|
||||
}
|
||||
if (this.editor) return this.editor.getValue({ lineEnding: '\n', preserveBOM: false });
|
||||
return this.valueCache;
|
||||
}
|
||||
|
||||
focus() {
|
||||
this.ensureValid();
|
||||
this.vditor?.focus();
|
||||
if (!this.editor || !this.model) return;
|
||||
this.editor.focus();
|
||||
const range = this.model.getFullModelRange();
|
||||
@@ -0,0 +1,39 @@
|
||||
import 'md-editor-rt/lib/style.css';
|
||||
|
||||
import Anchor from 'markdown-it-anchor';
|
||||
import Footnote from 'markdown-it-footnote';
|
||||
import Mark from 'markdown-it-mark';
|
||||
import MergeCells from 'markdown-it-merge-cells';
|
||||
import TOC from 'markdown-it-table-of-contents';
|
||||
import { config, MdEditor } from 'md-editor-rt';
|
||||
import Imsize from '../../backendlib/markdown-it-imsize';
|
||||
import { Media } from '../../backendlib/markdown-it-media';
|
||||
import { xssProtector } from '../../backendlib/markdown-it-xss';
|
||||
|
||||
const pagename = document.documentElement.getAttribute('data-page');
|
||||
const isProblemPage = ['problem_create', 'problem_edit'].includes(pagename);
|
||||
|
||||
config({
|
||||
markdownItConfig(mdit) {
|
||||
mdit.options.html = true;
|
||||
mdit.options.linkify = true;
|
||||
mdit.linkify.tlds('.py', false);
|
||||
mdit.linkify.tlds('.zip', false);
|
||||
mdit.linkify.tlds('.mov', false);
|
||||
mdit.use(Media);
|
||||
mdit.use(Footnote);
|
||||
mdit.use(Mark);
|
||||
mdit.use(Imsize);
|
||||
mdit.use(Anchor);
|
||||
mdit.use(TOC);
|
||||
mdit.use(MergeCells);
|
||||
mdit.use(xssProtector);
|
||||
if (isProblemPage) {
|
||||
mdit.core.ruler.before('normalize', 'xss', (state) => {
|
||||
state.src = state.src.replace(/file:\/\//g, pagename === 'problem_create' ? `/file/${UserContext._id}/` : './file/');
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export { MdEditor };
|
||||
@@ -1,5 +1,4 @@
|
||||
import { nanoid } from 'nanoid';
|
||||
import ReconnectingWebsocket from 'reconnecting-websocket';
|
||||
import { InfoDialog } from 'vj/components/dialog';
|
||||
import VjNotification from 'vj/components/notification/index';
|
||||
import {
|
||||
@@ -7,6 +6,7 @@ import {
|
||||
} from 'vj/constant/message';
|
||||
import { AutoloadPage } from 'vj/misc/Page';
|
||||
import { i18n, tpl } from 'vj/utils';
|
||||
import Sock from '../socket';
|
||||
|
||||
let previous: VjNotification;
|
||||
const onmessage = (msg) => {
|
||||
@@ -61,7 +61,7 @@ const onmessage = (msg) => {
|
||||
|
||||
const initWorkerMode = (endpoint) => {
|
||||
console.log('Messages: using SharedWorker');
|
||||
const worker = new SharedWorker('/messages-shared-worker.js', { name: 'HydroMessagesWorker' });
|
||||
const worker = new SharedWorker(new URL('./worker.ts', import.meta.url), { name: 'HydroMessagesWorker' });
|
||||
worker.port.start();
|
||||
window.addEventListener('beforeunload', () => {
|
||||
worker.port.postMessage({ type: 'unload' });
|
||||
@@ -128,12 +128,8 @@ const messagePage = new AutoloadPage('messagePage', (pagename) => {
|
||||
isMaster = true;
|
||||
localStorage.setItem('page.master', selfId);
|
||||
const masterChannel = new BroadcastChannel('hydro-messages');
|
||||
const sock = new ReconnectingWebsocket(endpoint);
|
||||
sock.onopen = () => console.log('Connected');
|
||||
sock.onerror = console.error;
|
||||
sock.onclose = (...args) => console.log('Closed', ...args);
|
||||
const sock = new Sock(endpoint);
|
||||
sock.onmessage = async (message) => {
|
||||
if (process.env.NODE_ENV !== 'production') console.log('onmessage: ', message);
|
||||
const payload = JSON.parse(message.data);
|
||||
masterChannel.postMessage({ type: 'message', payload });
|
||||
};
|
||||
|
||||
@@ -31,12 +31,17 @@ function broadcastMsg(message: any) {
|
||||
function initConn(path: string, port: MessagePort, cookie: any) {
|
||||
if (cookie !== lcookie) conn?.close();
|
||||
else if (conn && conn.readyState === conn.OPEN) return;
|
||||
const url = new URL(path, location.origin);
|
||||
if (cookie) url.searchParams.set('sid', cookie.split('sid=')[1].split(';')[0]);
|
||||
lcookie = cookie;
|
||||
console.log('Init connection for', path);
|
||||
conn?.close();
|
||||
conn = new ReconnectingWebsocket(path);
|
||||
conn = new ReconnectingWebsocket(url.toString());
|
||||
ports.push(port);
|
||||
conn.onopen = () => conn.send(cookie);
|
||||
conn.onopen = () => {
|
||||
console.log('Connected to', path);
|
||||
broadcastMsg({ type: 'open' });
|
||||
};
|
||||
conn.onerror = () => broadcastMsg({ type: 'error' });
|
||||
conn.onclose = (ev) => broadcastMsg({ type: 'close', error: ev.reason });
|
||||
conn.onmessage = (message) => {
|
||||
@@ -46,14 +51,9 @@ function initConn(path: string, port: MessagePort, cookie: any) {
|
||||
return;
|
||||
}
|
||||
const payload = JSON.parse(message.data);
|
||||
if (payload.event === 'auth') {
|
||||
if (['PermissionError', 'PrivilegeError'].includes(payload.error)) {
|
||||
broadcastMsg({ type: 'close', error: payload.error });
|
||||
conn.close();
|
||||
} else {
|
||||
console.log('Connected to', path);
|
||||
broadcastMsg({ type: 'open' });
|
||||
}
|
||||
if (['PermissionError', 'PrivilegeError'].includes(payload.error)) {
|
||||
broadcastMsg({ type: 'close', error: payload.error });
|
||||
conn.close();
|
||||
} else {
|
||||
broadcastMsg({ type: 'message', payload });
|
||||
let acked = false;
|
||||
|
||||
@@ -87,8 +87,8 @@ export function configYamlFormat(config: ProblemConfigFile) {
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(class MonacoEditor extends React.PureComponent<Props> {
|
||||
disposable = [];
|
||||
containerElement: HTMLElement;
|
||||
private __preventUpdate = false;
|
||||
private __preventFormat = false;
|
||||
__preventUpdate = false;
|
||||
__preventFormat = false;
|
||||
|
||||
editor: editor.IStandaloneCodeEditor;
|
||||
model: editor.ITextModel;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import $ from 'jquery';
|
||||
import _ from 'lodash';
|
||||
import DOMAttachedObject from 'vj/components/DOMAttachedObject';
|
||||
|
||||
const TAB_TRANSITION_DURATION = 300;
|
||||
@@ -144,5 +143,3 @@ export default class Tab extends DOMAttachedObject {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
_.assign(Tab, DOMAttachedObject);
|
||||
@@ -73,6 +73,26 @@
|
||||
background: $secondary-color
|
||||
content: ''
|
||||
|
||||
.section__tab-container:not(.immersive)
|
||||
.section__tab-header
|
||||
padding-top: 0 !important
|
||||
|
||||
.section__tab-header-item
|
||||
border-top: 3px solid transparent;
|
||||
transition: border-color 0.2s, background 0.2s, color 0.2s;
|
||||
transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
|
||||
margin-right: 0
|
||||
|
||||
&:hover
|
||||
border-color: #5f9fd6;
|
||||
background: #f8f8f8;
|
||||
|
||||
&.tab--active
|
||||
border-color: #ed5f82;
|
||||
|
||||
&.selected
|
||||
border-color: #ed5f82;
|
||||
|
||||
.section__tab-content
|
||||
position: relative
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ interface UploadOptions {
|
||||
pjax?: boolean;
|
||||
sidebar?: boolean;
|
||||
singleFileUploadCallback?: (file: File) => any;
|
||||
filenameCallback?: (file: File) => string;
|
||||
}
|
||||
export default async function uploadFiles(endpoint = '', files: File[] | FileList = [], options: UploadOptions = {}) {
|
||||
const dialog = new Dialog({
|
||||
@@ -38,7 +39,7 @@ export default async function uploadFiles(endpoint = '', files: File[] | FileLis
|
||||
if (Number.isNaN(+i)) continue;
|
||||
const file = files[i];
|
||||
const data = new FormData();
|
||||
data.append('filename', file.name);
|
||||
data.append('filename', options.filenameCallback?.(file) || file.name);
|
||||
data.append('file', file);
|
||||
if (options.type) data.append('type', options.type);
|
||||
data.append('operation', 'upload_file');
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import './polyfill';
|
||||
|
||||
import * as Sentry from '@sentry/browser';
|
||||
import $ from 'jquery';
|
||||
|
||||
@@ -29,17 +31,23 @@ console.log(
|
||||
window.UiContext = JSON.parse(window.UiContext);
|
||||
window.UserContext = JSON.parse(window.UserContext);
|
||||
if (process.env.NODE_ENV === 'production' && !UiContext.sentry_disable) {
|
||||
window.captureException = (e) => Sentry.captureException(e);
|
||||
window.captureException = (e) => {
|
||||
if (!e.isUserFacingError) Sentry.captureException(e);
|
||||
};
|
||||
Sentry.init({
|
||||
dsn: UiContext.sentry_dsn || 'https://04d1d77b095231d55e1202bdc912ff16@sentry.hydro.ac/3',
|
||||
release: process.env.VERSION,
|
||||
dsn: UiContext.sentry_dsn || 'https://2f95d53751e08c74c1af1c4b93ccaff7@sentry.hydro.ac/2',
|
||||
release: `hydro-web@${process.env.VERSION}`,
|
||||
integrations: [
|
||||
Sentry.browserTracingIntegration(),
|
||||
Sentry.replayIntegration(),
|
||||
Sentry.browserApiErrorsIntegration(),
|
||||
Sentry.replayIntegration({
|
||||
networkRequestHeaders: ['Content-Type'],
|
||||
networkResponseHeaders: ['Content-Type', 'Location'],
|
||||
}),
|
||||
],
|
||||
tracesSampleRate: 0.1,
|
||||
tracePropagationTargets: ['localhost', /^\//, window.location.host],
|
||||
replaysSessionSampleRate: 0.1,
|
||||
replaysSessionSampleRate: 0.03,
|
||||
replaysOnErrorSampleRate: 1.0,
|
||||
});
|
||||
}
|
||||
@@ -57,16 +65,10 @@ const PageLoader = '<div class="page-loader nojs--hide" style="display:none;"><d
|
||||
$('body').prepend(PageLoader);
|
||||
$('.page-loader').fadeIn(500);
|
||||
|
||||
const prefetch = Promise.all([
|
||||
fetch(`/constant/${UiContext.constantVersion}.js`).then((r) => r.text()),
|
||||
import('./api'),
|
||||
]);
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
Object.assign(window.UiContext, JSON.parse(window.UiContextNew));
|
||||
Object.assign(window.UserContext, JSON.parse(window.UserContextNew));
|
||||
const [data, HydroExports] = await prefetch;
|
||||
Object.assign(window, { HydroExports });
|
||||
eval(data); // eslint-disable-line no-eval
|
||||
await HydroExports.initPageLoader();
|
||||
window.HydroExports = await import('./api');
|
||||
await window._hydroLoad();
|
||||
await window.HydroExports.initPageLoader();
|
||||
}, false);
|
||||
@@ -10,13 +10,6 @@ class WikiHelpHandler extends Handler {
|
||||
noCheckPermView = true;
|
||||
|
||||
async get() {
|
||||
const LANGS = SettingModel.langs;
|
||||
const languages = {};
|
||||
for (const key in LANGS) {
|
||||
if (LANGS[key].hidden) continue;
|
||||
languages[`${LANGS[key].display}(${key})`] = LANGS[key].compile || LANGS[key].execute;
|
||||
}
|
||||
this.response.body = { languages };
|
||||
this.response.template = 'wiki_help.html';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@hydrooj/ui-default",
|
||||
"version": "4.50.2",
|
||||
"version": "4.51.4",
|
||||
"author": "undefined <i@undefined.moe>",
|
||||
"license": "AGPL-3.0",
|
||||
"main": "index.ts",
|
||||
@@ -14,13 +14,13 @@
|
||||
"cli": false
|
||||
},
|
||||
"devDependencies": {
|
||||
"@blueprintjs/core": "^5.10.3",
|
||||
"@blueprintjs/core": "^5.10.5",
|
||||
"@codingame/monaco-editor-wrapper": "^6.6.10",
|
||||
"@codingame/monaco-vscode-language-pack-ko": "5.3.0",
|
||||
"@codingame/monaco-vscode-language-pack-zh-hans": "5.3.0",
|
||||
"@codingame/monaco-vscode-language-pack-zh-hant": "5.3.0",
|
||||
"@codingame/monaco-vscode-typescript-language-features-default-extension": "5.3.0",
|
||||
"@cordisjs/core": "^3.16.2",
|
||||
"@cordisjs/core": "3.17.3",
|
||||
"@fontsource/dm-mono": "^5.0.20",
|
||||
"@fontsource/fira-code": "^5.0.18",
|
||||
"@fontsource/inconsolata": "^5.0.18",
|
||||
@@ -30,12 +30,13 @@
|
||||
"@fontsource/source-code-pro": "^5.0.18",
|
||||
"@fontsource/ubuntu-mono": "^5.0.20",
|
||||
"@hydrooj/utils": "workspace:^",
|
||||
"@sentry/browser": "^8.8.0",
|
||||
"@sentry/webpack-plugin": "^2.18.0",
|
||||
"@sentry/browser": "^8.15.0",
|
||||
"@sentry/webpack-plugin": "^2.20.1",
|
||||
"@simplewebauthn/browser": "7.4.0",
|
||||
"@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",
|
||||
@@ -45,25 +46,28 @@
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@types/redux-logger": "^3.0.13",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@types/vscode": "^1.89.0",
|
||||
"@types/webpack-env": "^1.18.5",
|
||||
"@vscode/codicons": "^0.0.36",
|
||||
"ajv": "^8.16.0",
|
||||
"allotment": "^1.20.2",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"browser-update": "^3.3.51",
|
||||
"browser-update": "^3.3.53",
|
||||
"chalk": "^5.3.0",
|
||||
"classnames": "^2.5.1",
|
||||
"clean-webpack-plugin": "^4.0.0",
|
||||
"clipboard": "^2.0.11",
|
||||
"copy-webpack-plugin": "^12.0.2",
|
||||
"core-js": "^3.37.1",
|
||||
"core-js-compat": "^3.37.1",
|
||||
"css-loader": "^6.11.0",
|
||||
"diff": "^5.2.0",
|
||||
"diff-dom": "^5.1.3",
|
||||
"echarts": "^5.5.0",
|
||||
"echarts": "^5.5.1",
|
||||
"emojis-keywords": "2.0.0",
|
||||
"emojis-list": "2.1.0",
|
||||
"esbuild-loader": "^4.1.0",
|
||||
"esbuild-loader": "^4.2.0",
|
||||
"fancy-log": "^2.0.0",
|
||||
"flatpickr": "^4.6.13",
|
||||
"graphiql": "^2.4.7",
|
||||
@@ -72,17 +76,19 @@
|
||||
"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.3",
|
||||
"mini-css-extract-plugin": "^2.9.0",
|
||||
"moment": "^2.30.1",
|
||||
"monaco-editor": "npm:@codingame/monaco-vscode-editor-api@5.3.0",
|
||||
"monaco-languageclient": "^8.4.0",
|
||||
"monaco-themes": "^0.4.4",
|
||||
"monaco-yaml": "^5.1.1",
|
||||
"monaco-yaml": "5.1.1",
|
||||
"nanoid": "^5.0.7",
|
||||
"normalize.css": "^8.0.1",
|
||||
"nprogress": "^0.2.0",
|
||||
@@ -95,7 +101,6 @@
|
||||
"prop-types": "^15.8.1",
|
||||
"qface": "^1.4.1",
|
||||
"qrcode": "^1.5.3",
|
||||
"queue-microtask": "^1.2.3",
|
||||
"react": "^18.3.1",
|
||||
"react-colorful": "^5.6.1",
|
||||
"react-dnd": "^16.0.1",
|
||||
@@ -129,7 +134,7 @@
|
||||
"vscode": "npm:@codingame/monaco-vscode-api@^5.3.0",
|
||||
"vscode-languageclient": "^9.0.1",
|
||||
"web-streams-polyfill": "^4.0.0",
|
||||
"webpack": "^5.91.0",
|
||||
"webpack": "^5.92.1",
|
||||
"webpack-bundle-analyzer": "^4.10.2",
|
||||
"webpack-dev-server": "^5.0.4",
|
||||
"webpack-manifest-plugin": "^5.0.0",
|
||||
@@ -142,10 +147,10 @@
|
||||
"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": "^8.6.7",
|
||||
"markdown-it-anchor": "^9.0.1",
|
||||
"markdown-it-attrs": "^4.1.6",
|
||||
"markdown-it-footnote": "^4.0.0",
|
||||
"markdown-it-imsize": "^2.0.1",
|
||||
@@ -155,6 +160,7 @@
|
||||
"nunjucks": "^3.2.4",
|
||||
"schemastery-jsonschema": "^1.1.0",
|
||||
"streamsaver": "^2.0.6",
|
||||
"uuid": "^10.0.0",
|
||||
"xss": "^1.0.15"
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
+33
-23
@@ -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 <div className="contest-problems" style={{ margin: '1em' }}>
|
||||
{pids.map((i) => <a href={`#p${i}`} className={ans[i] ? 'pass ' : ''}>
|
||||
<span class="id">{i}</span>
|
||||
{ans[i] && <span class="icon icon-check"></span>}
|
||||
<span className="id">{i}</span>
|
||||
{ans[i] && <span className="icon icon-check"></span>}
|
||||
</a>)}
|
||||
</div>;
|
||||
}
|
||||
|
||||
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
|
||||
? `<input type="submit" disabled class="button rounded primary disabled" value="${i18n('Login to Submit')}" />`
|
||||
: `<input type="submit" class="button rounded primary" value="${i18n('Submit')}" />`);
|
||||
$('.objective-input[type!=checkbox]').on('input', (e) => {
|
||||
$('.objective-input[type!=checkbox]').on('input', (e: JQuery.TriggeredEvent<HTMLInputElement>) => {
|
||||
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<HTMLInputElement>) => {
|
||||
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');
|
||||
@@ -154,7 +154,7 @@ async function handleSection(ev, sidebar, type) {
|
||||
$section.removeClass('animating');
|
||||
}
|
||||
|
||||
export default new NamedPage(['problem_create', 'problem_edit'], (pagename) => {
|
||||
export default new NamedPage(['problem_create', 'problem_edit'], () => {
|
||||
let confirmed = false;
|
||||
$(document).on('click', '[name="operation"]', (ev) => {
|
||||
ev.preventDefault();
|
||||
@@ -240,14 +240,6 @@ export default new NamedPage(['problem_create', 'problem_edit'], (pagename) => {
|
||||
await download(`${pdoc.docId} ${pdoc.title}.zip`, targets);
|
||||
}
|
||||
|
||||
setInterval(() => {
|
||||
$('img').each(function () {
|
||||
if (this.src.startsWith('file://')) {
|
||||
$(this).attr('src', $(this).attr('src').replace('file://', (pagename === 'problem_create' ? `/file/${UserContext._id}/` : './file/')));
|
||||
}
|
||||
});
|
||||
}, 500);
|
||||
|
||||
const $main = $('textarea[data-editor]');
|
||||
const $field = $('textarea[data-markdown-upload]');
|
||||
let content = $field.val();
|
||||
|
||||
@@ -1,17 +1,4 @@
|
||||
.page--problem_edit,.page--problem_create
|
||||
.section__tab-header-item
|
||||
border-top: 3px solid transparent;
|
||||
transition: border-color 0.2s, background 0.2s, color 0.2s;
|
||||
transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
|
||||
margin-right: 0
|
||||
|
||||
&:hover
|
||||
border-color: #5f9fd6;
|
||||
background: #f8f8f8;
|
||||
|
||||
&.tab--active
|
||||
border-color: #ed5f82;
|
||||
|
||||
.problem-sidebar-additional_file
|
||||
height: 270px
|
||||
overflow-y: auto
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import 'core-js/stable';
|
||||
import 'matchmedia-polyfill';
|
||||
|
||||
import browserUpdate from 'browser-update';
|
||||
import queueMicrotask from 'queue-microtask';
|
||||
|
||||
window.queueMicrotask = queueMicrotask;
|
||||
browserUpdate({
|
||||
required: {
|
||||
e: -10, f: -10, o: -3, s: -1, c: -10,
|
||||
@@ -13,11 +12,23 @@ browserUpdate({
|
||||
});
|
||||
|
||||
// monaco-editor requires this polyfill
|
||||
if (window.MediaQueryList) {
|
||||
if (window.MediaQueryList && !MediaQueryList.prototype.addEventListener) {
|
||||
MediaQueryList.prototype.addEventListener = (k, listener) => {
|
||||
MediaQueryList.prototype.addListener(listener);
|
||||
};
|
||||
}
|
||||
if (typeof window['WeakRef'] === 'undefined') {
|
||||
// @ts-ignore
|
||||
window.WeakRef = (function (wm) {
|
||||
function WeakRef(target) {
|
||||
wm.set(this, target);
|
||||
}
|
||||
WeakRef.prototype.deref = function () {
|
||||
return wm.get(this);
|
||||
};
|
||||
return WeakRef;
|
||||
}(new WeakMap()));
|
||||
}
|
||||
if (!(window.matchMedia('all').addListener || window.matchMedia('all').addEventListener)) {
|
||||
const localMatchMedia = window.matchMedia;
|
||||
const hasMediaQueries = localMatchMedia('only all').matches;
|
||||
|
||||
@@ -89,7 +89,7 @@ self.addEventListener('notificationclick', (event) => {
|
||||
});
|
||||
|
||||
const PRECACHE = `precache-${process.env.VERSION}`;
|
||||
const DO_NOT_PRECACHE = ['vditor', '.worker.js', 'fonts', 'i.monaco'];
|
||||
const DO_NOT_PRECACHE = ['.worker.js', 'fonts', 'i.monaco'];
|
||||
|
||||
function shouldCachePath(path: string) {
|
||||
if (!path.split('?')[0].split('/').pop()) return false;
|
||||
|
||||
@@ -43,8 +43,6 @@ nav_logo_dark:
|
||||
default: /components/navigation/nav-logo-small_dark.png
|
||||
preferredEditorType:
|
||||
type:
|
||||
wysiwyg: WYSIWYG
|
||||
ir: Instant Rendering
|
||||
sv: Split View
|
||||
monaco: Monaco Editor
|
||||
family: markdown
|
||||
|
||||
@@ -46,32 +46,18 @@
|
||||
<script>
|
||||
var _htmlNode = document.documentElement;
|
||||
_htmlNode.className = _htmlNode.className.replace(' nojs', ' hasjs');
|
||||
try {
|
||||
import('data:text/javascript,');Object.fromEntries([]);window.matchMedia('all').addEventListener('change',()=>{});
|
||||
} catch(e) {
|
||||
const s = document.createElement('script');
|
||||
s.async = false;
|
||||
s.src = "{{ '/polyfill.js' if process.env.DEV else (UiContext.cdn_prefix + 'polyfill-' + global.Hydro.version['ui-default'] + '.js') }}";
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
</script>
|
||||
{% endif %}
|
||||
</head>
|
||||
<body>
|
||||
{% if not isIE(handler.request.headers['user-agent']) and not handler.session.legacy %}
|
||||
<script>
|
||||
var UiContext = '{{ UiContext|json|jsesc|safe }}';
|
||||
var UserContext = '{{ UserContext|json|jsesc|safe }}';
|
||||
</script>
|
||||
{% set UiContext = Object.create(UiContext) %}
|
||||
{% set UserContext = Object.create(UserContext) %}
|
||||
{% if process.env.DEV %}
|
||||
<script type="text/javascript" src="/resource/hash/lang-{{ _('__id') }}.js?version={{ global.Hydro.version['ui-default'] }}"></script>
|
||||
<script type="text/javascript" src="/hydro.js?version={{ global.Hydro.version['ui-default'] }}"></script>
|
||||
{% else %}
|
||||
<script type="text/javascript" src="{{ UiContext.cdn_prefix }}resource/{{ global.Hydro.version['ui-default'] }}/lang-{{ _('__id') }}.js"></script>
|
||||
<script type="text/javascript" src="{{ UiContext.cdn_prefix }}hydro-{{ global.Hydro.version['ui-default'] }}.js"></script>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% set UiContext = Object.create(UiContext) %}
|
||||
{% set UserContext = Object.create(UserContext) %}
|
||||
</head>
|
||||
<body>
|
||||
{% if not isIE(handler.request.headers['user-agent']) and not handler.session.legacy %}
|
||||
<script type="text/javascript" src="/constant/{{ UiContext.constantVersion }}.js"></script>
|
||||
<script type="text/javascript" src="{{ UiContext.cdn_prefix }}resource/{{ global.Hydro.version['ui-default'] }}/lang-{{ _('__id') }}.js"></script>
|
||||
<script type="text/javascript" src="{{ UiContext.cdn_prefix }}hydro-{{ global.Hydro.version['ui-default'] }}.js"></script>
|
||||
{% endif %}
|
||||
{% block body %}{% endblock %}
|
||||
{% block script %}{% endblock %}
|
||||
|
||||
@@ -3,41 +3,71 @@
|
||||
<div class="row">
|
||||
<div class="medium-12 columns">
|
||||
<div class="section">
|
||||
<div class="section__body no-padding">
|
||||
<div data-fragment-id="status">
|
||||
<table class="data-table">
|
||||
<colgroup>
|
||||
<col class="col--id">
|
||||
<col class="col--status">
|
||||
<col class="col--os">
|
||||
<col class="col--cpu">
|
||||
<col class="col--memory">
|
||||
<col class="col--request">
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="col--id">{{ _('ID') }}</th>
|
||||
<th class="col--status">{{ _('Current Status') }}</th>
|
||||
<th class="col--os">{{ _('OS Info') }}</th>
|
||||
<th class="col--cpu">{{ _('CPU Info') }}</th>
|
||||
<th class="col--memory">{{ _('Memory Info') }}</th>
|
||||
<th class="col--request">{{ _('Handled requests') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{%- for doc in stats -%}
|
||||
<tr>
|
||||
<td class="col--id">{{ doc._id.toHexString()|truncate(8,True,'') }}</td>
|
||||
<td class="col--status">{% if doc.isOnline %}{{ doc.status }}{% else %}{{ datetimeSpan(doc.updateAt)|safe }}{% endif %}</td>
|
||||
<td class="col--os" data-tooltip="{{ doc.osinfo.kernel }}">{{ doc.osinfo.distro }} {{ doc.osinfo.release }} {{ doc.osinfo.codename }} {{ doc.osinfo.arch }}</td>
|
||||
<td class="col--cpu" data-tooltip="{{ doc.cpu.flags }}">{{ doc.cpu.manufacturer }} {{ doc.cpu.brand }}@{{ doc.cpu.speed }}GHz</td>
|
||||
<td class="col--memory"{% if doc.stack %} data-tooltip="Stack size: {{ doc.stack }}MB"{% endif %}>{{ size(doc.memory.used) }}/{{ size(doc.memory.total) }}</td>
|
||||
<td class="col--request">{{ doc.reqCount }}</td>
|
||||
</tr>
|
||||
{%- endfor -%}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="section__tab-container">
|
||||
<ul class="section__tabs">
|
||||
<li class="section__tab-item">
|
||||
<h1 class="section__tab-title">{{ _('Servers') }}</h1>
|
||||
<div class="section__tab-main">
|
||||
<table class="data-table">
|
||||
<colgroup>
|
||||
<col class="col--id">
|
||||
<col class="col--status">
|
||||
<col class="col--os">
|
||||
<col class="col--cpu">
|
||||
<col class="col--memory">
|
||||
<col class="col--request">
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="col--id">{{ _('ID') }}</th>
|
||||
<th class="col--status">{{ _('Current Status') }}</th>
|
||||
<th class="col--os">{{ _('OS Info') }}</th>
|
||||
<th class="col--cpu">{{ _('CPU Info') }}</th>
|
||||
<th class="col--memory">{{ _('Memory Info') }}</th>
|
||||
<th class="col--request">{{ _('Handled requests') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{%- for doc in stats -%}
|
||||
<tr>
|
||||
<td class="col--id">{{ doc._id.toHexString()|truncate(8,True,'') }}</td>
|
||||
<td class="col--status">{% if doc.isOnline %}{{ doc.status }}{% else %}{{ datetimeSpan(doc.updateAt)|safe }}{% endif %}</td>
|
||||
<td class="col--os" data-tooltip="{{ doc.osinfo.kernel }}">{{ doc.osinfo.distro }} {{ doc.osinfo.release }} {{ doc.osinfo.codename }} {{ doc.osinfo.arch }}</td>
|
||||
<td class="col--cpu" data-tooltip="{{ doc.cpu.flags }}">{{ doc.cpu.manufacturer }} {{ doc.cpu.brand }}@{{ doc.cpu.speed }}GHz</td>
|
||||
<td class="col--memory"{% if doc.stack %} data-tooltip="Stack size: {{ doc.stack }}MB"{% endif %}>{{ size(doc.memory.used) }}/{{ size(doc.memory.total) }}</td>
|
||||
<td class="col--request">{{ doc.reqCount }}</td>
|
||||
</tr>
|
||||
{%- endfor -%}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</li>
|
||||
{% if compilers|length %}
|
||||
<li class="section__tab-item">
|
||||
<h1 class="section__tab-title">{{ _('Compiler Versions') }}</h1>
|
||||
<div class="section__tab-main">
|
||||
<div class="section__body typo">
|
||||
{% for line in compilers %}
|
||||
<p>{{ line.key|join(', ') }}</p>
|
||||
<pre><code>{{ line.message }}</code></pre>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li class="section__tab-item">
|
||||
<h1 class="section__tab-title">{{ _('Compile Commands') }}</h1>
|
||||
<div class="section__tab-main">
|
||||
<div class="section__body typo">
|
||||
<ul>
|
||||
{%- for k, v in languages -%}
|
||||
<li>{{ k }} <pre>{{ v }}</pre></li>
|
||||
{%- endfor -%}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -17,14 +17,7 @@
|
||||
<h1 class="section__title" id="compiler" data-heading>{{ _("Compilers' Version and Parameters") }}</h1>
|
||||
</div>
|
||||
<div class="section__body typo">
|
||||
<p>{{ name }} 使用 <a href="https://github.com/hydro-dev/Hydro/tree/master/packages/hydrojudge">HydroJudge</a> 进行评测,编译参数如下:</p>
|
||||
{% if languages %}
|
||||
<blockquote><ul>
|
||||
{%- for k, v in languages -%}
|
||||
<li>{{ k }} <pre>{{ v }}</pre></li>
|
||||
{%- endfor -%}
|
||||
</ul></blockquote>
|
||||
{% endif %}
|
||||
<p>{{ name }} 使用 <a href="https://github.com/hydro-dev/Hydro/tree/master/packages/hydrojudge">HydroJudge</a> 进行评测,编译参数和当前所用编译器版本见状态页。</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section" data-heading-extract-to="#menu-item-wiki_help">
|
||||
|
||||
@@ -196,7 +196,7 @@ $hover-background-color = #424242
|
||||
background-color: #171717
|
||||
color: #999
|
||||
|
||||
pre:not(.vditor-sv), code
|
||||
pre, code
|
||||
background-color: #171717
|
||||
|
||||
.scratchpad__toolbar
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'normalize.css/normalize.css';
|
||||
import 'vditor/dist/index.css';
|
||||
import 'allotment/dist/style.css';
|
||||
import 'pickadate/lib/themes/classic.css';
|
||||
import 'pickadate/lib/themes/classic.date.css';
|
||||
|
||||
@@ -34,8 +34,8 @@ export function secureRandomString(digit = 32, dict = defaultDict) {
|
||||
|
||||
type Substitution = string | number | { templateRaw: true, html: string };
|
||||
|
||||
export function tpl(node: React.ReactNode, reactive?: boolean);
|
||||
export function tpl(pieces: TemplateStringsArray, ...substitutions: Substitution[]);
|
||||
export function tpl<T extends boolean = false>(node: React.ReactNode, reactive?: T): T extends true ? HTMLDivElement : string;
|
||||
export function tpl(pieces: TemplateStringsArray, ...substitutions: Substitution[]): string;
|
||||
export function tpl(pieces: TemplateStringsArray | React.ReactNode, ...substitutions: Substitution[] | boolean[]) {
|
||||
if (React.isValidElement(pieces)) {
|
||||
if (substitutions[0]) {
|
||||
@@ -49,7 +49,7 @@ export function tpl(pieces: TemplateStringsArray | React.ReactNode, ...substitut
|
||||
for (let i = 0; i < substitutions.length; ++i) {
|
||||
const subst = substitutions[i];
|
||||
let substHtml: string;
|
||||
if (typeof subst === 'object' && subst.templateRaw) {
|
||||
if (subst && typeof subst === 'object' && subst.templateRaw) {
|
||||
substHtml = subst.html;
|
||||
} else substHtml = _.escape(String(subst));
|
||||
result += substHtml + pieces[i + 1];
|
||||
@@ -82,7 +82,8 @@ export const zIndexManager = {
|
||||
};
|
||||
|
||||
export const request = {
|
||||
async ajax(options: Record<string, any>) {
|
||||
ajax(options: Record<string, any>) {
|
||||
const stack = new Error().stack;
|
||||
return new Promise<any>((resolve, reject) => {
|
||||
$
|
||||
.ajax({
|
||||
@@ -96,9 +97,12 @@ export const request = {
|
||||
if (textStatus === 'abort') {
|
||||
const err = new Error(i18n('Aborted')) as any;
|
||||
err.aborted = true;
|
||||
err.isUserFacingError = true;
|
||||
reject(err);
|
||||
} else if (jqXHR.readyState === 0) {
|
||||
reject(new Error(i18n('Network error')));
|
||||
const err = new Error(i18n('Network error')) as any;
|
||||
err.isUserFacingError = true;
|
||||
reject(err);
|
||||
} else if (typeof jqXHR.responseJSON === 'object' && jqXHR.responseJSON.error) {
|
||||
const { error } = jqXHR.responseJSON;
|
||||
if (error.params) {
|
||||
@@ -117,6 +121,9 @@ export const request = {
|
||||
}
|
||||
})
|
||||
.done(resolve);
|
||||
}).catch((e: Error) => {
|
||||
e.stack = stack;
|
||||
throw e;
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
@@ -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');
|
||||
},
|
||||
});
|
||||
Reference in new issue
Block a user