mirror of
https://github.com/remnawave/xray-monaco-editor.git
synced 2026-05-13 12:16:43 +00:00
feat: add react
This commit is contained in:
parent
862a935c0b
commit
57645d47c4
53 changed files with 10968 additions and 305 deletions
95
.eslintrc.cjs
Normal file
95
.eslintrc.cjs
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
module.exports = {
|
||||
env: { browser: true, es2020: true },
|
||||
extends: [
|
||||
"eslint:recommended",
|
||||
"airbnb-base",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:react-hooks/recommended",
|
||||
"plugin:storybook/recommended",
|
||||
"plugin:perfectionist/recommended-natural-legacy",
|
||||
"prettier",
|
||||
],
|
||||
ignorePatterns: ["dist", ".eslintrc.cjs", "plop", "plop/**", "plopfile.js", ".stylelintrc.js"],
|
||||
parser: "@typescript-eslint/parser",
|
||||
plugins: ["react-refresh", "import"],
|
||||
|
||||
settings: {
|
||||
"import/parsers": {
|
||||
"@typescript-eslint/parser": [".ts", ".tsx"],
|
||||
},
|
||||
"import/resolver": {
|
||||
node: true,
|
||||
typescript: {
|
||||
project: ".",
|
||||
},
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
"perfectionist/sort-imports": [
|
||||
"error",
|
||||
{
|
||||
type: "line-length",
|
||||
order: "desc",
|
||||
ignoreCase: true,
|
||||
specialCharacters: "keep",
|
||||
internalPattern: ["^~/.+"],
|
||||
tsconfigRootDir: ".",
|
||||
partitionByComment: false,
|
||||
partitionByNewLine: false,
|
||||
newlinesBetween: "always",
|
||||
maxLineLength: undefined,
|
||||
groups: [
|
||||
"type",
|
||||
["builtin", "external"],
|
||||
"internal-type",
|
||||
"internal",
|
||||
["parent-type", "sibling-type", "index-type"],
|
||||
["parent", "sibling", "index"],
|
||||
"object",
|
||||
"unknown",
|
||||
],
|
||||
customGroups: { type: {}, value: {} },
|
||||
environment: "node",
|
||||
},
|
||||
],
|
||||
"perfectionist/sort-objects": ["off"],
|
||||
indent: ["error", 4, { SwitchCase: 1 }],
|
||||
"max-classes-per-file": "off",
|
||||
"import/no-extraneous-dependencies": ["off"],
|
||||
"import/no-unresolved": "error",
|
||||
"import/prefer-default-export": "off",
|
||||
"import/extensions": "off",
|
||||
"no-bitwise": "off",
|
||||
"no-plusplus": "off",
|
||||
"no-restricted-syntax": ["off", "ForInStatement"],
|
||||
"react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
|
||||
"no-shadow": ["off"],
|
||||
"arrow-body-style": ["off"],
|
||||
"object-curly-spacing": ["error", "always"],
|
||||
"array-bracket-spacing": ["error", "never"],
|
||||
"no-underscore-dangle": [
|
||||
"off",
|
||||
{
|
||||
allow: ["_"],
|
||||
allowAfterThis: true,
|
||||
allowAfterSuper: true,
|
||||
allowAfterThisConstructor: true,
|
||||
enforceInMethodNames: false,
|
||||
},
|
||||
],
|
||||
semi: ["error", "never"],
|
||||
"comma-dangle": ["off"],
|
||||
"brace-style": ["error", "1tbs", { allowSingleLine: true }],
|
||||
"object-curly-newline": ["error", { multiline: true, consistent: true }],
|
||||
"react-hooks/exhaustive-deps": "off",
|
||||
"no-empty-pattern": "warn",
|
||||
"@typescript-eslint/ban-types": [
|
||||
"error",
|
||||
{
|
||||
types: {
|
||||
"{}": false,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
77
.github/workflows/deploy.yml
vendored
77
.github/workflows/deploy.yml
vendored
|
|
@ -1,53 +1,50 @@
|
|||
name: Deploy
|
||||
|
||||
on:
|
||||
# Runs on pushes targeting the default branch
|
||||
push:
|
||||
branches: ["main"]
|
||||
# Runs on pushes targeting the default branch
|
||||
push:
|
||||
branches: ['main']
|
||||
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
|
||||
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
# Allow one concurrent deployment
|
||||
concurrency:
|
||||
group: "pages"
|
||||
cancel-in-progress: true
|
||||
group: 'pages'
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
# Build job
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v1
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.23'
|
||||
check-latest: true
|
||||
- run: make build
|
||||
- run: mkdir public && cp index.html main.wasm wasm_exec.js xray.schema.json public
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v1
|
||||
with:
|
||||
path: public/
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v1
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.23'
|
||||
check-latest: true
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '18'
|
||||
cache: 'npm'
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
# Deployment job
|
||||
deploy:
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v1
|
||||
- run: make build
|
||||
|
||||
- name: Build Vite
|
||||
run: npm run build
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v1
|
||||
with:
|
||||
path: dist/
|
||||
|
|
|
|||
26
.gitignore
vendored
26
.gitignore
vendored
|
|
@ -2,3 +2,29 @@
|
|||
*.js
|
||||
assets
|
||||
*.schema.json
|
||||
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
|
|
|||
1
.npmrc
Normal file
1
.npmrc
Normal file
|
|
@ -0,0 +1 @@
|
|||
legacy-peer-deps=true
|
||||
8
.prettierrc
Normal file
8
.prettierrc
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"singleQuote": true,
|
||||
"singleAttributePerLine": false,
|
||||
"tabWidth": 4,
|
||||
"printWidth": 100,
|
||||
"semi": false,
|
||||
"trailingComma": "none"
|
||||
}
|
||||
8
Makefile
8
Makefile
|
|
@ -1,4 +1,6 @@
|
|||
build: main.wasm wasm_exec.js xray.schema.json
|
||||
mkdir -p public
|
||||
mv main.wasm wasm_exec.js xray.schema.json public/
|
||||
.PHONY: build
|
||||
|
||||
dev-lite:
|
||||
|
|
@ -36,8 +38,4 @@ assets/Xray-docs-next-main:
|
|||
cd assets && curl -fL https://github.com/XTLS/Xray-docs-next/archive/refs/heads/main.tar.gz | tar xvzf -
|
||||
|
||||
xray.schema.json: scrape-docs.py assets/Xray-docs-next-main
|
||||
grep -r '' assets/Xray-docs-next-main/docs/en/config/ | cut -d: -f2- | python3 scrape-docs.py > xray.schema.json
|
||||
|
||||
|
||||
serve:
|
||||
python3 -mhttp.server
|
||||
grep -r '' assets/Xray-docs-next-main/docs/en/config/ | cut -d: -f2- | python3 scrape-docs.py > xray.schema.json
|
||||
11
global.d.ts
vendored
Normal file
11
global.d.ts
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
declare global {
|
||||
interface Window {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
Go: typeof window.Go
|
||||
onWasmInitialized?: () => void
|
||||
|
||||
XrayParseConfig: (config: string) => null | string
|
||||
}
|
||||
}
|
||||
|
||||
export {}
|
||||
271
index.html
271
index.html
|
|
@ -1,262 +1,13 @@
|
|||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Xray Config Validator</title>
|
||||
|
||||
<script src="wasm_exec.js"></script>
|
||||
<script>
|
||||
const isReady = new Promise((resolve) => {
|
||||
window.onWasmInitialized = resolve;
|
||||
});
|
||||
|
||||
const go = new Go();
|
||||
|
||||
(async () => {
|
||||
const result = await WebAssembly.instantiateStreaming(fetch('main.wasm'), go.importObject);
|
||||
go.run(result.instance);
|
||||
await isReady;
|
||||
document.getElementById("version-info").innerText = `Xray version ${XrayGetVersion()}`;
|
||||
})();
|
||||
</script>
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html, body {
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", "Liberation Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
#editor {
|
||||
flex: 1;
|
||||
border-radius: 5px;
|
||||
border: 1px solid #ced4da;
|
||||
height: 500px;
|
||||
}
|
||||
|
||||
#output {
|
||||
padding: 10px;
|
||||
line-height: 1.2;
|
||||
margin-top: 20px;
|
||||
border-radius: 5px;
|
||||
border: 1px solid #ced4da;
|
||||
text-align: center;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
#output[data-status="ok"] {
|
||||
background-color: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
#output[data-status="error"] {
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
#output[data-status="warn"] {
|
||||
background-color: #fff3cd;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
#nav {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.8rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
margin-top: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.cool-color {
|
||||
background-image: linear-gradient(to right,#E70000, #FF8C00, #FFEF00, #00811F, #0044FF, #760089);
|
||||
-webkit-background-clip: text;
|
||||
color:transparent;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
background-color: #121212;
|
||||
color: #ffffff;
|
||||
}
|
||||
#editor {
|
||||
border: 1px solid #555;
|
||||
}
|
||||
#output {
|
||||
border: 1px solid #555;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.33.0/min/vs/loader.min.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<nav id="nav">
|
||||
<span id="version-info">Loading Xray…</span> |
|
||||
<a href="https://github.com/mmmray/xray-online">GitHub</a>
|
||||
<h1>Xray Config Validator</h1>
|
||||
</nav>
|
||||
<div id="editor"></div>
|
||||
<div id="output">
|
||||
Start typing your Xray config in the editor above.<br>
|
||||
Hover over fields for documentation. <span class="cool-color">Autocomplete</span> is available.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
require.config({ paths: { 'vs': 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.52.0/min/vs' }});
|
||||
require(['vs/editor/editor.main'], function() {
|
||||
const editorElement = document.getElementById('editor');
|
||||
const output = document.getElementById("output");
|
||||
|
||||
function updateEditorTheme() {
|
||||
const prefersDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
const theme = prefersDarkMode ? 'vs-dark' : 'vs-light';
|
||||
monaco.editor.setTheme(theme);
|
||||
}
|
||||
|
||||
|
||||
const editor = monaco.editor.create(editorElement, {
|
||||
value: `{
|
||||
"inbounds": [
|
||||
{
|
||||
"listen": "127.0.0.1",
|
||||
"port": 10808,
|
||||
"protocol": "socks",
|
||||
"settings": {
|
||||
"udp": true
|
||||
},
|
||||
"sniffing": {
|
||||
"enabled": true,
|
||||
"destOverride": [
|
||||
"http",
|
||||
"tls"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"protocol": "vless",
|
||||
"settings": {
|
||||
"vnext": [
|
||||
{
|
||||
"address": "",
|
||||
"port": 443,
|
||||
"users": [
|
||||
{
|
||||
"id": "user",
|
||||
"encryption": "none",
|
||||
"flow": "xtls-rprx-vision"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"streamSettings": {
|
||||
"network": "tcp",
|
||||
"security": "tls",
|
||||
"tlsSettings": {
|
||||
"serverName": "",
|
||||
"allowInsecure": false,
|
||||
"fingerprint": "chrome"
|
||||
}
|
||||
},
|
||||
"tag": "proxy"
|
||||
}
|
||||
]
|
||||
}`,
|
||||
language: 'json',
|
||||
theme: 'vs-light',
|
||||
automaticLayout: true
|
||||
});
|
||||
|
||||
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', updateEditorTheme);
|
||||
|
||||
|
||||
updateEditorTheme();
|
||||
|
||||
|
||||
function validateJSON(text) {
|
||||
try {
|
||||
JSON.parse(text);
|
||||
return null;
|
||||
} catch (error) {
|
||||
return error.message;
|
||||
}
|
||||
}
|
||||
|
||||
fetch('xray.schema.json')
|
||||
.then(response => response.json())
|
||||
.then(schema => {
|
||||
monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
|
||||
validate: true,
|
||||
allowComments: true,
|
||||
schemas: [{
|
||||
uri: "https://xray-config-schema",
|
||||
fileMatch: ["*"],
|
||||
schema: schema
|
||||
}]
|
||||
});
|
||||
})
|
||||
.catch(error => console.error('Scheme load error:', error));
|
||||
|
||||
editor.onDidChangeModelContent(function() {
|
||||
const jsonText = editor.getValue();
|
||||
const validationError = validateJSON(jsonText);
|
||||
|
||||
if (validationError) {
|
||||
output.dataset.status = "error";
|
||||
output.innerText = "Validation Error: " + validationError;
|
||||
} else {
|
||||
if (typeof XrayParseConfig === 'undefined') {
|
||||
output.dataset.status = "warn";
|
||||
output.innerText = "Xray is still loading. Check the browser console.";
|
||||
} else {
|
||||
let validationResult = XrayParseConfig(jsonText);
|
||||
if (validationResult) {
|
||||
output.dataset.status = "error";
|
||||
output.innerText = validationResult;
|
||||
} else {
|
||||
output.dataset.status = "ok";
|
||||
output.innerText = "Valid Xray Config!";
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener("resize", function() {
|
||||
editor.layout();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<script src="/wasm_exec.js"></script>
|
||||
<title>Xray Monaco Editor</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
9641
package-lock.json
generated
Normal file
9641
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
78
package.json
Normal file
78
package.json
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
{
|
||||
"name": "xray-monaco-edtitor",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mantine/carousel": "^7.12.2",
|
||||
"@mantine/charts": "^7.12.2",
|
||||
"@mantine/code-highlight": "^7.12.2",
|
||||
"@mantine/core": "^7.14.3",
|
||||
"@mantine/dates": "^7.12.2",
|
||||
"@mantine/dropzone": "^7.12.2",
|
||||
"@mantine/form": "^7.12.2",
|
||||
"@mantine/hooks": "^7.12.2",
|
||||
"@mantine/modals": "^7.12.2",
|
||||
"@mantine/notifications": "^7.12.2",
|
||||
"@mantine/nprogress": "^7.12.2",
|
||||
"@monaco-editor/react": "^4.6.0",
|
||||
"axios": "^1.7.9",
|
||||
"dayjs": "^1.11.13",
|
||||
"motion": "^11.13.1",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-icons": "^5.4.0",
|
||||
"zustand": "^4.5.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.16.0",
|
||||
"@ianvs/prettier-plugin-sort-imports": "^4.3.1",
|
||||
"@types/byte-size": "^8.1.2",
|
||||
"@types/bytes": "^3.1.4",
|
||||
"@types/color-hash": "^2.0.0",
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@types/mdx": "^2.0.13",
|
||||
"@types/node": "^20.11.19",
|
||||
"@types/react": "^18.3.5",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "^7.0.2",
|
||||
"@typescript-eslint/parser": "^7.0.2",
|
||||
"@vitejs/plugin-react": "^4.3.1",
|
||||
"@vitejs/plugin-react-swc": "^3.7.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-airbnb-base": "^15.0.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-import-resolver-typescript": "^3.6.1",
|
||||
"eslint-plugin-import": "^2.29.0",
|
||||
"eslint-plugin-perfectionist": "^4.1.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.5",
|
||||
"eslint-plugin-storybook": "^0.8.0",
|
||||
"globals": "^15.12.0",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"jsdom": "^25.0.0",
|
||||
"postcss": "^8.4.45",
|
||||
"postcss-preset-mantine": "1.17.0",
|
||||
"postcss-simple-vars": "^7.0.1",
|
||||
"prettier": "^3.3.3",
|
||||
"prop-types": "^15.8.1",
|
||||
"rollup-plugin-visualizer": "^5.12.0",
|
||||
"stylelint": "^16.9.0",
|
||||
"stylelint-config-standard-scss": "^13.1.0",
|
||||
"typescript": "^5.7.2",
|
||||
"typescript-eslint": "^8.16.0",
|
||||
"vite": "6.0.1",
|
||||
"vite-plugin-javascript-obfuscator": "^3.1.0",
|
||||
"vite-plugin-preload": "^0.4.0",
|
||||
"vite-plugin-webfont-dl": "^3.9.4",
|
||||
"vite-tsconfig-paths": "^5.0.1",
|
||||
"vite-plugin-wasm": "^3.3.0"
|
||||
}
|
||||
}
|
||||
19
postcss.config.cjs
Normal file
19
postcss.config.cjs
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
module.exports = {
|
||||
plugins: {
|
||||
'postcss-preset-mantine': {
|
||||
autoRem: true
|
||||
},
|
||||
'postcss-simple-vars': {
|
||||
variables: {
|
||||
'mantine-breakpoint-xs': '30em',
|
||||
'mantine-breakpoint-sm': '40em',
|
||||
'mantine-breakpoint-md': '48em',
|
||||
'mantine-breakpoint-lg': '64em',
|
||||
'mantine-breakpoint-xl': '80em',
|
||||
'mantine-breakpoint-2xl': '96em',
|
||||
'mantine-breakpoint-3xl': '120em',
|
||||
'mantine-breakpoint-4xl': '160em'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
24
src/app.tsx
Normal file
24
src/app.tsx
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import '@mantine/carousel/styles.layer.css'
|
||||
import '@mantine/charts/styles.layer.css'
|
||||
import '@mantine/code-highlight/styles.layer.css'
|
||||
import '@mantine/core/styles.layer.css'
|
||||
import '@mantine/dates/styles.layer.css'
|
||||
import '@mantine/dropzone/styles.layer.css'
|
||||
import '@mantine/notifications/styles.layer.css'
|
||||
import '@mantine/nprogress/styles.layer.css'
|
||||
|
||||
import './global.css'
|
||||
|
||||
import { NavigationProgress } from '@mantine/nprogress'
|
||||
import { MantineProvider } from '@mantine/core'
|
||||
|
||||
import { ConfigPageConnector } from '@pages/config/ui/connectors/config.page.connector'
|
||||
|
||||
export function App() {
|
||||
return (
|
||||
<MantineProvider defaultColorScheme="dark">
|
||||
<NavigationProgress />
|
||||
<ConfigPageConnector />
|
||||
</MantineProvider>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
import { PiCheckSquareOffset } from 'react-icons/pi'
|
||||
import { Button, Group } from '@mantine/core'
|
||||
|
||||
import { Props } from './interfaces'
|
||||
|
||||
export function ConfigEditorActionsFeature(props: Props) {
|
||||
const { editorRef } = props
|
||||
|
||||
const formatDocument = () => {
|
||||
if (!editorRef.current) return
|
||||
if (typeof editorRef.current !== 'object') return
|
||||
if (!('getAction' in editorRef.current)) return
|
||||
if (typeof editorRef.current.getAction !== 'function') return
|
||||
|
||||
editorRef.current.getAction('editor.action.formatDocument').run()
|
||||
}
|
||||
|
||||
return (
|
||||
<Group>
|
||||
<Button
|
||||
leftSection={<PiCheckSquareOffset size={16} />}
|
||||
mb="md"
|
||||
onClick={formatDocument}
|
||||
>
|
||||
Format
|
||||
</Button>
|
||||
</Group>
|
||||
)
|
||||
}
|
||||
2
src/features/config/config-editor-actions/ui/index.ts
Normal file
2
src/features/config/config-editor-actions/ui/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export * from './config-editor-actions.feature'
|
||||
export * from './interfaces'
|
||||
|
|
@ -0,0 +1 @@
|
|||
export * from './props.interface'
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
export interface Props {
|
||||
editorRef: any
|
||||
isConfigValid: boolean
|
||||
isSaving: boolean
|
||||
monacoRef: any
|
||||
setIsSaving: (value: boolean) => void
|
||||
setResult: (value: string) => void
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
import dayjs from 'dayjs'
|
||||
|
||||
export const ConfigValidationFeature = {
|
||||
validate: (
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
editorRef: any,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
monacoRef: any,
|
||||
setResult: (message: string) => void,
|
||||
setIsConfigValid: (isValid: boolean) => void
|
||||
) => {
|
||||
try {
|
||||
if (!editorRef.current) return
|
||||
if (!monacoRef.current) return
|
||||
if (typeof editorRef.current !== 'object') return
|
||||
if (typeof monacoRef.current !== 'object') return
|
||||
if (!('getValue' in editorRef.current)) return
|
||||
if (typeof editorRef.current.getValue !== 'function') return
|
||||
|
||||
const currentValue = editorRef.current.getValue()
|
||||
const validationResult = window.XrayParseConfig(currentValue)
|
||||
|
||||
setResult(
|
||||
`${dayjs().format('HH:mm:ss')} | ${validationResult || 'Xray config is valid.'}`
|
||||
)
|
||||
setIsConfigValid(!validationResult)
|
||||
} catch (err: unknown) {
|
||||
setResult(`${dayjs().format('HH:mm:ss')} | Validation error: ${(err as Error).message}`)
|
||||
setIsConfigValid(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
1
src/features/config/config-validation/ui/index.ts
Normal file
1
src/features/config/config-validation/ui/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from './config-validation.feature'
|
||||
1
src/features/config/monaco-setup/lib/index.ts
Normal file
1
src/features/config/monaco-setup/lib/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from './monaco-setup.feature'
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
import { Monaco } from '@monaco-editor/react'
|
||||
import axios from 'axios'
|
||||
|
||||
export const MonacoSetupFeature = {
|
||||
setup: async (monaco: Monaco) => {
|
||||
try {
|
||||
const response = await axios.get('/xray.schema.json')
|
||||
const schema = await response.data
|
||||
|
||||
monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
|
||||
allowComments: false,
|
||||
enableSchemaRequest: true,
|
||||
schemaRequest: 'warning',
|
||||
schemas: [
|
||||
{
|
||||
fileMatch: ['*'],
|
||||
schema,
|
||||
uri: 'https://xray-config-schema.json'
|
||||
}
|
||||
],
|
||||
validate: true
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Failed to load JSON schema:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
21
src/global.css
Normal file
21
src/global.css
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
@layer mantine, mantine-datatable;
|
||||
|
||||
html,
|
||||
body {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
#root {
|
||||
height: 100vh;
|
||||
|
||||
@mixin dark {
|
||||
.mrt-table-head-sort-button {
|
||||
border-radius: 0;
|
||||
border: none;
|
||||
}
|
||||
.mrt-table-head-cell-filter-label-icon {
|
||||
border-radius: 0;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
14
src/main.tsx
Normal file
14
src/main.tsx
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import { createRoot } from 'react-dom/client'
|
||||
import { StrictMode } from 'react'
|
||||
|
||||
import { App } from './app'
|
||||
|
||||
const container = document.getElementById('root')
|
||||
if (!container) throw new Error('Failed to find the root element')
|
||||
|
||||
const root = createRoot(container)
|
||||
root.render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>
|
||||
)
|
||||
7
src/pages/config/ui/components/config.module.css
Normal file
7
src/pages/config/ui/components/config.module.css
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
.root {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
23
src/pages/config/ui/components/config.page.component.tsx
Normal file
23
src/pages/config/ui/components/config.page.component.tsx
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import { Container } from '@mantine/core'
|
||||
|
||||
import { ConfigEditorWidget } from '@widgets/config/config-editor'
|
||||
import { HeaderWidget } from '@widgets/header'
|
||||
import { Page } from '@/shared/ui/page'
|
||||
|
||||
import { Props } from './interfaces'
|
||||
|
||||
export const ConfigPageComponent = (props: Props) => {
|
||||
const { config } = props
|
||||
|
||||
return (
|
||||
<>
|
||||
<HeaderWidget />
|
||||
|
||||
<Container size="lg">
|
||||
<Page title="Config">
|
||||
<ConfigEditorWidget config={config} />
|
||||
</Page>
|
||||
</Container>
|
||||
</>
|
||||
)
|
||||
}
|
||||
1
src/pages/config/ui/components/interfaces/index.ts
Normal file
1
src/pages/config/ui/components/interfaces/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from './props.interface'
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
export interface Props {
|
||||
config: Record<string, unknown> | string
|
||||
}
|
||||
53
src/pages/config/ui/connectors/config.page.connector.tsx
Normal file
53
src/pages/config/ui/connectors/config.page.connector.tsx
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import { fetchWithProgress } from '@/shared/utils/fetch-with-progress'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
import { LoadingScreen } from '@/shared/ui/loading-screen'
|
||||
import { DEFAULT_CONFIG } from '@/shared/constants'
|
||||
|
||||
import { ConfigPageComponent } from '../components/config.page.component'
|
||||
|
||||
export function ConfigPageConnector() {
|
||||
const [downloadProgress, setDownloadProgress] = useState(0)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
|
||||
const config = DEFAULT_CONFIG
|
||||
useEffect(() => {
|
||||
const initWasm = async () => {
|
||||
try {
|
||||
const go = new window.Go()
|
||||
const wasmInitialized = new Promise<void>((resolve) => {
|
||||
window.onWasmInitialized = () => {
|
||||
console.info('WASM module initialized')
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
|
||||
const wasmBytes = await fetchWithProgress('/main.wasm', setDownloadProgress)
|
||||
|
||||
const { instance } = await WebAssembly.instantiate(wasmBytes, go.importObject)
|
||||
go.run(instance)
|
||||
await wasmInitialized
|
||||
|
||||
if (typeof window.XrayParseConfig === 'function') {
|
||||
setIsLoading(false)
|
||||
} else {
|
||||
throw new Error('XrayParseConfig not initialized')
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
console.error('WASM initialization error:', err)
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
initWasm()
|
||||
return () => {
|
||||
delete window.onWasmInitialized
|
||||
}
|
||||
}, [])
|
||||
|
||||
if (isLoading) {
|
||||
return <LoadingScreen text={`WASM module is loading...`} value={downloadProgress} />
|
||||
}
|
||||
|
||||
return <ConfigPageComponent config={config} />
|
||||
}
|
||||
46
src/shared/constants/default-config.tsx
Normal file
46
src/shared/constants/default-config.tsx
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
export const DEFAULT_CONFIG = {
|
||||
inbounds: [
|
||||
{
|
||||
listen: '127.0.0.1',
|
||||
port: 10808,
|
||||
protocol: 'socks',
|
||||
settings: {
|
||||
udp: true
|
||||
},
|
||||
sniffing: {
|
||||
enabled: true,
|
||||
destOverride: ['http', 'tls']
|
||||
}
|
||||
}
|
||||
],
|
||||
outbounds: [
|
||||
{
|
||||
protocol: 'vless',
|
||||
settings: {
|
||||
vnext: [
|
||||
{
|
||||
address: '',
|
||||
port: 443,
|
||||
users: [
|
||||
{
|
||||
id: 'user',
|
||||
encryption: 'none',
|
||||
flow: 'xtls-rprx-vision'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
streamSettings: {
|
||||
network: 'tcp',
|
||||
security: 'tls',
|
||||
tlsSettings: {
|
||||
serverName: '',
|
||||
allowInsecure: false,
|
||||
fingerprint: 'chrome'
|
||||
}
|
||||
},
|
||||
tag: 'proxy'
|
||||
}
|
||||
]
|
||||
}
|
||||
1
src/shared/constants/index.ts
Normal file
1
src/shared/constants/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from './default-config'
|
||||
1
src/shared/lib/monaco-theme/index.ts
Normal file
1
src/shared/lib/monaco-theme/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from './monaco-theme'
|
||||
348
src/shared/lib/monaco-theme/monaco-theme.tsx
Normal file
348
src/shared/lib/monaco-theme/monaco-theme.tsx
Normal file
|
|
@ -0,0 +1,348 @@
|
|||
export const monacoTheme = {
|
||||
base: 'vs-dark',
|
||||
inherit: true,
|
||||
rules: [
|
||||
{
|
||||
background: '24292e',
|
||||
token: ''
|
||||
},
|
||||
{
|
||||
foreground: '959da5',
|
||||
token: 'comment'
|
||||
},
|
||||
{
|
||||
foreground: '959da5',
|
||||
token: 'punctuation.definition.comment'
|
||||
},
|
||||
{
|
||||
foreground: '959da5',
|
||||
token: 'string.comment'
|
||||
},
|
||||
{
|
||||
foreground: 'c8e1ff',
|
||||
token: 'constant'
|
||||
},
|
||||
{
|
||||
foreground: 'c8e1ff',
|
||||
token: 'entity.name.constant'
|
||||
},
|
||||
{
|
||||
foreground: 'c8e1ff',
|
||||
token: 'variable.other.constant'
|
||||
},
|
||||
{
|
||||
foreground: 'c8e1ff',
|
||||
token: 'variable.language'
|
||||
},
|
||||
{
|
||||
foreground: 'b392f0',
|
||||
token: 'entity'
|
||||
},
|
||||
{
|
||||
foreground: 'b392f0',
|
||||
token: 'entity.name'
|
||||
},
|
||||
{
|
||||
foreground: 'f6f8fa',
|
||||
token: 'variable.parameter.function'
|
||||
},
|
||||
{
|
||||
foreground: '7bcc72',
|
||||
token: 'entity.name.tag'
|
||||
},
|
||||
{
|
||||
foreground: 'ea4a5a',
|
||||
token: 'keyword'
|
||||
},
|
||||
{
|
||||
foreground: 'ea4a5a',
|
||||
token: 'storage'
|
||||
},
|
||||
{
|
||||
foreground: 'ea4a5a',
|
||||
token: 'storage.type'
|
||||
},
|
||||
{
|
||||
foreground: 'f6f8fa',
|
||||
token: 'storage.modifier.package'
|
||||
},
|
||||
{
|
||||
foreground: 'f6f8fa',
|
||||
token: 'storage.modifier.import'
|
||||
},
|
||||
{
|
||||
foreground: 'f6f8fa',
|
||||
token: 'storage.type.java'
|
||||
},
|
||||
{
|
||||
foreground: '79b8ff',
|
||||
token: 'string'
|
||||
},
|
||||
{
|
||||
foreground: '79b8ff',
|
||||
token: 'punctuation.definition.string'
|
||||
},
|
||||
{
|
||||
foreground: '79b8ff',
|
||||
token: 'string punctuation.section.embedded source'
|
||||
},
|
||||
{
|
||||
foreground: 'c8e1ff',
|
||||
token: 'support'
|
||||
},
|
||||
{
|
||||
foreground: 'c8e1ff',
|
||||
token: 'meta.property-name'
|
||||
},
|
||||
{
|
||||
foreground: 'fb8532',
|
||||
token: 'variable'
|
||||
},
|
||||
{
|
||||
foreground: 'f6f8fa',
|
||||
token: 'variable.other'
|
||||
},
|
||||
{
|
||||
foreground: 'd73a49',
|
||||
fontStyle: 'bold italic underline',
|
||||
token: 'invalid.broken'
|
||||
},
|
||||
{
|
||||
foreground: 'd73a49',
|
||||
fontStyle: 'bold italic underline',
|
||||
token: 'invalid.deprecated'
|
||||
},
|
||||
{
|
||||
foreground: 'fafbfc',
|
||||
background: 'd73a49',
|
||||
fontStyle: 'italic underline',
|
||||
token: 'invalid.illegal'
|
||||
},
|
||||
{
|
||||
foreground: 'fafbfc',
|
||||
background: 'd73a49',
|
||||
fontStyle: 'italic underline',
|
||||
token: 'carriage-return'
|
||||
},
|
||||
{
|
||||
foreground: 'd73a49',
|
||||
fontStyle: 'bold italic underline',
|
||||
token: 'invalid.unimplemented'
|
||||
},
|
||||
{
|
||||
foreground: 'd73a49',
|
||||
token: 'message.error'
|
||||
},
|
||||
{
|
||||
foreground: 'f6f8fa',
|
||||
token: 'string source'
|
||||
},
|
||||
{
|
||||
foreground: 'c8e1ff',
|
||||
token: 'string variable'
|
||||
},
|
||||
{
|
||||
foreground: '79b8ff',
|
||||
token: 'source.regexp'
|
||||
},
|
||||
{
|
||||
foreground: '79b8ff',
|
||||
token: 'string.regexp'
|
||||
},
|
||||
{
|
||||
foreground: '79b8ff',
|
||||
token: 'string.regexp.character-class'
|
||||
},
|
||||
{
|
||||
foreground: '79b8ff',
|
||||
token: 'string.regexp constant.character.escape'
|
||||
},
|
||||
{
|
||||
foreground: '79b8ff',
|
||||
token: 'string.regexp source.ruby.embedded'
|
||||
},
|
||||
{
|
||||
foreground: '79b8ff',
|
||||
token: 'string.regexp string.regexp.arbitrary-repitition'
|
||||
},
|
||||
{
|
||||
foreground: '7bcc72',
|
||||
fontStyle: 'bold',
|
||||
token: 'string.regexp constant.character.escape'
|
||||
},
|
||||
{
|
||||
foreground: 'c8e1ff',
|
||||
token: 'support.constant'
|
||||
},
|
||||
{
|
||||
foreground: 'c8e1ff',
|
||||
token: 'support.variable'
|
||||
},
|
||||
{
|
||||
foreground: 'c8e1ff',
|
||||
token: 'meta.module-reference'
|
||||
},
|
||||
{
|
||||
foreground: 'fb8532',
|
||||
token: 'markup.list'
|
||||
},
|
||||
{
|
||||
foreground: '0366d6',
|
||||
fontStyle: 'bold',
|
||||
token: 'markup.heading'
|
||||
},
|
||||
{
|
||||
foreground: '0366d6',
|
||||
fontStyle: 'bold',
|
||||
token: 'markup.heading entity.name'
|
||||
},
|
||||
{
|
||||
foreground: 'c8e1ff',
|
||||
token: 'markup.quote'
|
||||
},
|
||||
{
|
||||
foreground: 'f6f8fa',
|
||||
fontStyle: 'italic',
|
||||
token: 'markup.italic'
|
||||
},
|
||||
{
|
||||
foreground: 'f6f8fa',
|
||||
fontStyle: 'bold',
|
||||
token: 'markup.bold'
|
||||
},
|
||||
{
|
||||
foreground: 'c8e1ff',
|
||||
token: 'markup.raw'
|
||||
},
|
||||
{
|
||||
foreground: 'b31d28',
|
||||
background: 'ffeef0',
|
||||
token: 'markup.deleted'
|
||||
},
|
||||
{
|
||||
foreground: 'b31d28',
|
||||
background: 'ffeef0',
|
||||
token: 'meta.diff.header.from-file'
|
||||
},
|
||||
{
|
||||
foreground: 'b31d28',
|
||||
background: 'ffeef0',
|
||||
token: 'punctuation.definition.deleted'
|
||||
},
|
||||
{
|
||||
foreground: '176f2c',
|
||||
background: 'f0fff4',
|
||||
token: 'markup.inserted'
|
||||
},
|
||||
{
|
||||
foreground: '176f2c',
|
||||
background: 'f0fff4',
|
||||
token: 'meta.diff.header.to-file'
|
||||
},
|
||||
{
|
||||
foreground: '176f2c',
|
||||
background: 'f0fff4',
|
||||
token: 'punctuation.definition.inserted'
|
||||
},
|
||||
{
|
||||
foreground: 'b08800',
|
||||
background: 'fffdef',
|
||||
token: 'markup.changed'
|
||||
},
|
||||
{
|
||||
foreground: 'b08800',
|
||||
background: 'fffdef',
|
||||
token: 'punctuation.definition.changed'
|
||||
},
|
||||
{
|
||||
foreground: '2f363d',
|
||||
background: '959da5',
|
||||
token: 'markup.ignored'
|
||||
},
|
||||
{
|
||||
foreground: '2f363d',
|
||||
background: '959da5',
|
||||
token: 'markup.untracked'
|
||||
},
|
||||
{
|
||||
foreground: 'b392f0',
|
||||
fontStyle: 'bold',
|
||||
token: 'meta.diff.range'
|
||||
},
|
||||
{
|
||||
foreground: 'c8e1ff',
|
||||
token: 'meta.diff.header'
|
||||
},
|
||||
{
|
||||
foreground: '0366d6',
|
||||
fontStyle: 'bold',
|
||||
token: 'meta.separator'
|
||||
},
|
||||
{
|
||||
foreground: '0366d6',
|
||||
token: 'meta.output'
|
||||
},
|
||||
{
|
||||
foreground: 'ffeef0',
|
||||
token: 'brackethighlighter.tag'
|
||||
},
|
||||
{
|
||||
foreground: 'ffeef0',
|
||||
token: 'brackethighlighter.curly'
|
||||
},
|
||||
{
|
||||
foreground: 'ffeef0',
|
||||
token: 'brackethighlighter.round'
|
||||
},
|
||||
{
|
||||
foreground: 'ffeef0',
|
||||
token: 'brackethighlighter.square'
|
||||
},
|
||||
{
|
||||
foreground: 'ffeef0',
|
||||
token: 'brackethighlighter.angle'
|
||||
},
|
||||
{
|
||||
foreground: 'ffeef0',
|
||||
token: 'brackethighlighter.quote'
|
||||
},
|
||||
{
|
||||
foreground: 'd73a49',
|
||||
token: 'brackethighlighter.unmatched'
|
||||
},
|
||||
{
|
||||
foreground: 'd73a49',
|
||||
token: 'sublimelinter.mark.error'
|
||||
},
|
||||
{
|
||||
foreground: 'fb8532',
|
||||
token: 'sublimelinter.mark.warning'
|
||||
},
|
||||
{
|
||||
foreground: '6a737d',
|
||||
token: 'sublimelinter.gutter-mark'
|
||||
},
|
||||
{
|
||||
foreground: '79b8ff',
|
||||
fontStyle: 'underline',
|
||||
token: 'constant.other.reference.link'
|
||||
},
|
||||
{
|
||||
foreground: '79b8ff',
|
||||
fontStyle: 'underline',
|
||||
token: 'string.other.link'
|
||||
}
|
||||
],
|
||||
colors: {
|
||||
'editor.foreground': '#f6f8fa',
|
||||
'editor.background': '#24292e',
|
||||
'editor.selectionBackground': '#4c2889',
|
||||
'editor.inactiveSelectionBackground': '#444d56',
|
||||
'editor.lineHighlightBackground': '#444d56',
|
||||
'editorCursor.foreground': '#ffffff',
|
||||
'editorWhitespace.foreground': '#6a737d',
|
||||
'editorIndentGuide.background': '#6a737d',
|
||||
'editorIndentGuide.activeBackground': '#f6f8fa',
|
||||
'editor.selectionHighlightBorder': '#444d56'
|
||||
}
|
||||
}
|
||||
1
src/shared/ui/loading-screen/index.ts
Normal file
1
src/shared/ui/loading-screen/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from './loading-screen'
|
||||
28
src/shared/ui/loading-screen/loading-screen.tsx
Normal file
28
src/shared/ui/loading-screen/loading-screen.tsx
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import { Center, Progress, Stack, Text } from '@mantine/core'
|
||||
|
||||
export function LoadingScreen({
|
||||
height = '100%',
|
||||
text = undefined,
|
||||
value = 100
|
||||
}: {
|
||||
height?: string
|
||||
text?: string
|
||||
value?: number
|
||||
}) {
|
||||
return (
|
||||
<Center h={height}>
|
||||
<Stack align="center" gap="xs" w="100%">
|
||||
{text && <Text size="lg">{text}</Text>}
|
||||
<Progress
|
||||
animated
|
||||
color="green"
|
||||
maw="32rem"
|
||||
radius="xs"
|
||||
striped
|
||||
value={value}
|
||||
w="80%"
|
||||
/>
|
||||
</Stack>
|
||||
</Center>
|
||||
)
|
||||
}
|
||||
1
src/shared/ui/page/index.ts
Normal file
1
src/shared/ui/page/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from './page'
|
||||
42
src/shared/ui/page/page.tsx
Normal file
42
src/shared/ui/page/page.tsx
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import { forwardRef, ReactNode, useEffect } from 'react'
|
||||
import { AnimatePresence, motion } from 'framer-motion'
|
||||
import { nprogress } from '@mantine/nprogress'
|
||||
import { Box, BoxProps } from '@mantine/core'
|
||||
|
||||
interface PageProps extends BoxProps {
|
||||
children: ReactNode
|
||||
meta?: ReactNode
|
||||
title: string
|
||||
}
|
||||
|
||||
export const Page = forwardRef<HTMLDivElement, PageProps>(
|
||||
({ children, title = '', meta, ...other }, ref) => {
|
||||
useEffect(() => {
|
||||
nprogress.complete()
|
||||
return () => nprogress.start()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<title>{`${title}`}</title>
|
||||
{meta}
|
||||
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
initial={{ opacity: 0 }}
|
||||
transition={{
|
||||
duration: 0.3,
|
||||
ease: 'easeInOut'
|
||||
}}
|
||||
>
|
||||
<Box ref={ref} {...other}>
|
||||
{children}
|
||||
</Box>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</>
|
||||
)
|
||||
}
|
||||
)
|
||||
26
src/shared/ui/sticky-header/index.tsx
Normal file
26
src/shared/ui/sticky-header/index.tsx
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import { useWindowScroll } from '@mantine/hooks'
|
||||
import { Box, BoxProps } from '@mantine/core'
|
||||
import { ReactNode } from 'react'
|
||||
import clsx from 'clsx'
|
||||
|
||||
import classes from './sticky-header.module.css'
|
||||
|
||||
interface StickyHeaderProps extends BoxProps {
|
||||
children?: ReactNode
|
||||
offset?: number
|
||||
}
|
||||
|
||||
export function StickyHeader({ children, offset = 2, className, ...rest }: StickyHeaderProps) {
|
||||
const [scroll] = useWindowScroll()
|
||||
|
||||
return (
|
||||
<Box
|
||||
className={clsx(classes.root, className)}
|
||||
component="header"
|
||||
data-sticked={scroll.y > offset}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
20
src/shared/ui/sticky-header/sticky-header.module.css
Normal file
20
src/shared/ui/sticky-header/sticky-header.module.css
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
.root {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 199;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: var(--mantine-spacing-md);
|
||||
|
||||
backdrop-filter: blur(24px);
|
||||
|
||||
@media (min-width: $mantine-breakpoint-md) {
|
||||
padding-left: 1.25rem;
|
||||
padding-right: 1.25rem;
|
||||
}
|
||||
|
||||
@media (min-width: $mantine-breakpoint-lg) {
|
||||
padding-left: 1.5rem;
|
||||
padding-right: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
import axios from 'axios'
|
||||
|
||||
export const fetchWithProgress = async (url: string, onProgress?: (progress: number) => void) => {
|
||||
try {
|
||||
const response = await axios.get(url, {
|
||||
onDownloadProgress: (progressEvent) => {
|
||||
if (progressEvent.total) {
|
||||
const progress = Math.round((progressEvent.loaded * 100) / progressEvent.total)
|
||||
onProgress?.(progress)
|
||||
} else {
|
||||
onProgress?.(100)
|
||||
}
|
||||
},
|
||||
responseType: 'arraybuffer'
|
||||
})
|
||||
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('Download failed:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
1
src/shared/utils/fetch-with-progress/index.ts
Normal file
1
src/shared/utils/fetch-with-progress/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from './fetch-with-progress.util'
|
||||
1
src/vite-env.d.ts
vendored
Normal file
1
src/vite-env.d.ts
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
/// <reference types="vite/client" />
|
||||
101
src/widgets/config/config-editor/config-editor.widget.tsx
Normal file
101
src/widgets/config/config-editor/config-editor.widget.tsx
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
import { monacoTheme } from '@/shared/lib/monaco-theme'
|
||||
import Editor, { Monaco } from '@monaco-editor/react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { Box, Code, Paper } from '@mantine/core'
|
||||
|
||||
import { ConfigEditorActionsFeature } from '@features/config/config-editor-actions/ui'
|
||||
import { ConfigValidationFeature } from '@features/config/config-validation/ui'
|
||||
import { MonacoSetupFeature } from '@features/config/monaco-setup/lib'
|
||||
|
||||
import { Props } from './interfaces'
|
||||
|
||||
export function ConfigEditorWidget(props: Props) {
|
||||
const { config } = props
|
||||
const [result, setResult] = useState('')
|
||||
const [isConfigValid, setIsConfigValid] = useState(false)
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
|
||||
const editorRef = useRef<unknown>(null)
|
||||
const monacoRef = useRef<unknown>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (!monacoRef.current) return
|
||||
MonacoSetupFeature.setup(monacoRef.current as Monaco)
|
||||
}, [monacoRef.current])
|
||||
|
||||
const handleEditorDidMount = (monaco: Monaco) => {
|
||||
monaco.editor.defineTheme('GithubDark', {
|
||||
...monacoTheme
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Paper mb="md" p={0} radius="xs" withBorder>
|
||||
<Editor
|
||||
beforeMount={handleEditorDidMount}
|
||||
defaultLanguage="json"
|
||||
height="400px"
|
||||
loading={'Loading editor...'}
|
||||
onChange={() =>
|
||||
ConfigValidationFeature.validate(
|
||||
editorRef,
|
||||
monacoRef,
|
||||
setResult,
|
||||
setIsConfigValid
|
||||
)
|
||||
}
|
||||
onMount={(editor, monaco) => {
|
||||
editorRef.current = editor
|
||||
monacoRef.current = monaco
|
||||
ConfigValidationFeature.validate(
|
||||
editorRef,
|
||||
monacoRef,
|
||||
setResult,
|
||||
setIsConfigValid
|
||||
)
|
||||
}}
|
||||
options={{
|
||||
autoClosingBrackets: 'always',
|
||||
autoClosingQuotes: 'always',
|
||||
autoIndent: 'full',
|
||||
automaticLayout: true,
|
||||
bracketPairColorization: true,
|
||||
detectIndentation: true,
|
||||
folding: true,
|
||||
foldingStrategy: 'indentation',
|
||||
fontSize: 14,
|
||||
formatOnPaste: true,
|
||||
formatOnType: true,
|
||||
guides: {
|
||||
bracketPairs: true,
|
||||
indentation: true
|
||||
},
|
||||
insertSpaces: true,
|
||||
minimap: { enabled: false },
|
||||
quickSuggestions: true,
|
||||
scrollBeyondLastLine: false,
|
||||
tabSize: 2
|
||||
}}
|
||||
theme={'GithubDark'}
|
||||
value={JSON.stringify(config, null, 2)}
|
||||
/>
|
||||
</Paper>
|
||||
|
||||
<ConfigEditorActionsFeature
|
||||
editorRef={editorRef}
|
||||
isConfigValid={isConfigValid}
|
||||
isSaving={isSaving}
|
||||
monacoRef={monacoRef}
|
||||
setIsSaving={setIsSaving}
|
||||
setResult={setResult}
|
||||
/>
|
||||
|
||||
{result && (
|
||||
<Code block p="md">
|
||||
{result}
|
||||
</Code>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
2
src/widgets/config/config-editor/index.ts
Normal file
2
src/widgets/config/config-editor/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export * from './config-editor.widget'
|
||||
export * from './interfaces'
|
||||
1
src/widgets/config/config-editor/interfaces/index.ts
Normal file
1
src/widgets/config/config-editor/interfaces/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from './props.interface'
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
export interface Props {
|
||||
config: Record<string, unknown> | string
|
||||
}
|
||||
1
src/widgets/header/index.ts
Normal file
1
src/widgets/header/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from './ui/header.widget'
|
||||
10
src/widgets/header/ui/header.module.css
Normal file
10
src/widgets/header/ui/header.module.css
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
.root {
|
||||
z-index: 198;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
@media (min-width: $mantine-breakpoint-xl) {
|
||||
padding-top: 1.25rem;
|
||||
padding-bottom: 1.25rem;
|
||||
}
|
||||
}
|
||||
25
src/widgets/header/ui/header.widget.tsx
Normal file
25
src/widgets/header/ui/header.widget.tsx
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import { ActionIcon, Group, Title } from '@mantine/core'
|
||||
import { PiGithubLogo } from 'react-icons/pi'
|
||||
import { StickyHeader } from '@/shared/ui/sticky-header'
|
||||
|
||||
import classes from './header.module.css'
|
||||
|
||||
export function HeaderWidget() {
|
||||
return (
|
||||
<StickyHeader className={classes.root} px="md">
|
||||
<Group h="100%" justify="space-between">
|
||||
<Title order={3}>Xray Config Validator</Title>
|
||||
|
||||
<ActionIcon
|
||||
component="a"
|
||||
href="https://github.com/mmmray/xray-online"
|
||||
size="lg"
|
||||
target="_blank"
|
||||
variant="subtle"
|
||||
>
|
||||
<PiGithubLogo size={24} />
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
</StickyHeader>
|
||||
)
|
||||
}
|
||||
67
tsconfig.json
Normal file
67
tsconfig.json
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"removeComments": true,
|
||||
"target": "ES2020",
|
||||
"lib": [
|
||||
"ESNext",
|
||||
"DOM",
|
||||
"DOM.Iterable",
|
||||
],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
/* ------------------------------------------------------------ */
|
||||
/* Bundler mode */
|
||||
/* ------------------------------------------------------------ */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
/* ------------------------------------------------------------ */
|
||||
/* ------------------------------------------------------------ */
|
||||
"strict": true,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false, // TURN THIS
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"strictPropertyInitialization": false,
|
||||
"forceConsistentCasingInFileNames": false,
|
||||
/* ------------------------------------------------------------ */
|
||||
/* Paths */
|
||||
/* ------------------------------------------------------------ */
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@entitites/*": [
|
||||
"./src/entitites/*"
|
||||
],
|
||||
"@features/*": [
|
||||
"./src/features/*"
|
||||
],
|
||||
"@pages/*": [
|
||||
"./src/pages/*"
|
||||
],
|
||||
"@widgets/*": [
|
||||
"./src/widgets/*"
|
||||
],
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
],
|
||||
"@public/*": [
|
||||
"./public/*"
|
||||
],
|
||||
"@shared/*": [
|
||||
"./src/shared/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"src",
|
||||
"client.d.ts",
|
||||
"global.d.ts"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
13
tsconfig.node.json
Normal file
13
tsconfig.node.json
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true
|
||||
},
|
||||
"include": [
|
||||
"vite.config.ts"
|
||||
]
|
||||
}
|
||||
1
tsconfig.node.tsbuildinfo
Normal file
1
tsconfig.node.tsbuildinfo
Normal file
File diff suppressed because one or more lines are too long
1
tsconfig.tsbuildinfo
Normal file
1
tsconfig.tsbuildinfo
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/features/config/config-editor-actions/config-editor-actions.feature.tsx","./src/features/config/config-editor-actions/index.ts","./src/features/config/config-editor-actions/interfaces/index.ts","./src/features/config/config-editor-actions/interfaces/props.interface.tsx","./src/features/config/config-validation/config-validation.feature.tsx","./src/features/config/config-validation/index.ts","./src/features/config/monaco-setup/index.ts","./src/features/config/monaco-setup/monaco-setup.feature.tsx","./src/pages/config/ui/components/config.page.component.tsx","./src/pages/config/ui/components/interfaces/index.ts","./src/pages/config/ui/components/interfaces/props.interface.tsx","./src/pages/config/ui/connectors/config.page.connector.tsx","./src/shared/constants/default-config.tsx","./src/shared/constants/index.ts","./src/shared/monaco-theme/index.ts","./src/shared/monaco-theme/monaco-theme.tsx","./src/shared/sticky-header/index.tsx","./src/shared/ui/loading-screen/index.ts","./src/shared/ui/loading-screen/loading-screen.tsx","./src/shared/ui/page/index.ts","./src/shared/ui/page/page.tsx","./src/shared/utils/fetch-with-progress/fetch-with-progress.util.ts","./src/shared/utils/fetch-with-progress/index.ts","./src/widgets/config/config-editor/config-editor.widget.tsx","./src/widgets/config/config-editor/index.ts","./src/widgets/config/config-editor/interfaces/index.ts","./src/widgets/config/config-editor/interfaces/props.interface.tsx","./src/widgets/header/index.ts","./src/widgets/header/ui/header.widget.tsx","./global.d.ts"],"version":"5.7.2"}
|
||||
2
vite.config.d.ts
vendored
Normal file
2
vite.config.d.ts
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
declare const _default: import("vite").UserConfig;
|
||||
export default _default;
|
||||
25
vite.config.ts
Normal file
25
vite.config.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import tsconfigPaths from 'vite-tsconfig-paths'
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react(), tsconfigPaths()],
|
||||
publicDir: 'public',
|
||||
build: {
|
||||
target: 'esNext',
|
||||
outDir: 'dist'
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@entitites': fileURLToPath(new URL('./src/entitites', import.meta.url)),
|
||||
'@features': fileURLToPath(new URL('./src/features', import.meta.url)),
|
||||
'@pages': fileURLToPath(new URL('./src/pages', import.meta.url)),
|
||||
'@widgets': fileURLToPath(new URL('./src/widgets', import.meta.url)),
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||
'@public': fileURLToPath(new URL('./public', import.meta.url)),
|
||||
'@shared': fileURLToPath(new URL('./src/shared', import.meta.url))
|
||||
}
|
||||
}
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue