mirror of
https://github.com/alireza0/x-ui.git
synced 2026-03-14 13:31:41 +00:00
Compare commits
133 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c9e4918494 | ||
|
|
63b166ce40 | ||
|
|
2ab1726131 | ||
|
|
7dd5449cf4 | ||
|
|
ded780b11a | ||
|
|
2e44501573 | ||
|
|
3e7770ad97 | ||
|
|
0c380aebdd | ||
|
|
93f6ba897c | ||
|
|
b93adb7435 | ||
|
|
538fdb9860 | ||
|
|
c4ed595a1c | ||
|
|
5acfdf584e | ||
|
|
62ceaeba8f | ||
|
|
3c688d85e7 | ||
|
|
99a4cf4179 | ||
|
|
7aa242b64e | ||
|
|
cbb3c009cc | ||
|
|
d8e8195271 | ||
|
|
e31ec2cade | ||
|
|
af42723213 | ||
|
|
94117f94e8 | ||
|
|
b946f7a1d4 | ||
|
|
aaa0f66a37 | ||
|
|
ebbe2c8e5b | ||
|
|
0fdd4dcfb9 | ||
|
|
78fb3844fe | ||
|
|
9452f4fc15 | ||
|
|
5996f92fe5 | ||
|
|
4cabe80462 | ||
|
|
1cf0d52433 | ||
|
|
3cef5142d9 | ||
|
|
ac8af25cef | ||
|
|
aee2107f30 | ||
|
|
7971082beb | ||
|
|
d60290d0a1 | ||
|
|
0b1bc1908b | ||
|
|
a342ef9e90 | ||
|
|
317e4f1fdd | ||
|
|
fd82e701b6 | ||
|
|
046be5dd0f | ||
|
|
fa4c954444 | ||
|
|
03e1434051 | ||
|
|
5725420197 | ||
|
|
fd64ae5c85 | ||
|
|
ff59bb60ce | ||
|
|
7ec646fd0d | ||
|
|
902368fc03 | ||
|
|
57827225b4 | ||
|
|
d406d2925a | ||
|
|
2f2876ec90 | ||
|
|
20d00d31a1 | ||
|
|
48b327ccb5 | ||
|
|
9cc893a396 | ||
|
|
91e9c2d6b2 | ||
|
|
6bfaad48fd | ||
|
|
10779201f5 | ||
|
|
5b20505515 | ||
|
|
ed5db9390d | ||
|
|
5ea3333367 | ||
|
|
9a95530742 | ||
|
|
b5eb368b89 | ||
|
|
1f026d0039 | ||
|
|
056de927da | ||
|
|
4596a981d6 | ||
|
|
5cebf8faef | ||
|
|
2cf2da8ae2 | ||
|
|
da82d14c18 | ||
|
|
8da966a7f7 | ||
|
|
81b96507d0 | ||
|
|
0986791fb3 | ||
|
|
d4317a0a62 | ||
|
|
5fc7f132a1 | ||
|
|
bdb371367e | ||
|
|
407bebad05 | ||
|
|
10d85f9855 | ||
|
|
98c11dd83e | ||
|
|
86f6050697 | ||
|
|
e488dc18b1 | ||
|
|
7ed106a0f0 | ||
|
|
1f0c9acd89 | ||
|
|
99f9788175 | ||
|
|
f64e9dff8a | ||
|
|
1e0dabb434 | ||
|
|
30bb4efcc4 | ||
|
|
5028cf8e08 | ||
|
|
2736779353 | ||
|
|
a45760a9a3 | ||
|
|
d2f6938177 | ||
|
|
cbc4c144f2 | ||
|
|
3ec5ca3e8f | ||
|
|
5c0e55c708 | ||
|
|
d4942a7d83 | ||
|
|
3a913ca81a | ||
|
|
3dd8dbdecf | ||
|
|
02053fea83 | ||
|
|
b69f0cfe82 | ||
|
|
e2912448d6 | ||
|
|
211d31390d | ||
|
|
b10759d462 | ||
|
|
e372575f58 | ||
|
|
59d62afab5 | ||
|
|
5546883799 | ||
|
|
3eae6300e7 | ||
|
|
a8878bc1dc | ||
|
|
b41aef37e5 | ||
|
|
d68decd17b | ||
|
|
5f9ae30b71 | ||
|
|
1f4ff4b985 | ||
|
|
1daa8b8d7a | ||
|
|
ed8ef18460 | ||
|
|
f4cda8a4c0 | ||
|
|
144d1e79c8 | ||
|
|
7410b80e7a | ||
|
|
54197993db | ||
|
|
878e5507e6 | ||
|
|
94387d48e3 | ||
|
|
cfc11151d2 | ||
|
|
0dbb67d0bb | ||
|
|
a4b447e2d7 | ||
|
|
ca155bd871 | ||
|
|
fb8f27ad49 | ||
|
|
a88044500e | ||
|
|
3d0663d793 | ||
|
|
1735faa193 | ||
|
|
d1980581d9 | ||
|
|
052ceb5a2d | ||
|
|
bd075ed81d | ||
|
|
04b52ae637 | ||
|
|
739b4a1782 | ||
|
|
2ca3a05cca | ||
|
|
5e07e32388 | ||
|
|
871d46be72 |
27
.github/workflows/release.yml
vendored
27
.github/workflows/release.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v4
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.21'
|
||||
- name: build linux amd64 version
|
||||
@@ -26,12 +26,13 @@ jobs:
|
||||
mv xui-release x-ui
|
||||
mkdir bin
|
||||
cd bin
|
||||
wget https://github.com/XTLS/Xray-core/releases/download/v1.8.4/Xray-linux-64.zip
|
||||
wget https://github.com/XTLS/Xray-core/releases/download/v1.8.6/Xray-linux-64.zip
|
||||
unzip Xray-linux-64.zip
|
||||
rm -f Xray-linux-64.zip geoip.dat geosite.dat iran.dat
|
||||
rm -f Xray-linux-64.zip geoip.dat geosite.dat LICENSE README.md
|
||||
wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat
|
||||
wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat
|
||||
wget https://github.com/MasterKia/iran-hosted-domains/releases/latest/download/iran.dat
|
||||
wget -O geoip_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat
|
||||
wget -O geosite_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat
|
||||
mv xray xray-linux-amd64
|
||||
cd ..
|
||||
cd ..
|
||||
@@ -51,7 +52,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v4
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.21'
|
||||
- name: build linux arm64 version
|
||||
@@ -67,12 +68,13 @@ jobs:
|
||||
mv xui-release x-ui
|
||||
mkdir bin
|
||||
cd bin
|
||||
wget https://github.com/xtls/xray-core/releases/download/v1.8.4/Xray-linux-arm64-v8a.zip
|
||||
wget https://github.com/xtls/xray-core/releases/download/v1.8.6/Xray-linux-arm64-v8a.zip
|
||||
unzip Xray-linux-arm64-v8a.zip
|
||||
rm -f Xray-linux-64.zip geoip.dat geosite.dat iran.dat
|
||||
rm -f Xray-linux-arm64-v8a.zip geoip.dat geosite.dat LICENSE README.md
|
||||
wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat
|
||||
wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat
|
||||
wget https://github.com/Masterkia/iran-hosted-domains/releases/latest/download/iran.dat
|
||||
wget -O geoip_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat
|
||||
wget -O geosite_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat
|
||||
mv xray xray-linux-arm64
|
||||
cd ..
|
||||
cd ..
|
||||
@@ -92,7 +94,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v4
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.21'
|
||||
- name: build linux s390x version
|
||||
@@ -108,12 +110,13 @@ jobs:
|
||||
mv xui-release x-ui
|
||||
mkdir bin
|
||||
cd bin
|
||||
wget https://github.com/xtls/xray-core/releases/download/v1.8.4/Xray-linux-s390x.zip
|
||||
wget https://github.com/xtls/xray-core/releases/download/v1.8.6/Xray-linux-s390x.zip
|
||||
unzip Xray-linux-s390x.zip
|
||||
rm -f Xray-linux-64.zip geoip.dat geosite.dat iran.dat
|
||||
rm -f Xray-linux-s390x.zip geoip.dat geosite.dat LICENSE README.md
|
||||
wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat
|
||||
wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat
|
||||
wget https://github.com/Masterkia/iran-hosted-domains/releases/latest/download/iran.dat
|
||||
wget -O geoip_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat
|
||||
wget -O geosite_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat
|
||||
mv xray xray-linux-s390x
|
||||
cd ..
|
||||
cd ..
|
||||
|
||||
@@ -11,11 +11,12 @@ else
|
||||
fi
|
||||
mkdir -p build/bin
|
||||
cd build/bin
|
||||
wget "https://github.com/XTLS/Xray-core/releases/download/v1.8.4/Xray-linux-${ARCH}.zip"
|
||||
wget "https://github.com/XTLS/Xray-core/releases/download/v1.8.6/Xray-linux-${ARCH}.zip"
|
||||
unzip "Xray-linux-${ARCH}.zip"
|
||||
rm -f "Xray-linux-${ARCH}.zip" geoip.dat geosite.dat iran.dat
|
||||
rm -f "Xray-linux-${ARCH}.zip" geoip.dat geosite.dat LICENSE README.md
|
||||
mv xray "xray-linux-${FNAME}"
|
||||
wget "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat"
|
||||
wget "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat"
|
||||
wget "https://github.com/Masterkia/iran-hosted-domains/releases/latest/download/iran.dat"
|
||||
wget -O geoip_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat
|
||||
wget -O geosite_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat
|
||||
cd ../../
|
||||
|
||||
@@ -3,7 +3,9 @@ WORKDIR /app
|
||||
ARG TARGETARCH
|
||||
RUN apk --no-cache --update add build-base gcc wget unzip
|
||||
COPY . .
|
||||
RUN env CGO_ENABLED=1 go build -o build/x-ui main.go
|
||||
ENV CGO_ENABLED=1
|
||||
ENV CGO_CFLAGS="-D_LARGEFILE64_SOURCE"
|
||||
RUN go build -o build/x-ui main.go
|
||||
RUN ./DockerInitFiles.sh "$TARGETARCH"
|
||||
|
||||
FROM alpine
|
||||
|
||||
196
README.md
196
README.md
@@ -1,4 +1,5 @@
|
||||
# x-ui
|
||||
# X-UI
|
||||
**Advanced GUI panel based on Xray Core supports multiple protocols and languages**
|
||||
|
||||

|
||||

|
||||
@@ -8,35 +9,41 @@
|
||||
|
||||
> **Disclaimer: This project is only for personal learning and communication, please do not use it for illegal purposes, please do not use it in a production environment**
|
||||
|
||||
xray panel supporting multi-protocol, **Multi-lang (English,Farsi,Chinese,Russian)**
|
||||
|
||||
| Features | Enable? |
|
||||
| ------------------------------------ | :----------------: |
|
||||
| Multi-lang | :heavy_check_mark: |
|
||||
| Dark/Light Theme | :heavy_check_mark: |
|
||||
| Search in deep | :heavy_check_mark: |
|
||||
| Inbound Multi User | :heavy_check_mark: |
|
||||
| Multi User Traffic & Expiration time | :heavy_check_mark: |
|
||||
| REST API | :heavy_check_mark: |
|
||||
| Telegram BOT (admin + clients) | :heavy_check_mark: |
|
||||
| Backup database using Telegram BOT | :heavy_check_mark: |
|
||||
| Subscription link + userInfo | :heavy_check_mark: |
|
||||
| Calculate expire date on first usage | :heavy_check_mark: |
|
||||
|
||||
**If you think this project is helpful to you, you may wish to give a** :star2:
|
||||
|
||||
**Buy Me a Coffee :**
|
||||
|
||||
- Tron USDT (TRC20): `TYTq73Gj6dJ67qe58JVPD9zpjW2cc9XgVz`
|
||||
- Tezos (XTZ): tz2Wnh2SsY1eezXrcLChu6idWpgdHzUFQcts
|
||||
- USDT Tron (TRC20): `TYTq73Gj6dJ67qe58JVPD9zpjW2cc9XgVz`
|
||||
- Tezos (XTZ):
|
||||
`tz2Wnh2SsY1eezXrcLChu6idWpgdHzUFQcts`
|
||||
|
||||
# Install & Upgrade to latest version
|
||||
|
||||
## Quick Look
|
||||
| Features | Enable? |
|
||||
| -------------------------------------- | :----------------: |
|
||||
| Multi-Protocol | :heavy_check_mark: |
|
||||
| Multi-Language | :heavy_check_mark: |
|
||||
| Multi-User Inbounds | :heavy_check_mark: |
|
||||
| Advanced Traffic Routing | :heavy_check_mark: |
|
||||
| REST API | :heavy_check_mark: |
|
||||
|Show Online Users | :heavy_check_mark: |
|
||||
| Manage Users Traffic Data & Expiry Date| :heavy_check_mark: |
|
||||
| Apply Expiry Date based on First Usage | :heavy_check_mark: |
|
||||
| Telegram Bot (admin + clients) | :heavy_check_mark: |
|
||||
| Database Backup using Telegram Bot | :heavy_check_mark: |
|
||||
| Subscription Link + UserInfo | :heavy_check_mark: |
|
||||
| Search in Deep | :heavy_check_mark: |
|
||||
| Dark/Light Theme | :heavy_check_mark: |
|
||||
|
||||
|
||||
## Install & Upgrade to Latest Version
|
||||
|
||||
```sh
|
||||
bash <(curl -Ls https://raw.githubusercontent.com/alireza0/x-ui/master/install.sh)
|
||||
```
|
||||
|
||||
## Install custom version
|
||||
## Install Custom Version
|
||||
|
||||
To install your desired version you can add the version to the end of install command. Example for ver `0.5.2`:
|
||||
|
||||
@@ -44,7 +51,7 @@ To install your desired version you can add the version to the end of install co
|
||||
bash <(curl -Ls https://raw.githubusercontent.com/alireza0/x-ui/master/install.sh) 0.5.2
|
||||
```
|
||||
|
||||
## Manual install & upgrade
|
||||
## Manual Install & Upgrade
|
||||
|
||||
1. First download the latest compressed package from https://github.com/alireza0/x-ui/releases, generally choose Architecture `amd64`
|
||||
2. Then upload the compressed package to the server's `/root/` directory and login to the server with user `root`
|
||||
@@ -66,15 +73,15 @@ systemctl enable x-ui
|
||||
systemctl restart x-ui
|
||||
```
|
||||
|
||||
## Install using docker
|
||||
## Install Using Docker
|
||||
|
||||
1. install docker
|
||||
1. Install Docker
|
||||
|
||||
```shell
|
||||
curl -fsSL https://get.docker.com | sh
|
||||
```
|
||||
|
||||
2. install x-ui
|
||||
2. Install X-UI
|
||||
|
||||
```shell
|
||||
mkdir x-ui && cd x-ui
|
||||
@@ -93,59 +100,90 @@ docker run -itd \
|
||||
docker build -t x-ui .
|
||||
```
|
||||
|
||||
# Features
|
||||
## Languages
|
||||
|
||||
- System Status Monitoring
|
||||
- Search within all inbounds and clients
|
||||
- English
|
||||
- Chinese
|
||||
- Farsi
|
||||
- Russian
|
||||
- Vietnamese
|
||||
|
||||
## Features
|
||||
|
||||
- Supported protocols: VMess, VLESS, Trojan, Shadowsocks, Dokodemo-door, SOCKS, HTTP
|
||||
- Support XTLS native encryptions (Vision, REALITY)
|
||||
- Support advanced JSON editor GUI for Xray-Core configuration
|
||||
- Support advanced GUI for routing traffic (Reverse and Transparent proxy, Multi-Domain, Multi-Certificate, Multi-Port per inbound)
|
||||
- Support Multi-User per inbound
|
||||
- Support applying traffic data limits and expiry dates per user/inbound
|
||||
- Support system status monitoring
|
||||
- Support deep database search
|
||||
- Show traffic statistics
|
||||
- Show online users
|
||||
- Show users with expired date or exceeded traffic limits
|
||||
- Support subscription (multi) link
|
||||
- Support import/export database
|
||||
- Support One-Click SSL certificate application and automatic renewal
|
||||
- Support HTTPS for panel (self-provided domain name + SSL certificate)
|
||||
- Support Dark/Light theme UI
|
||||
- Support multi-user multi-protocol, web page visualization operation
|
||||
- Support multi-domain configuration and multi-certificate inbounds
|
||||
- Supported protocols: vmess, vless, trojan, shadowsocks, dokodemo-door, socks, http
|
||||
- Support for configuring more transport configurations
|
||||
- Traffic statistics, limit traffic, limit expiration time
|
||||
- Customizable xray configuration templates
|
||||
- Support subscription ( multi ) link
|
||||
- Detect users which are expiring or exceed traffic limit soon
|
||||
- Support https access panel (self-provided domain name + ssl certificate)
|
||||
- Support one-click SSL certificate application and automatic renewal
|
||||
- For more advanced configuration items, please refer to the panel
|
||||
- Support export/import database from panel
|
||||
|
||||
## suggestion system
|
||||
## Recommended OS
|
||||
|
||||
- CentOS 8+
|
||||
- Ubuntu 20+
|
||||
- Debian 10+
|
||||
- Fedora 36+
|
||||
|
||||
## API routes
|
||||
## Screenshots
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
|
||||
## API Routes
|
||||
|
||||
<details>
|
||||
<summary>Click for details</summary>
|
||||
|
||||
### Usage
|
||||
|
||||
- `/login` with `PUSH` user data: `{username: '', password: ''}` for login
|
||||
- `/xui/API/inbounds` base for following actions:
|
||||
|
||||
| Method | Path | Action |
|
||||
| :----: | ------------------------------- | ----------------------------------------- |
|
||||
| `GET` | `"/"` | Get all inbounds |
|
||||
| `GET` | `"/get/:id"` | Get inbound with inbound.id |
|
||||
| `GET` | `"/createbackup"` | Telegram bot sends backup to admins |
|
||||
| `POST` | `"/add"` | Add inbound |
|
||||
| `POST` | `"/del/:id"` | Delete Inbound |
|
||||
| `POST` | `"/update/:id"` | Update Inbound |
|
||||
| `POST` | `"/addClient/"` | Add Client to inbound |
|
||||
| `POST` | `"/:id/delClient/:clientId"` | Delete Client by clientId\* |
|
||||
| `POST` | `"/updateClient/:clientId"` | Update Client by clientId\* |
|
||||
| `POST` | `"/getClientTraffics/:email"` | Get Client's Traffic |
|
||||
| `POST` | `"/resetAllTraffics"` | Reset traffics of all inbounds |
|
||||
| `POST` | `"/resetAllClientTraffics/:id"` | Reset inbound clients traffics (-1: all) |
|
||||
| `POST` | `"/delDepletedClients/:id"` | Delete inbound depleted clients (-1: all) |
|
||||
| Method | Path | Action |
|
||||
| :----: | --------------------------------- | ----------------------------------------- |
|
||||
| `GET` | `"/"` | Get all inbounds |
|
||||
| `GET` | `"/get/:id"` | Get inbound with inbound.id |
|
||||
| `GET` | `"/createbackup"` | Telegram bot sends backup to admins |
|
||||
| `POST` | `"/add"` | Add inbound |
|
||||
| `POST` | `"/del/:id"` | Delete inbound |
|
||||
| `POST` | `"/update/:id"` | Update inbound |
|
||||
| `POST` | `"/addClient/"` | Add client to inbound |
|
||||
| `POST` | `"/:id/delClient/:clientId"` | Delete client by clientId\* |
|
||||
| `POST` | `"/updateClient/:clientId"` | Update client by clientId\* |
|
||||
| `GET` | `"/getClientTraffics/:email"` | Get client's traffic |
|
||||
| `POST` | `"/:id/resetClientTraffic/:email"` | Reset client's traffic |
|
||||
| `POST` | `"/resetAllTraffics"` | Reset traffics of all inbounds |
|
||||
| `POST` | `"/resetAllClientTraffics/:id"` | Reset inbound clients traffics (-1: all) |
|
||||
| `POST` | `"/delDepletedClients/:id"` | Delete inbound depleted clients (-1: all) |
|
||||
| `POST` | `"/onlines"` | Get online users ( list of emails ) |
|
||||
|
||||
\*- The field `clientId` should be filled by:
|
||||
|
||||
- `client.id` for VMESS and VLESS
|
||||
- `client.password` for TROJAN
|
||||
- `client.id` for VMess and VLESS
|
||||
- `client.password` for Trojan
|
||||
- `client.email` for Shadowsocks
|
||||
|
||||
# Environment Variables
|
||||
</details>
|
||||
|
||||
## Environment Variables
|
||||
|
||||
<details>
|
||||
<summary>Click for details</summary>
|
||||
|
||||
### Usage
|
||||
|
||||
| Variable | Type | Default |
|
||||
| -------------- | :--------------------------------------------: | :------------ |
|
||||
@@ -154,12 +192,9 @@ docker build -t x-ui .
|
||||
| XUI_BIN_FOLDER | `string` | `"bin"` |
|
||||
| XUI_DB_FOLDER | `string` | `"/etc/x-ui"` |
|
||||
|
||||
# Screenshot from Inbouds page
|
||||
</details>
|
||||
|
||||

|
||||

|
||||
|
||||
## SSL certificate application
|
||||
## SSL Certificate
|
||||
|
||||
<details>
|
||||
<summary>Click for details</summary>
|
||||
@@ -176,11 +211,13 @@ certbot certonly --standalone --register-unsafely-without-email --non-interactiv
|
||||
|
||||
</details>
|
||||
|
||||
## Tg robot use
|
||||
## Telegram Bot
|
||||
|
||||
<details>
|
||||
<summary>Click for details</summary>
|
||||
|
||||
### Usage
|
||||
|
||||
X-UI supports daily traffic notification, panel login reminder and other functions through the Tg robot. To use the Tg robot, you need to apply for the specific application tutorial. You can refer to the [blog](https://coderfan.net/how-to-use-telegram-bot-to-alarm-you-when-someone-login-into-your-vps.html)
|
||||
Set the robot-related parameters in the panel background, including:
|
||||
|
||||
@@ -200,13 +237,13 @@ Reference syntax:
|
||||
- @daily // Daily notification (00:00 in the morning)
|
||||
- @every 8h // notify every 8 hours
|
||||
|
||||
### Telegram Bot Features
|
||||
### Features
|
||||
|
||||
- Report periodic
|
||||
- Login notification
|
||||
- CPU threshold notification
|
||||
- Threshold for Expiration time and Traffic to report in advance
|
||||
- Support client report menu if client's telegram username added to the user's configurations
|
||||
- Support client report menu if client's telegram ID or telegram UserName added to the user's configurations
|
||||
- Support telegram traffic report searched with UUID (VMESS/VLESS) or Password (TROJAN) - anonymously
|
||||
- Menu based bot
|
||||
- Search client by email ( only admin )
|
||||
@@ -217,18 +254,23 @@ Reference syntax:
|
||||
- Multi language bot
|
||||
</details>
|
||||
|
||||
# T-Shoots:
|
||||
## Troubleshoots
|
||||
|
||||
**If you upgrade from an old version or other forks, for enable traffic for users you should do :**
|
||||
<details>
|
||||
<summary>Click for details</summary>
|
||||
|
||||
find this in config :
|
||||
### Enable Traffic Usage
|
||||
|
||||
Please be aware if you upgrade from an old X-UI version or other forks, by default data traffic usage for users may not work! it's recommended to follow below steps for enabeling:
|
||||
|
||||
1. Find this section in config file
|
||||
|
||||
```json
|
||||
"policy": {
|
||||
"system": {
|
||||
```
|
||||
|
||||
**and add this just after ` "policy": {` :**
|
||||
2. Add below section just after ` "policy": {` :
|
||||
|
||||
```json
|
||||
"levels": {
|
||||
@@ -239,7 +281,7 @@ find this in config :
|
||||
},
|
||||
```
|
||||
|
||||
**the final output is like :**
|
||||
- The final output is like:
|
||||
|
||||
```json
|
||||
"policy": {
|
||||
@@ -258,20 +300,20 @@ find this in config :
|
||||
"routing": {
|
||||
```
|
||||
|
||||
restart panel
|
||||
3. Save and restart panel
|
||||
|
||||
</details>
|
||||
|
||||
# a special thanks to
|
||||
## a Special Thanks to
|
||||
|
||||
- [HexaSoftwareTech](https://github.com/HexaSoftwareTech/)
|
||||
- [MHSanaei](https://github.com/MHSanaei)
|
||||
|
||||
# Acknowledgment
|
||||
## Acknowledgment
|
||||
|
||||
- [Iran Hosted Domains](https://github.com/bootmortis/iran-hosted-domains) (License: **MIT**): _A comprehensive list of Iranian domains and services that are hosted within the country._
|
||||
- [PersianBlocker](https://github.com/MasterKia/PersianBlocker) (License: **AGPLv3**): _An optimal and extensive list to block ads and trackers on Persian websites._
|
||||
- [Loyalsoldier](https://github.com/Loyalsoldier/v2ray-rules-dat) (License: **GPL-3.0**): _The enhanced version of V2Ray routing rule._
|
||||
- [Iran v2ray rules](https://github.com/chocolate4u/Iran-v2ray-rules) (License: **GPL-3.0**): _Enhanced v2ray/xray and v2ray/xray-clients routing rules with built-in Iranian domains and a focus on security and adblocking._
|
||||
|
||||
## Stargazers over time
|
||||
## Stargazers over Time
|
||||
|
||||
[](https://starchart.cc/alireza0/x-ui)
|
||||
|
||||
@@ -1 +1 @@
|
||||
1.6.0
|
||||
1.6.4
|
||||
@@ -110,3 +110,12 @@ func IsSQLiteDB(file io.Reader) (bool, error) {
|
||||
}
|
||||
return bytes.Equal(buf, signature), nil
|
||||
}
|
||||
|
||||
func Checkpoint() error {
|
||||
// Update WAL
|
||||
err := db.Exec("PRAGMA wal_checkpoint;").Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
68
go.mod
68
go.mod
@@ -1,71 +1,68 @@
|
||||
module x-ui
|
||||
|
||||
go 1.21
|
||||
|
||||
toolchain go1.21.0
|
||||
go 1.21.4
|
||||
|
||||
require (
|
||||
github.com/Workiva/go-datastructures v1.1.1
|
||||
github.com/gin-contrib/sessions v0.0.4
|
||||
github.com/gin-gonic/gin v1.9.1
|
||||
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1
|
||||
github.com/goccy/go-json v0.10.2
|
||||
github.com/nicksnyder/go-i18n/v2 v2.2.2
|
||||
github.com/nicksnyder/go-i18n/v2 v2.3.0
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
|
||||
github.com/pelletier/go-toml/v2 v2.1.0
|
||||
github.com/pelletier/go-toml/v2 v2.1.1
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/shirou/gopsutil/v3 v3.23.10
|
||||
github.com/xtls/xray-core v1.8.4
|
||||
github.com/shirou/gopsutil/v3 v3.23.11
|
||||
github.com/xtls/xray-core v1.8.6
|
||||
go.uber.org/atomic v1.11.0
|
||||
golang.org/x/text v0.13.0
|
||||
google.golang.org/grpc v1.59.0
|
||||
golang.org/x/text v0.14.0
|
||||
google.golang.org/grpc v1.60.0
|
||||
gorm.io/driver/sqlite v1.5.4
|
||||
gorm.io/gorm v1.25.5
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/andybalholm/brotli v1.0.5 // indirect
|
||||
github.com/andybalholm/brotli v1.0.6 // indirect
|
||||
github.com/bytedance/sonic v1.9.1 // indirect
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
||||
github.com/cloudflare/circl v1.3.6 // indirect
|
||||
github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140 // indirect
|
||||
github.com/francoispqt/gojay v1.2.13 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
||||
github.com/gaukas/godicttls v0.0.4 // indirect
|
||||
github.com/gin-contrib/gzip v0.0.6
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.14.0 // indirect
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
|
||||
github.com/golang/mock v1.6.0 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/google/btree v1.1.2 // indirect
|
||||
github.com/google/pprof v0.0.0-20230821062121-407c9e7a662f // indirect
|
||||
github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a // indirect
|
||||
github.com/gorilla/context v1.1.1 // indirect
|
||||
github.com/gorilla/securecookie v1.1.1 // indirect
|
||||
github.com/gorilla/sessions v1.2.1 // indirect
|
||||
github.com/gorilla/websocket v1.5.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.1 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/compress v1.16.7 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.5 // indirect
|
||||
github.com/klauspost/compress v1.17.2 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.6 // indirect
|
||||
github.com/leodido/go-urn v1.2.4 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.17 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/onsi/ginkgo/v2 v2.12.0 // indirect
|
||||
github.com/onsi/ginkgo/v2 v2.13.1 // indirect
|
||||
github.com/pires/go-proxyproto v0.7.0 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b // indirect
|
||||
github.com/quic-go/qtls-go1-20 v0.3.3 // indirect
|
||||
github.com/quic-go/quic-go v0.38.1 // indirect
|
||||
github.com/refraction-networking/utls v1.4.3 // indirect
|
||||
github.com/quic-go/qtls-go1-20 v0.4.1 // indirect
|
||||
github.com/quic-go/quic-go v0.40.0 // indirect
|
||||
github.com/refraction-networking/utls v1.5.4 // indirect
|
||||
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
|
||||
github.com/sagernet/sing v0.2.9 // indirect
|
||||
github.com/sagernet/sing-shadowsocks v0.2.4 // indirect
|
||||
github.com/sagernet/wireguard-go v0.0.0-20221116151939-c99467f53f2c // indirect
|
||||
github.com/sagernet/sing v0.2.17 // indirect
|
||||
github.com/sagernet/sing-shadowsocks v0.2.5 // indirect
|
||||
github.com/seiflotfy/cuckoofilter v0.0.0-20220411075957-e3b120b3f5fb // indirect
|
||||
github.com/shoenig/go-m1cpu v0.1.6 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.12 // indirect
|
||||
@@ -73,20 +70,25 @@ require (
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e // indirect
|
||||
github.com/xtls/reality v0.0.0-20230828171259-e426190d57f6 // indirect
|
||||
github.com/vishvananda/netlink v1.2.1-beta.2.0.20230316163032-ced5aaba43e3 // indirect
|
||||
github.com/vishvananda/netns v0.0.4 // indirect
|
||||
github.com/xtls/reality v0.0.0-20231112171332-de1173cf2b19 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.3 // indirect
|
||||
go.uber.org/mock v0.3.0 // indirect
|
||||
go4.org/netipx v0.0.0-20230824141953-6213f710f925 // indirect
|
||||
golang.org/x/arch v0.3.0 // indirect
|
||||
golang.org/x/crypto v0.12.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20230725093048-515e97ebf090 // indirect
|
||||
golang.org/x/mod v0.12.0 // indirect
|
||||
golang.org/x/net v0.14.0 // indirect
|
||||
golang.org/x/sys v0.13.0 // indirect
|
||||
golang.org/x/time v0.3.0 // indirect
|
||||
golang.org/x/tools v0.12.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect
|
||||
golang.org/x/crypto v0.15.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa // indirect
|
||||
golang.org/x/mod v0.14.0 // indirect
|
||||
golang.org/x/net v0.18.0 // indirect
|
||||
golang.org/x/sys v0.15.0 // indirect
|
||||
golang.org/x/time v0.4.0 // indirect
|
||||
golang.org/x/tools v0.15.0 // indirect
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
||||
golang.zx2c4.com/wireguard v0.0.0-20231022001213-2e0774f246fb // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17 // indirect
|
||||
google.golang.org/protobuf v1.31.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
gvisor.dev/gvisor v0.0.0-20230822212503-5bf4e5f98744 // indirect
|
||||
gvisor.dev/gvisor v0.0.0-20231104011432-48a6d7d5bd0b // indirect
|
||||
lukechampine.com/blake3 v1.2.1 // indirect
|
||||
)
|
||||
|
||||
217
go.sum
217
go.sum
@@ -8,13 +8,10 @@ dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1
|
||||
dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU=
|
||||
git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/toml v1.0.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
|
||||
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/Workiva/go-datastructures v1.1.1 h1:9G5u1UqKt6ABseAffHGNfbNQd7omRlWE5QaxNruzhE0=
|
||||
github.com/Workiva/go-datastructures v1.1.1/go.mod h1:1yZL+zfsztete+ePzZz/Zb1/t5BnDuE2Ya2MMGhzP6A=
|
||||
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
|
||||
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
|
||||
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
|
||||
github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
|
||||
github.com/antonlindstrom/pgstore v0.0.0-20200229204646-b08ebf1105e0/go.mod h1:2Ti6VUHVxpC0VSmTZzEvpzysnaGAfGBOoMIz5ykPyyw=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
@@ -30,7 +27,10 @@ github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cloudflare/circl v1.3.6 h1:/xbKIqSHbZXHwkhbrhrt2YOHIwYJlXH94E3tI/gDlUg=
|
||||
github.com/cloudflare/circl v1.3.6/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
|
||||
github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@@ -49,36 +49,43 @@ github.com/gaukas/godicttls v0.0.4/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67d
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 h1:Arcl6UOIS/kgO2nW3A65HN+7CMjSDP/gofXL4CZt1V4=
|
||||
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I=
|
||||
github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
|
||||
github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk=
|
||||
github.com/gin-contrib/sessions v0.0.4 h1:gq4fNa1Zmp564iHP5G6EBuktilEos8VKhe2sza1KMgo=
|
||||
github.com/gin-contrib/sessions v0.0.4/go.mod h1:pQ3sIyviBBGcxgyR8mkeJuXbeV3h3NYmhJADQTq5+Vo=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.7.4/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY=
|
||||
github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk=
|
||||
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
|
||||
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
|
||||
github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
|
||||
github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
|
||||
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
|
||||
github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
|
||||
github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY=
|
||||
github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
||||
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
||||
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
|
||||
github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
|
||||
github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
|
||||
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
|
||||
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc=
|
||||
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8=
|
||||
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
@@ -109,8 +116,8 @@ github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20230821062121-407c9e7a662f h1:pDhu5sgp8yJlEF/g6osliIIpF9K4F5jvkULXa4daRDQ=
|
||||
github.com/google/pprof v0.0.0-20230821062121-407c9e7a662f/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
|
||||
github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a h1:fEBsGL/sjAuJrgah5XqmmYsTLzJp/TO9Lhy39gkverk=
|
||||
github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
|
||||
github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
|
||||
github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
@@ -122,8 +129,8 @@ github.com/gorilla/sessions v1.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE
|
||||
github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||
github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
|
||||
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
|
||||
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
|
||||
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
|
||||
github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU=
|
||||
@@ -138,18 +145,22 @@ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHm
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/kidstuff/mongostore v0.0.0-20181113001930-e650cd85ee4b/go.mod h1:g2nVr8KZVXJSS97Jo8pJ0jgq29P6H7dG0oplUA86MQw=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
|
||||
github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||
github.com/klauspost/compress v1.17.2 h1:RlWWUY/Dr4fL8qk9YG7DTZ7PDgME2V4csBXA8L/ixi4=
|
||||
github.com/klauspost/compress v1.17.2/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc=
|
||||
github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
|
||||
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
|
||||
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
|
||||
github.com/lib/pq v1.10.3/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
@@ -159,6 +170,7 @@ github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a/go.mod h1:JKx41uQ
|
||||
github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
|
||||
github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
|
||||
@@ -166,8 +178,8 @@ github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/memcachier/mc v2.0.1+incompatible/go.mod h1:7bkvFE61leUBvXz+yxsOnGBQSZpBSPIMUQSmmSHvuXc=
|
||||
github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
|
||||
github.com/miekg/dns v1.1.55 h1:GoQ4hpsj0nFLYe+bWiCToyrBEJXkQfOOIvFGFy0lEgo=
|
||||
github.com/miekg/dns v1.1.55/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY=
|
||||
github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM=
|
||||
github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
@@ -177,22 +189,23 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
|
||||
github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.2.2 h1:Iv/FL6pvYmDqybEZkr4TrOv8jSHezwpE77K68kcaft8=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.2.2/go.mod h1:fF2++lPHlo+/kPaj3nB0uxtPwzlPm+BlgwGX7MkeGj0=
|
||||
github.com/onsi/ginkgo/v2 v2.12.0 h1:UIVDowFPwpg6yMUpPjGkYvf06K3RAiJXUhCxEwQVHRI=
|
||||
github.com/onsi/ginkgo/v2 v2.12.0/go.mod h1:ZNEzXISYlqpb8S36iN71ifqLi3vVD1rVJGvWRCJOUpQ=
|
||||
github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI=
|
||||
github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.3.0 h1:2NPsCsNFCVd7i+Su0xYsBrIhS3bE2XMv5gNTft2O+PQ=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.3.0/go.mod h1:nxYSZE9M0bf3Y70gPQjN9ha7XNHX7gMc814+6wVyEI4=
|
||||
github.com/onsi/ginkgo/v2 v2.13.1 h1:LNGfMbR2OVGBfXjvRZIZ2YCTQdGKtPLvuI1rMCCj3OU=
|
||||
github.com/onsi/ginkgo/v2 v2.13.1/go.mod h1:XStQ8QcGwLyF4HdfcZB8SFOS/MWCgDuXMSBe6zrvLgM=
|
||||
github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg=
|
||||
github.com/onsi/gomega v1.29.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ=
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88=
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
|
||||
github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
|
||||
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
|
||||
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
||||
github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
|
||||
github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
|
||||
github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
|
||||
github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
|
||||
github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI=
|
||||
github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
|
||||
github.com/pires/go-proxyproto v0.7.0 h1:IukmRewDQFWC7kfnb66CSomk2q/seBuilHBYFwyq0Hs=
|
||||
github.com/pires/go-proxyproto v0.7.0/go.mod h1:Vz/1JPY/OACxWGQNIRY2BeyDmpoaWmEP40O9LbuiFR4=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
@@ -205,28 +218,29 @@ github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:
|
||||
github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||
github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b/go.mod h1:wTPjTepVu7uJBYgZ0SdWHQlIas582j6cn2jgk4DDdlg=
|
||||
github.com/quic-go/qtls-go1-20 v0.3.3 h1:17/glZSLI9P9fDAeyCHBFSWSqJcwx1byhLwP5eUIDCM=
|
||||
github.com/quic-go/qtls-go1-20 v0.3.3/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k=
|
||||
github.com/quic-go/quic-go v0.38.1 h1:M36YWA5dEhEeT+slOu/SwMEucbYd0YFidxG3KlGPZaE=
|
||||
github.com/quic-go/quic-go v0.38.1/go.mod h1:ijnZM7JsFIkp4cRyjxJNIzdSfCLmUMg9wdyhGmg+SN4=
|
||||
github.com/refraction-networking/utls v1.4.3 h1:BdWS3BSzCwWCFfMIXP3mjLAyQkdmog7diaD/OqFbAzM=
|
||||
github.com/refraction-networking/utls v1.4.3/go.mod h1:4u9V/awOSBrRw6+federGmVJQfPtemEqLBXkML1b0bo=
|
||||
github.com/quic-go/qtls-go1-20 v0.4.1 h1:D33340mCNDAIKBqXuAvexTNMUByrYmFYVfKfDN5nfFs=
|
||||
github.com/quic-go/qtls-go1-20 v0.4.1/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k=
|
||||
github.com/quic-go/quic-go v0.40.0 h1:GYd1iznlKm7dpHD7pOVpUvItgMPo/jrMgDWZhMCecqw=
|
||||
github.com/quic-go/quic-go v0.40.0/go.mod h1:PeN7kuVJ4xZbxSv/4OX6S1USOX8MJvydwpTx31vx60c=
|
||||
github.com/refraction-networking/utls v1.5.4 h1:9k6EO2b8TaOGsQ7Pl7p9w6PUhx18/ZCeT0WNTZ7Uw4o=
|
||||
github.com/refraction-networking/utls v1.5.4/go.mod h1:SPuDbBmgLGp8s+HLNc83FuavwZCFoMmExj+ltUHiHUw=
|
||||
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg=
|
||||
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3/go.mod h1:HgjTstvQsPGkxUsCd2KWxErBblirPizecHcpD3ffK+s=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
|
||||
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/sagernet/sing v0.2.9 h1:3wsTz+JG5Wzy65eZnh6AuCrD2QqcRF6Iq6f7ttmJsAo=
|
||||
github.com/sagernet/sing v0.2.9/go.mod h1:Ta8nHnDLAwqySzKhGoKk4ZIB+vJ3GTKj7UPrWYvM+4w=
|
||||
github.com/sagernet/sing-shadowsocks v0.2.4 h1:s/CqXlvFAZhlIoHWUwPw5CoNnQ9Ibki9pckjuugtVfY=
|
||||
github.com/sagernet/sing-shadowsocks v0.2.4/go.mod h1:80fNKP0wnqlu85GZXV1H1vDPC/2t+dQbFggOw4XuFUM=
|
||||
github.com/sagernet/wireguard-go v0.0.0-20221116151939-c99467f53f2c h1:vK2wyt9aWYHHvNLWniwijBu/n4pySypiKRhN32u/JGo=
|
||||
github.com/sagernet/wireguard-go v0.0.0-20221116151939-c99467f53f2c/go.mod h1:euOmN6O5kk9dQmgSS8Df4psAl3TCjxOz0NW60EWkSaI=
|
||||
github.com/sagernet/sing v0.2.17 h1:vMPKb3MV0Aa5ws4dCJkRI8XEjrsUcDn810czd0FwmzI=
|
||||
github.com/sagernet/sing v0.2.17/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo=
|
||||
github.com/sagernet/sing-shadowsocks v0.2.5 h1:qxIttos4xu6ii7MTVJYA8EFQR7Q3KG6xMqmLJIFtBaY=
|
||||
github.com/sagernet/sing-shadowsocks v0.2.5/go.mod h1:MGWGkcU2xW2G2mfArT9/QqpVLOGU+dBaahZCtPHdt7A=
|
||||
github.com/seiflotfy/cuckoofilter v0.0.0-20220411075957-e3b120b3f5fb h1:XfLJSPIOUX+osiMraVgIrMR27uMXnRJWGm1+GL8/63U=
|
||||
github.com/seiflotfy/cuckoofilter v0.0.0-20220411075957-e3b120b3f5fb/go.mod h1:bR6DqgcAl1zTcOX8/pE2Qkj9XO00eCNqmKb7lXP8EAg=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/shirou/gopsutil/v3 v3.23.10 h1:/N42opWlYzegYaVkWejXWJpbzKv2JDy3mrgGzKsh9hM=
|
||||
github.com/shirou/gopsutil/v3 v3.23.10/go.mod h1:JIE26kpucQi+innVlAUnIEOSBhBUkirr5b44yr55+WE=
|
||||
github.com/shirou/gopsutil/v3 v3.23.11 h1:i3jP9NjCPUz7FiZKxlMnODZkdSIp2gnzfrvsu9CuWEQ=
|
||||
github.com/shirou/gopsutil/v3 v3.23.11/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM=
|
||||
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
|
||||
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
|
||||
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
|
||||
@@ -270,34 +284,38 @@ github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
|
||||
github.com/tinylib/msgp v1.1.5/go.mod h1:eQsjooMTnV42mHu917E26IogZ2930nFyBQdofk10Udg=
|
||||
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
|
||||
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
|
||||
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
|
||||
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
|
||||
github.com/ttacon/chalk v0.0.0-20160626202418-22c06c80ed31/go.mod h1:onvgF043R+lC5RZ8IT9rBXDaEDnpnw/Cl+HFiw+v/7Q=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
|
||||
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
|
||||
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
||||
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e h1:5QefA066A1tF8gHIiADmOVOV5LS43gt3ONnlEl3xkwI=
|
||||
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e/go.mod h1:5t19P9LBIrNamL6AcMQOncg/r10y3Pc01AbHeMhwlpU=
|
||||
github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU=
|
||||
github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM=
|
||||
github.com/xtls/reality v0.0.0-20230828171259-e426190d57f6 h1:T+YCYGfFdzyaKTDCdZn/hEiKvsw6yUfd+e4hze0rCUw=
|
||||
github.com/xtls/reality v0.0.0-20230828171259-e426190d57f6/go.mod h1:rkuAY1S9F8eI8gDiPDYvACE8e2uwkyg8qoOTuwWov7Y=
|
||||
github.com/xtls/xray-core v1.8.4 h1:YEoY3iLx/5zoNbt5HORG5LtPyzwICInFfoS+oPkYDIw=
|
||||
github.com/xtls/xray-core v1.8.4/go.mod h1:GGD9elFSHa4IqOArW8gzMsEksPIqK/jdNLo8RcSMfnI=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/vishvananda/netlink v1.2.1-beta.2.0.20230316163032-ced5aaba43e3 h1:tkMT5pTye+1NlKIXETU78NXw0fyjnaNHmJyyLyzw8+U=
|
||||
github.com/vishvananda/netlink v1.2.1-beta.2.0.20230316163032-ced5aaba43e3/go.mod h1:cAAsePK2e15YDAMJNyOpGYEWNe4sIghTY7gpz4cX/Ik=
|
||||
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
|
||||
github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
|
||||
github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
|
||||
github.com/xtls/reality v0.0.0-20231112171332-de1173cf2b19 h1:capMfFYRgH9BCLd6A3Er/cH3A9Nz3CU2KwxwOQZIePI=
|
||||
github.com/xtls/reality v0.0.0-20231112171332-de1173cf2b19/go.mod h1:dm4y/1QwzjGaK17ofi0Vs6NpKAHegZky8qk6J2JJZAE=
|
||||
github.com/xtls/xray-core v1.8.6 h1:tr3nk/fZnFfCsmgZv7B3RC72N5qUC88oMGVLlybDey8=
|
||||
github.com/xtls/xray-core v1.8.6/go.mod h1:hj2EB8rtcLdlTC8zxiWm5xL+C0k2Aie9Pk0mXtDEP6U=
|
||||
github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw=
|
||||
github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
go.uber.org/mock v0.3.0 h1:3mUxI1No2/60yUYax92Pt8eNOEecx2D3lcXZh2NEZJo=
|
||||
go.uber.org/mock v0.3.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
|
||||
go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE=
|
||||
go4.org/netipx v0.0.0-20230824141953-6213f710f925 h1:eeQDDVKFkx0g4Hyy8pHgmZaK0EqB4SD6rvKbUdN3ziQ=
|
||||
go4.org/netipx v0.0.0-20230824141953-6213f710f925/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
|
||||
@@ -308,22 +326,18 @@ golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+
|
||||
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk=
|
||||
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
|
||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA=
|
||||
golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20230725093048-515e97ebf090 h1:Di6/M8l0O2lCLc6VVRWhgCiApHV8MnQurBnFSHsQtNY=
|
||||
golang.org/x/exp v0.0.0-20230725093048-515e97ebf090/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
|
||||
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ=
|
||||
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE=
|
||||
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
|
||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
|
||||
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -333,13 +347,9 @@ golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73r
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14=
|
||||
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
|
||||
golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg=
|
||||
golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
@@ -349,12 +359,8 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
|
||||
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -363,49 +369,43 @@ golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220804214406-8e32c043e418/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
|
||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
|
||||
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
|
||||
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.4.0 h1:Z81tqI5ddIoXDPvVQ7/7CC9TnLM7ubaFG2qXYd5BbYY=
|
||||
golang.org/x/time v0.4.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20201022035929-9cf592e881e9/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.12.0 h1:YW6HUoUmYBpwSgyaGaZq1fHjrBjX1rlpZ54T6mu2kss=
|
||||
golang.org/x/tools v0.12.0/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/tools v0.15.0 h1:zdAyfUGbYmuVokhzVmghFl2ZJh5QhcfebBgmVPFYA+8=
|
||||
golang.org/x/tools v0.15.0/go.mod h1:hpksKq4dtpQWS1uQ61JkdqWM3LscIS6Slf+VVkm+wQk=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20231022001213-2e0774f246fb h1:c5tyN8sSp8jSDxdCCDXVOpJwYXXhmTkNMt+g0zTSOic=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20231022001213-2e0774f246fb/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA=
|
||||
google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
|
||||
google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
|
||||
google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y=
|
||||
@@ -418,30 +418,33 @@ google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoA
|
||||
google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg=
|
||||
google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d h1:uvYuEyMHKNt+lT4K3bN6fGswmK8qSvcreM3BwjDh+y4=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17 h1:Jyp0Hsi0bmHXG6k9eATXoYtjd6e2UzZ1SCn/wIupY14=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:oQ5rr10WTTMvP4A36n8JpR1OrO1BEiV4f78CneXZxkA=
|
||||
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
||||
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
|
||||
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk=
|
||||
google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98=
|
||||
google.golang.org/grpc v1.60.0 h1:6FQAR0kM31P6MRdeluor2w2gPaS4SVNrD/DNTxrQ15k=
|
||||
google.golang.org/grpc v1.60.0/go.mod h1:OlCHIeLYqSSsLi6i49B5QGdzaMZK9+M7LXN2FKz4eGM=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
||||
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/sqlite v1.5.4 h1:IqXwXi8M/ZlPzH/947tn5uik3aYQslP9BVveoax0nV0=
|
||||
@@ -449,8 +452,8 @@ gorm.io/driver/sqlite v1.5.4/go.mod h1:qxAuCol+2r6PannQDpOP1FP6ag3mKi4esLnB/jHed
|
||||
gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls=
|
||||
gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
||||
grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o=
|
||||
gvisor.dev/gvisor v0.0.0-20230822212503-5bf4e5f98744 h1:tE44CyJgxEGzoPtHs9GI7ddKdgEGCREQBP54AmaVM+I=
|
||||
gvisor.dev/gvisor v0.0.0-20230822212503-5bf4e5f98744/go.mod h1:lYEMhXbxgudVhALYsMQrBaUAjM3NMinh8mKL1CJv7rc=
|
||||
gvisor.dev/gvisor v0.0.0-20231104011432-48a6d7d5bd0b h1:yqkg3pTifuKukuWanp8spDsL4irJkHF5WI0J47hU87o=
|
||||
gvisor.dev/gvisor v0.0.0-20231104011432-48a6d7d5bd0b/go.mod h1:10sU+Uh5KKNv1+2x2A0Gvzt8FjD3ASIhorV3YsauXhk=
|
||||
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
|
||||
30
install.sh
30
install.sh
@@ -163,20 +163,22 @@ install_x-ui() {
|
||||
systemctl start x-ui
|
||||
echo -e "${green}x-ui v${last_version}${plain} installation finished, it is up and running now..."
|
||||
echo -e ""
|
||||
echo -e "x-ui control menu usages: "
|
||||
echo -e "----------------------------------------------"
|
||||
echo -e "x-ui - Enter Admin menu"
|
||||
echo -e "x-ui start - Start x-ui"
|
||||
echo -e "x-ui stop - Stop x-ui"
|
||||
echo -e "x-ui restart - Restart x-ui"
|
||||
echo -e "x-ui status - Show x-ui status"
|
||||
echo -e "x-ui enable - Enable x-ui on system startup"
|
||||
echo -e "x-ui disable - Disable x-ui on system startup"
|
||||
echo -e "x-ui log - Check x-ui logs"
|
||||
echo -e "x-ui update - Update x-ui"
|
||||
echo -e "x-ui install - Install x-ui"
|
||||
echo -e "x-ui uninstall - Uninstall x-ui"
|
||||
echo -e "----------------------------------------------"
|
||||
echo "X-UI Control Menu Usage"
|
||||
echo "------------------------------------------"
|
||||
echo "SUBCOMMANDS:"
|
||||
echo "x-ui - Admin Management Script"
|
||||
echo "x-ui start - Start"
|
||||
echo "x-ui stop - Stop"
|
||||
echo "x-ui restart - Restart"
|
||||
echo "x-ui status - Current Status"
|
||||
echo "x-ui enable - Enable Autostart on OS Startup"
|
||||
echo "x-ui disable - Disable Autostart on OS Startup"
|
||||
echo "x-ui log - Check Logs"
|
||||
echo "x-ui update - Update"
|
||||
echo "x-ui install - Install"
|
||||
echo "x-ui uninstall - Uninstall"
|
||||
echo "x-ui help - Control Menu Usage"
|
||||
echo "------------------------------------------"
|
||||
}
|
||||
|
||||
echo -e "${green}Excuting...${plain}"
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 324 KiB After Width: | Height: | Size: 167 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 316 KiB After Width: | Height: | Size: 172 KiB |
BIN
media/outbounds.png
Normal file
BIN
media/outbounds.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 83 KiB |
BIN
media/rules.png
Normal file
BIN
media/rules.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 128 KiB |
@@ -19,8 +19,9 @@ import (
|
||||
type SubService struct {
|
||||
address string
|
||||
showInfo bool
|
||||
remarkModel string
|
||||
inboundService service.InboundService
|
||||
settingServics service.SettingService
|
||||
settingService service.SettingService
|
||||
}
|
||||
|
||||
func (s *SubService) GetSubs(subId string, host string, showInfo bool) ([]string, []string, error) {
|
||||
@@ -34,6 +35,10 @@ func (s *SubService) GetSubs(subId string, host string, showInfo bool) ([]string
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
s.remarkModel, err = s.settingService.GetRemarkModel()
|
||||
if err != nil {
|
||||
s.remarkModel = "-ieo"
|
||||
}
|
||||
for _, inbound := range inbounds {
|
||||
clients, err := s.inboundService.GetClients(inbound)
|
||||
if err != nil {
|
||||
@@ -53,6 +58,7 @@ func (s *SubService) GetSubs(subId string, host string, showInfo bool) ([]string
|
||||
json.Unmarshal([]byte(fallbackMaster.StreamSettings), &masterStream)
|
||||
stream["security"] = masterStream["security"]
|
||||
stream["tlsSettings"] = masterStream["tlsSettings"]
|
||||
stream["externalProxy"] = masterStream["externalProxy"]
|
||||
modifiedStream, _ := json.MarshalIndent(stream, "", " ")
|
||||
inbound.StreamSettings = string(modifiedStream)
|
||||
}
|
||||
@@ -87,7 +93,7 @@ func (s *SubService) GetSubs(subId string, host string, showInfo bool) ([]string
|
||||
}
|
||||
}
|
||||
headers = append(headers, fmt.Sprintf("upload=%d; download=%d; total=%d; expire=%d", traffic.Up, traffic.Down, traffic.Total, traffic.ExpiryTime/1000))
|
||||
updateInterval, _ := s.settingServics.GetSubUpdates()
|
||||
updateInterval, _ := s.settingService.GetSubUpdates()
|
||||
headers = append(headers, fmt.Sprintf("%d", updateInterval))
|
||||
headers = append(headers, subId)
|
||||
return result, headers, nil
|
||||
@@ -96,7 +102,14 @@ func (s *SubService) GetSubs(subId string, host string, showInfo bool) ([]string
|
||||
func (s *SubService) getInboundsBySubId(subId string) ([]*model.Inbound, error) {
|
||||
db := database.GetDB()
|
||||
var inbounds []*model.Inbound
|
||||
err := db.Model(model.Inbound{}).Preload("ClientStats").Where("settings like ? and enable = ?", fmt.Sprintf(`%%"subId": "%s"%%`, subId), true).Find(&inbounds).Error
|
||||
err := db.Model(model.Inbound{}).Preload("ClientStats").Where(`id in (
|
||||
SELECT DISTINCT inbounds.id
|
||||
FROM inbounds,
|
||||
JSON_EACH(JSON_EXTRACT(inbounds.settings, '$.clients')) AS client
|
||||
WHERE
|
||||
protocol in ('vmess','vless','trojan','shadowsocks')
|
||||
AND JSON_EXTRACT(client.value, '$.subId') = ? AND enable = ?
|
||||
)`, subId, true).Find(&inbounds).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -196,7 +209,6 @@ func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
|
||||
}
|
||||
|
||||
security, _ := stream["security"].(string)
|
||||
var domains []interface{}
|
||||
obj["tls"] = security
|
||||
if security == "tls" {
|
||||
tlsSetting, _ := stream["tlsSettings"].(map[string]interface{})
|
||||
@@ -208,24 +220,18 @@ func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
|
||||
}
|
||||
obj["alpn"] = strings.Join(alpn, ",")
|
||||
}
|
||||
if sniValue, ok := searchKey(tlsSetting, "serverName"); ok {
|
||||
obj["sni"], _ = sniValue.(string)
|
||||
}
|
||||
|
||||
tlsSettings, _ := searchKey(tlsSetting, "settings")
|
||||
if tlsSetting != nil {
|
||||
if sniValue, ok := searchKey(tlsSettings, "serverName"); ok {
|
||||
obj["sni"], _ = sniValue.(string)
|
||||
}
|
||||
if fpValue, ok := searchKey(tlsSettings, "fingerprint"); ok {
|
||||
obj["fp"], _ = fpValue.(string)
|
||||
}
|
||||
if insecure, ok := searchKey(tlsSettings, "allowInsecure"); ok {
|
||||
obj["allowInsecure"], _ = insecure.(bool)
|
||||
}
|
||||
if domainSettings, ok := searchKey(tlsSettings, "domains"); ok {
|
||||
domains, _ = domainSettings.([]interface{})
|
||||
}
|
||||
}
|
||||
serverName, _ := tlsSetting["serverName"].(string)
|
||||
if serverName != "" {
|
||||
obj["add"] = serverName
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,16 +245,30 @@ func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
|
||||
}
|
||||
obj["id"] = clients[clientIndex].ID
|
||||
|
||||
if len(domains) > 0 {
|
||||
externalProxies, _ := stream["externalProxy"].([]interface{})
|
||||
|
||||
if len(externalProxies) > 0 {
|
||||
links := ""
|
||||
for index, d := range domains {
|
||||
domain := d.(map[string]interface{})
|
||||
obj["ps"] = s.genRemark(inbound, email, domain["remark"].(string))
|
||||
obj["add"] = domain["domain"].(string)
|
||||
for index, externalProxy := range externalProxies {
|
||||
ep, _ := externalProxy.(map[string]interface{})
|
||||
newSecurity, _ := ep["forceTls"].(string)
|
||||
newObj := map[string]interface{}{}
|
||||
for key, value := range obj {
|
||||
if !(newSecurity == "none" && (key == "alpn" || key == "sni" || key == "fp" || key == "allowInsecure")) {
|
||||
newObj[key] = value
|
||||
}
|
||||
}
|
||||
newObj["ps"] = s.genRemark(inbound, email, ep["remark"].(string))
|
||||
newObj["add"] = ep["dest"].(string)
|
||||
newObj["port"] = int(ep["port"].(float64))
|
||||
|
||||
if newSecurity != "same" {
|
||||
newObj["tls"] = newSecurity
|
||||
}
|
||||
if index > 0 {
|
||||
links += "\n"
|
||||
}
|
||||
jsonStr, _ := json.MarshalIndent(obj, "", " ")
|
||||
jsonStr, _ := json.MarshalIndent(newObj, "", " ")
|
||||
links += "vmess://" + base64.StdEncoding.EncodeToString(jsonStr)
|
||||
}
|
||||
return links
|
||||
@@ -323,7 +343,6 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
|
||||
}
|
||||
|
||||
security, _ := stream["security"].(string)
|
||||
var domains []interface{}
|
||||
if security == "tls" {
|
||||
params["security"] = "tls"
|
||||
tlsSetting, _ := stream["tlsSettings"].(map[string]interface{})
|
||||
@@ -335,11 +354,12 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
|
||||
if len(alpn) > 0 {
|
||||
params["alpn"] = strings.Join(alpn, ",")
|
||||
}
|
||||
if sniValue, ok := searchKey(tlsSetting, "serverName"); ok {
|
||||
params["sni"], _ = sniValue.(string)
|
||||
}
|
||||
|
||||
tlsSettings, _ := searchKey(tlsSetting, "settings")
|
||||
if tlsSetting != nil {
|
||||
if sniValue, ok := searchKey(tlsSettings, "serverName"); ok {
|
||||
params["sni"], _ = sniValue.(string)
|
||||
}
|
||||
if fpValue, ok := searchKey(tlsSettings, "fingerprint"); ok {
|
||||
params["fp"], _ = fpValue.(string)
|
||||
}
|
||||
@@ -348,19 +368,11 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
|
||||
params["allowInsecure"] = "1"
|
||||
}
|
||||
}
|
||||
if domainSettings, ok := searchKey(tlsSettings, "domains"); ok {
|
||||
domains, _ = domainSettings.([]interface{})
|
||||
}
|
||||
}
|
||||
|
||||
if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 {
|
||||
params["flow"] = clients[clientIndex].Flow
|
||||
}
|
||||
|
||||
serverName, _ := tlsSetting["serverName"].(string)
|
||||
if serverName != "" {
|
||||
address = serverName
|
||||
}
|
||||
}
|
||||
|
||||
if security == "reality" {
|
||||
@@ -389,11 +401,6 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
|
||||
params["spx"] = spx
|
||||
}
|
||||
}
|
||||
if serverName, ok := searchKey(realitySettings, "serverName"); ok {
|
||||
if sname, ok := serverName.(string); ok && len(sname) > 0 {
|
||||
address = sname
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 {
|
||||
@@ -401,6 +408,44 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
|
||||
}
|
||||
}
|
||||
|
||||
externalProxies, _ := stream["externalProxy"].([]interface{})
|
||||
|
||||
if len(externalProxies) > 0 {
|
||||
links := ""
|
||||
for index, externalProxy := range externalProxies {
|
||||
ep, _ := externalProxy.(map[string]interface{})
|
||||
newSecurity, _ := ep["forceTls"].(string)
|
||||
dest, _ := ep["dest"].(string)
|
||||
port := int(ep["port"].(float64))
|
||||
link := fmt.Sprintf("vless://%s@%s:%d", uuid, dest, port)
|
||||
|
||||
if newSecurity != "same" {
|
||||
params["security"] = newSecurity
|
||||
} else {
|
||||
params["security"] = security
|
||||
}
|
||||
url, _ := url.Parse(link)
|
||||
q := url.Query()
|
||||
|
||||
for k, v := range params {
|
||||
if !(newSecurity == "none" && (k == "alpn" || k == "sni" || k == "fp" || k == "allowInsecure")) {
|
||||
q.Add(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
// Set the new query values on the URL
|
||||
url.RawQuery = q.Encode()
|
||||
|
||||
url.Fragment = s.genRemark(inbound, email, ep["remark"].(string))
|
||||
|
||||
if index > 0 {
|
||||
links += "\n"
|
||||
}
|
||||
links += url.String()
|
||||
}
|
||||
return links
|
||||
}
|
||||
|
||||
link := fmt.Sprintf("vless://%s@%s:%d", uuid, address, port)
|
||||
url, _ := url.Parse(link)
|
||||
q := url.Query()
|
||||
@@ -412,20 +457,6 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
|
||||
// Set the new query values on the URL
|
||||
url.RawQuery = q.Encode()
|
||||
|
||||
if len(domains) > 0 {
|
||||
links := ""
|
||||
for index, d := range domains {
|
||||
domain := d.(map[string]interface{})
|
||||
url.Fragment = s.genRemark(inbound, email, domain["remark"].(string))
|
||||
url.Host = fmt.Sprintf("%s:%d", domain["domain"].(string), port)
|
||||
if index > 0 {
|
||||
links += "\n"
|
||||
}
|
||||
links += url.String()
|
||||
}
|
||||
return links
|
||||
}
|
||||
|
||||
url.Fragment = s.genRemark(inbound, email, "")
|
||||
return url.String()
|
||||
}
|
||||
@@ -493,7 +524,6 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
|
||||
}
|
||||
|
||||
security, _ := stream["security"].(string)
|
||||
var domains []interface{}
|
||||
if security == "tls" {
|
||||
params["security"] = "tls"
|
||||
tlsSetting, _ := stream["tlsSettings"].(map[string]interface{})
|
||||
@@ -505,11 +535,12 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
|
||||
if len(alpn) > 0 {
|
||||
params["alpn"] = strings.Join(alpn, ",")
|
||||
}
|
||||
if sniValue, ok := searchKey(tlsSetting, "serverName"); ok {
|
||||
params["sni"], _ = sniValue.(string)
|
||||
}
|
||||
|
||||
tlsSettings, _ := searchKey(tlsSetting, "settings")
|
||||
if tlsSetting != nil {
|
||||
if sniValue, ok := searchKey(tlsSettings, "serverName"); ok {
|
||||
params["sni"], _ = sniValue.(string)
|
||||
}
|
||||
if fpValue, ok := searchKey(tlsSettings, "fingerprint"); ok {
|
||||
params["fp"], _ = fpValue.(string)
|
||||
}
|
||||
@@ -518,14 +549,6 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
|
||||
params["allowInsecure"] = "1"
|
||||
}
|
||||
}
|
||||
if domainSettings, ok := searchKey(tlsSettings, "domains"); ok {
|
||||
domains, _ = domainSettings.([]interface{})
|
||||
}
|
||||
}
|
||||
|
||||
serverName, _ := tlsSetting["serverName"].(string)
|
||||
if serverName != "" {
|
||||
address = serverName
|
||||
}
|
||||
}
|
||||
|
||||
@@ -555,12 +578,45 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
|
||||
params["spx"] = spx
|
||||
}
|
||||
}
|
||||
if serverName, ok := searchKey(realitySettings, "serverName"); ok {
|
||||
if sname, ok := serverName.(string); ok && len(sname) > 0 {
|
||||
address = sname
|
||||
}
|
||||
}
|
||||
|
||||
externalProxies, _ := stream["externalProxy"].([]interface{})
|
||||
|
||||
if len(externalProxies) > 0 {
|
||||
links := ""
|
||||
for index, externalProxy := range externalProxies {
|
||||
ep, _ := externalProxy.(map[string]interface{})
|
||||
newSecurity, _ := ep["forceTls"].(string)
|
||||
dest, _ := ep["dest"].(string)
|
||||
port := int(ep["port"].(float64))
|
||||
link := fmt.Sprintf("trojan://%s@%s:%d", password, dest, port)
|
||||
|
||||
if newSecurity != "same" {
|
||||
params["security"] = newSecurity
|
||||
} else {
|
||||
params["security"] = security
|
||||
}
|
||||
url, _ := url.Parse(link)
|
||||
q := url.Query()
|
||||
|
||||
for k, v := range params {
|
||||
if !(newSecurity == "none" && (k == "alpn" || k == "sni" || k == "fp" || k == "allowInsecure")) {
|
||||
q.Add(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
// Set the new query values on the URL
|
||||
url.RawQuery = q.Encode()
|
||||
|
||||
url.Fragment = s.genRemark(inbound, email, ep["remark"].(string))
|
||||
|
||||
if index > 0 {
|
||||
links += "\n"
|
||||
}
|
||||
links += url.String()
|
||||
}
|
||||
return links
|
||||
}
|
||||
|
||||
link := fmt.Sprintf("trojan://%s@%s:%d", password, address, port)
|
||||
@@ -575,20 +631,6 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
|
||||
// Set the new query values on the URL
|
||||
url.RawQuery = q.Encode()
|
||||
|
||||
if len(domains) > 0 {
|
||||
links := ""
|
||||
for index, d := range domains {
|
||||
domain := d.(map[string]interface{})
|
||||
url.Fragment = s.genRemark(inbound, email, domain["remark"].(string))
|
||||
url.Host = fmt.Sprintf("%s:%d", domain["domain"].(string), port)
|
||||
if index > 0 {
|
||||
links += "\n"
|
||||
}
|
||||
links += url.String()
|
||||
}
|
||||
return links
|
||||
}
|
||||
|
||||
url.Fragment = s.genRemark(inbound, email, "")
|
||||
return url.String()
|
||||
}
|
||||
@@ -658,10 +700,78 @@ func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) st
|
||||
}
|
||||
}
|
||||
|
||||
security, _ := stream["security"].(string)
|
||||
if security == "tls" {
|
||||
params["security"] = "tls"
|
||||
tlsSetting, _ := stream["tlsSettings"].(map[string]interface{})
|
||||
alpns, _ := tlsSetting["alpn"].([]interface{})
|
||||
var alpn []string
|
||||
for _, a := range alpns {
|
||||
alpn = append(alpn, a.(string))
|
||||
}
|
||||
if len(alpn) > 0 {
|
||||
params["alpn"] = strings.Join(alpn, ",")
|
||||
}
|
||||
if sniValue, ok := searchKey(tlsSetting, "serverName"); ok {
|
||||
params["sni"], _ = sniValue.(string)
|
||||
}
|
||||
|
||||
tlsSettings, _ := searchKey(tlsSetting, "settings")
|
||||
if tlsSetting != nil {
|
||||
if fpValue, ok := searchKey(tlsSettings, "fingerprint"); ok {
|
||||
params["fp"], _ = fpValue.(string)
|
||||
}
|
||||
if insecure, ok := searchKey(tlsSettings, "allowInsecure"); ok {
|
||||
if insecure.(bool) {
|
||||
params["allowInsecure"] = "1"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
encPart := fmt.Sprintf("%s:%s", method, clients[clientIndex].Password)
|
||||
if method[0] == '2' {
|
||||
encPart = fmt.Sprintf("%s:%s:%s", method, inboundPassword, clients[clientIndex].Password)
|
||||
}
|
||||
|
||||
externalProxies, _ := stream["externalProxy"].([]interface{})
|
||||
|
||||
if len(externalProxies) > 0 {
|
||||
links := ""
|
||||
for index, externalProxy := range externalProxies {
|
||||
ep, _ := externalProxy.(map[string]interface{})
|
||||
newSecurity, _ := ep["forceTls"].(string)
|
||||
dest, _ := ep["dest"].(string)
|
||||
port := int(ep["port"].(float64))
|
||||
link := fmt.Sprintf("ss://%s@%s:%d", base64.StdEncoding.EncodeToString([]byte(encPart)), dest, port)
|
||||
|
||||
if newSecurity != "same" {
|
||||
params["security"] = newSecurity
|
||||
} else {
|
||||
params["security"] = security
|
||||
}
|
||||
url, _ := url.Parse(link)
|
||||
q := url.Query()
|
||||
|
||||
for k, v := range params {
|
||||
if !(newSecurity == "none" && (k == "alpn" || k == "sni" || k == "fp" || k == "allowInsecure")) {
|
||||
q.Add(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
// Set the new query values on the URL
|
||||
url.RawQuery = q.Encode()
|
||||
|
||||
url.Fragment = s.genRemark(inbound, email, ep["remark"].(string))
|
||||
|
||||
if index > 0 {
|
||||
links += "\n"
|
||||
}
|
||||
links += url.String()
|
||||
}
|
||||
return links
|
||||
}
|
||||
|
||||
link := fmt.Sprintf("ss://%s@%s:%d", base64.StdEncoding.EncodeToString([]byte(encPart)), address, inbound.Port)
|
||||
url, _ := url.Parse(link)
|
||||
q := url.Query()
|
||||
@@ -672,22 +782,36 @@ func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) st
|
||||
|
||||
// Set the new query values on the URL
|
||||
url.RawQuery = q.Encode()
|
||||
|
||||
url.Fragment = s.genRemark(inbound, email, "")
|
||||
return url.String()
|
||||
}
|
||||
|
||||
func (s *SubService) genRemark(inbound *model.Inbound, email string, extra string) string {
|
||||
var remark []string
|
||||
separationChar := string(s.remarkModel[0])
|
||||
orderChars := s.remarkModel[1:]
|
||||
orders := map[byte]string{
|
||||
'i': "",
|
||||
'e': "",
|
||||
'o': "",
|
||||
}
|
||||
if len(email) > 0 {
|
||||
if len(inbound.Remark) > 0 {
|
||||
remark = append(remark, inbound.Remark)
|
||||
orders['e'] = email
|
||||
}
|
||||
if len(inbound.Remark) > 0 {
|
||||
orders['i'] = inbound.Remark
|
||||
}
|
||||
if len(extra) > 0 {
|
||||
orders['o'] = extra
|
||||
}
|
||||
|
||||
var remark []string
|
||||
for i := 0; i < len(orderChars); i++ {
|
||||
char := orderChars[i]
|
||||
order, exists := orders[char]
|
||||
if exists && order != "" {
|
||||
remark = append(remark, order)
|
||||
}
|
||||
remark = append(remark, email)
|
||||
if len(extra) > 0 {
|
||||
remark = append(remark, extra)
|
||||
}
|
||||
} else {
|
||||
return inbound.Remark
|
||||
}
|
||||
|
||||
if s.showInfo {
|
||||
@@ -704,7 +828,7 @@ func (s *SubService) genRemark(inbound *model.Inbound, email string, extra strin
|
||||
// Get remained days
|
||||
if statsExist {
|
||||
if !stats.Enable {
|
||||
return fmt.Sprintf("⛔️N/A-%s", strings.Join(remark, "-"))
|
||||
return fmt.Sprintf("⛔️N/A%s%s", separationChar, strings.Join(remark, separationChar))
|
||||
}
|
||||
if vol := stats.Total - (stats.Up + stats.Down); vol > 0 {
|
||||
remark = append(remark, fmt.Sprintf("%s%s", common.FormatTraffic(vol), "📊"))
|
||||
@@ -718,7 +842,7 @@ func (s *SubService) genRemark(inbound *model.Inbound, email string, extra strin
|
||||
}
|
||||
}
|
||||
}
|
||||
return strings.Join(remark, "-")
|
||||
return strings.Join(remark, separationChar)
|
||||
}
|
||||
|
||||
func searchKey(data interface{}, key string) (interface{}, bool) {
|
||||
|
||||
File diff suppressed because one or more lines are too long
8
web/assets/ant-design-vue@1.7.2/antd.min.css
vendored
8
web/assets/ant-design-vue@1.7.2/antd.min.css
vendored
File diff suppressed because one or more lines are too long
3
web/assets/ant-design-vue@1.7.2/antd.min.js
vendored
3
web/assets/ant-design-vue@1.7.2/antd.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
3
web/assets/ant-design-vue@1.7.8/antd-with-locales.min.js
vendored
Normal file
3
web/assets/ant-design-vue@1.7.8/antd-with-locales.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
8
web/assets/ant-design-vue@1.7.8/antd.min.css
vendored
Normal file
8
web/assets/ant-design-vue@1.7.8/antd.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
3
web/assets/ant-design-vue@1.7.8/antd.min.js
vendored
Normal file
3
web/assets/ant-design-vue@1.7.8/antd.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
web/assets/ant-design-vue@1.7.8/antd.min.js.map
Normal file
1
web/assets/ant-design-vue@1.7.8/antd.min.js.map
Normal file
File diff suppressed because one or more lines are too long
344
web/assets/codemirror/codemirror.css
Normal file
344
web/assets/codemirror/codemirror.css
Normal file
@@ -0,0 +1,344 @@
|
||||
/* BASICS */
|
||||
|
||||
.CodeMirror {
|
||||
/* Set height, width, borders, and global font properties here */
|
||||
font-family: monospace;
|
||||
height: 300px;
|
||||
color: black;
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
/* PADDING */
|
||||
|
||||
.CodeMirror-lines {
|
||||
padding: 4px 0; /* Vertical padding around content */
|
||||
}
|
||||
.CodeMirror pre.CodeMirror-line,
|
||||
.CodeMirror pre.CodeMirror-line-like {
|
||||
padding: 0 4px; /* Horizontal padding of content */
|
||||
}
|
||||
|
||||
.CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
|
||||
background-color: white; /* The little square between H and V scrollbars */
|
||||
}
|
||||
|
||||
/* GUTTER */
|
||||
|
||||
.CodeMirror-gutters {
|
||||
border-right: 1px solid #ddd;
|
||||
background-color: #f7f7f7;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.CodeMirror-linenumbers {}
|
||||
.CodeMirror-linenumber {
|
||||
padding: 0 3px 0 5px;
|
||||
min-width: 20px;
|
||||
text-align: right;
|
||||
color: #999;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.CodeMirror-guttermarker { color: black; }
|
||||
.CodeMirror-guttermarker-subtle { color: #999; }
|
||||
|
||||
/* CURSOR */
|
||||
|
||||
.CodeMirror-cursor {
|
||||
border-left: 1px solid black;
|
||||
border-right: none;
|
||||
width: 0;
|
||||
}
|
||||
/* Shown when moving in bi-directional text */
|
||||
.CodeMirror div.CodeMirror-secondarycursor {
|
||||
border-left: 1px solid silver;
|
||||
}
|
||||
.cm-fat-cursor .CodeMirror-cursor {
|
||||
width: auto;
|
||||
border: 0 !important;
|
||||
background: #7e7;
|
||||
}
|
||||
.cm-fat-cursor div.CodeMirror-cursors {
|
||||
z-index: 1;
|
||||
}
|
||||
.cm-fat-cursor .CodeMirror-line::selection,
|
||||
.cm-fat-cursor .CodeMirror-line > span::selection,
|
||||
.cm-fat-cursor .CodeMirror-line > span > span::selection { background: transparent; }
|
||||
.cm-fat-cursor .CodeMirror-line::-moz-selection,
|
||||
.cm-fat-cursor .CodeMirror-line > span::-moz-selection,
|
||||
.cm-fat-cursor .CodeMirror-line > span > span::-moz-selection { background: transparent; }
|
||||
.cm-fat-cursor { caret-color: transparent; }
|
||||
@-moz-keyframes blink {
|
||||
0% {}
|
||||
50% { background-color: transparent; }
|
||||
100% {}
|
||||
}
|
||||
@-webkit-keyframes blink {
|
||||
0% {}
|
||||
50% { background-color: transparent; }
|
||||
100% {}
|
||||
}
|
||||
@keyframes blink {
|
||||
0% {}
|
||||
50% { background-color: transparent; }
|
||||
100% {}
|
||||
}
|
||||
|
||||
/* Can style cursor different in overwrite (non-insert) mode */
|
||||
.CodeMirror-overwrite .CodeMirror-cursor {}
|
||||
|
||||
.cm-tab { display: inline-block; text-decoration: inherit; }
|
||||
|
||||
.CodeMirror-rulers {
|
||||
position: absolute;
|
||||
left: 0; right: 0; top: -50px; bottom: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.CodeMirror-ruler {
|
||||
border-left: 1px solid #ccc;
|
||||
top: 0; bottom: 0;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
/* DEFAULT THEME */
|
||||
|
||||
.cm-s-default .cm-header {color: blue;}
|
||||
.cm-s-default .cm-quote {color: #090;}
|
||||
.cm-negative {color: #d44;}
|
||||
.cm-positive {color: #292;}
|
||||
.cm-header, .cm-strong {font-weight: bold;}
|
||||
.cm-em {font-style: italic;}
|
||||
.cm-link {text-decoration: underline;}
|
||||
.cm-strikethrough {text-decoration: line-through;}
|
||||
|
||||
.cm-s-default .cm-keyword {color: #708;}
|
||||
.cm-s-default .cm-atom {color: #219;}
|
||||
.cm-s-default .cm-number {color: #164;}
|
||||
.cm-s-default .cm-def {color: #00f;}
|
||||
.cm-s-default .cm-variable,
|
||||
.cm-s-default .cm-punctuation,
|
||||
.cm-s-default .cm-property,
|
||||
.cm-s-default .cm-operator {}
|
||||
.cm-s-default .cm-variable-2 {color: #05a;}
|
||||
.cm-s-default .cm-variable-3, .cm-s-default .cm-type {color: #085;}
|
||||
.cm-s-default .cm-comment {color: #a50;}
|
||||
.cm-s-default .cm-string {color: #a11;}
|
||||
.cm-s-default .cm-string-2 {color: #f50;}
|
||||
.cm-s-default .cm-meta {color: #555;}
|
||||
.cm-s-default .cm-qualifier {color: #555;}
|
||||
.cm-s-default .cm-builtin {color: #30a;}
|
||||
.cm-s-default .cm-bracket {color: #997;}
|
||||
.cm-s-default .cm-tag {color: #170;}
|
||||
.cm-s-default .cm-attribute {color: #00c;}
|
||||
.cm-s-default .cm-hr {color: #999;}
|
||||
.cm-s-default .cm-link {color: #00c;}
|
||||
|
||||
.cm-s-default .cm-error {color: #f00;}
|
||||
.cm-invalidchar {color: #f00;}
|
||||
|
||||
.CodeMirror-composing { border-bottom: 2px solid; }
|
||||
|
||||
/* Default styles for common addons */
|
||||
|
||||
div.CodeMirror span.CodeMirror-matchingbracket {color: #0b0;}
|
||||
div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #a22;}
|
||||
.CodeMirror-matchingtag { background: rgba(255, 150, 0, .3); }
|
||||
.CodeMirror-activeline-background {background: #e8f2ff;}
|
||||
|
||||
/* STOP */
|
||||
|
||||
/* The rest of this file contains styles related to the mechanics of
|
||||
the editor. You probably shouldn't touch them. */
|
||||
|
||||
.CodeMirror {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.CodeMirror-scroll {
|
||||
overflow: scroll !important; /* Things will break if this is overridden */
|
||||
/* 50px is the magic margin used to hide the element's real scrollbars */
|
||||
/* See overflow: hidden in .CodeMirror */
|
||||
margin-bottom: -50px; margin-right: -50px;
|
||||
padding-bottom: 50px;
|
||||
height: 100%;
|
||||
outline: none; /* Prevent dragging from highlighting the element */
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
}
|
||||
.CodeMirror-sizer {
|
||||
position: relative;
|
||||
border-right: 50px solid transparent;
|
||||
}
|
||||
|
||||
/* The fake, visible scrollbars. Used to force redraw during scrolling
|
||||
before actual scrolling happens, thus preventing shaking and
|
||||
flickering artifacts. */
|
||||
.CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
|
||||
position: absolute;
|
||||
z-index: 6;
|
||||
display: none;
|
||||
outline: none;
|
||||
}
|
||||
.CodeMirror-vscrollbar {
|
||||
right: 0; top: 0;
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
.CodeMirror-hscrollbar {
|
||||
bottom: 0; left: 0;
|
||||
overflow-y: hidden;
|
||||
overflow-x: scroll;
|
||||
}
|
||||
.CodeMirror-scrollbar-filler {
|
||||
right: 0; bottom: 0;
|
||||
}
|
||||
.CodeMirror-gutter-filler {
|
||||
left: 0; bottom: 0;
|
||||
}
|
||||
|
||||
.CodeMirror-gutters {
|
||||
position: absolute; left: 0; top: 0;
|
||||
min-height: 100%;
|
||||
z-index: 3;
|
||||
}
|
||||
.CodeMirror-gutter {
|
||||
white-space: normal;
|
||||
height: 100%;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
margin-bottom: -50px;
|
||||
}
|
||||
.CodeMirror-gutter-wrapper {
|
||||
position: absolute;
|
||||
z-index: 4;
|
||||
background: none !important;
|
||||
border: none !important;
|
||||
}
|
||||
.CodeMirror-gutter-background {
|
||||
position: absolute;
|
||||
top: 0; bottom: 0;
|
||||
z-index: 4;
|
||||
}
|
||||
.CodeMirror-gutter-elt {
|
||||
position: absolute;
|
||||
cursor: default;
|
||||
z-index: 4;
|
||||
}
|
||||
.CodeMirror-gutter-wrapper ::selection { background-color: transparent }
|
||||
.CodeMirror-gutter-wrapper ::-moz-selection { background-color: transparent }
|
||||
|
||||
.CodeMirror-lines {
|
||||
cursor: text;
|
||||
min-height: 1px; /* prevents collapsing before first draw */
|
||||
}
|
||||
.CodeMirror pre.CodeMirror-line,
|
||||
.CodeMirror pre.CodeMirror-line-like {
|
||||
/* Reset some styles that the rest of the page might have set */
|
||||
-moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0;
|
||||
border-width: 0;
|
||||
background: transparent;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
margin: 0;
|
||||
white-space: pre;
|
||||
word-wrap: normal;
|
||||
line-height: inherit;
|
||||
color: inherit;
|
||||
z-index: 2;
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
-webkit-font-variant-ligatures: contextual;
|
||||
font-variant-ligatures: contextual;
|
||||
}
|
||||
.CodeMirror-wrap pre.CodeMirror-line,
|
||||
.CodeMirror-wrap pre.CodeMirror-line-like {
|
||||
word-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
word-break: normal;
|
||||
}
|
||||
|
||||
.CodeMirror-linebackground {
|
||||
position: absolute;
|
||||
left: 0; right: 0; top: 0; bottom: 0;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.CodeMirror-linewidget {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
padding: 0.1px; /* Force widget margins to stay inside of the container */
|
||||
}
|
||||
|
||||
.CodeMirror-widget {}
|
||||
|
||||
.CodeMirror-rtl pre { direction: rtl; }
|
||||
|
||||
.CodeMirror-code {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* Force content-box sizing for the elements where we expect it */
|
||||
.CodeMirror-scroll,
|
||||
.CodeMirror-sizer,
|
||||
.CodeMirror-gutter,
|
||||
.CodeMirror-gutters,
|
||||
.CodeMirror-linenumber {
|
||||
-moz-box-sizing: content-box;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
|
||||
.CodeMirror-measure {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.CodeMirror-cursor {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
}
|
||||
.CodeMirror-measure pre { position: static; }
|
||||
|
||||
div.CodeMirror-cursors {
|
||||
visibility: hidden;
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
}
|
||||
div.CodeMirror-dragcursors {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.CodeMirror-focused div.CodeMirror-cursors {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.CodeMirror-selected { background: #d9d9d9; }
|
||||
.CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; }
|
||||
.CodeMirror-crosshair { cursor: crosshair; }
|
||||
.CodeMirror-line::selection, .CodeMirror-line > span::selection, .CodeMirror-line > span > span::selection { background: #d7d4f0; }
|
||||
.CodeMirror-line::-moz-selection, .CodeMirror-line > span::-moz-selection, .CodeMirror-line > span > span::-moz-selection { background: #d7d4f0; }
|
||||
|
||||
.cm-searching {
|
||||
background-color: #ffa;
|
||||
background-color: rgba(255, 255, 0, .4);
|
||||
}
|
||||
|
||||
/* Used to force a border model for a node */
|
||||
.cm-force-border { padding-right: .1px; }
|
||||
|
||||
@media print {
|
||||
/* Hide the cursor when printing */
|
||||
.CodeMirror div.CodeMirror-cursors {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
/* See issue #2901 */
|
||||
.cm-tab-wrap-hack:after { content: ''; }
|
||||
|
||||
/* Help users use markselection to safely style text background */
|
||||
span.CodeMirror-selectedtext { background: none; }
|
||||
9874
web/assets/codemirror/codemirror.js
Normal file
9874
web/assets/codemirror/codemirror.js
Normal file
File diff suppressed because it is too large
Load Diff
119
web/assets/codemirror/fold/brace-fold.js
Normal file
119
web/assets/codemirror/fold/brace-fold.js
Normal file
@@ -0,0 +1,119 @@
|
||||
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
||||
// Distributed under an MIT license: https://codemirror.net/5/LICENSE
|
||||
|
||||
(function(mod) {
|
||||
if (typeof exports == "object" && typeof module == "object") // CommonJS
|
||||
mod(require("../../lib/codemirror"));
|
||||
else if (typeof define == "function" && define.amd) // AMD
|
||||
define(["../../lib/codemirror"], mod);
|
||||
else // Plain browser env
|
||||
mod(CodeMirror);
|
||||
})(function(CodeMirror) {
|
||||
"use strict";
|
||||
|
||||
function bracketFolding(pairs) {
|
||||
return function(cm, start) {
|
||||
var line = start.line, lineText = cm.getLine(line);
|
||||
|
||||
function findOpening(pair) {
|
||||
var tokenType;
|
||||
for (var at = start.ch, pass = 0;;) {
|
||||
var found = at <= 0 ? -1 : lineText.lastIndexOf(pair[0], at - 1);
|
||||
if (found == -1) {
|
||||
if (pass == 1) break;
|
||||
pass = 1;
|
||||
at = lineText.length;
|
||||
continue;
|
||||
}
|
||||
if (pass == 1 && found < start.ch) break;
|
||||
tokenType = cm.getTokenTypeAt(CodeMirror.Pos(line, found + 1));
|
||||
if (!/^(comment|string)/.test(tokenType)) return {ch: found + 1, tokenType: tokenType, pair: pair};
|
||||
at = found - 1;
|
||||
}
|
||||
}
|
||||
|
||||
function findRange(found) {
|
||||
var count = 1, lastLine = cm.lastLine(), end, startCh = found.ch, endCh
|
||||
outer: for (var i = line; i <= lastLine; ++i) {
|
||||
var text = cm.getLine(i), pos = i == line ? startCh : 0;
|
||||
for (;;) {
|
||||
var nextOpen = text.indexOf(found.pair[0], pos), nextClose = text.indexOf(found.pair[1], pos);
|
||||
if (nextOpen < 0) nextOpen = text.length;
|
||||
if (nextClose < 0) nextClose = text.length;
|
||||
pos = Math.min(nextOpen, nextClose);
|
||||
if (pos == text.length) break;
|
||||
if (cm.getTokenTypeAt(CodeMirror.Pos(i, pos + 1)) == found.tokenType) {
|
||||
if (pos == nextOpen) ++count;
|
||||
else if (!--count) { end = i; endCh = pos; break outer; }
|
||||
}
|
||||
++pos;
|
||||
}
|
||||
}
|
||||
|
||||
if (end == null || line == end) return null
|
||||
return {from: CodeMirror.Pos(line, startCh),
|
||||
to: CodeMirror.Pos(end, endCh)};
|
||||
}
|
||||
|
||||
var found = []
|
||||
for (var i = 0; i < pairs.length; i++) {
|
||||
var open = findOpening(pairs[i])
|
||||
if (open) found.push(open)
|
||||
}
|
||||
found.sort(function(a, b) { return a.ch - b.ch })
|
||||
for (var i = 0; i < found.length; i++) {
|
||||
var range = findRange(found[i])
|
||||
if (range) return range
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
CodeMirror.registerHelper("fold", "brace", bracketFolding([["{", "}"], ["[", "]"]]));
|
||||
|
||||
CodeMirror.registerHelper("fold", "brace-paren", bracketFolding([["{", "}"], ["[", "]"], ["(", ")"]]));
|
||||
|
||||
CodeMirror.registerHelper("fold", "import", function(cm, start) {
|
||||
function hasImport(line) {
|
||||
if (line < cm.firstLine() || line > cm.lastLine()) return null;
|
||||
var start = cm.getTokenAt(CodeMirror.Pos(line, 1));
|
||||
if (!/\S/.test(start.string)) start = cm.getTokenAt(CodeMirror.Pos(line, start.end + 1));
|
||||
if (start.type != "keyword" || start.string != "import") return null;
|
||||
// Now find closing semicolon, return its position
|
||||
for (var i = line, e = Math.min(cm.lastLine(), line + 10); i <= e; ++i) {
|
||||
var text = cm.getLine(i), semi = text.indexOf(";");
|
||||
if (semi != -1) return {startCh: start.end, end: CodeMirror.Pos(i, semi)};
|
||||
}
|
||||
}
|
||||
|
||||
var startLine = start.line, has = hasImport(startLine), prev;
|
||||
if (!has || hasImport(startLine - 1) || ((prev = hasImport(startLine - 2)) && prev.end.line == startLine - 1))
|
||||
return null;
|
||||
for (var end = has.end;;) {
|
||||
var next = hasImport(end.line + 1);
|
||||
if (next == null) break;
|
||||
end = next.end;
|
||||
}
|
||||
return {from: cm.clipPos(CodeMirror.Pos(startLine, has.startCh + 1)), to: end};
|
||||
});
|
||||
|
||||
CodeMirror.registerHelper("fold", "include", function(cm, start) {
|
||||
function hasInclude(line) {
|
||||
if (line < cm.firstLine() || line > cm.lastLine()) return null;
|
||||
var start = cm.getTokenAt(CodeMirror.Pos(line, 1));
|
||||
if (!/\S/.test(start.string)) start = cm.getTokenAt(CodeMirror.Pos(line, start.end + 1));
|
||||
if (start.type == "meta" && start.string.slice(0, 8) == "#include") return start.start + 8;
|
||||
}
|
||||
|
||||
var startLine = start.line, has = hasInclude(startLine);
|
||||
if (has == null || hasInclude(startLine - 1) != null) return null;
|
||||
for (var end = startLine;;) {
|
||||
var next = hasInclude(end + 1);
|
||||
if (next == null) break;
|
||||
++end;
|
||||
}
|
||||
return {from: CodeMirror.Pos(startLine, has + 1),
|
||||
to: cm.clipPos(CodeMirror.Pos(end))};
|
||||
});
|
||||
|
||||
});
|
||||
159
web/assets/codemirror/fold/foldcode.js
Normal file
159
web/assets/codemirror/fold/foldcode.js
Normal file
@@ -0,0 +1,159 @@
|
||||
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
||||
// Distributed under an MIT license: https://codemirror.net/5/LICENSE
|
||||
|
||||
(function(mod) {
|
||||
if (typeof exports == "object" && typeof module == "object") // CommonJS
|
||||
mod(require("../../lib/codemirror"));
|
||||
else if (typeof define == "function" && define.amd) // AMD
|
||||
define(["../../lib/codemirror"], mod);
|
||||
else // Plain browser env
|
||||
mod(CodeMirror);
|
||||
})(function(CodeMirror) {
|
||||
"use strict";
|
||||
|
||||
function doFold(cm, pos, options, force) {
|
||||
if (options && options.call) {
|
||||
var finder = options;
|
||||
options = null;
|
||||
} else {
|
||||
var finder = getOption(cm, options, "rangeFinder");
|
||||
}
|
||||
if (typeof pos == "number") pos = CodeMirror.Pos(pos, 0);
|
||||
var minSize = getOption(cm, options, "minFoldSize");
|
||||
|
||||
function getRange(allowFolded) {
|
||||
var range = finder(cm, pos);
|
||||
if (!range || range.to.line - range.from.line < minSize) return null;
|
||||
if (force === "fold") return range;
|
||||
|
||||
var marks = cm.findMarksAt(range.from);
|
||||
for (var i = 0; i < marks.length; ++i) {
|
||||
if (marks[i].__isFold) {
|
||||
if (!allowFolded) return null;
|
||||
range.cleared = true;
|
||||
marks[i].clear();
|
||||
}
|
||||
}
|
||||
return range;
|
||||
}
|
||||
|
||||
var range = getRange(true);
|
||||
if (getOption(cm, options, "scanUp")) while (!range && pos.line > cm.firstLine()) {
|
||||
pos = CodeMirror.Pos(pos.line - 1, 0);
|
||||
range = getRange(false);
|
||||
}
|
||||
if (!range || range.cleared || force === "unfold") return;
|
||||
|
||||
var myWidget = makeWidget(cm, options, range);
|
||||
CodeMirror.on(myWidget, "mousedown", function(e) {
|
||||
myRange.clear();
|
||||
CodeMirror.e_preventDefault(e);
|
||||
});
|
||||
var myRange = cm.markText(range.from, range.to, {
|
||||
replacedWith: myWidget,
|
||||
clearOnEnter: getOption(cm, options, "clearOnEnter"),
|
||||
__isFold: true
|
||||
});
|
||||
myRange.on("clear", function(from, to) {
|
||||
CodeMirror.signal(cm, "unfold", cm, from, to);
|
||||
});
|
||||
CodeMirror.signal(cm, "fold", cm, range.from, range.to);
|
||||
}
|
||||
|
||||
function makeWidget(cm, options, range) {
|
||||
var widget = getOption(cm, options, "widget");
|
||||
|
||||
if (typeof widget == "function") {
|
||||
widget = widget(range.from, range.to);
|
||||
}
|
||||
|
||||
if (typeof widget == "string") {
|
||||
var text = document.createTextNode(widget);
|
||||
widget = document.createElement("span");
|
||||
widget.appendChild(text);
|
||||
widget.className = "CodeMirror-foldmarker";
|
||||
} else if (widget) {
|
||||
widget = widget.cloneNode(true)
|
||||
}
|
||||
return widget;
|
||||
}
|
||||
|
||||
// Clumsy backwards-compatible interface
|
||||
CodeMirror.newFoldFunction = function(rangeFinder, widget) {
|
||||
return function(cm, pos) { doFold(cm, pos, {rangeFinder: rangeFinder, widget: widget}); };
|
||||
};
|
||||
|
||||
// New-style interface
|
||||
CodeMirror.defineExtension("foldCode", function(pos, options, force) {
|
||||
doFold(this, pos, options, force);
|
||||
});
|
||||
|
||||
CodeMirror.defineExtension("isFolded", function(pos) {
|
||||
var marks = this.findMarksAt(pos);
|
||||
for (var i = 0; i < marks.length; ++i)
|
||||
if (marks[i].__isFold) return true;
|
||||
});
|
||||
|
||||
CodeMirror.commands.toggleFold = function(cm) {
|
||||
cm.foldCode(cm.getCursor());
|
||||
};
|
||||
CodeMirror.commands.fold = function(cm) {
|
||||
cm.foldCode(cm.getCursor(), null, "fold");
|
||||
};
|
||||
CodeMirror.commands.unfold = function(cm) {
|
||||
cm.foldCode(cm.getCursor(), { scanUp: false }, "unfold");
|
||||
};
|
||||
CodeMirror.commands.foldAll = function(cm) {
|
||||
cm.operation(function() {
|
||||
for (var i = cm.firstLine(), e = cm.lastLine(); i <= e; i++)
|
||||
cm.foldCode(CodeMirror.Pos(i, 0), { scanUp: false }, "fold");
|
||||
});
|
||||
};
|
||||
CodeMirror.commands.unfoldAll = function(cm) {
|
||||
cm.operation(function() {
|
||||
for (var i = cm.firstLine(), e = cm.lastLine(); i <= e; i++)
|
||||
cm.foldCode(CodeMirror.Pos(i, 0), { scanUp: false }, "unfold");
|
||||
});
|
||||
};
|
||||
|
||||
CodeMirror.registerHelper("fold", "combine", function() {
|
||||
var funcs = Array.prototype.slice.call(arguments, 0);
|
||||
return function(cm, start) {
|
||||
for (var i = 0; i < funcs.length; ++i) {
|
||||
var found = funcs[i](cm, start);
|
||||
if (found) return found;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
CodeMirror.registerHelper("fold", "auto", function(cm, start) {
|
||||
var helpers = cm.getHelpers(start, "fold");
|
||||
for (var i = 0; i < helpers.length; i++) {
|
||||
var cur = helpers[i](cm, start);
|
||||
if (cur) return cur;
|
||||
}
|
||||
});
|
||||
|
||||
var defaultOptions = {
|
||||
rangeFinder: CodeMirror.fold.auto,
|
||||
widget: "\u2194",
|
||||
minFoldSize: 0,
|
||||
scanUp: false,
|
||||
clearOnEnter: true
|
||||
};
|
||||
|
||||
CodeMirror.defineOption("foldOptions", null);
|
||||
|
||||
function getOption(cm, options, name) {
|
||||
if (options && options[name] !== undefined)
|
||||
return options[name];
|
||||
var editorOptions = cm.options.foldOptions;
|
||||
if (editorOptions && editorOptions[name] !== undefined)
|
||||
return editorOptions[name];
|
||||
return defaultOptions[name];
|
||||
}
|
||||
|
||||
CodeMirror.defineExtension("foldOption", function(options, name) {
|
||||
return getOption(this, options, name);
|
||||
});
|
||||
});
|
||||
20
web/assets/codemirror/fold/foldgutter.css
Normal file
20
web/assets/codemirror/fold/foldgutter.css
Normal file
@@ -0,0 +1,20 @@
|
||||
.CodeMirror-foldmarker {
|
||||
color: blue;
|
||||
text-shadow: #b9f 1px 1px 2px, #b9f -1px -1px 2px, #b9f 1px -1px 2px, #b9f -1px 1px 2px;
|
||||
font-family: arial;
|
||||
line-height: .3;
|
||||
cursor: pointer;
|
||||
}
|
||||
.CodeMirror-foldgutter {
|
||||
width: .7em;
|
||||
}
|
||||
.CodeMirror-foldgutter-open,
|
||||
.CodeMirror-foldgutter-folded {
|
||||
cursor: pointer;
|
||||
}
|
||||
.CodeMirror-foldgutter-open:after {
|
||||
content: "\25BE";
|
||||
}
|
||||
.CodeMirror-foldgutter-folded:after {
|
||||
content: "\25B8";
|
||||
}
|
||||
169
web/assets/codemirror/fold/foldgutter.js
Normal file
169
web/assets/codemirror/fold/foldgutter.js
Normal file
@@ -0,0 +1,169 @@
|
||||
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
||||
// Distributed under an MIT license: https://codemirror.net/5/LICENSE
|
||||
|
||||
(function(mod) {
|
||||
if (typeof exports == "object" && typeof module == "object") // CommonJS
|
||||
mod(require("../../lib/codemirror"), require("./foldcode"));
|
||||
else if (typeof define == "function" && define.amd) // AMD
|
||||
define(["../../lib/codemirror", "./foldcode"], mod);
|
||||
else // Plain browser env
|
||||
mod(CodeMirror);
|
||||
})(function(CodeMirror) {
|
||||
"use strict";
|
||||
|
||||
CodeMirror.defineOption("foldGutter", false, function(cm, val, old) {
|
||||
if (old && old != CodeMirror.Init) {
|
||||
cm.clearGutter(cm.state.foldGutter.options.gutter);
|
||||
cm.state.foldGutter = null;
|
||||
cm.off("gutterClick", onGutterClick);
|
||||
cm.off("changes", onChange);
|
||||
cm.off("viewportChange", onViewportChange);
|
||||
cm.off("fold", onFold);
|
||||
cm.off("unfold", onFold);
|
||||
cm.off("swapDoc", onChange);
|
||||
cm.off("optionChange", optionChange);
|
||||
}
|
||||
if (val) {
|
||||
cm.state.foldGutter = new State(parseOptions(val));
|
||||
updateInViewport(cm);
|
||||
cm.on("gutterClick", onGutterClick);
|
||||
cm.on("changes", onChange);
|
||||
cm.on("viewportChange", onViewportChange);
|
||||
cm.on("fold", onFold);
|
||||
cm.on("unfold", onFold);
|
||||
cm.on("swapDoc", onChange);
|
||||
cm.on("optionChange", optionChange);
|
||||
}
|
||||
});
|
||||
|
||||
var Pos = CodeMirror.Pos;
|
||||
|
||||
function State(options) {
|
||||
this.options = options;
|
||||
this.from = this.to = 0;
|
||||
}
|
||||
|
||||
function parseOptions(opts) {
|
||||
if (opts === true) opts = {};
|
||||
if (opts.gutter == null) opts.gutter = "CodeMirror-foldgutter";
|
||||
if (opts.indicatorOpen == null) opts.indicatorOpen = "CodeMirror-foldgutter-open";
|
||||
if (opts.indicatorFolded == null) opts.indicatorFolded = "CodeMirror-foldgutter-folded";
|
||||
return opts;
|
||||
}
|
||||
|
||||
function isFolded(cm, line) {
|
||||
var marks = cm.findMarks(Pos(line, 0), Pos(line + 1, 0));
|
||||
for (var i = 0; i < marks.length; ++i) {
|
||||
if (marks[i].__isFold) {
|
||||
var fromPos = marks[i].find(-1);
|
||||
if (fromPos && fromPos.line === line)
|
||||
return marks[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function marker(spec) {
|
||||
if (typeof spec == "string") {
|
||||
var elt = document.createElement("div");
|
||||
elt.className = spec + " CodeMirror-guttermarker-subtle";
|
||||
return elt;
|
||||
} else {
|
||||
return spec.cloneNode(true);
|
||||
}
|
||||
}
|
||||
|
||||
function updateFoldInfo(cm, from, to) {
|
||||
var opts = cm.state.foldGutter.options, cur = from - 1;
|
||||
var minSize = cm.foldOption(opts, "minFoldSize");
|
||||
var func = cm.foldOption(opts, "rangeFinder");
|
||||
// we can reuse the built-in indicator element if its className matches the new state
|
||||
var clsFolded = typeof opts.indicatorFolded == "string" && classTest(opts.indicatorFolded);
|
||||
var clsOpen = typeof opts.indicatorOpen == "string" && classTest(opts.indicatorOpen);
|
||||
cm.eachLine(from, to, function(line) {
|
||||
++cur;
|
||||
var mark = null;
|
||||
var old = line.gutterMarkers;
|
||||
if (old) old = old[opts.gutter];
|
||||
if (isFolded(cm, cur)) {
|
||||
if (clsFolded && old && clsFolded.test(old.className)) return;
|
||||
mark = marker(opts.indicatorFolded);
|
||||
} else {
|
||||
var pos = Pos(cur, 0);
|
||||
var range = func && func(cm, pos);
|
||||
if (range && range.to.line - range.from.line >= minSize) {
|
||||
if (clsOpen && old && clsOpen.test(old.className)) return;
|
||||
mark = marker(opts.indicatorOpen);
|
||||
}
|
||||
}
|
||||
if (!mark && !old) return;
|
||||
cm.setGutterMarker(line, opts.gutter, mark);
|
||||
});
|
||||
}
|
||||
|
||||
// copied from CodeMirror/src/util/dom.js
|
||||
function classTest(cls) { return new RegExp("(^|\\s)" + cls + "(?:$|\\s)\\s*") }
|
||||
|
||||
function updateInViewport(cm) {
|
||||
var vp = cm.getViewport(), state = cm.state.foldGutter;
|
||||
if (!state) return;
|
||||
cm.operation(function() {
|
||||
updateFoldInfo(cm, vp.from, vp.to);
|
||||
});
|
||||
state.from = vp.from; state.to = vp.to;
|
||||
}
|
||||
|
||||
function onGutterClick(cm, line, gutter) {
|
||||
var state = cm.state.foldGutter;
|
||||
if (!state) return;
|
||||
var opts = state.options;
|
||||
if (gutter != opts.gutter) return;
|
||||
var folded = isFolded(cm, line);
|
||||
if (folded) folded.clear();
|
||||
else cm.foldCode(Pos(line, 0), opts);
|
||||
}
|
||||
|
||||
function optionChange(cm, option) {
|
||||
if (option == "mode") onChange(cm)
|
||||
}
|
||||
|
||||
function onChange(cm) {
|
||||
var state = cm.state.foldGutter;
|
||||
if (!state) return;
|
||||
var opts = state.options;
|
||||
state.from = state.to = 0;
|
||||
clearTimeout(state.changeUpdate);
|
||||
state.changeUpdate = setTimeout(function() { updateInViewport(cm); }, opts.foldOnChangeTimeSpan || 600);
|
||||
}
|
||||
|
||||
function onViewportChange(cm) {
|
||||
var state = cm.state.foldGutter;
|
||||
if (!state) return;
|
||||
var opts = state.options;
|
||||
clearTimeout(state.changeUpdate);
|
||||
state.changeUpdate = setTimeout(function() {
|
||||
var vp = cm.getViewport();
|
||||
if (state.from == state.to || vp.from - state.to > 20 || state.from - vp.to > 20) {
|
||||
updateInViewport(cm);
|
||||
} else {
|
||||
cm.operation(function() {
|
||||
if (vp.from < state.from) {
|
||||
updateFoldInfo(cm, vp.from, state.from);
|
||||
state.from = vp.from;
|
||||
}
|
||||
if (vp.to > state.to) {
|
||||
updateFoldInfo(cm, state.to, vp.to);
|
||||
state.to = vp.to;
|
||||
}
|
||||
});
|
||||
}
|
||||
}, opts.updateViewportTimeSpan || 400);
|
||||
}
|
||||
|
||||
function onFold(cm, from) {
|
||||
var state = cm.state.foldGutter;
|
||||
if (!state) return;
|
||||
var line = from.line;
|
||||
if (line >= state.from && line < state.to)
|
||||
updateFoldInfo(cm, line, line + 1);
|
||||
}
|
||||
});
|
||||
162
web/assets/codemirror/hint/javascript-hint.js
Normal file
162
web/assets/codemirror/hint/javascript-hint.js
Normal file
@@ -0,0 +1,162 @@
|
||||
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
||||
// Distributed under an MIT license: https://codemirror.net/5/LICENSE
|
||||
|
||||
(function(mod) {
|
||||
if (typeof exports == "object" && typeof module == "object") // CommonJS
|
||||
mod(require("../../lib/codemirror"));
|
||||
else if (typeof define == "function" && define.amd) // AMD
|
||||
define(["../../lib/codemirror"], mod);
|
||||
else // Plain browser env
|
||||
mod(CodeMirror);
|
||||
})(function(CodeMirror) {
|
||||
var Pos = CodeMirror.Pos;
|
||||
|
||||
function forEach(arr, f) {
|
||||
for (var i = 0, e = arr.length; i < e; ++i) f(arr[i]);
|
||||
}
|
||||
|
||||
function arrayContains(arr, item) {
|
||||
if (!Array.prototype.indexOf) {
|
||||
var i = arr.length;
|
||||
while (i--) {
|
||||
if (arr[i] === item) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return arr.indexOf(item) != -1;
|
||||
}
|
||||
|
||||
function scriptHint(editor, keywords, getToken, options) {
|
||||
// Find the token at the cursor
|
||||
var cur = editor.getCursor(), token = getToken(editor, cur);
|
||||
if (/\b(?:string|comment)\b/.test(token.type)) return;
|
||||
var innerMode = CodeMirror.innerMode(editor.getMode(), token.state);
|
||||
if (innerMode.mode.helperType === "json") return;
|
||||
token.state = innerMode.state;
|
||||
|
||||
// If it's not a 'word-style' token, ignore the token.
|
||||
if (!/^[\w$_]*$/.test(token.string)) {
|
||||
token = {start: cur.ch, end: cur.ch, string: "", state: token.state,
|
||||
type: token.string == "." ? "property" : null};
|
||||
} else if (token.end > cur.ch) {
|
||||
token.end = cur.ch;
|
||||
token.string = token.string.slice(0, cur.ch - token.start);
|
||||
}
|
||||
|
||||
var tprop = token;
|
||||
// If it is a property, find out what it is a property of.
|
||||
while (tprop.type == "property") {
|
||||
tprop = getToken(editor, Pos(cur.line, tprop.start));
|
||||
if (tprop.string != ".") return;
|
||||
tprop = getToken(editor, Pos(cur.line, tprop.start));
|
||||
if (!context) var context = [];
|
||||
context.push(tprop);
|
||||
}
|
||||
return {list: getCompletions(token, context, keywords, options),
|
||||
from: Pos(cur.line, token.start),
|
||||
to: Pos(cur.line, token.end)};
|
||||
}
|
||||
|
||||
function javascriptHint(editor, options) {
|
||||
return scriptHint(editor, javascriptKeywords,
|
||||
function (e, cur) {return e.getTokenAt(cur);},
|
||||
options);
|
||||
};
|
||||
CodeMirror.registerHelper("hint", "javascript", javascriptHint);
|
||||
|
||||
function getCoffeeScriptToken(editor, cur) {
|
||||
// This getToken, it is for coffeescript, imitates the behavior of
|
||||
// getTokenAt method in javascript.js, that is, returning "property"
|
||||
// type and treat "." as independent token.
|
||||
var token = editor.getTokenAt(cur);
|
||||
if (cur.ch == token.start + 1 && token.string.charAt(0) == '.') {
|
||||
token.end = token.start;
|
||||
token.string = '.';
|
||||
token.type = "property";
|
||||
}
|
||||
else if (/^\.[\w$_]*$/.test(token.string)) {
|
||||
token.type = "property";
|
||||
token.start++;
|
||||
token.string = token.string.replace(/\./, '');
|
||||
}
|
||||
return token;
|
||||
}
|
||||
|
||||
function coffeescriptHint(editor, options) {
|
||||
return scriptHint(editor, coffeescriptKeywords, getCoffeeScriptToken, options);
|
||||
}
|
||||
CodeMirror.registerHelper("hint", "coffeescript", coffeescriptHint);
|
||||
|
||||
var stringProps = ("charAt charCodeAt indexOf lastIndexOf substring substr slice trim trimLeft trimRight " +
|
||||
"toUpperCase toLowerCase split concat match replace search").split(" ");
|
||||
var arrayProps = ("length concat join splice push pop shift unshift slice reverse sort indexOf " +
|
||||
"lastIndexOf every some filter forEach map reduce reduceRight ").split(" ");
|
||||
var funcProps = "prototype apply call bind".split(" ");
|
||||
var javascriptKeywords = ("break case catch class const continue debugger default delete do else export extends false finally for function " +
|
||||
"if in import instanceof new null return super switch this throw true try typeof var void while with yield").split(" ");
|
||||
var coffeescriptKeywords = ("and break catch class continue delete do else extends false finally for " +
|
||||
"if in instanceof isnt new no not null of off on or return switch then throw true try typeof until void while with yes").split(" ");
|
||||
|
||||
function forAllProps(obj, callback) {
|
||||
if (!Object.getOwnPropertyNames || !Object.getPrototypeOf) {
|
||||
for (var name in obj) callback(name)
|
||||
} else {
|
||||
for (var o = obj; o; o = Object.getPrototypeOf(o))
|
||||
Object.getOwnPropertyNames(o).forEach(callback)
|
||||
}
|
||||
}
|
||||
|
||||
function getCompletions(token, context, keywords, options) {
|
||||
var found = [], start = token.string, global = options && options.globalScope || window;
|
||||
function maybeAdd(str) {
|
||||
if (str.lastIndexOf(start, 0) == 0 && !arrayContains(found, str)) found.push(str);
|
||||
}
|
||||
function gatherCompletions(obj) {
|
||||
if (typeof obj == "string") forEach(stringProps, maybeAdd);
|
||||
else if (obj instanceof Array) forEach(arrayProps, maybeAdd);
|
||||
else if (obj instanceof Function) forEach(funcProps, maybeAdd);
|
||||
forAllProps(obj, maybeAdd)
|
||||
}
|
||||
|
||||
if (context && context.length) {
|
||||
// If this is a property, see if it belongs to some object we can
|
||||
// find in the current environment.
|
||||
var obj = context.pop(), base;
|
||||
if (obj.type && obj.type.indexOf("variable") === 0) {
|
||||
if (options && options.additionalContext)
|
||||
base = options.additionalContext[obj.string];
|
||||
if (!options || options.useGlobalScope !== false)
|
||||
base = base || global[obj.string];
|
||||
} else if (obj.type == "string") {
|
||||
base = "";
|
||||
} else if (obj.type == "atom") {
|
||||
base = 1;
|
||||
} else if (obj.type == "function") {
|
||||
if (global.jQuery != null && (obj.string == '$' || obj.string == 'jQuery') &&
|
||||
(typeof global.jQuery == 'function'))
|
||||
base = global.jQuery();
|
||||
else if (global._ != null && (obj.string == '_') && (typeof global._ == 'function'))
|
||||
base = global._();
|
||||
}
|
||||
while (base != null && context.length)
|
||||
base = base[context.pop().string];
|
||||
if (base != null) gatherCompletions(base);
|
||||
} else {
|
||||
// If not, just look in the global object, any local scope, and optional additional-context
|
||||
// (reading into JS mode internals to get at the local and global variables)
|
||||
for (var v = token.state.localVars; v; v = v.next) maybeAdd(v.name);
|
||||
for (var c = token.state.context; c; c = c.prev)
|
||||
for (var v = c.vars; v; v = v.next) maybeAdd(v.name)
|
||||
for (var v = token.state.globalVars; v; v = v.next) maybeAdd(v.name);
|
||||
if (options && options.additionalContext != null)
|
||||
for (var key in options.additionalContext)
|
||||
maybeAdd(key);
|
||||
if (!options || options.useGlobalScope !== false)
|
||||
gatherCompletions(global);
|
||||
forEach(keywords, maybeAdd);
|
||||
}
|
||||
return found;
|
||||
}
|
||||
});
|
||||
960
web/assets/codemirror/javascript.js
Normal file
960
web/assets/codemirror/javascript.js
Normal file
@@ -0,0 +1,960 @@
|
||||
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
||||
// Distributed under an MIT license: https://codemirror.net/5/LICENSE
|
||||
|
||||
(function(mod) {
|
||||
if (typeof exports == "object" && typeof module == "object") // CommonJS
|
||||
mod(require("../../lib/codemirror"));
|
||||
else if (typeof define == "function" && define.amd) // AMD
|
||||
define(["../../lib/codemirror"], mod);
|
||||
else // Plain browser env
|
||||
mod(CodeMirror);
|
||||
})(function(CodeMirror) {
|
||||
"use strict";
|
||||
|
||||
CodeMirror.defineMode("javascript", function(config, parserConfig) {
|
||||
var indentUnit = config.indentUnit;
|
||||
var statementIndent = parserConfig.statementIndent;
|
||||
var jsonldMode = parserConfig.jsonld;
|
||||
var jsonMode = parserConfig.json || jsonldMode;
|
||||
var trackScope = parserConfig.trackScope !== false
|
||||
var isTS = parserConfig.typescript;
|
||||
var wordRE = parserConfig.wordCharacters || /[\w$\xa1-\uffff]/;
|
||||
|
||||
// Tokenizer
|
||||
|
||||
var keywords = function(){
|
||||
function kw(type) {return {type: type, style: "keyword"};}
|
||||
var A = kw("keyword a"), B = kw("keyword b"), C = kw("keyword c"), D = kw("keyword d");
|
||||
var operator = kw("operator"), atom = {type: "atom", style: "atom"};
|
||||
|
||||
return {
|
||||
"if": kw("if"), "while": A, "with": A, "else": B, "do": B, "try": B, "finally": B,
|
||||
"return": D, "break": D, "continue": D, "new": kw("new"), "delete": C, "void": C, "throw": C,
|
||||
"debugger": kw("debugger"), "var": kw("var"), "const": kw("var"), "let": kw("var"),
|
||||
"function": kw("function"), "catch": kw("catch"),
|
||||
"for": kw("for"), "switch": kw("switch"), "case": kw("case"), "default": kw("default"),
|
||||
"in": operator, "typeof": operator, "instanceof": operator,
|
||||
"true": atom, "false": atom, "null": atom, "undefined": atom, "NaN": atom, "Infinity": atom,
|
||||
"this": kw("this"), "class": kw("class"), "super": kw("atom"),
|
||||
"yield": C, "export": kw("export"), "import": kw("import"), "extends": C,
|
||||
"await": C
|
||||
};
|
||||
}();
|
||||
|
||||
var isOperatorChar = /[+\-*&%=<>!?|~^@]/;
|
||||
var isJsonldKeyword = /^@(context|id|value|language|type|container|list|set|reverse|index|base|vocab|graph)"/;
|
||||
|
||||
function readRegexp(stream) {
|
||||
var escaped = false, next, inSet = false;
|
||||
while ((next = stream.next()) != null) {
|
||||
if (!escaped) {
|
||||
if (next == "/" && !inSet) return;
|
||||
if (next == "[") inSet = true;
|
||||
else if (inSet && next == "]") inSet = false;
|
||||
}
|
||||
escaped = !escaped && next == "\\";
|
||||
}
|
||||
}
|
||||
|
||||
// Used as scratch variables to communicate multiple values without
|
||||
// consing up tons of objects.
|
||||
var type, content;
|
||||
function ret(tp, style, cont) {
|
||||
type = tp; content = cont;
|
||||
return style;
|
||||
}
|
||||
function tokenBase(stream, state) {
|
||||
var ch = stream.next();
|
||||
if (ch == '"' || ch == "'") {
|
||||
state.tokenize = tokenString(ch);
|
||||
return state.tokenize(stream, state);
|
||||
} else if (ch == "." && stream.match(/^\d[\d_]*(?:[eE][+\-]?[\d_]+)?/)) {
|
||||
return ret("number", "number");
|
||||
} else if (ch == "." && stream.match("..")) {
|
||||
return ret("spread", "meta");
|
||||
} else if (/[\[\]{}\(\),;\:\.]/.test(ch)) {
|
||||
return ret(ch);
|
||||
} else if (ch == "=" && stream.eat(">")) {
|
||||
return ret("=>", "operator");
|
||||
} else if (ch == "0" && stream.match(/^(?:x[\dA-Fa-f_]+|o[0-7_]+|b[01_]+)n?/)) {
|
||||
return ret("number", "number");
|
||||
} else if (/\d/.test(ch)) {
|
||||
stream.match(/^[\d_]*(?:n|(?:\.[\d_]*)?(?:[eE][+\-]?[\d_]+)?)?/);
|
||||
return ret("number", "number");
|
||||
} else if (ch == "/") {
|
||||
if (stream.eat("*")) {
|
||||
state.tokenize = tokenComment;
|
||||
return tokenComment(stream, state);
|
||||
} else if (stream.eat("/")) {
|
||||
stream.skipToEnd();
|
||||
return ret("comment", "comment");
|
||||
} else if (expressionAllowed(stream, state, 1)) {
|
||||
readRegexp(stream);
|
||||
stream.match(/^\b(([gimyus])(?![gimyus]*\2))+\b/);
|
||||
return ret("regexp", "string-2");
|
||||
} else {
|
||||
stream.eat("=");
|
||||
return ret("operator", "operator", stream.current());
|
||||
}
|
||||
} else if (ch == "`") {
|
||||
state.tokenize = tokenQuasi;
|
||||
return tokenQuasi(stream, state);
|
||||
} else if (ch == "#" && stream.peek() == "!") {
|
||||
stream.skipToEnd();
|
||||
return ret("meta", "meta");
|
||||
} else if (ch == "#" && stream.eatWhile(wordRE)) {
|
||||
return ret("variable", "property")
|
||||
} else if (ch == "<" && stream.match("!--") ||
|
||||
(ch == "-" && stream.match("->") && !/\S/.test(stream.string.slice(0, stream.start)))) {
|
||||
stream.skipToEnd()
|
||||
return ret("comment", "comment")
|
||||
} else if (isOperatorChar.test(ch)) {
|
||||
if (ch != ">" || !state.lexical || state.lexical.type != ">") {
|
||||
if (stream.eat("=")) {
|
||||
if (ch == "!" || ch == "=") stream.eat("=")
|
||||
} else if (/[<>*+\-|&?]/.test(ch)) {
|
||||
stream.eat(ch)
|
||||
if (ch == ">") stream.eat(ch)
|
||||
}
|
||||
}
|
||||
if (ch == "?" && stream.eat(".")) return ret(".")
|
||||
return ret("operator", "operator", stream.current());
|
||||
} else if (wordRE.test(ch)) {
|
||||
stream.eatWhile(wordRE);
|
||||
var word = stream.current()
|
||||
if (state.lastType != ".") {
|
||||
if (keywords.propertyIsEnumerable(word)) {
|
||||
var kw = keywords[word]
|
||||
return ret(kw.type, kw.style, word)
|
||||
}
|
||||
if (word == "async" && stream.match(/^(\s|\/\*([^*]|\*(?!\/))*?\*\/)*[\[\(\w]/, false))
|
||||
return ret("async", "keyword", word)
|
||||
}
|
||||
return ret("variable", "variable", word)
|
||||
}
|
||||
}
|
||||
|
||||
function tokenString(quote) {
|
||||
return function(stream, state) {
|
||||
var escaped = false, next;
|
||||
if (jsonldMode && stream.peek() == "@" && stream.match(isJsonldKeyword)){
|
||||
state.tokenize = tokenBase;
|
||||
return ret("jsonld-keyword", "meta");
|
||||
}
|
||||
while ((next = stream.next()) != null) {
|
||||
if (next == quote && !escaped) break;
|
||||
escaped = !escaped && next == "\\";
|
||||
}
|
||||
if (!escaped) state.tokenize = tokenBase;
|
||||
return ret("string", "string");
|
||||
};
|
||||
}
|
||||
|
||||
function tokenComment(stream, state) {
|
||||
var maybeEnd = false, ch;
|
||||
while (ch = stream.next()) {
|
||||
if (ch == "/" && maybeEnd) {
|
||||
state.tokenize = tokenBase;
|
||||
break;
|
||||
}
|
||||
maybeEnd = (ch == "*");
|
||||
}
|
||||
return ret("comment", "comment");
|
||||
}
|
||||
|
||||
function tokenQuasi(stream, state) {
|
||||
var escaped = false, next;
|
||||
while ((next = stream.next()) != null) {
|
||||
if (!escaped && (next == "`" || next == "$" && stream.eat("{"))) {
|
||||
state.tokenize = tokenBase;
|
||||
break;
|
||||
}
|
||||
escaped = !escaped && next == "\\";
|
||||
}
|
||||
return ret("quasi", "string-2", stream.current());
|
||||
}
|
||||
|
||||
var brackets = "([{}])";
|
||||
// This is a crude lookahead trick to try and notice that we're
|
||||
// parsing the argument patterns for a fat-arrow function before we
|
||||
// actually hit the arrow token. It only works if the arrow is on
|
||||
// the same line as the arguments and there's no strange noise
|
||||
// (comments) in between. Fallback is to only notice when we hit the
|
||||
// arrow, and not declare the arguments as locals for the arrow
|
||||
// body.
|
||||
function findFatArrow(stream, state) {
|
||||
if (state.fatArrowAt) state.fatArrowAt = null;
|
||||
var arrow = stream.string.indexOf("=>", stream.start);
|
||||
if (arrow < 0) return;
|
||||
|
||||
if (isTS) { // Try to skip TypeScript return type declarations after the arguments
|
||||
var m = /:\s*(?:\w+(?:<[^>]*>|\[\])?|\{[^}]*\})\s*$/.exec(stream.string.slice(stream.start, arrow))
|
||||
if (m) arrow = m.index
|
||||
}
|
||||
|
||||
var depth = 0, sawSomething = false;
|
||||
for (var pos = arrow - 1; pos >= 0; --pos) {
|
||||
var ch = stream.string.charAt(pos);
|
||||
var bracket = brackets.indexOf(ch);
|
||||
if (bracket >= 0 && bracket < 3) {
|
||||
if (!depth) { ++pos; break; }
|
||||
if (--depth == 0) { if (ch == "(") sawSomething = true; break; }
|
||||
} else if (bracket >= 3 && bracket < 6) {
|
||||
++depth;
|
||||
} else if (wordRE.test(ch)) {
|
||||
sawSomething = true;
|
||||
} else if (/["'\/`]/.test(ch)) {
|
||||
for (;; --pos) {
|
||||
if (pos == 0) return
|
||||
var next = stream.string.charAt(pos - 1)
|
||||
if (next == ch && stream.string.charAt(pos - 2) != "\\") { pos--; break }
|
||||
}
|
||||
} else if (sawSomething && !depth) {
|
||||
++pos;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (sawSomething && !depth) state.fatArrowAt = pos;
|
||||
}
|
||||
|
||||
// Parser
|
||||
|
||||
var atomicTypes = {"atom": true, "number": true, "variable": true, "string": true,
|
||||
"regexp": true, "this": true, "import": true, "jsonld-keyword": true};
|
||||
|
||||
function JSLexical(indented, column, type, align, prev, info) {
|
||||
this.indented = indented;
|
||||
this.column = column;
|
||||
this.type = type;
|
||||
this.prev = prev;
|
||||
this.info = info;
|
||||
if (align != null) this.align = align;
|
||||
}
|
||||
|
||||
function inScope(state, varname) {
|
||||
if (!trackScope) return false
|
||||
for (var v = state.localVars; v; v = v.next)
|
||||
if (v.name == varname) return true;
|
||||
for (var cx = state.context; cx; cx = cx.prev) {
|
||||
for (var v = cx.vars; v; v = v.next)
|
||||
if (v.name == varname) return true;
|
||||
}
|
||||
}
|
||||
|
||||
function parseJS(state, style, type, content, stream) {
|
||||
var cc = state.cc;
|
||||
// Communicate our context to the combinators.
|
||||
// (Less wasteful than consing up a hundred closures on every call.)
|
||||
cx.state = state; cx.stream = stream; cx.marked = null, cx.cc = cc; cx.style = style;
|
||||
|
||||
if (!state.lexical.hasOwnProperty("align"))
|
||||
state.lexical.align = true;
|
||||
|
||||
while(true) {
|
||||
var combinator = cc.length ? cc.pop() : jsonMode ? expression : statement;
|
||||
if (combinator(type, content)) {
|
||||
while(cc.length && cc[cc.length - 1].lex)
|
||||
cc.pop()();
|
||||
if (cx.marked) return cx.marked;
|
||||
if (type == "variable" && inScope(state, content)) return "variable-2";
|
||||
return style;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Combinator utils
|
||||
|
||||
var cx = {state: null, column: null, marked: null, cc: null};
|
||||
function pass() {
|
||||
for (var i = arguments.length - 1; i >= 0; i--) cx.cc.push(arguments[i]);
|
||||
}
|
||||
function cont() {
|
||||
pass.apply(null, arguments);
|
||||
return true;
|
||||
}
|
||||
function inList(name, list) {
|
||||
for (var v = list; v; v = v.next) if (v.name == name) return true
|
||||
return false;
|
||||
}
|
||||
function register(varname) {
|
||||
var state = cx.state;
|
||||
cx.marked = "def";
|
||||
if (!trackScope) return
|
||||
if (state.context) {
|
||||
if (state.lexical.info == "var" && state.context && state.context.block) {
|
||||
// FIXME function decls are also not block scoped
|
||||
var newContext = registerVarScoped(varname, state.context)
|
||||
if (newContext != null) {
|
||||
state.context = newContext
|
||||
return
|
||||
}
|
||||
} else if (!inList(varname, state.localVars)) {
|
||||
state.localVars = new Var(varname, state.localVars)
|
||||
return
|
||||
}
|
||||
}
|
||||
// Fall through means this is global
|
||||
if (parserConfig.globalVars && !inList(varname, state.globalVars))
|
||||
state.globalVars = new Var(varname, state.globalVars)
|
||||
}
|
||||
function registerVarScoped(varname, context) {
|
||||
if (!context) {
|
||||
return null
|
||||
} else if (context.block) {
|
||||
var inner = registerVarScoped(varname, context.prev)
|
||||
if (!inner) return null
|
||||
if (inner == context.prev) return context
|
||||
return new Context(inner, context.vars, true)
|
||||
} else if (inList(varname, context.vars)) {
|
||||
return context
|
||||
} else {
|
||||
return new Context(context.prev, new Var(varname, context.vars), false)
|
||||
}
|
||||
}
|
||||
|
||||
function isModifier(name) {
|
||||
return name == "public" || name == "private" || name == "protected" || name == "abstract" || name == "readonly"
|
||||
}
|
||||
|
||||
// Combinators
|
||||
|
||||
function Context(prev, vars, block) { this.prev = prev; this.vars = vars; this.block = block }
|
||||
function Var(name, next) { this.name = name; this.next = next }
|
||||
|
||||
var defaultVars = new Var("this", new Var("arguments", null))
|
||||
function pushcontext() {
|
||||
cx.state.context = new Context(cx.state.context, cx.state.localVars, false)
|
||||
cx.state.localVars = defaultVars
|
||||
}
|
||||
function pushblockcontext() {
|
||||
cx.state.context = new Context(cx.state.context, cx.state.localVars, true)
|
||||
cx.state.localVars = null
|
||||
}
|
||||
pushcontext.lex = pushblockcontext.lex = true
|
||||
function popcontext() {
|
||||
cx.state.localVars = cx.state.context.vars
|
||||
cx.state.context = cx.state.context.prev
|
||||
}
|
||||
popcontext.lex = true
|
||||
function pushlex(type, info) {
|
||||
var result = function() {
|
||||
var state = cx.state, indent = state.indented;
|
||||
if (state.lexical.type == "stat") indent = state.lexical.indented;
|
||||
else for (var outer = state.lexical; outer && outer.type == ")" && outer.align; outer = outer.prev)
|
||||
indent = outer.indented;
|
||||
state.lexical = new JSLexical(indent, cx.stream.column(), type, null, state.lexical, info);
|
||||
};
|
||||
result.lex = true;
|
||||
return result;
|
||||
}
|
||||
function poplex() {
|
||||
var state = cx.state;
|
||||
if (state.lexical.prev) {
|
||||
if (state.lexical.type == ")")
|
||||
state.indented = state.lexical.indented;
|
||||
state.lexical = state.lexical.prev;
|
||||
}
|
||||
}
|
||||
poplex.lex = true;
|
||||
|
||||
function expect(wanted) {
|
||||
function exp(type) {
|
||||
if (type == wanted) return cont();
|
||||
else if (wanted == ";" || type == "}" || type == ")" || type == "]") return pass();
|
||||
else return cont(exp);
|
||||
};
|
||||
return exp;
|
||||
}
|
||||
|
||||
function statement(type, value) {
|
||||
if (type == "var") return cont(pushlex("vardef", value), vardef, expect(";"), poplex);
|
||||
if (type == "keyword a") return cont(pushlex("form"), parenExpr, statement, poplex);
|
||||
if (type == "keyword b") return cont(pushlex("form"), statement, poplex);
|
||||
if (type == "keyword d") return cx.stream.match(/^\s*$/, false) ? cont() : cont(pushlex("stat"), maybeexpression, expect(";"), poplex);
|
||||
if (type == "debugger") return cont(expect(";"));
|
||||
if (type == "{") return cont(pushlex("}"), pushblockcontext, block, poplex, popcontext);
|
||||
if (type == ";") return cont();
|
||||
if (type == "if") {
|
||||
if (cx.state.lexical.info == "else" && cx.state.cc[cx.state.cc.length - 1] == poplex)
|
||||
cx.state.cc.pop()();
|
||||
return cont(pushlex("form"), parenExpr, statement, poplex, maybeelse);
|
||||
}
|
||||
if (type == "function") return cont(functiondef);
|
||||
if (type == "for") return cont(pushlex("form"), pushblockcontext, forspec, statement, popcontext, poplex);
|
||||
if (type == "class" || (isTS && value == "interface")) {
|
||||
cx.marked = "keyword"
|
||||
return cont(pushlex("form", type == "class" ? type : value), className, poplex)
|
||||
}
|
||||
if (type == "variable") {
|
||||
if (isTS && value == "declare") {
|
||||
cx.marked = "keyword"
|
||||
return cont(statement)
|
||||
} else if (isTS && (value == "module" || value == "enum" || value == "type") && cx.stream.match(/^\s*\w/, false)) {
|
||||
cx.marked = "keyword"
|
||||
if (value == "enum") return cont(enumdef);
|
||||
else if (value == "type") return cont(typename, expect("operator"), typeexpr, expect(";"));
|
||||
else return cont(pushlex("form"), pattern, expect("{"), pushlex("}"), block, poplex, poplex)
|
||||
} else if (isTS && value == "namespace") {
|
||||
cx.marked = "keyword"
|
||||
return cont(pushlex("form"), expression, statement, poplex)
|
||||
} else if (isTS && value == "abstract") {
|
||||
cx.marked = "keyword"
|
||||
return cont(statement)
|
||||
} else {
|
||||
return cont(pushlex("stat"), maybelabel);
|
||||
}
|
||||
}
|
||||
if (type == "switch") return cont(pushlex("form"), parenExpr, expect("{"), pushlex("}", "switch"), pushblockcontext,
|
||||
block, poplex, poplex, popcontext);
|
||||
if (type == "case") return cont(expression, expect(":"));
|
||||
if (type == "default") return cont(expect(":"));
|
||||
if (type == "catch") return cont(pushlex("form"), pushcontext, maybeCatchBinding, statement, poplex, popcontext);
|
||||
if (type == "export") return cont(pushlex("stat"), afterExport, poplex);
|
||||
if (type == "import") return cont(pushlex("stat"), afterImport, poplex);
|
||||
if (type == "async") return cont(statement)
|
||||
if (value == "@") return cont(expression, statement)
|
||||
return pass(pushlex("stat"), expression, expect(";"), poplex);
|
||||
}
|
||||
function maybeCatchBinding(type) {
|
||||
if (type == "(") return cont(funarg, expect(")"))
|
||||
}
|
||||
function expression(type, value) {
|
||||
return expressionInner(type, value, false);
|
||||
}
|
||||
function expressionNoComma(type, value) {
|
||||
return expressionInner(type, value, true);
|
||||
}
|
||||
function parenExpr(type) {
|
||||
if (type != "(") return pass()
|
||||
return cont(pushlex(")"), maybeexpression, expect(")"), poplex)
|
||||
}
|
||||
function expressionInner(type, value, noComma) {
|
||||
if (cx.state.fatArrowAt == cx.stream.start) {
|
||||
var body = noComma ? arrowBodyNoComma : arrowBody;
|
||||
if (type == "(") return cont(pushcontext, pushlex(")"), commasep(funarg, ")"), poplex, expect("=>"), body, popcontext);
|
||||
else if (type == "variable") return pass(pushcontext, pattern, expect("=>"), body, popcontext);
|
||||
}
|
||||
|
||||
var maybeop = noComma ? maybeoperatorNoComma : maybeoperatorComma;
|
||||
if (atomicTypes.hasOwnProperty(type)) return cont(maybeop);
|
||||
if (type == "function") return cont(functiondef, maybeop);
|
||||
if (type == "class" || (isTS && value == "interface")) { cx.marked = "keyword"; return cont(pushlex("form"), classExpression, poplex); }
|
||||
if (type == "keyword c" || type == "async") return cont(noComma ? expressionNoComma : expression);
|
||||
if (type == "(") return cont(pushlex(")"), maybeexpression, expect(")"), poplex, maybeop);
|
||||
if (type == "operator" || type == "spread") return cont(noComma ? expressionNoComma : expression);
|
||||
if (type == "[") return cont(pushlex("]"), arrayLiteral, poplex, maybeop);
|
||||
if (type == "{") return contCommasep(objprop, "}", null, maybeop);
|
||||
if (type == "quasi") return pass(quasi, maybeop);
|
||||
if (type == "new") return cont(maybeTarget(noComma));
|
||||
return cont();
|
||||
}
|
||||
function maybeexpression(type) {
|
||||
if (type.match(/[;\}\)\],]/)) return pass();
|
||||
return pass(expression);
|
||||
}
|
||||
|
||||
function maybeoperatorComma(type, value) {
|
||||
if (type == ",") return cont(maybeexpression);
|
||||
return maybeoperatorNoComma(type, value, false);
|
||||
}
|
||||
function maybeoperatorNoComma(type, value, noComma) {
|
||||
var me = noComma == false ? maybeoperatorComma : maybeoperatorNoComma;
|
||||
var expr = noComma == false ? expression : expressionNoComma;
|
||||
if (type == "=>") return cont(pushcontext, noComma ? arrowBodyNoComma : arrowBody, popcontext);
|
||||
if (type == "operator") {
|
||||
if (/\+\+|--/.test(value) || isTS && value == "!") return cont(me);
|
||||
if (isTS && value == "<" && cx.stream.match(/^([^<>]|<[^<>]*>)*>\s*\(/, false))
|
||||
return cont(pushlex(">"), commasep(typeexpr, ">"), poplex, me);
|
||||
if (value == "?") return cont(expression, expect(":"), expr);
|
||||
return cont(expr);
|
||||
}
|
||||
if (type == "quasi") { return pass(quasi, me); }
|
||||
if (type == ";") return;
|
||||
if (type == "(") return contCommasep(expressionNoComma, ")", "call", me);
|
||||
if (type == ".") return cont(property, me);
|
||||
if (type == "[") return cont(pushlex("]"), maybeexpression, expect("]"), poplex, me);
|
||||
if (isTS && value == "as") { cx.marked = "keyword"; return cont(typeexpr, me) }
|
||||
if (type == "regexp") {
|
||||
cx.state.lastType = cx.marked = "operator"
|
||||
cx.stream.backUp(cx.stream.pos - cx.stream.start - 1)
|
||||
return cont(expr)
|
||||
}
|
||||
}
|
||||
function quasi(type, value) {
|
||||
if (type != "quasi") return pass();
|
||||
if (value.slice(value.length - 2) != "${") return cont(quasi);
|
||||
return cont(maybeexpression, continueQuasi);
|
||||
}
|
||||
function continueQuasi(type) {
|
||||
if (type == "}") {
|
||||
cx.marked = "string-2";
|
||||
cx.state.tokenize = tokenQuasi;
|
||||
return cont(quasi);
|
||||
}
|
||||
}
|
||||
function arrowBody(type) {
|
||||
findFatArrow(cx.stream, cx.state);
|
||||
return pass(type == "{" ? statement : expression);
|
||||
}
|
||||
function arrowBodyNoComma(type) {
|
||||
findFatArrow(cx.stream, cx.state);
|
||||
return pass(type == "{" ? statement : expressionNoComma);
|
||||
}
|
||||
function maybeTarget(noComma) {
|
||||
return function(type) {
|
||||
if (type == ".") return cont(noComma ? targetNoComma : target);
|
||||
else if (type == "variable" && isTS) return cont(maybeTypeArgs, noComma ? maybeoperatorNoComma : maybeoperatorComma)
|
||||
else return pass(noComma ? expressionNoComma : expression);
|
||||
};
|
||||
}
|
||||
function target(_, value) {
|
||||
if (value == "target") { cx.marked = "keyword"; return cont(maybeoperatorComma); }
|
||||
}
|
||||
function targetNoComma(_, value) {
|
||||
if (value == "target") { cx.marked = "keyword"; return cont(maybeoperatorNoComma); }
|
||||
}
|
||||
function maybelabel(type) {
|
||||
if (type == ":") return cont(poplex, statement);
|
||||
return pass(maybeoperatorComma, expect(";"), poplex);
|
||||
}
|
||||
function property(type) {
|
||||
if (type == "variable") {cx.marked = "property"; return cont();}
|
||||
}
|
||||
function objprop(type, value) {
|
||||
if (type == "async") {
|
||||
cx.marked = "property";
|
||||
return cont(objprop);
|
||||
} else if (type == "variable" || cx.style == "keyword") {
|
||||
cx.marked = "property";
|
||||
if (value == "get" || value == "set") return cont(getterSetter);
|
||||
var m // Work around fat-arrow-detection complication for detecting typescript typed arrow params
|
||||
if (isTS && cx.state.fatArrowAt == cx.stream.start && (m = cx.stream.match(/^\s*:\s*/, false)))
|
||||
cx.state.fatArrowAt = cx.stream.pos + m[0].length
|
||||
return cont(afterprop);
|
||||
} else if (type == "number" || type == "string") {
|
||||
cx.marked = jsonldMode ? "property" : (cx.style + " property");
|
||||
return cont(afterprop);
|
||||
} else if (type == "jsonld-keyword") {
|
||||
return cont(afterprop);
|
||||
} else if (isTS && isModifier(value)) {
|
||||
cx.marked = "keyword"
|
||||
return cont(objprop)
|
||||
} else if (type == "[") {
|
||||
return cont(expression, maybetype, expect("]"), afterprop);
|
||||
} else if (type == "spread") {
|
||||
return cont(expressionNoComma, afterprop);
|
||||
} else if (value == "*") {
|
||||
cx.marked = "keyword";
|
||||
return cont(objprop);
|
||||
} else if (type == ":") {
|
||||
return pass(afterprop)
|
||||
}
|
||||
}
|
||||
function getterSetter(type) {
|
||||
if (type != "variable") return pass(afterprop);
|
||||
cx.marked = "property";
|
||||
return cont(functiondef);
|
||||
}
|
||||
function afterprop(type) {
|
||||
if (type == ":") return cont(expressionNoComma);
|
||||
if (type == "(") return pass(functiondef);
|
||||
}
|
||||
function commasep(what, end, sep) {
|
||||
function proceed(type, value) {
|
||||
if (sep ? sep.indexOf(type) > -1 : type == ",") {
|
||||
var lex = cx.state.lexical;
|
||||
if (lex.info == "call") lex.pos = (lex.pos || 0) + 1;
|
||||
return cont(function(type, value) {
|
||||
if (type == end || value == end) return pass()
|
||||
return pass(what)
|
||||
}, proceed);
|
||||
}
|
||||
if (type == end || value == end) return cont();
|
||||
if (sep && sep.indexOf(";") > -1) return pass(what)
|
||||
return cont(expect(end));
|
||||
}
|
||||
return function(type, value) {
|
||||
if (type == end || value == end) return cont();
|
||||
return pass(what, proceed);
|
||||
};
|
||||
}
|
||||
function contCommasep(what, end, info) {
|
||||
for (var i = 3; i < arguments.length; i++)
|
||||
cx.cc.push(arguments[i]);
|
||||
return cont(pushlex(end, info), commasep(what, end), poplex);
|
||||
}
|
||||
function block(type) {
|
||||
if (type == "}") return cont();
|
||||
return pass(statement, block);
|
||||
}
|
||||
function maybetype(type, value) {
|
||||
if (isTS) {
|
||||
if (type == ":") return cont(typeexpr);
|
||||
if (value == "?") return cont(maybetype);
|
||||
}
|
||||
}
|
||||
function maybetypeOrIn(type, value) {
|
||||
if (isTS && (type == ":" || value == "in")) return cont(typeexpr)
|
||||
}
|
||||
function mayberettype(type) {
|
||||
if (isTS && type == ":") {
|
||||
if (cx.stream.match(/^\s*\w+\s+is\b/, false)) return cont(expression, isKW, typeexpr)
|
||||
else return cont(typeexpr)
|
||||
}
|
||||
}
|
||||
function isKW(_, value) {
|
||||
if (value == "is") {
|
||||
cx.marked = "keyword"
|
||||
return cont()
|
||||
}
|
||||
}
|
||||
function typeexpr(type, value) {
|
||||
if (value == "keyof" || value == "typeof" || value == "infer" || value == "readonly") {
|
||||
cx.marked = "keyword"
|
||||
return cont(value == "typeof" ? expressionNoComma : typeexpr)
|
||||
}
|
||||
if (type == "variable" || value == "void") {
|
||||
cx.marked = "type"
|
||||
return cont(afterType)
|
||||
}
|
||||
if (value == "|" || value == "&") return cont(typeexpr)
|
||||
if (type == "string" || type == "number" || type == "atom") return cont(afterType);
|
||||
if (type == "[") return cont(pushlex("]"), commasep(typeexpr, "]", ","), poplex, afterType)
|
||||
if (type == "{") return cont(pushlex("}"), typeprops, poplex, afterType)
|
||||
if (type == "(") return cont(commasep(typearg, ")"), maybeReturnType, afterType)
|
||||
if (type == "<") return cont(commasep(typeexpr, ">"), typeexpr)
|
||||
if (type == "quasi") { return pass(quasiType, afterType); }
|
||||
}
|
||||
function maybeReturnType(type) {
|
||||
if (type == "=>") return cont(typeexpr)
|
||||
}
|
||||
function typeprops(type) {
|
||||
if (type.match(/[\}\)\]]/)) return cont()
|
||||
if (type == "," || type == ";") return cont(typeprops)
|
||||
return pass(typeprop, typeprops)
|
||||
}
|
||||
function typeprop(type, value) {
|
||||
if (type == "variable" || cx.style == "keyword") {
|
||||
cx.marked = "property"
|
||||
return cont(typeprop)
|
||||
} else if (value == "?" || type == "number" || type == "string") {
|
||||
return cont(typeprop)
|
||||
} else if (type == ":") {
|
||||
return cont(typeexpr)
|
||||
} else if (type == "[") {
|
||||
return cont(expect("variable"), maybetypeOrIn, expect("]"), typeprop)
|
||||
} else if (type == "(") {
|
||||
return pass(functiondecl, typeprop)
|
||||
} else if (!type.match(/[;\}\)\],]/)) {
|
||||
return cont()
|
||||
}
|
||||
}
|
||||
function quasiType(type, value) {
|
||||
if (type != "quasi") return pass();
|
||||
if (value.slice(value.length - 2) != "${") return cont(quasiType);
|
||||
return cont(typeexpr, continueQuasiType);
|
||||
}
|
||||
function continueQuasiType(type) {
|
||||
if (type == "}") {
|
||||
cx.marked = "string-2";
|
||||
cx.state.tokenize = tokenQuasi;
|
||||
return cont(quasiType);
|
||||
}
|
||||
}
|
||||
function typearg(type, value) {
|
||||
if (type == "variable" && cx.stream.match(/^\s*[?:]/, false) || value == "?") return cont(typearg)
|
||||
if (type == ":") return cont(typeexpr)
|
||||
if (type == "spread") return cont(typearg)
|
||||
return pass(typeexpr)
|
||||
}
|
||||
function afterType(type, value) {
|
||||
if (value == "<") return cont(pushlex(">"), commasep(typeexpr, ">"), poplex, afterType)
|
||||
if (value == "|" || type == "." || value == "&") return cont(typeexpr)
|
||||
if (type == "[") return cont(typeexpr, expect("]"), afterType)
|
||||
if (value == "extends" || value == "implements") { cx.marked = "keyword"; return cont(typeexpr) }
|
||||
if (value == "?") return cont(typeexpr, expect(":"), typeexpr)
|
||||
}
|
||||
function maybeTypeArgs(_, value) {
|
||||
if (value == "<") return cont(pushlex(">"), commasep(typeexpr, ">"), poplex, afterType)
|
||||
}
|
||||
function typeparam() {
|
||||
return pass(typeexpr, maybeTypeDefault)
|
||||
}
|
||||
function maybeTypeDefault(_, value) {
|
||||
if (value == "=") return cont(typeexpr)
|
||||
}
|
||||
function vardef(_, value) {
|
||||
if (value == "enum") {cx.marked = "keyword"; return cont(enumdef)}
|
||||
return pass(pattern, maybetype, maybeAssign, vardefCont);
|
||||
}
|
||||
function pattern(type, value) {
|
||||
if (isTS && isModifier(value)) { cx.marked = "keyword"; return cont(pattern) }
|
||||
if (type == "variable") { register(value); return cont(); }
|
||||
if (type == "spread") return cont(pattern);
|
||||
if (type == "[") return contCommasep(eltpattern, "]");
|
||||
if (type == "{") return contCommasep(proppattern, "}");
|
||||
}
|
||||
function proppattern(type, value) {
|
||||
if (type == "variable" && !cx.stream.match(/^\s*:/, false)) {
|
||||
register(value);
|
||||
return cont(maybeAssign);
|
||||
}
|
||||
if (type == "variable") cx.marked = "property";
|
||||
if (type == "spread") return cont(pattern);
|
||||
if (type == "}") return pass();
|
||||
if (type == "[") return cont(expression, expect(']'), expect(':'), proppattern);
|
||||
return cont(expect(":"), pattern, maybeAssign);
|
||||
}
|
||||
function eltpattern() {
|
||||
return pass(pattern, maybeAssign)
|
||||
}
|
||||
function maybeAssign(_type, value) {
|
||||
if (value == "=") return cont(expressionNoComma);
|
||||
}
|
||||
function vardefCont(type) {
|
||||
if (type == ",") return cont(vardef);
|
||||
}
|
||||
function maybeelse(type, value) {
|
||||
if (type == "keyword b" && value == "else") return cont(pushlex("form", "else"), statement, poplex);
|
||||
}
|
||||
function forspec(type, value) {
|
||||
if (value == "await") return cont(forspec);
|
||||
if (type == "(") return cont(pushlex(")"), forspec1, poplex);
|
||||
}
|
||||
function forspec1(type) {
|
||||
if (type == "var") return cont(vardef, forspec2);
|
||||
if (type == "variable") return cont(forspec2);
|
||||
return pass(forspec2)
|
||||
}
|
||||
function forspec2(type, value) {
|
||||
if (type == ")") return cont()
|
||||
if (type == ";") return cont(forspec2)
|
||||
if (value == "in" || value == "of") { cx.marked = "keyword"; return cont(expression, forspec2) }
|
||||
return pass(expression, forspec2)
|
||||
}
|
||||
function functiondef(type, value) {
|
||||
if (value == "*") {cx.marked = "keyword"; return cont(functiondef);}
|
||||
if (type == "variable") {register(value); return cont(functiondef);}
|
||||
if (type == "(") return cont(pushcontext, pushlex(")"), commasep(funarg, ")"), poplex, mayberettype, statement, popcontext);
|
||||
if (isTS && value == "<") return cont(pushlex(">"), commasep(typeparam, ">"), poplex, functiondef)
|
||||
}
|
||||
function functiondecl(type, value) {
|
||||
if (value == "*") {cx.marked = "keyword"; return cont(functiondecl);}
|
||||
if (type == "variable") {register(value); return cont(functiondecl);}
|
||||
if (type == "(") return cont(pushcontext, pushlex(")"), commasep(funarg, ")"), poplex, mayberettype, popcontext);
|
||||
if (isTS && value == "<") return cont(pushlex(">"), commasep(typeparam, ">"), poplex, functiondecl)
|
||||
}
|
||||
function typename(type, value) {
|
||||
if (type == "keyword" || type == "variable") {
|
||||
cx.marked = "type"
|
||||
return cont(typename)
|
||||
} else if (value == "<") {
|
||||
return cont(pushlex(">"), commasep(typeparam, ">"), poplex)
|
||||
}
|
||||
}
|
||||
function funarg(type, value) {
|
||||
if (value == "@") cont(expression, funarg)
|
||||
if (type == "spread") return cont(funarg);
|
||||
if (isTS && isModifier(value)) { cx.marked = "keyword"; return cont(funarg); }
|
||||
if (isTS && type == "this") return cont(maybetype, maybeAssign)
|
||||
return pass(pattern, maybetype, maybeAssign);
|
||||
}
|
||||
function classExpression(type, value) {
|
||||
// Class expressions may have an optional name.
|
||||
if (type == "variable") return className(type, value);
|
||||
return classNameAfter(type, value);
|
||||
}
|
||||
function className(type, value) {
|
||||
if (type == "variable") {register(value); return cont(classNameAfter);}
|
||||
}
|
||||
function classNameAfter(type, value) {
|
||||
if (value == "<") return cont(pushlex(">"), commasep(typeparam, ">"), poplex, classNameAfter)
|
||||
if (value == "extends" || value == "implements" || (isTS && type == ",")) {
|
||||
if (value == "implements") cx.marked = "keyword";
|
||||
return cont(isTS ? typeexpr : expression, classNameAfter);
|
||||
}
|
||||
if (type == "{") return cont(pushlex("}"), classBody, poplex);
|
||||
}
|
||||
function classBody(type, value) {
|
||||
if (type == "async" ||
|
||||
(type == "variable" &&
|
||||
(value == "static" || value == "get" || value == "set" || (isTS && isModifier(value))) &&
|
||||
cx.stream.match(/^\s+#?[\w$\xa1-\uffff]/, false))) {
|
||||
cx.marked = "keyword";
|
||||
return cont(classBody);
|
||||
}
|
||||
if (type == "variable" || cx.style == "keyword") {
|
||||
cx.marked = "property";
|
||||
return cont(classfield, classBody);
|
||||
}
|
||||
if (type == "number" || type == "string") return cont(classfield, classBody);
|
||||
if (type == "[")
|
||||
return cont(expression, maybetype, expect("]"), classfield, classBody)
|
||||
if (value == "*") {
|
||||
cx.marked = "keyword";
|
||||
return cont(classBody);
|
||||
}
|
||||
if (isTS && type == "(") return pass(functiondecl, classBody)
|
||||
if (type == ";" || type == ",") return cont(classBody);
|
||||
if (type == "}") return cont();
|
||||
if (value == "@") return cont(expression, classBody)
|
||||
}
|
||||
function classfield(type, value) {
|
||||
if (value == "!") return cont(classfield)
|
||||
if (value == "?") return cont(classfield)
|
||||
if (type == ":") return cont(typeexpr, maybeAssign)
|
||||
if (value == "=") return cont(expressionNoComma)
|
||||
var context = cx.state.lexical.prev, isInterface = context && context.info == "interface"
|
||||
return pass(isInterface ? functiondecl : functiondef)
|
||||
}
|
||||
function afterExport(type, value) {
|
||||
if (value == "*") { cx.marked = "keyword"; return cont(maybeFrom, expect(";")); }
|
||||
if (value == "default") { cx.marked = "keyword"; return cont(expression, expect(";")); }
|
||||
if (type == "{") return cont(commasep(exportField, "}"), maybeFrom, expect(";"));
|
||||
return pass(statement);
|
||||
}
|
||||
function exportField(type, value) {
|
||||
if (value == "as") { cx.marked = "keyword"; return cont(expect("variable")); }
|
||||
if (type == "variable") return pass(expressionNoComma, exportField);
|
||||
}
|
||||
function afterImport(type) {
|
||||
if (type == "string") return cont();
|
||||
if (type == "(") return pass(expression);
|
||||
if (type == ".") return pass(maybeoperatorComma);
|
||||
return pass(importSpec, maybeMoreImports, maybeFrom);
|
||||
}
|
||||
function importSpec(type, value) {
|
||||
if (type == "{") return contCommasep(importSpec, "}");
|
||||
if (type == "variable") register(value);
|
||||
if (value == "*") cx.marked = "keyword";
|
||||
return cont(maybeAs);
|
||||
}
|
||||
function maybeMoreImports(type) {
|
||||
if (type == ",") return cont(importSpec, maybeMoreImports)
|
||||
}
|
||||
function maybeAs(_type, value) {
|
||||
if (value == "as") { cx.marked = "keyword"; return cont(importSpec); }
|
||||
}
|
||||
function maybeFrom(_type, value) {
|
||||
if (value == "from") { cx.marked = "keyword"; return cont(expression); }
|
||||
}
|
||||
function arrayLiteral(type) {
|
||||
if (type == "]") return cont();
|
||||
return pass(commasep(expressionNoComma, "]"));
|
||||
}
|
||||
function enumdef() {
|
||||
return pass(pushlex("form"), pattern, expect("{"), pushlex("}"), commasep(enummember, "}"), poplex, poplex)
|
||||
}
|
||||
function enummember() {
|
||||
return pass(pattern, maybeAssign);
|
||||
}
|
||||
|
||||
function isContinuedStatement(state, textAfter) {
|
||||
return state.lastType == "operator" || state.lastType == "," ||
|
||||
isOperatorChar.test(textAfter.charAt(0)) ||
|
||||
/[,.]/.test(textAfter.charAt(0));
|
||||
}
|
||||
|
||||
function expressionAllowed(stream, state, backUp) {
|
||||
return state.tokenize == tokenBase &&
|
||||
/^(?:operator|sof|keyword [bcd]|case|new|export|default|spread|[\[{}\(,;:]|=>)$/.test(state.lastType) ||
|
||||
(state.lastType == "quasi" && /\{\s*$/.test(stream.string.slice(0, stream.pos - (backUp || 0))))
|
||||
}
|
||||
|
||||
// Interface
|
||||
|
||||
return {
|
||||
startState: function(basecolumn) {
|
||||
var state = {
|
||||
tokenize: tokenBase,
|
||||
lastType: "sof",
|
||||
cc: [],
|
||||
lexical: new JSLexical((basecolumn || 0) - indentUnit, 0, "block", false),
|
||||
localVars: parserConfig.localVars,
|
||||
context: parserConfig.localVars && new Context(null, null, false),
|
||||
indented: basecolumn || 0
|
||||
};
|
||||
if (parserConfig.globalVars && typeof parserConfig.globalVars == "object")
|
||||
state.globalVars = parserConfig.globalVars;
|
||||
return state;
|
||||
},
|
||||
|
||||
token: function(stream, state) {
|
||||
if (stream.sol()) {
|
||||
if (!state.lexical.hasOwnProperty("align"))
|
||||
state.lexical.align = false;
|
||||
state.indented = stream.indentation();
|
||||
findFatArrow(stream, state);
|
||||
}
|
||||
if (state.tokenize != tokenComment && stream.eatSpace()) return null;
|
||||
var style = state.tokenize(stream, state);
|
||||
if (type == "comment") return style;
|
||||
state.lastType = type == "operator" && (content == "++" || content == "--") ? "incdec" : type;
|
||||
return parseJS(state, style, type, content, stream);
|
||||
},
|
||||
|
||||
indent: function(state, textAfter) {
|
||||
if (state.tokenize == tokenComment || state.tokenize == tokenQuasi) return CodeMirror.Pass;
|
||||
if (state.tokenize != tokenBase) return 0;
|
||||
var firstChar = textAfter && textAfter.charAt(0), lexical = state.lexical, top
|
||||
// Kludge to prevent 'maybelse' from blocking lexical scope pops
|
||||
if (!/^\s*else\b/.test(textAfter)) for (var i = state.cc.length - 1; i >= 0; --i) {
|
||||
var c = state.cc[i];
|
||||
if (c == poplex) lexical = lexical.prev;
|
||||
else if (c != maybeelse && c != popcontext) break;
|
||||
}
|
||||
while ((lexical.type == "stat" || lexical.type == "form") &&
|
||||
(firstChar == "}" || ((top = state.cc[state.cc.length - 1]) &&
|
||||
(top == maybeoperatorComma || top == maybeoperatorNoComma) &&
|
||||
!/^[,\.=+\-*:?[\(]/.test(textAfter))))
|
||||
lexical = lexical.prev;
|
||||
if (statementIndent && lexical.type == ")" && lexical.prev.type == "stat")
|
||||
lexical = lexical.prev;
|
||||
var type = lexical.type, closing = firstChar == type;
|
||||
|
||||
if (type == "vardef") return lexical.indented + (state.lastType == "operator" || state.lastType == "," ? lexical.info.length + 1 : 0);
|
||||
else if (type == "form" && firstChar == "{") return lexical.indented;
|
||||
else if (type == "form") return lexical.indented + indentUnit;
|
||||
else if (type == "stat")
|
||||
return lexical.indented + (isContinuedStatement(state, textAfter) ? statementIndent || indentUnit : 0);
|
||||
else if (lexical.info == "switch" && !closing && parserConfig.doubleIndentSwitch != false)
|
||||
return lexical.indented + (/^(?:case|default)\b/.test(textAfter) ? indentUnit : 2 * indentUnit);
|
||||
else if (lexical.align) return lexical.column + (closing ? 0 : 1);
|
||||
else return lexical.indented + (closing ? 0 : indentUnit);
|
||||
},
|
||||
|
||||
electricInput: /^\s*(?:case .*?:|default:|\{|\})$/,
|
||||
blockCommentStart: jsonMode ? null : "/*",
|
||||
blockCommentEnd: jsonMode ? null : "*/",
|
||||
blockCommentContinue: jsonMode ? null : " * ",
|
||||
lineComment: jsonMode ? null : "//",
|
||||
fold: "brace",
|
||||
closeBrackets: "()[]{}''\"\"``",
|
||||
|
||||
helperType: jsonMode ? "json" : "javascript",
|
||||
jsonldMode: jsonldMode,
|
||||
jsonMode: jsonMode,
|
||||
|
||||
expressionAllowed: expressionAllowed,
|
||||
|
||||
skipExpression: function(state) {
|
||||
parseJS(state, "atom", "atom", "true", new CodeMirror.StringStream("", 2, null))
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
CodeMirror.registerHelper("wordChars", "javascript", /[\w$]/);
|
||||
|
||||
CodeMirror.defineMIME("text/javascript", "javascript");
|
||||
CodeMirror.defineMIME("text/ecmascript", "javascript");
|
||||
CodeMirror.defineMIME("application/javascript", "javascript");
|
||||
CodeMirror.defineMIME("application/x-javascript", "javascript");
|
||||
CodeMirror.defineMIME("application/ecmascript", "javascript");
|
||||
CodeMirror.defineMIME("application/json", { name: "javascript", json: true });
|
||||
CodeMirror.defineMIME("application/x-json", { name: "javascript", json: true });
|
||||
CodeMirror.defineMIME("application/manifest+json", { name: "javascript", json: true })
|
||||
CodeMirror.defineMIME("application/ld+json", { name: "javascript", jsonld: true });
|
||||
CodeMirror.defineMIME("text/typescript", { name: "javascript", typescript: true });
|
||||
CodeMirror.defineMIME("application/typescript", { name: "javascript", typescript: true });
|
||||
|
||||
});
|
||||
32076
web/assets/codemirror/jshint.js
Normal file
32076
web/assets/codemirror/jshint.js
Normal file
File diff suppressed because one or more lines are too long
1
web/assets/codemirror/jsonlint.js
Normal file
1
web/assets/codemirror/jsonlint.js
Normal file
File diff suppressed because one or more lines are too long
65
web/assets/codemirror/lint/javascript-lint.js
Normal file
65
web/assets/codemirror/lint/javascript-lint.js
Normal file
@@ -0,0 +1,65 @@
|
||||
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
||||
// Distributed under an MIT license: https://codemirror.net/5/LICENSE
|
||||
|
||||
// Depends on jshint.js from https://github.com/jshint/jshint
|
||||
|
||||
(function(mod) {
|
||||
if (typeof exports == "object" && typeof module == "object") // CommonJS
|
||||
mod(require("../../lib/codemirror"));
|
||||
else if (typeof define == "function" && define.amd) // AMD
|
||||
define(["../../lib/codemirror"], mod);
|
||||
else // Plain browser env
|
||||
mod(CodeMirror);
|
||||
})(function(CodeMirror) {
|
||||
"use strict";
|
||||
// declare global: JSHINT
|
||||
|
||||
function validator(text, options) {
|
||||
if (!window.JSHINT) {
|
||||
if (window.console) {
|
||||
window.console.error("Error: window.JSHINT not defined, CodeMirror JavaScript linting cannot run.");
|
||||
}
|
||||
return [];
|
||||
}
|
||||
if (!options.indent) // JSHint error.character actually is a column index, this fixes underlining on lines using tabs for indentation
|
||||
options.indent = 1; // JSHint default value is 4
|
||||
JSHINT(text, options, options.globals);
|
||||
var errors = JSHINT.data().errors, result = [];
|
||||
if (errors) parseErrors(errors, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
CodeMirror.registerHelper("lint", "javascript", validator);
|
||||
|
||||
function parseErrors(errors, output) {
|
||||
for ( var i = 0; i < errors.length; i++) {
|
||||
var error = errors[i];
|
||||
if (error) {
|
||||
if (error.line <= 0) {
|
||||
if (window.console) {
|
||||
window.console.warn("Cannot display JSHint error (invalid line " + error.line + ")", error);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
var start = error.character - 1, end = start + 1;
|
||||
if (error.evidence) {
|
||||
var index = error.evidence.substring(start).search(/.\b/);
|
||||
if (index > -1) {
|
||||
end += index;
|
||||
}
|
||||
}
|
||||
|
||||
// Convert to format expected by validation service
|
||||
var hint = {
|
||||
message: error.reason,
|
||||
severity: error.code ? (error.code.startsWith('W') ? "warning" : "error") : "error",
|
||||
from: CodeMirror.Pos(error.line - 1, start),
|
||||
to: CodeMirror.Pos(error.line - 1, end)
|
||||
};
|
||||
|
||||
output.push(hint);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
79
web/assets/codemirror/lint/lint.css
Normal file
79
web/assets/codemirror/lint/lint.css
Normal file
@@ -0,0 +1,79 @@
|
||||
/* The lint marker gutter */
|
||||
.CodeMirror-lint-markers {
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
.CodeMirror-lint-tooltip {
|
||||
background-color: #ffd;
|
||||
border: 1px solid black;
|
||||
border-radius: 4px 4px 4px 4px;
|
||||
color: black;
|
||||
font-family: monospace;
|
||||
font-size: 10pt;
|
||||
overflow: hidden;
|
||||
padding: 2px 5px;
|
||||
position: fixed;
|
||||
white-space: pre;
|
||||
white-space: pre-wrap;
|
||||
z-index: 100;
|
||||
max-width: 600px;
|
||||
opacity: 0;
|
||||
transition: opacity .4s;
|
||||
-moz-transition: opacity .4s;
|
||||
-webkit-transition: opacity .4s;
|
||||
-o-transition: opacity .4s;
|
||||
-ms-transition: opacity .4s;
|
||||
}
|
||||
|
||||
.CodeMirror-lint-mark {
|
||||
background-position: left bottom;
|
||||
background-repeat: repeat-x;
|
||||
}
|
||||
|
||||
.CodeMirror-lint-mark-warning {
|
||||
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJFhQXEbhTg7YAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAMklEQVQI12NkgIIvJ3QXMjAwdDN+OaEbysDA4MPAwNDNwMCwiOHLCd1zX07o6kBVGQEAKBANtobskNMAAAAASUVORK5CYII=");
|
||||
}
|
||||
|
||||
.CodeMirror-lint-mark-error {
|
||||
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJDw4cOCW1/KIAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAHElEQVQI12NggIL/DAz/GdA5/xkY/qPKMDAwAADLZwf5rvm+LQAAAABJRU5ErkJggg==");
|
||||
}
|
||||
|
||||
.CodeMirror-lint-marker {
|
||||
background-position: center center;
|
||||
background-repeat: no-repeat;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
vertical-align: middle;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.CodeMirror-lint-message {
|
||||
padding-left: 18px;
|
||||
background-position: top left;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.CodeMirror-lint-marker-warning, .CodeMirror-lint-message-warning {
|
||||
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAANlBMVEX/uwDvrwD/uwD/uwD/uwD/uwD/uwD/uwD/uwD6twD/uwAAAADurwD2tQD7uAD+ugAAAAD/uwDhmeTRAAAADHRSTlMJ8mN1EYcbmiixgACm7WbuAAAAVklEQVR42n3PUQqAIBBFUU1LLc3u/jdbOJoW1P08DA9Gba8+YWJ6gNJoNYIBzAA2chBth5kLmG9YUoG0NHAUwFXwO9LuBQL1giCQb8gC9Oro2vp5rncCIY8L8uEx5ZkAAAAASUVORK5CYII=");
|
||||
}
|
||||
|
||||
.CodeMirror-lint-marker-error, .CodeMirror-lint-message-error {
|
||||
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAHlBMVEW7AAC7AACxAAC7AAC7AAAAAAC4AAC5AAD///+7AAAUdclpAAAABnRSTlMXnORSiwCK0ZKSAAAATUlEQVR42mWPOQ7AQAgDuQLx/z8csYRmPRIFIwRGnosRrpamvkKi0FTIiMASR3hhKW+hAN6/tIWhu9PDWiTGNEkTtIOucA5Oyr9ckPgAWm0GPBog6v4AAAAASUVORK5CYII=");
|
||||
}
|
||||
|
||||
.CodeMirror-lint-marker-multiple {
|
||||
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAHCAMAAADzjKfhAAAACVBMVEUAAAAAAAC/v7914kyHAAAAAXRSTlMAQObYZgAAACNJREFUeNo1ioEJAAAIwmz/H90iFFSGJgFMe3gaLZ0od+9/AQZ0ADosbYraAAAAAElFTkSuQmCC");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right bottom;
|
||||
width: 100%; height: 100%;
|
||||
}
|
||||
|
||||
.CodeMirror-lint-line-error {
|
||||
background-color: rgba(183, 76, 81, 0.08);
|
||||
}
|
||||
|
||||
.CodeMirror-lint-line-warning {
|
||||
background-color: rgba(255, 211, 0, 0.1);
|
||||
}
|
||||
288
web/assets/codemirror/lint/lint.js
Normal file
288
web/assets/codemirror/lint/lint.js
Normal file
@@ -0,0 +1,288 @@
|
||||
// CodeMirror, copyright (c) by Marijn Haverbeke and others
|
||||
// Distributed under an MIT license: https://codemirror.net/5/LICENSE
|
||||
|
||||
(function(mod) {
|
||||
if (typeof exports == "object" && typeof module == "object") // CommonJS
|
||||
mod(require("../../lib/codemirror"));
|
||||
else if (typeof define == "function" && define.amd) // AMD
|
||||
define(["../../lib/codemirror"], mod);
|
||||
else // Plain browser env
|
||||
mod(CodeMirror);
|
||||
})(function(CodeMirror) {
|
||||
"use strict";
|
||||
var GUTTER_ID = "CodeMirror-lint-markers";
|
||||
var LINT_LINE_ID = "CodeMirror-lint-line-";
|
||||
|
||||
function showTooltip(cm, e, content) {
|
||||
var tt = document.createElement("div");
|
||||
tt.className = "CodeMirror-lint-tooltip cm-s-" + cm.options.theme;
|
||||
tt.appendChild(content.cloneNode(true));
|
||||
if (cm.state.lint.options.selfContain)
|
||||
cm.getWrapperElement().appendChild(tt);
|
||||
else
|
||||
document.body.appendChild(tt);
|
||||
|
||||
function position(e) {
|
||||
if (!tt.parentNode) return CodeMirror.off(document, "mousemove", position);
|
||||
var top = Math.max(0, e.clientY - tt.offsetHeight - 5);
|
||||
var left = Math.max(0, Math.min(e.clientX + 5, tt.ownerDocument.defaultView.innerWidth - tt.offsetWidth));
|
||||
tt.style.top = top + "px"
|
||||
tt.style.left = left + "px";
|
||||
}
|
||||
CodeMirror.on(document, "mousemove", position);
|
||||
position(e);
|
||||
if (tt.style.opacity != null) tt.style.opacity = 1;
|
||||
return tt;
|
||||
}
|
||||
function rm(elt) {
|
||||
if (elt.parentNode) elt.parentNode.removeChild(elt);
|
||||
}
|
||||
function hideTooltip(tt) {
|
||||
if (!tt.parentNode) return;
|
||||
if (tt.style.opacity == null) rm(tt);
|
||||
tt.style.opacity = 0;
|
||||
setTimeout(function() { rm(tt); }, 600);
|
||||
}
|
||||
|
||||
function showTooltipFor(cm, e, content, node) {
|
||||
var tooltip = showTooltip(cm, e, content);
|
||||
function hide() {
|
||||
CodeMirror.off(node, "mouseout", hide);
|
||||
if (tooltip) { hideTooltip(tooltip); tooltip = null; }
|
||||
}
|
||||
var poll = setInterval(function() {
|
||||
if (tooltip) for (var n = node;; n = n.parentNode) {
|
||||
if (n && n.nodeType == 11) n = n.host;
|
||||
if (n == document.body) return;
|
||||
if (!n) { hide(); break; }
|
||||
}
|
||||
if (!tooltip) return clearInterval(poll);
|
||||
}, 400);
|
||||
CodeMirror.on(node, "mouseout", hide);
|
||||
}
|
||||
|
||||
function LintState(cm, conf, hasGutter) {
|
||||
this.marked = [];
|
||||
if (conf instanceof Function) conf = {getAnnotations: conf};
|
||||
if (!conf || conf === true) conf = {};
|
||||
this.options = {};
|
||||
this.linterOptions = conf.options || {};
|
||||
for (var prop in defaults) this.options[prop] = defaults[prop];
|
||||
for (var prop in conf) {
|
||||
if (defaults.hasOwnProperty(prop)) {
|
||||
if (conf[prop] != null) this.options[prop] = conf[prop];
|
||||
} else if (!conf.options) {
|
||||
this.linterOptions[prop] = conf[prop];
|
||||
}
|
||||
}
|
||||
this.timeout = null;
|
||||
this.hasGutter = hasGutter;
|
||||
this.onMouseOver = function(e) { onMouseOver(cm, e); };
|
||||
this.waitingFor = 0
|
||||
}
|
||||
|
||||
var defaults = {
|
||||
highlightLines: false,
|
||||
tooltips: true,
|
||||
delay: 500,
|
||||
lintOnChange: true,
|
||||
getAnnotations: null,
|
||||
async: false,
|
||||
selfContain: null,
|
||||
formatAnnotation: null,
|
||||
onUpdateLinting: null
|
||||
}
|
||||
|
||||
function clearMarks(cm) {
|
||||
var state = cm.state.lint;
|
||||
if (state.hasGutter) cm.clearGutter(GUTTER_ID);
|
||||
if (state.options.highlightLines) clearErrorLines(cm);
|
||||
for (var i = 0; i < state.marked.length; ++i)
|
||||
state.marked[i].clear();
|
||||
state.marked.length = 0;
|
||||
}
|
||||
|
||||
function clearErrorLines(cm) {
|
||||
cm.eachLine(function(line) {
|
||||
var has = line.wrapClass && /\bCodeMirror-lint-line-\w+\b/.exec(line.wrapClass);
|
||||
if (has) cm.removeLineClass(line, "wrap", has[0]);
|
||||
})
|
||||
}
|
||||
|
||||
function makeMarker(cm, labels, severity, multiple, tooltips) {
|
||||
var marker = document.createElement("div"), inner = marker;
|
||||
marker.className = "CodeMirror-lint-marker CodeMirror-lint-marker-" + severity;
|
||||
if (multiple) {
|
||||
inner = marker.appendChild(document.createElement("div"));
|
||||
inner.className = "CodeMirror-lint-marker CodeMirror-lint-marker-multiple";
|
||||
}
|
||||
|
||||
if (tooltips != false) CodeMirror.on(inner, "mouseover", function(e) {
|
||||
showTooltipFor(cm, e, labels, inner);
|
||||
});
|
||||
|
||||
return marker;
|
||||
}
|
||||
|
||||
function getMaxSeverity(a, b) {
|
||||
if (a == "error") return a;
|
||||
else return b;
|
||||
}
|
||||
|
||||
function groupByLine(annotations) {
|
||||
var lines = [];
|
||||
for (var i = 0; i < annotations.length; ++i) {
|
||||
var ann = annotations[i], line = ann.from.line;
|
||||
(lines[line] || (lines[line] = [])).push(ann);
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
function annotationTooltip(ann) {
|
||||
var severity = ann.severity;
|
||||
if (!severity) severity = "error";
|
||||
var tip = document.createElement("div");
|
||||
tip.className = "CodeMirror-lint-message CodeMirror-lint-message-" + severity;
|
||||
if (typeof ann.messageHTML != 'undefined') {
|
||||
tip.innerHTML = ann.messageHTML;
|
||||
} else {
|
||||
tip.appendChild(document.createTextNode(ann.message));
|
||||
}
|
||||
return tip;
|
||||
}
|
||||
|
||||
function lintAsync(cm, getAnnotations) {
|
||||
var state = cm.state.lint
|
||||
var id = ++state.waitingFor
|
||||
function abort() {
|
||||
id = -1
|
||||
cm.off("change", abort)
|
||||
}
|
||||
cm.on("change", abort)
|
||||
getAnnotations(cm.getValue(), function(annotations, arg2) {
|
||||
cm.off("change", abort)
|
||||
if (state.waitingFor != id) return
|
||||
if (arg2 && annotations instanceof CodeMirror) annotations = arg2
|
||||
cm.operation(function() {updateLinting(cm, annotations)})
|
||||
}, state.linterOptions, cm);
|
||||
}
|
||||
|
||||
function startLinting(cm) {
|
||||
var state = cm.state.lint;
|
||||
if (!state) return;
|
||||
var options = state.options;
|
||||
/*
|
||||
* Passing rules in `options` property prevents JSHint (and other linters) from complaining
|
||||
* about unrecognized rules like `onUpdateLinting`, `delay`, `lintOnChange`, etc.
|
||||
*/
|
||||
var getAnnotations = options.getAnnotations || cm.getHelper(CodeMirror.Pos(0, 0), "lint");
|
||||
if (!getAnnotations) return;
|
||||
if (options.async || getAnnotations.async) {
|
||||
lintAsync(cm, getAnnotations)
|
||||
} else {
|
||||
var annotations = getAnnotations(cm.getValue(), state.linterOptions, cm);
|
||||
if (!annotations) return;
|
||||
if (annotations.then) annotations.then(function(issues) {
|
||||
cm.operation(function() {updateLinting(cm, issues)})
|
||||
});
|
||||
else cm.operation(function() {updateLinting(cm, annotations)})
|
||||
}
|
||||
}
|
||||
|
||||
function updateLinting(cm, annotationsNotSorted) {
|
||||
var state = cm.state.lint;
|
||||
if (!state) return;
|
||||
var options = state.options;
|
||||
clearMarks(cm);
|
||||
|
||||
var annotations = groupByLine(annotationsNotSorted);
|
||||
|
||||
for (var line = 0; line < annotations.length; ++line) {
|
||||
var anns = annotations[line];
|
||||
if (!anns) continue;
|
||||
|
||||
var maxSeverity = null;
|
||||
var tipLabel = state.hasGutter && document.createDocumentFragment();
|
||||
|
||||
for (var i = 0; i < anns.length; ++i) {
|
||||
var ann = anns[i];
|
||||
var severity = ann.severity;
|
||||
if (!severity) severity = "error";
|
||||
maxSeverity = getMaxSeverity(maxSeverity, severity);
|
||||
|
||||
if (options.formatAnnotation) ann = options.formatAnnotation(ann);
|
||||
if (state.hasGutter) tipLabel.appendChild(annotationTooltip(ann));
|
||||
|
||||
if (ann.to) state.marked.push(cm.markText(ann.from, ann.to, {
|
||||
className: "CodeMirror-lint-mark CodeMirror-lint-mark-" + severity,
|
||||
__annotation: ann
|
||||
}));
|
||||
}
|
||||
if (state.hasGutter)
|
||||
cm.setGutterMarker(line, GUTTER_ID, makeMarker(cm, tipLabel, maxSeverity, anns.length > 1,
|
||||
options.tooltips));
|
||||
|
||||
if (options.highlightLines)
|
||||
cm.addLineClass(line, "wrap", LINT_LINE_ID + maxSeverity);
|
||||
}
|
||||
if (options.onUpdateLinting) options.onUpdateLinting(annotationsNotSorted, annotations, cm);
|
||||
}
|
||||
|
||||
function onChange(cm) {
|
||||
var state = cm.state.lint;
|
||||
if (!state) return;
|
||||
clearTimeout(state.timeout);
|
||||
state.timeout = setTimeout(function(){startLinting(cm);}, state.options.delay);
|
||||
}
|
||||
|
||||
function popupTooltips(cm, annotations, e) {
|
||||
var target = e.target || e.srcElement;
|
||||
var tooltip = document.createDocumentFragment();
|
||||
for (var i = 0; i < annotations.length; i++) {
|
||||
var ann = annotations[i];
|
||||
tooltip.appendChild(annotationTooltip(ann));
|
||||
}
|
||||
showTooltipFor(cm, e, tooltip, target);
|
||||
}
|
||||
|
||||
function onMouseOver(cm, e) {
|
||||
var target = e.target || e.srcElement;
|
||||
if (!/\bCodeMirror-lint-mark-/.test(target.className)) return;
|
||||
var box = target.getBoundingClientRect(), x = (box.left + box.right) / 2, y = (box.top + box.bottom) / 2;
|
||||
var spans = cm.findMarksAt(cm.coordsChar({left: x, top: y}, "client"));
|
||||
|
||||
var annotations = [];
|
||||
for (var i = 0; i < spans.length; ++i) {
|
||||
var ann = spans[i].__annotation;
|
||||
if (ann) annotations.push(ann);
|
||||
}
|
||||
if (annotations.length) popupTooltips(cm, annotations, e);
|
||||
}
|
||||
|
||||
CodeMirror.defineOption("lint", false, function(cm, val, old) {
|
||||
if (old && old != CodeMirror.Init) {
|
||||
clearMarks(cm);
|
||||
if (cm.state.lint.options.lintOnChange !== false)
|
||||
cm.off("change", onChange);
|
||||
CodeMirror.off(cm.getWrapperElement(), "mouseover", cm.state.lint.onMouseOver);
|
||||
clearTimeout(cm.state.lint.timeout);
|
||||
delete cm.state.lint;
|
||||
}
|
||||
|
||||
if (val) {
|
||||
var gutters = cm.getOption("gutters"), hasLintGutter = false;
|
||||
for (var i = 0; i < gutters.length; ++i) if (gutters[i] == GUTTER_ID) hasLintGutter = true;
|
||||
var state = cm.state.lint = new LintState(cm, val, hasLintGutter);
|
||||
if (state.options.lintOnChange)
|
||||
cm.on("change", onChange);
|
||||
if (state.options.tooltips != false && state.options.tooltips != "gutter")
|
||||
CodeMirror.on(cm.getWrapperElement(), "mouseover", state.onMouseOver);
|
||||
|
||||
startLinting(cm);
|
||||
}
|
||||
});
|
||||
|
||||
CodeMirror.defineExtension("performLint", function() {
|
||||
startLinting(this);
|
||||
});
|
||||
});
|
||||
86
web/assets/codemirror/xq.css
Normal file
86
web/assets/codemirror/xq.css
Normal file
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
Copyright (C) 2011 by MarkLogic Corporation
|
||||
Author: Mike Brevoort <mike@brevoort.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
*/
|
||||
.cm-s-xq.CodeMirror { border-radius: 1.5rem; border: 1px solid #d9d9d9; height: auto; }
|
||||
.cm-s-xq.CodeMirror:hover { background-color: #edf4fa; border-color: #2f67c2; transition: all .3s; }
|
||||
.cm-s-xq .CodeMirror-gutters { border-right: 1px solid #ddd; background-color: rgb(221 221 221 / 20%); white-space: nowrap; }
|
||||
.cm-s-xq span.cm-keyword { line-height: 1em; font-weight: bold; color: #5A5CAD; }
|
||||
.cm-s-xq span.cm-atom { color: #7A316F; font-weight:bold; }
|
||||
.cm-s-xq span.cm-number { color: #389E0D; }
|
||||
.cm-s-xq span.cm-def { text-decoration:underline; }
|
||||
.cm-s-xq span.cm-variable { color: black; }
|
||||
.cm-s-xq span.cm-variable-2 { color:black; }
|
||||
.cm-s-xq span.cm-variable-3, .cm-s-xq span.cm-type { color: black; }
|
||||
.cm-s-xq span.cm-property { color: #0e49b5; }
|
||||
.cm-s-xq span.cm-operator {}
|
||||
.cm-s-xq span.cm-comment { color: #bbbbbb; font-style: italic; }
|
||||
.cm-s-xq span.cm-string {}
|
||||
.cm-s-xq span.cm-meta { color: yellow; }
|
||||
.cm-s-xq span.cm-qualifier { color: grey; }
|
||||
.cm-s-xq span.cm-builtin { color: #7EA656; }
|
||||
.cm-s-xq span.cm-bracket { color: #cc7; }
|
||||
.cm-s-xq span.cm-tag { color: #3F7F7F; }
|
||||
.cm-s-xq span.cm-attribute { color: #7F007F; }
|
||||
.cm-s-xq span.cm-error { color: #e04141; }
|
||||
|
||||
.cm-s-xq .CodeMirror-activeline-background { background: #e8f2ff; }
|
||||
.cm-s-xq .CodeMirror-matchingbracket { outline:1px solid grey;color:black !important;background:yellow; }
|
||||
|
||||
.dark .cm-s-xq.CodeMirror { background-color: #222D42; border-color: #2c3950; color: rgb(255 255 255 / 65%); }
|
||||
.dark .cm-s-xq.CodeMirror:hover { background-color: #0e2040; border-color: #0e49b5; transition: all .3s; }
|
||||
.dark .cm-s-xq div.CodeMirror-selected { background: rgba(0, 0, 0, 0.5); }
|
||||
.dark .cm-s-xq .CodeMirror-line::selection, .dark .cm-s-xq .CodeMirror-line > span::selection, .dark .cm-s-xq .CodeMirror-line > span > span::selection { background: rgba(39, 0, 122, 0.99); }
|
||||
.dark .cm-s-xq .CodeMirror-line::-moz-selection, .dark .cm-s-xq .CodeMirror-line > span::-moz-selection, .dark .cm-s-xq .CodeMirror-line > span > span::-moz-selection { background: rgba(39, 0, 122, 0.99); }
|
||||
.dark .cm-s-xq .CodeMirror-gutters { background: rgb(0 0 0 / 30%); border-right: 1px solid #2c3950; }
|
||||
.dark .cm-s-xq .CodeMirror-guttermarker { color: #FFBD40; }
|
||||
.dark .cm-s-xq .CodeMirror-guttermarker-subtle { color: rgb(255 255 255 / 70%); }
|
||||
.dark .cm-s-xq .CodeMirror-linenumber { color: rgb(255 255 255 / 50%); }
|
||||
.dark .cm-s-xq .CodeMirror-cursor { border-left: 1px solid white; }
|
||||
|
||||
.dark .cm-s-xq span.cm-keyword { color: #FFBD40; }
|
||||
.dark .cm-s-xq span.cm-atom { color: #c099ff; }
|
||||
.dark .cm-s-xq span.cm-number { color: #9ccfd8; }
|
||||
.dark .cm-s-xq span.cm-def { color: #FFF; text-decoration:underline; }
|
||||
.dark .cm-s-xq span.cm-variable { color: #FFF; }
|
||||
.dark .cm-s-xq span.cm-variable-2 { color: #EEE; }
|
||||
.dark .cm-s-xq span.cm-variable-3, .dark .cm-s-xq span.cm-type { color: #DDD; }
|
||||
.dark .cm-s-xq span.cm-property { color: #f6c177 }
|
||||
.dark .cm-s-xq span.cm-operator {}
|
||||
.dark .cm-s-xq span.cm-comment { color: gray; }
|
||||
.dark .cm-s-xq span.cm-string {}
|
||||
.dark .cm-s-xq span.cm-meta { color: yellow; }
|
||||
.dark .cm-s-xq span.cm-qualifier { color: #FFF700; }
|
||||
.dark .cm-s-xq span.cm-builtin { color: #30a; }
|
||||
.dark .cm-s-xq span.cm-bracket { color: #cc7; }
|
||||
.dark .cm-s-xq span.cm-tag { color: #FFBD40; }
|
||||
.dark .cm-s-xq span.cm-attribute { color: #FFF700; }
|
||||
.dark .cm-s-xq span.cm-error { color: #e04141; }
|
||||
|
||||
.dark .cm-s-xq .CodeMirror-activeline-background { background: #27282E; }
|
||||
.dark .cm-s-xq .CodeMirror-matchingbracket { outline:1px solid grey; color:white !important; }
|
||||
|
||||
.Line-Hover{transition: all .2s;}
|
||||
.Line-Hover:hover{ background-color: rgb(4 48 143 / 5%) !important; }
|
||||
.dark .Line-Hover:hover{ background-color: rgb(0 0 0 / 20%) !important; }
|
||||
|
||||
.CodeMirror-foldmarker { color: #fc8800; text-shadow: #ffd8aa 1px 1px 2px, #ffd8aa -1px -1px 2px, #ffd8aa 1px -1px 2px, #ffd8aa -1px 1px 2px; font-family: arial; line-height: .3; cursor: pointer; }
|
||||
.dark .CodeMirror-foldmarker { color: #ffffff; text-shadow: #bbb 1px 1px 2px, #bbb -1px -1px 2px, #bbb 1px -1px 2px, #bbb -1px 1px 2px; font-family: arial; line-height: .3; cursor: pointer; }
|
||||
@@ -8,7 +8,7 @@ body {
|
||||
}
|
||||
|
||||
body {
|
||||
color: rgba(0,0,0,.65);
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
font-size: 14px;
|
||||
font-variant: tabular-nums;
|
||||
line-height: 1.5;
|
||||
@@ -21,12 +21,12 @@ html {
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-ms-text-size-adjust: 100%;
|
||||
-ms-overflow-style: scrollbar;
|
||||
-webkit-tap-highlight-color: rgba(0,0,0,0);
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
::selection {
|
||||
color: #0e49b5;
|
||||
background-color: #0e49b530;
|
||||
background-color: #d2ddf1;
|
||||
}
|
||||
|
||||
#app {
|
||||
@@ -41,7 +41,8 @@ html {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.ant-layout, .ant-layout * {
|
||||
.ant-layout,
|
||||
.ant-layout * {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@@ -52,16 +53,17 @@ html {
|
||||
style attribute {
|
||||
text-align: center;
|
||||
}
|
||||
.ant-table-tbody>tr>td, .ant-table-thead>tr>th {
|
||||
padding: 16px;
|
||||
.ant-table-tbody > tr > td,
|
||||
.ant-table-thead > tr > th {
|
||||
padding: 12px 8px;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
.ant-table-thead>tr>th {
|
||||
color: rgba(0,0,0,.85);
|
||||
.ant-table-thead > tr > th {
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
font-weight: 500;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
transition: background .3s ease;
|
||||
transition: background 0.3s ease;
|
||||
}
|
||||
.ant-table-row-cell-break-word {
|
||||
word-wrap: break-word;
|
||||
@@ -79,7 +81,7 @@ style attribute {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
color: rgba(0,0,0,.65);
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
font-size: 14px;
|
||||
font-variant: tabular-nums;
|
||||
line-height: 1.5;
|
||||
@@ -88,6 +90,9 @@ style attribute {
|
||||
position: relative;
|
||||
clear: both;
|
||||
}
|
||||
.ant-table-body {
|
||||
overflow-x: auto !important;
|
||||
}
|
||||
.ant-card-hoverable {
|
||||
cursor: auto;
|
||||
cursor: pointer;
|
||||
@@ -96,16 +101,16 @@ style attribute {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
color: rgba(0,0,0,.65);
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
font-size: 14px;
|
||||
font-variant: tabular-nums;
|
||||
line-height: 1.5;
|
||||
list-style: none;
|
||||
font-feature-settings: "tnum";
|
||||
position: relative;
|
||||
background: #fff;
|
||||
background-color: #fff;
|
||||
border-radius: 2px;
|
||||
transition: all .3s;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.ant-space {
|
||||
@@ -121,11 +126,18 @@ style attribute {
|
||||
display: none;
|
||||
}
|
||||
.ant-card {
|
||||
margin: .5rem;
|
||||
margin: 0.5rem;
|
||||
}
|
||||
.ant-tabs {
|
||||
margin: .5rem;
|
||||
padding: .5rem;
|
||||
margin: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
.ant-modal-body {
|
||||
padding: 10px;
|
||||
}
|
||||
.ant-form-item-label {
|
||||
line-height: 1.5;
|
||||
padding: 8px 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,7 +154,7 @@ style attribute {
|
||||
cursor: auto;
|
||||
}
|
||||
|
||||
.ant-card+.ant-card {
|
||||
.ant-card + .ant-card {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
@@ -159,7 +171,7 @@ style attribute {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: #fff;
|
||||
background-color: #fff;
|
||||
right: -40px;
|
||||
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.15);
|
||||
border-radius: 0 4px 4px 0;
|
||||
@@ -167,27 +179,45 @@ style attribute {
|
||||
|
||||
.ant-menu:not(.ant-menu-horizontal) .ant-menu-item-selected {
|
||||
background-color: #04308f !important;
|
||||
background-image: linear-gradient( 270deg, rgba(123, 199, 77, 0) 30%, #2f67c2, rgba(123, 199, 77, 0) 100% );
|
||||
background-image: linear-gradient(
|
||||
270deg,
|
||||
rgba(123, 199, 77, 0) 30%,
|
||||
#2f67c2,
|
||||
rgba(123, 199, 77, 0) 100%
|
||||
);
|
||||
background-repeat: no-repeat;
|
||||
animation: ma-bg-move linear 6.6s infinite;
|
||||
color: #fff;
|
||||
border-radius: 0.5rem
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
@-webkit-keyframes ma-bg-move {
|
||||
0% {background-position: -500px 0;}
|
||||
100% {background-position: 1000px 0;}
|
||||
0% {
|
||||
background-position: -500px 0;
|
||||
}
|
||||
100% {
|
||||
background-position: 1000px 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes ma-bg-move {
|
||||
0% {background-position: -500px 0;}
|
||||
50% {background-position: 1000px 0;}
|
||||
100% {background-position: 1000px 0;}
|
||||
0% {
|
||||
background-position: -500px 0;
|
||||
}
|
||||
50% {
|
||||
background-position: 1000px 0;
|
||||
}
|
||||
100% {
|
||||
background-position: 1000px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-menu-item-active,
|
||||
.ant-menu-item:hover,
|
||||
.ant-menu-submenu-active,
|
||||
.ant-menu-submenu-title:hover,
|
||||
.ant-menu:not(.ant-menu-inline) .ant-menu-submenu-open{
|
||||
color:#0e49b5;
|
||||
.ant-menu:not(.ant-menu-inline) .ant-menu-submenu-open {
|
||||
color: #0e49b5;
|
||||
background-color: #dce9f5;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
@@ -202,13 +232,13 @@ style attribute {
|
||||
}
|
||||
.ant-layout-sider-children,
|
||||
.ant-pagination ul {
|
||||
margin-top:-.1px;
|
||||
padding:0.5rem
|
||||
margin-top: -0.1px;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.ant-dropdown-menu,
|
||||
.ant-select-dropdown-menu {
|
||||
padding: .5rem;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
.ant-dropdown-menu-item,
|
||||
.ant-dropdown-menu-item:hover,
|
||||
@@ -216,7 +246,7 @@ style attribute {
|
||||
.ant-select-dropdown-menu-item:hover,
|
||||
.ant-select-dropdown-menu-item-selected,
|
||||
.ant-select-selection--multiple .ant-select-selection__choice {
|
||||
border-radius: .5rem;
|
||||
border-radius: 0.5rem;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
@@ -237,98 +267,130 @@ style attribute {
|
||||
.fade-in-linear-enter,
|
||||
.fade-in-linear-leave,
|
||||
.fade-in-linear-leave-active {
|
||||
opacity: 0
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.fade-in-linear-enter-active, .fade-in-linear-leave-active {
|
||||
-webkit-transition: opacity .2s linear;
|
||||
transition: opacity .2s linear
|
||||
.fade-in-linear-enter-active,
|
||||
.fade-in-linear-leave-active {
|
||||
-webkit-transition: opacity 0.2s linear;
|
||||
transition: opacity 0.2s linear;
|
||||
}
|
||||
|
||||
.fade-in-linear-enter-active, .fade-in-linear-leave-active {
|
||||
-webkit-transition: opacity .2s linear;
|
||||
transition: opacity .2s linear
|
||||
.fade-in-linear-enter-active,
|
||||
.fade-in-linear-leave-active {
|
||||
-webkit-transition: opacity 0.2s linear;
|
||||
transition: opacity 0.2s linear;
|
||||
}
|
||||
|
||||
.fade-in-enter-active, .fade-in-leave-active {
|
||||
-webkit-transition: all .3s cubic-bezier(.55, 0, .1, 1);
|
||||
transition: all .3s cubic-bezier(.55, 0, .1, 1)
|
||||
.fade-in-enter-active,
|
||||
.fade-in-leave-active {
|
||||
-webkit-transition: all 0.3s cubic-bezier(0.55, 0, 0.1, 1);
|
||||
transition: all 0.3s cubic-bezier(0.55, 0, 0.1, 1);
|
||||
}
|
||||
|
||||
.zoom-in-center-enter-active, .zoom-in-center-leave-active {
|
||||
-webkit-transition: all .3s cubic-bezier(.55, 0, .1, 1);
|
||||
transition: all .3s cubic-bezier(.55, 0, .1, 1)
|
||||
.zoom-in-center-enter-active,
|
||||
.zoom-in-center-leave-active {
|
||||
-webkit-transition: all 0.3s cubic-bezier(0.55, 0, 0.1, 1);
|
||||
transition: all 0.3s cubic-bezier(0.55, 0, 0.1, 1);
|
||||
}
|
||||
|
||||
.zoom-in-center-enter, .zoom-in-center-leave-active {
|
||||
.zoom-in-center-enter,
|
||||
.zoom-in-center-leave-active {
|
||||
opacity: 0;
|
||||
-webkit-transform: scaleX(0);
|
||||
transform: scaleX(0)
|
||||
transform: scaleX(0);
|
||||
}
|
||||
|
||||
.zoom-in-top-enter-active, .zoom-in-top-leave-active {
|
||||
.zoom-in-top-enter-active,
|
||||
.zoom-in-top-leave-active {
|
||||
opacity: 1;
|
||||
-webkit-transform: scaleY(1);
|
||||
transform: scaleY(1);
|
||||
-webkit-transition: opacity .3s cubic-bezier(.23, 1, .32, 1), -webkit-transform .3s cubic-bezier(.23, 1, .32, 1);
|
||||
transition: opacity .3s cubic-bezier(.23, 1, .32, 1), -webkit-transform .3s cubic-bezier(.23, 1, .32, 1);
|
||||
transition: transform .3s cubic-bezier(.23, 1, .32, 1), opacity .3s cubic-bezier(.23, 1, .32, 1);
|
||||
transition: transform .3s cubic-bezier(.23, 1, .32, 1), opacity .3s cubic-bezier(.23, 1, .32, 1), -webkit-transform .3s cubic-bezier(.23, 1, .32, 1);
|
||||
-webkit-transition: opacity 0.3s cubic-bezier(0.23, 1, 0.32, 1),
|
||||
-webkit-transform 0.3s cubic-bezier(0.23, 1, 0.32, 1);
|
||||
transition: opacity 0.3s cubic-bezier(0.23, 1, 0.32, 1),
|
||||
-webkit-transform 0.3s cubic-bezier(0.23, 1, 0.32, 1);
|
||||
transition: transform 0.3s cubic-bezier(0.23, 1, 0.32, 1),
|
||||
opacity 0.3s cubic-bezier(0.23, 1, 0.32, 1);
|
||||
transition: transform 0.3s cubic-bezier(0.23, 1, 0.32, 1),
|
||||
opacity 0.3s cubic-bezier(0.23, 1, 0.32, 1),
|
||||
-webkit-transform 0.3s cubic-bezier(0.23, 1, 0.32, 1);
|
||||
-webkit-transform-origin: center top;
|
||||
transform-origin: center top
|
||||
transform-origin: center top;
|
||||
}
|
||||
|
||||
.zoom-in-top-enter, .zoom-in-top-leave-active {
|
||||
.zoom-in-top-enter,
|
||||
.zoom-in-top-leave-active {
|
||||
opacity: 0;
|
||||
-webkit-transform: scaleY(0);
|
||||
transform: scaleY(0)
|
||||
transform: scaleY(0);
|
||||
}
|
||||
|
||||
.zoom-in-bottom-enter-active, .zoom-in-bottom-leave-active {
|
||||
.zoom-in-bottom-enter-active,
|
||||
.zoom-in-bottom-leave-active {
|
||||
opacity: 1;
|
||||
-webkit-transform: scaleY(1);
|
||||
transform: scaleY(1);
|
||||
-webkit-transition: opacity .3s cubic-bezier(.23, 1, .32, 1), -webkit-transform .3s cubic-bezier(.23, 1, .32, 1);
|
||||
transition: opacity .3s cubic-bezier(.23, 1, .32, 1), -webkit-transform .3s cubic-bezier(.23, 1, .32, 1);
|
||||
transition: transform .3s cubic-bezier(.23, 1, .32, 1), opacity .3s cubic-bezier(.23, 1, .32, 1);
|
||||
transition: transform .3s cubic-bezier(.23, 1, .32, 1), opacity .3s cubic-bezier(.23, 1, .32, 1), -webkit-transform .3s cubic-bezier(.23, 1, .32, 1);
|
||||
-webkit-transition: opacity 0.3s cubic-bezier(0.23, 1, 0.32, 1),
|
||||
-webkit-transform 0.3s cubic-bezier(0.23, 1, 0.32, 1);
|
||||
transition: opacity 0.3s cubic-bezier(0.23, 1, 0.32, 1),
|
||||
-webkit-transform 0.3s cubic-bezier(0.23, 1, 0.32, 1);
|
||||
transition: transform 0.3s cubic-bezier(0.23, 1, 0.32, 1),
|
||||
opacity 0.3s cubic-bezier(0.23, 1, 0.32, 1);
|
||||
transition: transform 0.3s cubic-bezier(0.23, 1, 0.32, 1),
|
||||
opacity 0.3s cubic-bezier(0.23, 1, 0.32, 1),
|
||||
-webkit-transform 0.3s cubic-bezier(0.23, 1, 0.32, 1);
|
||||
-webkit-transform-origin: center bottom;
|
||||
transform-origin: center bottom
|
||||
transform-origin: center bottom;
|
||||
}
|
||||
|
||||
.zoom-in-bottom-enter, .zoom-in-bottom-leave-active {
|
||||
.zoom-in-bottom-enter,
|
||||
.zoom-in-bottom-leave-active {
|
||||
opacity: 0;
|
||||
-webkit-transform: scaleY(0);
|
||||
transform: scaleY(0)
|
||||
transform: scaleY(0);
|
||||
}
|
||||
|
||||
.zoom-in-left-enter-active, .zoom-in-left-leave-active {
|
||||
.zoom-in-left-enter-active,
|
||||
.zoom-in-left-leave-active {
|
||||
opacity: 1;
|
||||
-webkit-transform: scale(1, 1);
|
||||
transform: scale(1, 1);
|
||||
-webkit-transition: opacity .3s cubic-bezier(.23, 1, .32, 1), -webkit-transform .3s cubic-bezier(.23, 1, .32, 1);
|
||||
transition: opacity .3s cubic-bezier(.23, 1, .32, 1), -webkit-transform .3s cubic-bezier(.23, 1, .32, 1);
|
||||
transition: transform .3s cubic-bezier(.23, 1, .32, 1), opacity .3s cubic-bezier(.23, 1, .32, 1);
|
||||
transition: transform .3s cubic-bezier(.23, 1, .32, 1), opacity .3s cubic-bezier(.23, 1, .32, 1), -webkit-transform .3s cubic-bezier(.23, 1, .32, 1);
|
||||
-webkit-transition: opacity 0.3s cubic-bezier(0.23, 1, 0.32, 1),
|
||||
-webkit-transform 0.3s cubic-bezier(0.23, 1, 0.32, 1);
|
||||
transition: opacity 0.3s cubic-bezier(0.23, 1, 0.32, 1),
|
||||
-webkit-transform 0.3s cubic-bezier(0.23, 1, 0.32, 1);
|
||||
transition: transform 0.3s cubic-bezier(0.23, 1, 0.32, 1),
|
||||
opacity 0.3s cubic-bezier(0.23, 1, 0.32, 1);
|
||||
transition: transform 0.3s cubic-bezier(0.23, 1, 0.32, 1),
|
||||
opacity 0.3s cubic-bezier(0.23, 1, 0.32, 1),
|
||||
-webkit-transform 0.3s cubic-bezier(0.23, 1, 0.32, 1);
|
||||
-webkit-transform-origin: top left;
|
||||
transform-origin: top left
|
||||
transform-origin: top left;
|
||||
}
|
||||
|
||||
.zoom-in-left-enter, .zoom-in-left-leave-active {
|
||||
.zoom-in-left-enter,
|
||||
.zoom-in-left-leave-active {
|
||||
opacity: 0;
|
||||
-webkit-transform: scale(.45, .45);
|
||||
transform: scale(.45, .45)
|
||||
-webkit-transform: scale(0.45, 0.45);
|
||||
transform: scale(0.45, 0.45);
|
||||
}
|
||||
|
||||
.list-enter-active, .list-leave-active {
|
||||
-webkit-transition: all .3s;
|
||||
transition: all .3s
|
||||
.list-enter-active,
|
||||
.list-leave-active {
|
||||
-webkit-transition: all 0.3s;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.list-enter, .list-leave-active {
|
||||
.list-enter,
|
||||
.list-leave-active {
|
||||
opacity: 0;
|
||||
-webkit-transform: translateY(-30px);
|
||||
transform: translateY(-30px)
|
||||
transform: translateY(-30px);
|
||||
}
|
||||
|
||||
.ant-tooltip-inner {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.ant-list-item-meta-title {
|
||||
@@ -336,19 +398,14 @@ style attribute {
|
||||
}
|
||||
|
||||
.ant-progress-inner {
|
||||
background-color: #EBEEF5;
|
||||
background-color: #ebeef5;
|
||||
}
|
||||
|
||||
.deactive-client .ant-collapse-header{
|
||||
color:rgb(255, 255, 255) !important;
|
||||
.deactive-client .ant-collapse-header {
|
||||
color: rgb(255, 255, 255) !important;
|
||||
background-color: rgb(255, 127, 127);
|
||||
}
|
||||
|
||||
.ant-table-tbody>tr>td,
|
||||
.ant-table-thead>tr>th{
|
||||
padding:16px 5px;
|
||||
}
|
||||
|
||||
.ant-table-expand-icon-th,
|
||||
.ant-table-row-expand-icon-cell {
|
||||
width: 30px;
|
||||
@@ -359,6 +416,10 @@ style attribute {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.ant-form-item {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.ant-setting-textarea {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
@@ -393,28 +454,32 @@ style attribute {
|
||||
}
|
||||
.ant-tag-orange,
|
||||
.ant-alert-warning {
|
||||
background-color:#fff6E6;
|
||||
background-color: #fff6e6;
|
||||
border-color: #ffd98c;
|
||||
color: #ffa031;
|
||||
}
|
||||
.ant-tag-red,
|
||||
.ant-alert-error {
|
||||
background-color:#fff0f0;
|
||||
background-color: #fff0f0;
|
||||
border-color: #fb9d9d;
|
||||
color: #e04141;
|
||||
}
|
||||
|
||||
.ant-input::placeholder {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.ant-input:hover,
|
||||
.ant-input:focus {
|
||||
background-color: #edf4fa;
|
||||
}
|
||||
|
||||
.delete-icon:hover {
|
||||
color: #E04141;
|
||||
color: #e04141;
|
||||
}
|
||||
|
||||
.normal-icon:hover {
|
||||
color: #0E49B5;
|
||||
color: #0e49b5;
|
||||
}
|
||||
|
||||
/* DARK THEME */
|
||||
@@ -436,18 +501,18 @@ style attribute {
|
||||
.dark .ant-table,
|
||||
.dark .ant-collapse-content,
|
||||
.dark .ant-tabs {
|
||||
background-color: #151F31;
|
||||
background-color: #151f31;
|
||||
color: #ffffffa6;
|
||||
}
|
||||
|
||||
.dark .ant-card-hoverable:hover,
|
||||
.dark .ant-space-item>.ant-tabs:hover {
|
||||
.dark .ant-space-item > .ant-tabs:hover {
|
||||
box-shadow: 0 1px 10px -1px rgb(154 175 238 / 80%);
|
||||
}
|
||||
|
||||
.dark>.ant-layout,
|
||||
.dark > .ant-layout,
|
||||
.dark .drawer-handle,
|
||||
.dark .ant-table-thead>tr>th,
|
||||
.dark .ant-table-thead > tr > th,
|
||||
.dark .ant-table-expanded-row,
|
||||
.dark .ant-table-expanded-row:hover,
|
||||
.dark .ant-table-expanded-row .ant-table-tbody,
|
||||
@@ -456,7 +521,7 @@ style attribute {
|
||||
color: rgb(255 255 255 /65%);
|
||||
}
|
||||
|
||||
.dark .ant-table-expanded-row .ant-table-thead>tr:first-child>th {
|
||||
.dark .ant-table-expanded-row .ant-table-thead > tr:first-child > th {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
@@ -465,17 +530,29 @@ style attribute {
|
||||
border-color: #151f31;
|
||||
}
|
||||
|
||||
.dark .ant-table-tbody>tr>td,
|
||||
.dark .ant-table-thead>tr>th,
|
||||
.dark .ant-table-bordered,
|
||||
.dark .ant-table-bordered.ant-table-empty .ant-table-placeholder,
|
||||
.dark .ant-table-bordered .ant-table-body > table,
|
||||
.dark .ant-table-bordered .ant-table-fixed-left table,
|
||||
.dark .ant-table-bordered .ant-table-fixed-right table,
|
||||
.dark .ant-table-bordered .ant-table-header > table,
|
||||
.dark .ant-table-bordered .ant-table-thead > tr:not(:last-child) > th,
|
||||
.dark .ant-table-bordered .ant-table-tbody > tr > td,
|
||||
.dark .ant-table-bordered .ant-table-thead > tr > th {
|
||||
border-color: #2c3950;
|
||||
}
|
||||
|
||||
.dark .ant-table-tbody > tr > td,
|
||||
.dark .ant-table-thead > tr > th,
|
||||
.dark .ant-card-head,
|
||||
.dark .ant-modal-header,
|
||||
.dark .ant-collapse>.ant-collapse-item,
|
||||
.dark .ant-collapse > .ant-collapse-item,
|
||||
.dark .ant-tabs-bar,
|
||||
.dark .ant-list-split .ant-list-item,
|
||||
.dark .ant-popover-title,
|
||||
.dark .ant-calendar-header,
|
||||
.dark .ant-calendar-input-wrap {
|
||||
border-bottom-color: #2C3950;
|
||||
border-bottom-color: #2c3950;
|
||||
}
|
||||
|
||||
.dark .ant-modal-footer,
|
||||
@@ -489,8 +566,7 @@ style attribute {
|
||||
.dark .ant-progress-text,
|
||||
.dark .ant-card-head,
|
||||
.dark .ant-form,
|
||||
.dark .ant-collapse>.ant-collapse-item>.ant-collapse-header,
|
||||
.dark .ant-form-item i,
|
||||
.dark .ant-collapse > .ant-collapse-item > .ant-collapse-header,
|
||||
.dark .ant-modal-close-x,
|
||||
.dark .ant-pagination-item a,
|
||||
.dark li:not(.ant-pagination-disabled) i,
|
||||
@@ -498,7 +574,6 @@ style attribute {
|
||||
.dark .ant-tabs-tab-arrow-show:not(.ant-tabs-tab-btn-disabled),
|
||||
.dark .anticon-close,
|
||||
.dark .ant-list-item-meta-title,
|
||||
.dark .ant-list-item-meta-description,
|
||||
.dark .ant-select-selection i,
|
||||
.dark .ant-modal-confirm-title,
|
||||
.dark .ant-modal-confirm-content,
|
||||
@@ -507,13 +582,30 @@ style attribute {
|
||||
.dark .ant-divider-inner-text,
|
||||
.dark .ant-popover-title,
|
||||
.dark .ant-popover-inner-content,
|
||||
.dark h2 {
|
||||
color: rgb(255 255 255 / 65%);
|
||||
.dark h2,
|
||||
.dark .ant-modal-title,
|
||||
.dark .ant-form-item-label > label,
|
||||
.dark .ant-checkbox-wrapper,
|
||||
.dark .ant-form-item,
|
||||
.dark .ant-calendar-footer .ant-calendar-today-btn,
|
||||
.dark .ant-calendar-footer .ant-calendar-time-picker-btn,
|
||||
.dark .ant-calendar-day-select,
|
||||
.dark .ant-calendar-month-select,
|
||||
.dark .ant-calendar-year-select,
|
||||
.dark .ant-calendar-date,
|
||||
.dark .ant-calendar-year-panel-year,
|
||||
.dark .ant-calendar-month-panel-month,
|
||||
.dark .ant-calendar-decade-panel-decade {
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
}
|
||||
|
||||
.dark .ant-list-item-meta-description {
|
||||
color: rgba(255, 255, 255, 0.45);
|
||||
}
|
||||
|
||||
.dark .ant-pagination-disabled i,
|
||||
.dark .ant-tabs-tab-btn-disabled {
|
||||
color: rgb(255 255 255 / 25%);
|
||||
color: rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
|
||||
.dark .ant-input,
|
||||
@@ -527,13 +619,16 @@ style attribute {
|
||||
.dark .ant-empty-normal,
|
||||
.dark.ant-select-dropdown,
|
||||
.dark .ant-select-dropdown,
|
||||
.dark .ant-select-dropdown li,
|
||||
.dark .ant-select-dropdown-menu-item,
|
||||
.dark .ant-divider:not(.ant-divider-with-text-center),
|
||||
.dark .ant-calendar-input,
|
||||
.dark .client-table-header,
|
||||
.dark .ant-select-selection--multiple .ant-select-selection__choice,
|
||||
.dark .ant-calendar-time-picker-inner {
|
||||
background-color: #222D42;
|
||||
background-color: #222d42;
|
||||
border-color: #2c3950;
|
||||
color: rgb(255 255 255 / 65%);
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
}
|
||||
|
||||
.dark .ant-select-selection:hover,
|
||||
@@ -542,13 +637,13 @@ style attribute {
|
||||
.dark .ant-input-number:focus,
|
||||
.dark .ant-input:hover,
|
||||
.dark .ant-input:focus {
|
||||
background-color: rgb(14 73 181 / 30%);
|
||||
border-color: #0E49B5;
|
||||
background-color: rgba(14, 73, 181, 0.3);
|
||||
border-color: #0e49b5;
|
||||
}
|
||||
|
||||
.dark .ant-btn:not(.ant-btn-primary):not(.ant-btn-danger) {
|
||||
color: rgb(255 255 255 / 65%);
|
||||
background-color: rgb(14 73 181 / 30%);
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
background-color: rgba(14, 73, 181, 0.3);
|
||||
border: 1px solid #0e49b5;
|
||||
}
|
||||
|
||||
@@ -556,25 +651,29 @@ style attribute {
|
||||
.dark .ant-radio-button-wrapper:before {
|
||||
color: rgb(255 255 255 / 65%);
|
||||
background-color: rgb(14 73 181 / 30%);
|
||||
border: 1px solid #0e49b5;
|
||||
border-left: inherit;
|
||||
border-color: #0e49b5;
|
||||
}
|
||||
|
||||
.dark .ant-btn:focus:not(.ant-btn-primary):not(.ant-btn-danger) ,
|
||||
.dark .ant-btn:hover:not(.ant-btn-primary):not(.ant-btn-danger) {
|
||||
color: #ffffff;
|
||||
.dark .ant-btn:focus:not(.ant-btn-primary):not(.ant-btn-danger),
|
||||
.dark .ant-btn:hover:not(.ant-btn-primary):not(.ant-btn-danger) {
|
||||
color: #fff;
|
||||
background-color: rgb(14 73 181 / 50%);
|
||||
border-color: #0e49b5;
|
||||
}
|
||||
|
||||
.dark .ant-btn-primary[disabled],
|
||||
.dark .ant-btn-danger[disabled],
|
||||
.dark .ant-calendar-ok-btn-disabled {
|
||||
color: rgb(255 255 255 / 35%);
|
||||
background-color: #2c3950;
|
||||
border-color: #42516c;
|
||||
}
|
||||
|
||||
.dark .ant-table-tbody>tr:hover:not(.ant-table-expanded-row):not(.ant-table-row-selected)>td {
|
||||
.dark
|
||||
.ant-table-tbody
|
||||
> tr:hover:not(.ant-table-expanded-row):not(.ant-table-row-selected)
|
||||
> td,
|
||||
.dark .client-table-odd-row {
|
||||
background-color: #122444;
|
||||
}
|
||||
|
||||
@@ -590,10 +689,7 @@ style attribute {
|
||||
border-color: #0e49b5;
|
||||
}
|
||||
|
||||
.dark .ant-switch:not(.ant-switch-checked) {
|
||||
background-color: #2C3950;
|
||||
}
|
||||
|
||||
.dark .ant-switch:not(.ant-switch-checked),
|
||||
.dark .ant-progress-line .ant-progress-inner {
|
||||
background-color: #2c3950;
|
||||
}
|
||||
@@ -604,11 +700,11 @@ style attribute {
|
||||
|
||||
.ant-dropdown-menu-dark,
|
||||
.dark .ant-popover-inner {
|
||||
background-color: #222D42;
|
||||
background-color: #222d42;
|
||||
}
|
||||
|
||||
.dark>.ant-popover-content>.ant-popover-arrow {
|
||||
border-color: #222D42;
|
||||
.dark > .ant-popover-content > .ant-popover-arrow {
|
||||
border-color: #222d42;
|
||||
}
|
||||
|
||||
.ant-dropdown-menu-dark .ant-dropdown-menu-item:hover,
|
||||
@@ -623,11 +719,11 @@ style attribute {
|
||||
}
|
||||
|
||||
.dark .ant-alert-message {
|
||||
color: rgb(255 255 255 /85%);
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
}
|
||||
|
||||
.dark .ant-tag {
|
||||
color: rgb(255 255 255 / 65%);
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
background-color: #ffffff0a;
|
||||
border-color: #344461;
|
||||
}
|
||||
@@ -641,7 +737,7 @@ style attribute {
|
||||
.dark .ant-tag-red,
|
||||
.dark .ant-alert-error {
|
||||
background-color: #291515;
|
||||
border-color: #5C2626;
|
||||
border-color: #5c2626;
|
||||
color: #e04141;
|
||||
}
|
||||
|
||||
@@ -661,7 +757,7 @@ style attribute {
|
||||
.dark .ant-tag-purple {
|
||||
background-color: #2c1e32;
|
||||
border-color: #49394e;
|
||||
color: #f2eaf1;
|
||||
color: #cfb9cc;
|
||||
}
|
||||
|
||||
.dark .ant-modal-content,
|
||||
@@ -669,19 +765,6 @@ style attribute {
|
||||
background-color: #181f2c;
|
||||
}
|
||||
|
||||
.dark .ant-modal-title,
|
||||
.dark .ant-form-item-label>label,
|
||||
.dark .ant-checkbox-wrapper,
|
||||
.dark .ant-form-item,
|
||||
.dark .ant-calendar-footer .ant-calendar-today-btn,
|
||||
.dark .ant-calendar-footer .ant-calendar-time-picker-btn,
|
||||
.dark .ant-calendar-day-select,
|
||||
.dark .ant-calendar-month-select,
|
||||
.dark .ant-calendar-year-select,
|
||||
.dark .ant-calendar-date {
|
||||
color: rgb(255 255 255 / 65%);
|
||||
}
|
||||
|
||||
.dark .ant-calendar-next-month-btn-day .ant-calendar-date,
|
||||
.dark .ant-calendar-last-month-cell .ant-calendar-date {
|
||||
color: #2c3950;
|
||||
@@ -705,28 +788,264 @@ style attribute {
|
||||
}
|
||||
|
||||
.dark .ant-calendar-time-picker-select li:focus {
|
||||
color: #ffffff;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
outline: none;
|
||||
background-color: #0e49b5;
|
||||
}
|
||||
|
||||
.dark .ant-calendar-time-picker-select {
|
||||
border-right-color: #2C3950;
|
||||
border-right-color: #2c3950;
|
||||
}
|
||||
|
||||
.has-warning .ant-input,
|
||||
.has-warning .ant-input:hover {
|
||||
background-color: #fff6e6;
|
||||
border-color: #ffd98c;
|
||||
}
|
||||
|
||||
.has-warning .ant-input::placeholder {
|
||||
color: #faad14;
|
||||
}
|
||||
|
||||
.has-warning .ant-input:not([disabled]):hover {
|
||||
border-color: #ffd98c;
|
||||
}
|
||||
|
||||
.dark .has-warning .ant-input,
|
||||
.dark .has-warning .ant-input:hover {
|
||||
border-color: #784e1d;
|
||||
background: rgb(49, 35, 19);
|
||||
}
|
||||
|
||||
.dark .has-warning .ant-input::placeholder {
|
||||
color: rgb(255 160 49 / 70%);
|
||||
}
|
||||
|
||||
.dark .has-warning .anticon {
|
||||
color: #ffa031;
|
||||
}
|
||||
|
||||
.dark .has-success .anticon {
|
||||
color: #61bf39;
|
||||
animation-name: diffZoomIn1 !important;
|
||||
}
|
||||
|
||||
.dark .anticon-close-circle {
|
||||
color: #E04141;
|
||||
color: #e04141;
|
||||
}
|
||||
|
||||
.dark .ant-spin-nested-loading>div>.ant-spin .ant-spin-text {
|
||||
.dark .ant-spin-nested-loading > div > .ant-spin .ant-spin-text {
|
||||
text-shadow: 0 1px 2px #00000077;
|
||||
}
|
||||
|
||||
.dark .ant-spin {
|
||||
color: #ffffff;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.dark .ant-spin-dot-item {
|
||||
background-color: #ffffffff;
|
||||
}
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.ant-menu,
|
||||
.ant-radio-button-wrapper {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.ant-calendar-date:hover {
|
||||
background-color: #dae9f5;
|
||||
}
|
||||
|
||||
.ant-calendar-date:active {
|
||||
background-color: #dae9f5;
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
}
|
||||
|
||||
.ant-calendar-today .ant-calendar-date {
|
||||
color: #0e49b5;
|
||||
font-weight: 700;
|
||||
border-color: #0e49b5;
|
||||
}
|
||||
|
||||
.dark .ant-calendar-today .ant-calendar-date {
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
border-color: #0e49b5;
|
||||
}
|
||||
|
||||
.ant-calendar-selected-day .ant-calendar-date {
|
||||
background: #0e49b5;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
li.ant-select-dropdown-menu-item:empty:after {
|
||||
content: "None";
|
||||
font-weight: normal;
|
||||
color: rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.dark li.ant-select-dropdown-menu-item:empty:after {
|
||||
content: "None";
|
||||
font-weight: normal;
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.ant-select-dropdown.ant-select-dropdown--multiple
|
||||
.ant-select-dropdown-menu-item:hover
|
||||
.ant-select-selected-icon {
|
||||
color: rgba(0, 0, 0, 0.87);
|
||||
}
|
||||
|
||||
.dark.ant-select-dropdown.ant-select-dropdown--multiple
|
||||
.ant-select-dropdown-menu-item:hover
|
||||
.ant-select-selected-icon {
|
||||
color: rgb(255, 255, 255);
|
||||
}
|
||||
|
||||
.ant-select-dropdown.ant-select-dropdown--multiple
|
||||
.ant-select-dropdown-menu-item-selected
|
||||
.ant-select-selected-icon,
|
||||
.ant-select-dropdown.ant-select-dropdown--multiple
|
||||
.ant-select-dropdown-menu-item-selected:hover
|
||||
.ant-select-selected-icon {
|
||||
color: #3c89e8;
|
||||
}
|
||||
.ant-select-selection:hover,
|
||||
.ant-input-number-focused,
|
||||
.ant-input-number:hover {
|
||||
background-color: #edf4fa;
|
||||
}
|
||||
|
||||
.dark .ant-input-number-handler:active {
|
||||
background-color: #0e49b5;
|
||||
}
|
||||
|
||||
.dark .ant-input-number-handler:hover .ant-input-number-handler-down-inner,
|
||||
.dark .ant-input-number-handler:hover .ant-input-number-handler-up-inner {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.dark .ant-input-number-handler-down {
|
||||
border-top: 1px solid rgba(217, 217, 217, 0.3);
|
||||
}
|
||||
|
||||
.dark .ant-calendar-year-panel-header .ant-calendar-year-panel-century-select,
|
||||
.dark .ant-calendar-year-panel-header .ant-calendar-year-panel-decade-select,
|
||||
.dark .ant-calendar-year-panel-header .ant-calendar-year-panel-month-select,
|
||||
.dark
|
||||
.ant-calendar-year-panel-header
|
||||
.ant-calendar-year-panel-year-select
|
||||
.dark
|
||||
.ant-calendar-month-panel-header
|
||||
.ant-calendar-month-panel-century-select,
|
||||
.dark .ant-calendar-month-panel-header .ant-calendar-month-panel-decade-select,
|
||||
.dark .ant-calendar-month-panel-header .ant-calendar-month-panel-month-select,
|
||||
.dark .ant-calendar-month-panel-header .ant-calendar-month-panel-year-select {
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
}
|
||||
|
||||
.dark .ant-calendar-year-panel-header {
|
||||
border-bottom: 1px solid #222d42;
|
||||
}
|
||||
|
||||
.dark .ant-calendar-year-panel-last-decade-cell .ant-calendar-year-panel-year,
|
||||
.dark .ant-calendar-year-panel-next-decade-cell .ant-calendar-year-panel-year {
|
||||
color: rgba(255, 255, 255, 0.35);
|
||||
}
|
||||
|
||||
.dark .ant-calendar-year-panel-year:hover,
|
||||
.dark .ant-calendar-month-panel-month:hover,
|
||||
.dark .ant-calendar-decade-panel-decade:hover {
|
||||
background-color: #222d42;
|
||||
}
|
||||
|
||||
.dark .ant-calendar-header a:hover {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.dark .ant-calendar-month-panel-header {
|
||||
background-color: #101828;
|
||||
border-bottom: 1px solid #222d42;
|
||||
}
|
||||
|
||||
.dark .ant-calendar-year-panel,
|
||||
.dark .ant-calendar table {
|
||||
background-color: #101828;
|
||||
}
|
||||
|
||||
.dark .ant-calendar-year-panel-selected-cell .ant-calendar-year-panel-year,
|
||||
.dark
|
||||
.ant-calendar-year-panel-selected-cell
|
||||
.ant-calendar-year-panel-year:hover,
|
||||
.dark .ant-calendar-month-panel-selected-cell .ant-calendar-month-panel-month,
|
||||
.dark
|
||||
.ant-calendar-month-panel-selected-cell
|
||||
.ant-calendar-month-panel-month:hover,
|
||||
.dark
|
||||
.ant-calendar-decade-panel-selected-cell
|
||||
.ant-calendar-decade-panel-decade,
|
||||
.dark
|
||||
.ant-calendar-decade-panel-selected-cell
|
||||
.ant-calendar-decade-panel-decade:hover {
|
||||
color: #fff;
|
||||
background-color: #0e49b5;
|
||||
}
|
||||
|
||||
.dark .ant-calendar-last-month-cell .ant-calendar-date,
|
||||
.dark .ant-calendar-last-month-cell .ant-calendar-date:hover,
|
||||
.dark .ant-calendar-next-month-btn-day .ant-calendar-date,
|
||||
.dark .ant-calendar-next-month-btn-day .ant-calendar-date:hover {
|
||||
color: rgb(255 255 255 / 25%);
|
||||
background: transparent;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.dark .ant-calendar-today .ant-calendar-date:hover {
|
||||
color: #fff;
|
||||
border-color: #0e49b5;
|
||||
background-color: #0e49b5;
|
||||
}
|
||||
|
||||
.dark
|
||||
.ant-calendar-decade-panel-last-century-cell
|
||||
.ant-calendar-decade-panel-decade,
|
||||
.dark
|
||||
.ant-calendar-decade-panel-next-century-cell
|
||||
.ant-calendar-decade-panel-decade {
|
||||
color: rgb(255 255 255 / 25%);
|
||||
}
|
||||
|
||||
.dark .ant-calendar-decade-panel-header {
|
||||
border-bottom: 1px solid #222d42;
|
||||
background-color: #101828;
|
||||
}
|
||||
|
||||
.dark .ant-checkbox-inner {
|
||||
background-color: rgba(14, 73, 181, 0.3);
|
||||
border-color: rgba(14, 73, 181, 0.3);
|
||||
}
|
||||
|
||||
.dark .ant-checkbox-checked .ant-checkbox-inner {
|
||||
background-color: #0e49b5;
|
||||
border-color: #0e49b5;
|
||||
}
|
||||
|
||||
.dark .ant-calendar-input {
|
||||
background-color: #101828;
|
||||
}
|
||||
|
||||
.dark .ant-calendar-input::placeholder {
|
||||
color: rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
|
||||
.ant-input-number-handler-wrap {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.ant-input-number-handler {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.ant-input-number {
|
||||
overflow: clip;
|
||||
}
|
||||
|
||||
@@ -19,6 +19,11 @@ const supportLangs = [
|
||||
value: 'ru-RU',
|
||||
icon: '🇷🇺',
|
||||
},
|
||||
{
|
||||
name: 'Tiếng Việt',
|
||||
value: 'vi-VN',
|
||||
icon: '🇻🇳',
|
||||
},
|
||||
];
|
||||
|
||||
function getLang() {
|
||||
@@ -60,4 +65,4 @@ function isSupportLang(lang) {
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,30 +1,3 @@
|
||||
class User {
|
||||
|
||||
constructor() {
|
||||
this.username = "";
|
||||
this.password = "";
|
||||
}
|
||||
}
|
||||
|
||||
class Msg {
|
||||
|
||||
constructor(success, msg, obj) {
|
||||
this.success = false;
|
||||
this.msg = "";
|
||||
this.obj = null;
|
||||
|
||||
if (success != null) {
|
||||
this.success = success;
|
||||
}
|
||||
if (msg != null) {
|
||||
this.msg = msg;
|
||||
}
|
||||
if (obj != null) {
|
||||
this.obj = obj;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class DBInbound {
|
||||
|
||||
constructor(data) {
|
||||
@@ -163,59 +136,9 @@ class DBInbound {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
genLink(address=this.address, remark=this.remark, clientIndex=0) {
|
||||
const inbound = this.toInbound();
|
||||
return inbound.genLink(address, remark, clientIndex);
|
||||
}
|
||||
|
||||
get genInboundLinks() {
|
||||
genInboundLinks(remarkModel) {
|
||||
const inbound = this.toInbound();
|
||||
return inbound.genInboundLinks(this.address, this.remark);
|
||||
}
|
||||
}
|
||||
|
||||
class AllSetting {
|
||||
|
||||
constructor(data) {
|
||||
this.webListen = "";
|
||||
this.webDomain = "";
|
||||
this.webPort = 54321;
|
||||
this.webCertFile = "";
|
||||
this.webKeyFile = "";
|
||||
this.webBasePath = "/";
|
||||
this.sessionMaxAge = "";
|
||||
this.pageSize = 0;
|
||||
this.expireDiff = "";
|
||||
this.trafficDiff = "";
|
||||
this.tgBotEnable = false;
|
||||
this.tgBotToken = "";
|
||||
this.tgBotChatId = "";
|
||||
this.tgRunTime = "@daily";
|
||||
this.tgBotBackup = false;
|
||||
this.tgBotLoginNotify = false;
|
||||
this.tgCpu = "";
|
||||
this.tgLang = "";
|
||||
this.subEnable = false;
|
||||
this.subListen = "";
|
||||
this.subPort = "2096";
|
||||
this.subPath = "/sub/";
|
||||
this.subDomain = "";
|
||||
this.subCertFile = "";
|
||||
this.subKeyFile = "";
|
||||
this.subUpdates = 0;
|
||||
this.subEncrypt = true;
|
||||
this.subShowInfo = false;
|
||||
|
||||
this.timeLocation = "Asia/Tehran";
|
||||
|
||||
if (data == null) {
|
||||
return
|
||||
}
|
||||
ObjectUtil.cloneProps(this, data);
|
||||
}
|
||||
|
||||
equals(other) {
|
||||
return ObjectUtil.equals(this, other);
|
||||
return inbound.genInboundLinks(this.remark,remarkModel);
|
||||
}
|
||||
}
|
||||
900
web/assets/js/model/outbound.js
Normal file
900
web/assets/js/model/outbound.js
Normal file
@@ -0,0 +1,900 @@
|
||||
const Protocols = {
|
||||
Freedom: "freedom",
|
||||
Blackhole: "blackhole",
|
||||
DNS: "dns",
|
||||
VMess: "vmess",
|
||||
VLESS: "vless",
|
||||
Trojan: "trojan",
|
||||
Shadowsocks: "shadowsocks",
|
||||
Socks: "socks",
|
||||
HTTP: "http",
|
||||
};
|
||||
|
||||
const SSMethods = {
|
||||
AES_256_GCM: 'aes-256-gcm',
|
||||
AES_128_GCM: 'aes-128-gcm',
|
||||
CHACHA20_POLY1305: 'chacha20-poly1305',
|
||||
CHACHA20_IETF_POLY1305: 'chacha20-ietf-poly1305',
|
||||
XCHACHA20_POLY1305: 'xchacha20-poly1305',
|
||||
XCHACHA20_IETF_POLY1305: 'xchacha20-ietf-poly1305',
|
||||
BLAKE3_AES_128_GCM: '2022-blake3-aes-128-gcm',
|
||||
BLAKE3_AES_256_GCM: '2022-blake3-aes-256-gcm',
|
||||
BLAKE3_CHACHA20_POLY1305: '2022-blake3-chacha20-poly1305',
|
||||
};
|
||||
|
||||
const TLS_FLOW_CONTROL = {
|
||||
VISION: "xtls-rprx-vision",
|
||||
VISION_UDP443: "xtls-rprx-vision-udp443",
|
||||
};
|
||||
|
||||
const UTLS_FINGERPRINT = {
|
||||
UTLS_CHROME: "chrome",
|
||||
UTLS_FIREFOX: "firefox",
|
||||
UTLS_SAFARI: "safari",
|
||||
UTLS_IOS: "ios",
|
||||
UTLS_android: "android",
|
||||
UTLS_EDGE: "edge",
|
||||
UTLS_360: "360",
|
||||
UTLS_QQ: "qq",
|
||||
UTLS_RANDOM: "random",
|
||||
UTLS_RANDOMIZED: "randomized",
|
||||
};
|
||||
|
||||
const ALPN_OPTION = {
|
||||
H3: "h3",
|
||||
H2: "h2",
|
||||
HTTP1: "http/1.1",
|
||||
};
|
||||
|
||||
const outboundDomainStrategies = [
|
||||
"AsIs",
|
||||
"UseIP",
|
||||
"UseIPv4",
|
||||
"UseIPv6"
|
||||
]
|
||||
|
||||
Object.freeze(Protocols);
|
||||
Object.freeze(SSMethods);
|
||||
Object.freeze(TLS_FLOW_CONTROL);
|
||||
Object.freeze(ALPN_OPTION);
|
||||
Object.freeze(outboundDomainStrategies);
|
||||
|
||||
class CommonClass {
|
||||
|
||||
static toJsonArray(arr) {
|
||||
return arr.map(obj => obj.toJson());
|
||||
}
|
||||
|
||||
static fromJson() {
|
||||
return new CommonClass();
|
||||
}
|
||||
|
||||
toJson() {
|
||||
return this;
|
||||
}
|
||||
|
||||
toString(format=true) {
|
||||
return format ? JSON.stringify(this.toJson(), null, 2) : JSON.stringify(this.toJson());
|
||||
}
|
||||
}
|
||||
|
||||
class TcpStreamSettings extends CommonClass {
|
||||
constructor(type='none', host, path) {
|
||||
super();
|
||||
this.type = type;
|
||||
this.host = host;
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
static fromJson(json={}) {
|
||||
let header = json.header;
|
||||
if (!header) return new TcpStreamSettings();
|
||||
if(header.type == 'http' && header.request){
|
||||
return new TcpStreamSettings(
|
||||
header.type,
|
||||
header.request.headers.Host.join(','),
|
||||
header.request.path.join(','),
|
||||
);
|
||||
}
|
||||
return new TcpStreamSettings(header.type,'','');
|
||||
}
|
||||
|
||||
toJson() {
|
||||
return {
|
||||
header: {
|
||||
type: this.type,
|
||||
request: this.type === 'http' ? {
|
||||
headers: {
|
||||
Host: ObjectUtil.isEmpty(this.host) ? [] : this.host.split(',')
|
||||
},
|
||||
path: ObjectUtil.isEmpty(this.path) ? ["/"] : this.path.split(',')
|
||||
} : undefined,
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class KcpStreamSettings extends CommonClass {
|
||||
constructor(mtu=1350, tti=20,
|
||||
uplinkCapacity=5,
|
||||
downlinkCapacity=20,
|
||||
congestion=false,
|
||||
readBufferSize=2,
|
||||
writeBufferSize=2,
|
||||
type='none',
|
||||
seed='',
|
||||
) {
|
||||
super();
|
||||
this.mtu = mtu;
|
||||
this.tti = tti;
|
||||
this.upCap = uplinkCapacity;
|
||||
this.downCap = downlinkCapacity;
|
||||
this.congestion = congestion;
|
||||
this.readBuffer = readBufferSize;
|
||||
this.writeBuffer = writeBufferSize;
|
||||
this.type = type;
|
||||
this.seed = seed;
|
||||
}
|
||||
|
||||
static fromJson(json={}) {
|
||||
return new KcpStreamSettings(
|
||||
json.mtu,
|
||||
json.tti,
|
||||
json.uplinkCapacity,
|
||||
json.downlinkCapacity,
|
||||
json.congestion,
|
||||
json.readBufferSize,
|
||||
json.writeBufferSize,
|
||||
ObjectUtil.isEmpty(json.header) ? 'none' : json.header.type,
|
||||
json.seed,
|
||||
);
|
||||
}
|
||||
|
||||
toJson() {
|
||||
return {
|
||||
mtu: this.mtu,
|
||||
tti: this.tti,
|
||||
uplinkCapacity: this.upCap,
|
||||
downlinkCapacity: this.downCap,
|
||||
congestion: this.congestion,
|
||||
readBufferSize: this.readBuffer,
|
||||
writeBufferSize: this.writeBuffer,
|
||||
header: {
|
||||
type: this.type,
|
||||
},
|
||||
seed: this.seed,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class WsStreamSettings extends CommonClass {
|
||||
constructor(path='/', host='') {
|
||||
super();
|
||||
this.path = path;
|
||||
this.host = host;
|
||||
}
|
||||
|
||||
static fromJson(json={}) {
|
||||
return new WsStreamSettings(
|
||||
json.path,
|
||||
json.headers && !ObjectUtil.isEmpty(json.headers.Host) ? json.headers.Host : '',
|
||||
);
|
||||
}
|
||||
|
||||
toJson() {
|
||||
return {
|
||||
path: this.path,
|
||||
headers: ObjectUtil.isEmpty(this.host) ? undefined : {Host: this.host},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class HttpStreamSettings extends CommonClass {
|
||||
constructor(path='/', host='') {
|
||||
super();
|
||||
this.path = path;
|
||||
this.host = host;
|
||||
}
|
||||
|
||||
static fromJson(json={}) {
|
||||
return new HttpStreamSettings(
|
||||
json.path,
|
||||
json.host ? json.host.join(',') : '',
|
||||
);
|
||||
}
|
||||
|
||||
toJson() {
|
||||
return {
|
||||
path: this.path,
|
||||
host: ObjectUtil.isEmpty(this.host) ? [''] : this.host.split(','),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class QuicStreamSettings extends CommonClass {
|
||||
constructor(security='none',
|
||||
key='', type='none') {
|
||||
super();
|
||||
this.security = security;
|
||||
this.key = key;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
static fromJson(json={}) {
|
||||
return new QuicStreamSettings(
|
||||
json.security,
|
||||
json.key,
|
||||
json.header ? json.header.type : 'none',
|
||||
);
|
||||
}
|
||||
|
||||
toJson() {
|
||||
return {
|
||||
security: this.security,
|
||||
key: this.key,
|
||||
header: {
|
||||
type: this.type,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class GrpcStreamSettings extends CommonClass {
|
||||
constructor(serviceName="", multiMode=false) {
|
||||
super();
|
||||
this.serviceName = serviceName;
|
||||
this.multiMode = multiMode;
|
||||
}
|
||||
|
||||
static fromJson(json={}) {
|
||||
return new GrpcStreamSettings(json.serviceName, json.multiMode);
|
||||
}
|
||||
|
||||
toJson() {
|
||||
return {
|
||||
serviceName: this.serviceName,
|
||||
multiMode: this.multiMode,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class TlsStreamSettings extends CommonClass {
|
||||
constructor(serverName='',
|
||||
alpn=[],
|
||||
fingerprint = '',
|
||||
allowInsecure = false) {
|
||||
super();
|
||||
this.serverName = serverName;
|
||||
this.alpn = alpn;
|
||||
this.fingerprint = fingerprint;
|
||||
this.allowInsecure = allowInsecure;
|
||||
}
|
||||
|
||||
static fromJson(json={}) {
|
||||
return new TlsStreamSettings(
|
||||
json.serverName,
|
||||
json.alpn,
|
||||
json.fingerprint,
|
||||
json.allowInsecure,
|
||||
);
|
||||
}
|
||||
|
||||
toJson() {
|
||||
return {
|
||||
serverName: this.serverName,
|
||||
alpn: this.alpn,
|
||||
fingerprint: this.fingerprint,
|
||||
allowInsecure: this.allowInsecure,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class RealityStreamSettings extends CommonClass {
|
||||
constructor(publicKey = '', fingerprint = '', serverName = '', shortId = '', spiderX = '/') {
|
||||
super();
|
||||
this.publicKey = publicKey;
|
||||
this.fingerprint = fingerprint;
|
||||
this.serverName = serverName;
|
||||
this.shortId = shortId
|
||||
this.spiderX = spiderX;
|
||||
}
|
||||
static fromJson(json = {}) {
|
||||
return new RealityStreamSettings(
|
||||
json.publicKey,
|
||||
json.fingerprint,
|
||||
json.serverName,
|
||||
json.shortId,
|
||||
json.spiderX,
|
||||
);
|
||||
}
|
||||
toJson() {
|
||||
return {
|
||||
publicKey: this.publicKey,
|
||||
fingerprint: this.fingerprint,
|
||||
serverName: this.serverName,
|
||||
shortId: this.shortId,
|
||||
spiderX: this.spiderX,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
class StreamSettings extends CommonClass {
|
||||
constructor(network='tcp',
|
||||
security='none',
|
||||
tlsSettings=new TlsStreamSettings(),
|
||||
realitySettings = new RealityStreamSettings(),
|
||||
tcpSettings=new TcpStreamSettings(),
|
||||
kcpSettings=new KcpStreamSettings(),
|
||||
wsSettings=new WsStreamSettings(),
|
||||
httpSettings=new HttpStreamSettings(),
|
||||
quicSettings=new QuicStreamSettings(),
|
||||
grpcSettings=new GrpcStreamSettings(),
|
||||
) {
|
||||
super();
|
||||
this.network = network;
|
||||
this.security = security;
|
||||
this.tls = tlsSettings;
|
||||
this.reality = realitySettings;
|
||||
this.tcp = tcpSettings;
|
||||
this.kcp = kcpSettings;
|
||||
this.ws = wsSettings;
|
||||
this.http = httpSettings;
|
||||
this.quic = quicSettings;
|
||||
this.grpc = grpcSettings;
|
||||
}
|
||||
|
||||
get isTls() {
|
||||
return this.security === 'tls';
|
||||
}
|
||||
|
||||
get isReality() {
|
||||
return this.security === "reality";
|
||||
}
|
||||
|
||||
static fromJson(json={}) {
|
||||
return new StreamSettings(
|
||||
json.network,
|
||||
json.security,
|
||||
TlsStreamSettings.fromJson(json.tlsSettings),
|
||||
RealityStreamSettings.fromJson(json.realitySettings),
|
||||
TcpStreamSettings.fromJson(json.tcpSettings),
|
||||
KcpStreamSettings.fromJson(json.kcpSettings),
|
||||
WsStreamSettings.fromJson(json.wsSettings),
|
||||
HttpStreamSettings.fromJson(json.httpSettings),
|
||||
QuicStreamSettings.fromJson(json.quicSettings),
|
||||
GrpcStreamSettings.fromJson(json.grpcSettings),
|
||||
);
|
||||
}
|
||||
|
||||
toJson() {
|
||||
const network = this.network;
|
||||
return {
|
||||
network: network,
|
||||
security: this.security,
|
||||
tlsSettings: this.security == 'tls' ? this.tls.toJson() : undefined,
|
||||
realitySettings: this.security == 'reality' ? this.reality.toJson() : undefined,
|
||||
tcpSettings: network === 'tcp' ? this.tcp.toJson() : undefined,
|
||||
kcpSettings: network === 'kcp' ? this.kcp.toJson() : undefined,
|
||||
wsSettings: network === 'ws' ? this.ws.toJson() : undefined,
|
||||
httpSettings: network === 'http' ? this.http.toJson() : undefined,
|
||||
quicSettings: network === 'quic' ? this.quic.toJson() : undefined,
|
||||
grpcSettings: network === 'grpc' ? this.grpc.toJson() : undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class Outbound extends CommonClass {
|
||||
constructor(
|
||||
tag='',
|
||||
protocol=Protocols.VMess,
|
||||
settings=null,
|
||||
streamSettings = new StreamSettings(),
|
||||
) {
|
||||
super();
|
||||
this.tag = tag;
|
||||
this._protocol = protocol;
|
||||
this.settings = settings == null ? Outbound.Settings.getSettings(protocol) : settings;
|
||||
this.stream = streamSettings;
|
||||
}
|
||||
|
||||
get protocol() {
|
||||
return this._protocol;
|
||||
}
|
||||
|
||||
set protocol(protocol) {
|
||||
this._protocol = protocol;
|
||||
this.settings = Outbound.Settings.getSettings(protocol);
|
||||
this.stream = new StreamSettings();
|
||||
}
|
||||
|
||||
canEnableTls() {
|
||||
if (![Protocols.VMess, Protocols.VLESS, Protocols.Trojan].includes(this.protocol)) return false;
|
||||
return ["tcp", "ws", "http", "quic", "grpc"].includes(this.stream.network);
|
||||
}
|
||||
|
||||
//this is used for xtls-rprx-vision
|
||||
canEnableTlsFlow() {
|
||||
if ((this.stream.security != 'none') && (this.stream.network === "tcp")) {
|
||||
return this.protocol === Protocols.VLESS;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
canEnableReality() {
|
||||
if (![Protocols.VLESS, Protocols.Trojan].includes(this.protocol)) return false;
|
||||
return ["tcp", "http", "grpc"].includes(this.stream.network);
|
||||
}
|
||||
|
||||
canEnableStream() {
|
||||
return [Protocols.VMess, Protocols.VLESS, Protocols.Trojan, Protocols.Shadowsocks].includes(this.protocol);
|
||||
}
|
||||
|
||||
hasVnext() {
|
||||
return [Protocols.VMess, Protocols.VLESS].includes(this.protocol);
|
||||
}
|
||||
|
||||
hasServers() {
|
||||
return [Protocols.Trojan, Protocols.Shadowsocks, Protocols.Socks, Protocols.HTTP].includes(this.protocol);
|
||||
}
|
||||
|
||||
hasAddressPort() {
|
||||
return [
|
||||
Protocols.DNS,
|
||||
Protocols.VMess,
|
||||
Protocols.VLESS,
|
||||
Protocols.Trojan,
|
||||
Protocols.Shadowsocks,
|
||||
Protocols.Socks,
|
||||
Protocols.HTTP
|
||||
].includes(this.protocol);
|
||||
}
|
||||
|
||||
hasUsername() {
|
||||
return [Protocols.Socks, Protocols.HTTP].includes(this.protocol);
|
||||
}
|
||||
|
||||
static fromJson(json={}) {
|
||||
return new Outbound(
|
||||
json.tag,
|
||||
json.protocol,
|
||||
Outbound.Settings.fromJson(json.protocol, json.settings),
|
||||
StreamSettings.fromJson(json.streamSettings),
|
||||
)
|
||||
}
|
||||
|
||||
toJson() {
|
||||
return {
|
||||
tag: this.tag == '' ? undefined : this.tag,
|
||||
protocol: this.protocol,
|
||||
settings: this.settings instanceof CommonClass ? this.settings.toJson() : this.settings,
|
||||
streamSettings: this.canEnableStream() ? this.stream.toJson() : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
static fromLink(link) {
|
||||
data = link.split('://');
|
||||
if(data.length !=2) return null;
|
||||
switch(data[0].toLowerCase()){
|
||||
case Protocols.VMess:
|
||||
return this.fromVmessLink(JSON.parse(Base64.decode(data[1])));
|
||||
case Protocols.VLESS:
|
||||
case Protocols.Trojan:
|
||||
case 'ss':
|
||||
return this.fromParamLink(link);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static fromVmessLink(json={}){
|
||||
let stream = new StreamSettings(json.net, json.tls);
|
||||
|
||||
let network = json.net;
|
||||
if (network === 'tcp') {
|
||||
stream.tcp = new TcpStreamSettings(
|
||||
json.type,
|
||||
json.host ?? '',
|
||||
json.path ?? '');
|
||||
} else if (network === 'kcp') {
|
||||
stream.kcp = new KcpStreamSettings();
|
||||
stream.type = json.type;
|
||||
stream.seed = json.path;
|
||||
} else if (network === 'ws') {
|
||||
stream.ws = new WsStreamSettings(json.path,json.host);
|
||||
} else if (network === 'http' || network == 'h2') {
|
||||
stream.network = 'http'
|
||||
stream.http = new HttpStreamSettings(
|
||||
json.path,
|
||||
json.host);
|
||||
} else if (network === 'quic') {
|
||||
stream.quic = new QuicStreamSettings(
|
||||
json.host ? json.host : 'none',
|
||||
json.path,
|
||||
json.type ? json.type : 'none');
|
||||
} else if (network === 'grpc') {
|
||||
stream.grpc = new GrpcStreamSettings(json.path, json.type == 'multi');
|
||||
}
|
||||
|
||||
if(json.tls && json.tls == 'tls'){
|
||||
stream.tls = new TlsStreamSettings(
|
||||
json.sni,
|
||||
json.alpn ? json.alpn.split(',') : [],
|
||||
json.fp,
|
||||
json.allowInsecure);
|
||||
}
|
||||
|
||||
|
||||
return new Outbound(json.ps, Protocols.VMess, new Outbound.VmessSettings(json.add, json.port, json.id), stream);
|
||||
}
|
||||
|
||||
static fromParamLink(link){
|
||||
const url = new URL(link);
|
||||
let type = url.searchParams.get('type');
|
||||
let security = url.searchParams.get('security') ?? 'none';
|
||||
let stream = new StreamSettings(type, security);
|
||||
|
||||
let headerType = url.searchParams.get('headerType');
|
||||
let host = url.searchParams.get('host');
|
||||
let path = url.searchParams.get('path');
|
||||
|
||||
if (type === 'tcp') {
|
||||
stream.tcp = new TcpStreamSettings(headerType ?? 'none', host, path);
|
||||
} else if (type === 'kcp') {
|
||||
stream.kcp = new KcpStreamSettings();
|
||||
stream.kcp.type = headerType ?? 'none';
|
||||
stream.kcp.seed = path;
|
||||
} else if (type === 'ws') {
|
||||
stream.ws = new WsStreamSettings(path,host);
|
||||
} else if (type === 'http' || type == 'h2') {
|
||||
stream.http = new HttpStreamSettings(path,host);
|
||||
} else if (type === 'quic') {
|
||||
stream.quic = new QuicStreamSettings(
|
||||
url.searchParams.get('quicSecurity') ?? 'none',
|
||||
url.searchParams.get('key') ?? '',
|
||||
headerType ?? 'none');
|
||||
} else if (type === 'grpc') {
|
||||
stream.grpc = new GrpcStreamSettings(url.searchParams.get('serviceName') ?? '', url.searchParams.get('mode') == 'multi');
|
||||
}
|
||||
|
||||
if(security == 'tls'){
|
||||
let fp=url.searchParams.get('fp') ?? 'none';
|
||||
let alpn=url.searchParams.get('alpn');
|
||||
let allowInsecure=url.searchParams.get('allowInsecure');
|
||||
let sni=url.searchParams.get('sni') ?? '';
|
||||
stream.tls = new TlsStreamSettings(sni, alpn ? alpn.split(',') : [], fp, allowInsecure == 1);
|
||||
}
|
||||
|
||||
if(security == 'reality'){
|
||||
let pbk=url.searchParams.get('pbk');
|
||||
let fp=url.searchParams.get('fp');
|
||||
let sni=url.searchParams.get('sni') ?? '';
|
||||
let sid=url.searchParams.get('sid') ?? '';
|
||||
let spx=url.searchParams.get('spx') ?? '';
|
||||
stream.reality = new RealityStreamSettings(pbk, fp, sni, sid, spx);
|
||||
}
|
||||
|
||||
let data = link.split('?');
|
||||
if(data.length != 2) return null;
|
||||
|
||||
const regex = /([^@]+):\/\/([^@]+)@([^:]+):(\d+)\?(.*)$/;
|
||||
const match = link.match(regex);
|
||||
|
||||
if (!match) return null;
|
||||
let [, protocol, userData, address, port, ] = match;
|
||||
port *= 1;
|
||||
if(protocol == 'ss') {
|
||||
protocol = 'shadowsocks';
|
||||
userData = atob(userData).split(':');
|
||||
}
|
||||
var settings;
|
||||
switch(protocol){
|
||||
case Protocols.VLESS:
|
||||
settings = new Outbound.VLESSSettings(address, port, userData, url.searchParams.get('flow') ?? '');
|
||||
break;
|
||||
case Protocols.Trojan:
|
||||
settings = new Outbound.TrojanSettings(address, port, userData);
|
||||
break;
|
||||
case Protocols.Shadowsocks:
|
||||
let method = userData.splice(0,1)[0];
|
||||
settings = new Outbound.ShadowsocksSettings(address, port, userData.join(":"), method, true);
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
let remark = decodeURIComponent(url.hash);
|
||||
// Remove '#' from url.hash
|
||||
remark = remark.length > 0 ? remark.substring(1) : 'out-' + protocol + '-' + port;
|
||||
return new Outbound(remark, protocol, settings, stream);
|
||||
}
|
||||
}
|
||||
|
||||
Outbound.Settings = class extends CommonClass {
|
||||
constructor(protocol) {
|
||||
super();
|
||||
this.protocol = protocol;
|
||||
}
|
||||
|
||||
static getSettings(protocol) {
|
||||
switch (protocol) {
|
||||
case Protocols.Freedom: return new Outbound.FreedomSettings();
|
||||
case Protocols.Blackhole: return new Outbound.BlackholeSettings();
|
||||
case Protocols.DNS: return new Outbound.DNSSettings();
|
||||
case Protocols.VMess: return new Outbound.VmessSettings();
|
||||
case Protocols.VLESS: return new Outbound.VLESSSettings();
|
||||
case Protocols.Trojan: return new Outbound.TrojanSettings();
|
||||
case Protocols.Shadowsocks: return new Outbound.ShadowsocksSettings();
|
||||
case Protocols.Socks: return new Outbound.SocksSettings();
|
||||
case Protocols.HTTP: return new Outbound.HttpSettings();
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
|
||||
static fromJson(protocol, json) {
|
||||
switch (protocol) {
|
||||
case Protocols.Freedom: return Outbound.FreedomSettings.fromJson(json);
|
||||
case Protocols.Blackhole: return Outbound.BlackholeSettings.fromJson(json);
|
||||
case Protocols.DNS: return Outbound.DNSSettings.fromJson(json);
|
||||
case Protocols.VMess: return Outbound.VmessSettings.fromJson(json);
|
||||
case Protocols.VLESS: return Outbound.VLESSSettings.fromJson(json);
|
||||
case Protocols.Trojan: return Outbound.TrojanSettings.fromJson(json);
|
||||
case Protocols.Shadowsocks: return Outbound.ShadowsocksSettings.fromJson(json);
|
||||
case Protocols.Socks: return Outbound.SocksSettings.fromJson(json);
|
||||
case Protocols.HTTP: return Outbound.HttpSettings.fromJson(json);
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
|
||||
toJson() {
|
||||
return {};
|
||||
}
|
||||
};
|
||||
Outbound.FreedomSettings = class extends CommonClass {
|
||||
constructor(domainStrategy='', fragment={}) {
|
||||
super();
|
||||
this.domainStrategy = domainStrategy;
|
||||
this.fragment = fragment;
|
||||
}
|
||||
|
||||
static fromJson(json={}) {
|
||||
return new Outbound.FreedomSettings(
|
||||
json.domainStrategy,
|
||||
json.fragment ? Outbound.FreedomSettings.Fragment.fromJson(json.fragment) : undefined,
|
||||
);
|
||||
}
|
||||
|
||||
toJson() {
|
||||
return {
|
||||
domainStrategy: ObjectUtil.isEmpty(this.domainStrategy) ? undefined : this.domainStrategy,
|
||||
fragment: Object.keys(this.fragment).length === 0 ? undefined : this.fragment,
|
||||
};
|
||||
}
|
||||
};
|
||||
Outbound.FreedomSettings.Fragment = class extends CommonClass {
|
||||
constructor(packets='1-3',length='',interval=''){
|
||||
super();
|
||||
this.packets = packets;
|
||||
this.length = length;
|
||||
this.interval = interval;
|
||||
}
|
||||
|
||||
static fromJson(json={}) {
|
||||
return new Outbound.FreedomSettings.Fragment(
|
||||
json.packets,
|
||||
json.length,
|
||||
json.interval,
|
||||
);
|
||||
}
|
||||
};
|
||||
Outbound.BlackholeSettings = class extends CommonClass {
|
||||
constructor(type) {
|
||||
super();
|
||||
this.type;
|
||||
}
|
||||
|
||||
static fromJson(json={}) {
|
||||
return new Outbound.BlackholeSettings(
|
||||
json.response ? json.response.type : undefined,
|
||||
);
|
||||
}
|
||||
|
||||
toJson() {
|
||||
return {
|
||||
response: ObjectUtil.isEmpty(this.type) ? undefined : {type: this.type},
|
||||
};
|
||||
}
|
||||
};
|
||||
Outbound.DNSSettings = class extends CommonClass {
|
||||
constructor(network='udp', address='1.1.1.1', port=53) {
|
||||
super();
|
||||
this.network = network;
|
||||
this.address = address;
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
static fromJson(json={}){
|
||||
return new Outbound.DNSSettings(
|
||||
json.network,
|
||||
json.address,
|
||||
json.port,
|
||||
);
|
||||
}
|
||||
};
|
||||
Outbound.VmessSettings = class extends CommonClass {
|
||||
constructor(address, port, id) {
|
||||
super();
|
||||
this.address = address;
|
||||
this.port = port;
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
static fromJson(json={}) {
|
||||
if(ObjectUtil.isArrEmpty(json.vnext)) return new Outbound.VmessSettings();
|
||||
return new Outbound.VmessSettings(
|
||||
json.vnext[0].address,
|
||||
json.vnext[0].port,
|
||||
json.vnext[0].users[0].id,
|
||||
);
|
||||
}
|
||||
|
||||
toJson() {
|
||||
return {
|
||||
vnext: [{
|
||||
address: this.address,
|
||||
port: this.port,
|
||||
users: [{id: this.id}],
|
||||
}],
|
||||
};
|
||||
}
|
||||
};
|
||||
Outbound.VLESSSettings = class extends CommonClass {
|
||||
constructor(address, port, id, flow, encryption='none') {
|
||||
super();
|
||||
this.address = address;
|
||||
this.port = port;
|
||||
this.id = id;
|
||||
this.flow = flow;
|
||||
this.encryption = encryption
|
||||
}
|
||||
|
||||
static fromJson(json={}) {
|
||||
if(ObjectUtil.isArrEmpty(json.vnext)) return new Outbound.VLESSSettings();
|
||||
return new Outbound.VLESSSettings(
|
||||
json.vnext[0].address,
|
||||
json.vnext[0].port,
|
||||
json.vnext[0].users[0].id,
|
||||
json.vnext[0].users[0].flow,
|
||||
json.vnext[0].users[0].encryption,
|
||||
);
|
||||
}
|
||||
|
||||
toJson() {
|
||||
return {
|
||||
vnext: [{
|
||||
address: this.address,
|
||||
port: this.port,
|
||||
users: [{id: this.id, flow: this.flow, encryption: 'none',}],
|
||||
}],
|
||||
};
|
||||
}
|
||||
};
|
||||
Outbound.TrojanSettings = class extends CommonClass {
|
||||
constructor(address, port, password) {
|
||||
super();
|
||||
this.address = address;
|
||||
this.port = port;
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
static fromJson(json={}) {
|
||||
if(ObjectUtil.isArrEmpty(json.servers)) return new Outbound.TrojanSettings();
|
||||
return new Outbound.TrojanSettings(
|
||||
json.servers[0].address,
|
||||
json.servers[0].port,
|
||||
json.servers[0].password,
|
||||
);
|
||||
}
|
||||
|
||||
toJson() {
|
||||
return {
|
||||
servers: [{
|
||||
address: this.address,
|
||||
port: this.port,
|
||||
password: this.password,
|
||||
}],
|
||||
};
|
||||
}
|
||||
};
|
||||
Outbound.ShadowsocksSettings = class extends CommonClass {
|
||||
constructor(address, port, password, method, uot) {
|
||||
super();
|
||||
this.address = address;
|
||||
this.port = port;
|
||||
this.password = password;
|
||||
this.method = method;
|
||||
this.uot = uot;
|
||||
}
|
||||
|
||||
static fromJson(json={}) {
|
||||
let servers = json.servers;
|
||||
if(ObjectUtil.isArrEmpty(servers)) servers=[{}];
|
||||
return new Outbound.ShadowsocksSettings(
|
||||
servers[0].address,
|
||||
servers[0].port,
|
||||
servers[0].password,
|
||||
servers[0].method,
|
||||
servers[0].uot,
|
||||
);
|
||||
}
|
||||
|
||||
toJson() {
|
||||
return {
|
||||
servers: [{
|
||||
address: this.address,
|
||||
port: this.port,
|
||||
password: this.password,
|
||||
method: this.method,
|
||||
uot: this.uot,
|
||||
}],
|
||||
};
|
||||
}
|
||||
};
|
||||
Outbound.SocksSettings = class extends CommonClass {
|
||||
constructor(address, port, user, password) {
|
||||
super();
|
||||
this.address = address;
|
||||
this.port = port;
|
||||
this.user = user;
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
static fromJson(json={}) {
|
||||
servers = json.servers;
|
||||
if(ObjectUtil.isArrEmpty(servers)) servers=[{users: [{}]}];
|
||||
return new Outbound.SocksSettings(
|
||||
servers[0].address,
|
||||
servers[0].port,
|
||||
ObjectUtil.isArrEmpty(servers[0].users) ? '' : servers[0].users[0].user,
|
||||
ObjectUtil.isArrEmpty(servers[0].password) ? '' : servers[0].users[0].password,
|
||||
);
|
||||
}
|
||||
|
||||
toJson() {
|
||||
return {
|
||||
servers: [{
|
||||
address: this.address,
|
||||
port: this.port,
|
||||
users: ObjectUtil.isEmpty(this.user) ? [] : [{user: this.user, password: this.password}],
|
||||
}],
|
||||
};
|
||||
}
|
||||
};
|
||||
Outbound.HttpSettings = class extends CommonClass {
|
||||
constructor(address, port, user, password) {
|
||||
super();
|
||||
this.address = address;
|
||||
this.port = port;
|
||||
this.user = user;
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
static fromJson(json={}) {
|
||||
servers = json.servers;
|
||||
if(ObjectUtil.isArrEmpty(servers)) servers=[{users: [{}]}];
|
||||
return new Outbound.HttpSettings(
|
||||
servers[0].address,
|
||||
servers[0].port,
|
||||
ObjectUtil.isArrEmpty(servers[0].users) ? '' : servers[0].users[0].user,
|
||||
ObjectUtil.isArrEmpty(servers[0].password) ? '' : servers[0].users[0].password,
|
||||
);
|
||||
}
|
||||
|
||||
toJson() {
|
||||
return {
|
||||
servers: [{
|
||||
address: this.address,
|
||||
port: this.port,
|
||||
users: ObjectUtil.isEmpty(this.user) ? [] : [{user: this.user, password: this.password}],
|
||||
}],
|
||||
};
|
||||
}
|
||||
};
|
||||
46
web/assets/js/model/setting.js
Normal file
46
web/assets/js/model/setting.js
Normal file
@@ -0,0 +1,46 @@
|
||||
class AllSetting {
|
||||
|
||||
constructor(data) {
|
||||
this.webListen = "";
|
||||
this.webDomain = "";
|
||||
this.webPort = 54321;
|
||||
this.webCertFile = "";
|
||||
this.webKeyFile = "";
|
||||
this.webBasePath = "/";
|
||||
this.sessionMaxAge = "";
|
||||
this.pageSize = 0;
|
||||
this.expireDiff = "";
|
||||
this.trafficDiff = "";
|
||||
this.remarkModel = "-ieo";
|
||||
this.tgBotEnable = false;
|
||||
this.tgBotToken = "";
|
||||
this.tgBotChatId = "";
|
||||
this.tgRunTime = "@daily";
|
||||
this.tgBotBackup = false;
|
||||
this.tgBotLoginNotify = false;
|
||||
this.tgCpu = "";
|
||||
this.tgLang = "";
|
||||
this.subEnable = false;
|
||||
this.subListen = "";
|
||||
this.subPort = "2096";
|
||||
this.subPath = "/sub/";
|
||||
this.subDomain = "";
|
||||
this.subCertFile = "";
|
||||
this.subKeyFile = "";
|
||||
this.subUpdates = 0;
|
||||
this.subEncrypt = true;
|
||||
this.subShowInfo = false;
|
||||
this.subURI = '';
|
||||
|
||||
this.timeLocation = "Asia/Tehran";
|
||||
|
||||
if (data == null) {
|
||||
return
|
||||
}
|
||||
ObjectUtil.cloneProps(this, data);
|
||||
}
|
||||
|
||||
equals(other) {
|
||||
return ObjectUtil.equals(this, other);
|
||||
}
|
||||
}
|
||||
@@ -8,13 +8,6 @@ const Protocols = {
|
||||
HTTP: 'http',
|
||||
};
|
||||
|
||||
const VmessMethods = {
|
||||
AES_128_GCM: 'aes-128-gcm',
|
||||
CHACHA20_POLY1305: 'chacha20-poly1305',
|
||||
AUTO: 'auto',
|
||||
NONE: 'none',
|
||||
};
|
||||
|
||||
const SSMethods = {
|
||||
AES_256_GCM: 'aes-256-gcm',
|
||||
AES_128_GCM: 'aes-128-gcm',
|
||||
@@ -86,7 +79,6 @@ const SNIFFING_OPTION = {
|
||||
};
|
||||
|
||||
Object.freeze(Protocols);
|
||||
Object.freeze(VmessMethods);
|
||||
Object.freeze(SSMethods);
|
||||
Object.freeze(TLS_FLOW_CONTROL);
|
||||
Object.freeze(TLS_VERSION_OPTION);
|
||||
@@ -413,7 +405,7 @@ class HttpStreamSettings extends XrayCommonClass {
|
||||
}
|
||||
|
||||
class QuicStreamSettings extends XrayCommonClass {
|
||||
constructor(security=VmessMethods.NONE,
|
||||
constructor(security='none',
|
||||
key='', type='none') {
|
||||
super();
|
||||
this.security = security;
|
||||
@@ -469,7 +461,7 @@ class TlsStreamSettings extends XrayCommonClass {
|
||||
alpn=[],
|
||||
settings=new TlsStreamSettings.Settings()) {
|
||||
super();
|
||||
this.server = serverName;
|
||||
this.sni = serverName;
|
||||
this.minVersion = minVersion;
|
||||
this.maxVersion = maxVersion;
|
||||
this.cipherSuites = cipherSuites;
|
||||
@@ -511,7 +503,7 @@ class TlsStreamSettings extends XrayCommonClass {
|
||||
|
||||
toJson() {
|
||||
return {
|
||||
serverName: this.server,
|
||||
serverName: this.sni,
|
||||
minVersion: this.minVersion,
|
||||
maxVersion: this.maxVersion,
|
||||
cipherSuites: this.cipherSuites,
|
||||
@@ -570,27 +562,21 @@ TlsStreamSettings.Cert = class extends XrayCommonClass {
|
||||
};
|
||||
|
||||
TlsStreamSettings.Settings = class extends XrayCommonClass {
|
||||
constructor(allowInsecure = false, fingerprint = '', serverName = '', domains = []) {
|
||||
constructor(allowInsecure = false, fingerprint = '') {
|
||||
super();
|
||||
this.allowInsecure = allowInsecure;
|
||||
this.fingerprint = fingerprint;
|
||||
this.serverName = serverName;
|
||||
this.domains = domains;
|
||||
}
|
||||
static fromJson(json = {}) {
|
||||
return new TlsStreamSettings.Settings(
|
||||
json.allowInsecure,
|
||||
json.fingerprint,
|
||||
json.serverName,
|
||||
json.domains,
|
||||
);
|
||||
}
|
||||
toJson() {
|
||||
return {
|
||||
allowInsecure: this.allowInsecure,
|
||||
fingerprint: this.fingerprint,
|
||||
serverName: this.serverName,
|
||||
domains: this.domains,
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -708,6 +694,7 @@ class SockoptStreamSettings extends XrayCommonClass {
|
||||
class StreamSettings extends XrayCommonClass {
|
||||
constructor(network='tcp',
|
||||
security='none',
|
||||
externalProxy = [],
|
||||
tlsSettings=new TlsStreamSettings(),
|
||||
realitySettings = new RealityStreamSettings(),
|
||||
tcpSettings=new TcpStreamSettings(),
|
||||
@@ -721,6 +708,7 @@ class StreamSettings extends XrayCommonClass {
|
||||
super();
|
||||
this.network = network;
|
||||
this.security = security;
|
||||
this.externalProxy = externalProxy;
|
||||
this.tls = tlsSettings;
|
||||
this.reality = realitySettings;
|
||||
this.tcp = tcpSettings;
|
||||
@@ -765,10 +753,10 @@ class StreamSettings extends XrayCommonClass {
|
||||
}
|
||||
|
||||
static fromJson(json={}) {
|
||||
|
||||
return new StreamSettings(
|
||||
json.network,
|
||||
json.security,
|
||||
json.externalProxy,
|
||||
TlsStreamSettings.fromJson(json.tlsSettings),
|
||||
RealityStreamSettings.fromJson(json.realitySettings),
|
||||
TcpStreamSettings.fromJson(json.tcpSettings),
|
||||
@@ -786,6 +774,7 @@ class StreamSettings extends XrayCommonClass {
|
||||
return {
|
||||
network: network,
|
||||
security: this.security,
|
||||
externalProxy: this.externalProxy,
|
||||
tlsSettings: this.isTls ? this.tls.toJson() : undefined,
|
||||
realitySettings: this.isReality ? this.reality.toJson() : undefined,
|
||||
tcpSettings: network === 'tcp' ? this.tcp.toJson() : undefined,
|
||||
@@ -844,6 +833,16 @@ class Inbound extends XrayCommonClass {
|
||||
return this.clientStats;
|
||||
}
|
||||
|
||||
get clients() {
|
||||
switch (this.protocol) {
|
||||
case Protocols.VMESS: return this.settings.vmesses;
|
||||
case Protocols.VLESS: return this.settings.vlesses;
|
||||
case Protocols.TROJAN: return this.settings.trojans;
|
||||
case Protocols.SHADOWSOCKS: return this.isSSMultiUser ? this.settings.shadowsockses : null;
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
|
||||
get protocol() {
|
||||
return this._protocol;
|
||||
}
|
||||
@@ -852,31 +851,7 @@ class Inbound extends XrayCommonClass {
|
||||
this._protocol = protocol;
|
||||
this.settings = Inbound.Settings.getSettings(protocol);
|
||||
if (protocol === Protocols.TROJAN) {
|
||||
this.tls = true;
|
||||
}
|
||||
}
|
||||
|
||||
get tls() {
|
||||
return this.stream.security === 'tls';
|
||||
}
|
||||
|
||||
set tls(isTls) {
|
||||
if (isTls) {
|
||||
this.stream.security = 'tls';
|
||||
} else {
|
||||
this.stream.security = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
get reality() {
|
||||
return this.stream.security === 'reality';
|
||||
}
|
||||
|
||||
set reality(isReality) {
|
||||
if (isReality) {
|
||||
this.stream.security = 'reality';
|
||||
} else {
|
||||
this.stream.security = 'none';
|
||||
this.stream.isTls = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -929,9 +904,8 @@ class Inbound extends XrayCommonClass {
|
||||
}
|
||||
|
||||
get serverName() {
|
||||
if (this.stream.isTls || this.stream.isReality) {
|
||||
return this.stream.tls.server;
|
||||
}
|
||||
if (this.stream.isTls) return this.stream.tls.sni;
|
||||
if (this.stream.isReality) return this.stream.reality.serverNames;
|
||||
return "";
|
||||
}
|
||||
|
||||
@@ -982,108 +956,34 @@ class Inbound extends XrayCommonClass {
|
||||
}
|
||||
|
||||
isExpiry(index) {
|
||||
switch (this.protocol) {
|
||||
case Protocols.VMESS:
|
||||
if(this.settings.vmesses[index].expiryTime > 0)
|
||||
return this.settings.vmesses[index].expiryTime < new Date().getTime();
|
||||
return false
|
||||
case Protocols.VLESS:
|
||||
if(this.settings.vlesses[index].expiryTime > 0)
|
||||
return this.settings.vlesses[index].expiryTime < new Date().getTime();
|
||||
return false
|
||||
case Protocols.TROJAN:
|
||||
if(this.settings.trojans[index].expiryTime > 0)
|
||||
return this.settings.trojans[index].expiryTime < new Date().getTime();
|
||||
return false
|
||||
case Protocols.SHADOWSOCKS:
|
||||
if(this.settings.shadowsockses.length > 0 && this.settings.shadowsockses[index].expiryTime > 0)
|
||||
return this.settings.shadowsockses[index].expiryTime < new Date().getTime();
|
||||
return false
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
let exp = this.clients[index].expiryTime;
|
||||
return exp > 0 ? exp < new Date().getTime() : false;
|
||||
}
|
||||
|
||||
canEnableTls() {
|
||||
switch (this.protocol) {
|
||||
case Protocols.VMESS:
|
||||
case Protocols.VLESS:
|
||||
case Protocols.TROJAN:
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (this.network) {
|
||||
case "tcp":
|
||||
case "ws":
|
||||
case "http":
|
||||
case "quic":
|
||||
case "grpc":
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
if(![Protocols.VMESS, Protocols.VLESS, Protocols.TROJAN, Protocols.SHADOWSOCKS].includes(this.protocol)) return false;
|
||||
return ["tcp", "ws", "http", "quic", "grpc"].includes(this.network);
|
||||
}
|
||||
|
||||
//this is used for xtls-rprx-vision
|
||||
canEnableTlsFlow() {
|
||||
if ((this.stream.security != 'none') && (this.network === "tcp")) {
|
||||
switch (this.protocol) {
|
||||
case Protocols.VLESS:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
return this.protocol === Protocols.VLESS;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
canSetTls() {
|
||||
return this.canEnableTls();
|
||||
}
|
||||
|
||||
canEnableReality() {
|
||||
switch (this.protocol) {
|
||||
case Protocols.VLESS:
|
||||
case Protocols.TROJAN:
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (this.network) {
|
||||
case "tcp":
|
||||
case "http":
|
||||
case "grpc":
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
if(![Protocols.VLESS, Protocols.TROJAN].includes(this.protocol)) return false;
|
||||
return ["tcp", "http", "grpc"].includes(this.network);
|
||||
}
|
||||
|
||||
canEnableStream() {
|
||||
switch (this.protocol) {
|
||||
case Protocols.VMESS:
|
||||
case Protocols.VLESS:
|
||||
case Protocols.TROJAN:
|
||||
case Protocols.SHADOWSOCKS:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
return [Protocols.VMESS, Protocols.VLESS, Protocols.TROJAN, Protocols.SHADOWSOCKS].includes(this.protocol);
|
||||
}
|
||||
|
||||
canSniffing() {
|
||||
switch (this.protocol) {
|
||||
case Protocols.VMESS:
|
||||
case Protocols.VLESS:
|
||||
case Protocols.TROJAN:
|
||||
case Protocols.SHADOWSOCKS:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
return [Protocols.VMESS, Protocols.VLESS, Protocols.TROJAN, Protocols.SHADOWSOCKS].includes(this.protocol);
|
||||
}
|
||||
|
||||
reset() {
|
||||
@@ -1096,19 +996,20 @@ class Inbound extends XrayCommonClass {
|
||||
this.sniffing = new Sniffing();
|
||||
}
|
||||
|
||||
genVmessLink(address='', remark='', clientIndex=0) {
|
||||
genVmessLink(address='', port=this.port, forceTls, remark='', clientId) {
|
||||
if (this.protocol !== Protocols.VMESS) {
|
||||
return '';
|
||||
}
|
||||
const security = forceTls == 'same' ? this.stream.security : forceTls;
|
||||
let obj = {
|
||||
v: '2',
|
||||
ps: remark,
|
||||
add: address,
|
||||
port: this.port,
|
||||
id: this.settings.vmesses[clientIndex].id,
|
||||
port: port,
|
||||
id: clientId,
|
||||
net: this.stream.network,
|
||||
type: 'none',
|
||||
tls: this.stream.security,
|
||||
tls: security,
|
||||
};
|
||||
let network = this.stream.network;
|
||||
if (network === 'tcp') {
|
||||
@@ -1148,12 +1049,9 @@ class Inbound extends XrayCommonClass {
|
||||
}
|
||||
}
|
||||
|
||||
if (this.stream.security === 'tls') {
|
||||
if (!ObjectUtil.isEmpty(this.stream.tls.server)) {
|
||||
obj.add = this.stream.tls.server;
|
||||
}
|
||||
if (!ObjectUtil.isEmpty(this.stream.tls.settings.serverName)){
|
||||
obj.sni = this.stream.tls.settings.serverName;
|
||||
if (security === 'tls') {
|
||||
if (!ObjectUtil.isEmpty(this.stream.tls.sni)){
|
||||
obj.sni = this.stream.tls.sni;
|
||||
}
|
||||
if (!ObjectUtil.isEmpty(this.stream.tls.settings.fingerprint)){
|
||||
obj.fp = this.stream.tls.settings.fingerprint;
|
||||
@@ -1169,11 +1067,10 @@ class Inbound extends XrayCommonClass {
|
||||
return 'vmess://' + base64(JSON.stringify(obj, null, 2));
|
||||
}
|
||||
|
||||
genVLESSLink(address = '', remark='', clientIndex=0) {
|
||||
const settings = this.settings;
|
||||
const uuid = settings.vlesses[clientIndex].id;
|
||||
const port = this.port;
|
||||
genVLESSLink(address = '', port=this.port, forceTls, remark='', clientId, flow) {
|
||||
const uuid = clientId;
|
||||
const type = this.stream.network;
|
||||
const security = forceTls == 'same' ? this.stream.security : forceTls;
|
||||
const params = new Map();
|
||||
params.set("type", this.stream.network);
|
||||
switch (type) {
|
||||
@@ -1224,25 +1121,24 @@ class Inbound extends XrayCommonClass {
|
||||
break;
|
||||
}
|
||||
|
||||
if (this.tls) {
|
||||
if (security === 'tls') {
|
||||
params.set("security", "tls");
|
||||
params.set("fp" , this.stream.tls.settings.fingerprint);
|
||||
params.set("alpn", this.stream.tls.alpn);
|
||||
if(this.stream.tls.settings.allowInsecure){
|
||||
params.set("allowInsecure", "1");
|
||||
}
|
||||
if (!ObjectUtil.isEmpty(this.stream.tls.server)) {
|
||||
address = this.stream.tls.server;
|
||||
}
|
||||
if (this.stream.tls.settings.serverName !== ''){
|
||||
params.set("sni", this.stream.tls.settings.serverName);
|
||||
}
|
||||
if (type === "tcp" && this.settings.vlesses[clientIndex].flow.length > 0) {
|
||||
params.set("flow", this.settings.vlesses[clientIndex].flow);
|
||||
if (this.stream.isTls){
|
||||
params.set("fp" , this.stream.tls.settings.fingerprint);
|
||||
params.set("alpn", this.stream.tls.alpn);
|
||||
if(this.stream.tls.settings.allowInsecure){
|
||||
params.set("allowInsecure", "1");
|
||||
}
|
||||
if (!ObjectUtil.isEmpty(this.stream.tls.sni)){
|
||||
params.set("sni", this.stream.tls.sni);
|
||||
}
|
||||
if (type == "tcp" && !ObjectUtil.isEmpty(flow)) {
|
||||
params.set("flow", flow);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.reality) {
|
||||
if (security === 'reality') {
|
||||
params.set("security", "reality");
|
||||
params.set("pbk", this.stream.reality.settings.publicKey);
|
||||
params.set("fp", this.stream.reality.settings.fingerprint);
|
||||
@@ -1252,14 +1148,11 @@ class Inbound extends XrayCommonClass {
|
||||
if (this.stream.reality.shortIds.length > 0) {
|
||||
params.set("sid", this.stream.reality.shortIds.split(",")[0]);
|
||||
}
|
||||
if (!ObjectUtil.isEmpty(this.stream.reality.settings.serverName)) {
|
||||
address = this.stream.reality.settings.serverName;
|
||||
}
|
||||
if (!ObjectUtil.isEmpty(this.stream.reality.settings.spiderX)) {
|
||||
params.set("spx", this.stream.reality.settings.spiderX);
|
||||
}
|
||||
if (this.stream.network === 'tcp' && !ObjectUtil.isEmpty(this.settings.vlesses[clientIndex].flow)) {
|
||||
params.set("flow", this.settings.vlesses[clientIndex].flow);
|
||||
if (type == 'tcp' && !ObjectUtil.isEmpty(flow)) {
|
||||
params.set("flow", flow);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1272,10 +1165,10 @@ class Inbound extends XrayCommonClass {
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
genSSLink(address='', remark='', clientIndex = 0) {
|
||||
genSSLink(address='', port=this.port, forceTls, remark='', clientPassword) {
|
||||
let settings = this.settings;
|
||||
const port = this.port;
|
||||
const type = this.stream.network;
|
||||
const security = forceTls == 'same' ? this.stream.security : forceTls;
|
||||
const params = new Map();
|
||||
params.set("type", this.stream.network);
|
||||
switch (type) {
|
||||
@@ -1326,11 +1219,26 @@ class Inbound extends XrayCommonClass {
|
||||
break;
|
||||
}
|
||||
|
||||
if (security === 'tls') {
|
||||
params.set("security", "tls");
|
||||
if (this.stream.isTls){
|
||||
params.set("fp" , this.stream.tls.settings.fingerprint);
|
||||
params.set("alpn", this.stream.tls.alpn);
|
||||
if(this.stream.tls.settings.allowInsecure){
|
||||
params.set("allowInsecure", "1");
|
||||
}
|
||||
if (!ObjectUtil.isEmpty(this.stream.tls.sni)){
|
||||
params.set("sni", this.stream.tls.sni);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let password = new Array();
|
||||
if (this.isSS2022) password.push(settings.password);
|
||||
if (this.isSSMultiUser) password.push(settings.shadowsockses[clientIndex].password);
|
||||
if (this.isSSMultiUser) password.push(clientPassword);
|
||||
|
||||
let link = `ss://${safeBase64(settings.method + ':' + password.join(':'))}@${address}:${this.port}`;
|
||||
let link = `ss://${safeBase64(settings.method + ':' + password.join(':'))}@${address}:${port}`;
|
||||
const url = new URL(link);
|
||||
for (const [key, value] of params) {
|
||||
url.searchParams.set(key, value)
|
||||
@@ -1339,9 +1247,8 @@ class Inbound extends XrayCommonClass {
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
genTrojanLink(address = '', remark = '', clientIndex = 0) {
|
||||
let settings = this.settings;
|
||||
const port = this.port;
|
||||
genTrojanLink(address = '', port=this.port, forceTls, remark = '', clientPassword) {
|
||||
const security = forceTls == 'same' ? this.stream.security : forceTls;
|
||||
const type = this.stream.network;
|
||||
const params = new Map();
|
||||
params.set("type", this.stream.network);
|
||||
@@ -1393,22 +1300,21 @@ class Inbound extends XrayCommonClass {
|
||||
break;
|
||||
}
|
||||
|
||||
if (this.tls) {
|
||||
if (security === 'tls') {
|
||||
params.set("security", "tls");
|
||||
params.set("fp" , this.stream.tls.settings.fingerprint);
|
||||
params.set("alpn", this.stream.tls.alpn);
|
||||
if(this.stream.tls.settings.allowInsecure){
|
||||
params.set("allowInsecure", "1");
|
||||
if (this.stream.isTls){
|
||||
params.set("fp" , this.stream.tls.settings.fingerprint);
|
||||
params.set("alpn", this.stream.tls.alpn);
|
||||
if(this.stream.tls.settings.allowInsecure){
|
||||
params.set("allowInsecure", "1");
|
||||
}
|
||||
if (!ObjectUtil.isEmpty(this.stream.tls.sni)){
|
||||
params.set("sni", this.stream.tls.sni);
|
||||
}
|
||||
}
|
||||
if (!ObjectUtil.isEmpty(this.stream.tls.server)) {
|
||||
address = this.stream.tls.server;
|
||||
}
|
||||
if (this.stream.tls.settings.serverName !== ''){
|
||||
params.set("sni", this.stream.tls.settings.serverName);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.reality) {
|
||||
if (security === 'reality') {
|
||||
params.set("security", "reality");
|
||||
params.set("pbk", this.stream.reality.settings.publicKey);
|
||||
params.set("fp", this.stream.reality.settings.fingerprint);
|
||||
@@ -1418,15 +1324,12 @@ class Inbound extends XrayCommonClass {
|
||||
if (this.stream.reality.shortIds.length > 0) {
|
||||
params.set("sid", this.stream.reality.shortIds.split(",")[0]);
|
||||
}
|
||||
if (!ObjectUtil.isEmpty(this.stream.reality.settings.serverName)) {
|
||||
address = this.stream.reality.settings.serverName;
|
||||
}
|
||||
if (!ObjectUtil.isEmpty(this.stream.reality.settings.spiderX)) {
|
||||
params.set("spx", this.stream.reality.settings.spiderX);
|
||||
}
|
||||
}
|
||||
|
||||
const link = `trojan://${settings.trojans[clientIndex].password}@${address}:${this.port}`;
|
||||
const link = `trojan://${clientPassword}@${address}:${port}`;
|
||||
const url = new URL(link);
|
||||
for (const [key, value] of params) {
|
||||
url.searchParams.set(key, value)
|
||||
@@ -1435,38 +1338,63 @@ class Inbound extends XrayCommonClass {
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
genLink(address='', remark='', clientIndex=0) {
|
||||
genLink(address='', port=this.port, forceTls='same', remark='', client) {
|
||||
switch (this.protocol) {
|
||||
case Protocols.VMESS:
|
||||
return this.genVmessLink(address, remark, clientIndex);
|
||||
return this.genVmessLink(address, port, forceTls, remark, client.id);
|
||||
case Protocols.VLESS:
|
||||
return this.genVLESSLink(address, remark, clientIndex);
|
||||
return this.genVLESSLink(address, port, forceTls, remark, client.id, client.flow);
|
||||
case Protocols.SHADOWSOCKS:
|
||||
return this.genSSLink(address, remark, clientIndex);
|
||||
return this.genSSLink(address, port, forceTls, remark, this.isSSMultiUser ? client.password : '');
|
||||
case Protocols.TROJAN:
|
||||
return this.genTrojanLink(address, remark, clientIndex);
|
||||
return this.genTrojanLink(address, port, forceTls, remark, client.password);
|
||||
default: return '';
|
||||
}
|
||||
}
|
||||
|
||||
genInboundLinks(address = '', remark = '') {
|
||||
let link = '';
|
||||
switch (this.protocol) {
|
||||
case Protocols.VMESS:
|
||||
case Protocols.VLESS:
|
||||
case Protocols.TROJAN:
|
||||
case Protocols.SHADOWSOCKS:
|
||||
JSON.parse(this.settings).clients.forEach((client,index) => {
|
||||
if(this.tls && !ObjectUtil.isArrEmpty(this.stream.tls.settings.domains)){
|
||||
this.stream.tls.settings.domains.forEach((domain) => {
|
||||
link += this.genLink(domain.domain, [remark, client.email, domain.remark].filter(x => x.length > 0).join('-'), index) + '\r\n';
|
||||
});
|
||||
} else {
|
||||
link += this.genLink(address, [remark, client.email].filter(x => x.length > 0).join('-'), index) + '\r\n';
|
||||
}
|
||||
genAllLinks(remark='', remarkModel = '-ieo', client){
|
||||
let result = [];
|
||||
let email = client ? client.email : '';
|
||||
let addr = !ObjectUtil.isEmpty(this.listen) && this.listen !== "0.0.0.0" ? this.listen : location.hostname;
|
||||
let port = this.port;
|
||||
const separationChar = remarkModel.charAt(0);
|
||||
const orderChars = remarkModel.slice(1);
|
||||
let orders = {
|
||||
'i': remark,
|
||||
'e': client ? client.email : '',
|
||||
'o': '',
|
||||
};
|
||||
if(ObjectUtil.isArrEmpty(this.stream.externalProxy)){
|
||||
let r = orderChars.split('').map(char => orders[char]).filter(x => x.length > 0).join(separationChar);
|
||||
result.push({
|
||||
remark: r,
|
||||
link: this.genLink(addr, port, 'same', r, client)
|
||||
});
|
||||
} else {
|
||||
this.stream.externalProxy.forEach((ep) => {
|
||||
orders['o'] = ep.remark;
|
||||
let r = orderChars.split('').map(char => orders[char]).filter(x => x.length > 0).join(separationChar);
|
||||
result.push({
|
||||
remark: r,
|
||||
link: this.genLink(ep.dest, ep.port, ep.forceTls, r, client)
|
||||
});
|
||||
return link;
|
||||
default: return '';
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
genInboundLinks(remark = '', remarkModel = '-ieo') {
|
||||
if(this.clients){
|
||||
let links = [];
|
||||
this.clients.forEach((client) => {
|
||||
this.genAllLinks(remark,remarkModel,client).forEach(l => {
|
||||
links.push(l.link);
|
||||
})
|
||||
});
|
||||
return links.join('\r\n');
|
||||
} else {
|
||||
if(this.protocol == Protocols.SHADOWSOCKS && !this.isSSMultiUser) return this.genSSLink(this.listen, this.port, 'same', remark);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1485,7 +1413,7 @@ class Inbound extends XrayCommonClass {
|
||||
|
||||
toJson() {
|
||||
let streamSettings;
|
||||
if (this.canEnableStream() || this.protocol === Protocols.TROJAN) {
|
||||
if (this.canEnableStream()) {
|
||||
streamSettings = this.stream.toJson();
|
||||
}
|
||||
return {
|
||||
@@ -1635,7 +1563,7 @@ Inbound.VLESSSettings = class extends Inbound.Settings {
|
||||
fallbacks=[],) {
|
||||
super(protocol);
|
||||
this.vlesses = vlesses;
|
||||
this.decryption = 'none'; // Using decryption is not implemented here
|
||||
this.decryption = decryption;
|
||||
this.fallbacks = fallbacks;
|
||||
}
|
||||
|
||||
@@ -1767,11 +1695,11 @@ Inbound.TrojanSettings = class extends Inbound.Settings {
|
||||
this.fallbacks = fallbacks;
|
||||
}
|
||||
|
||||
addTrojanFallback() {
|
||||
addFallback() {
|
||||
this.fallbacks.push(new Inbound.TrojanSettings.Fallback());
|
||||
}
|
||||
|
||||
delTrojanFallback(index) {
|
||||
delFallback(index) {
|
||||
this.fallbacks.splice(index, 1);
|
||||
}
|
||||
|
||||
|
||||
@@ -110,25 +110,38 @@ function usageColor(data, threshold, total) {
|
||||
}
|
||||
}
|
||||
|
||||
function clientUsageColor(clientStats, trafficDiff) {
|
||||
switch (true) {
|
||||
case !clientStats || clientStats.total == 0:
|
||||
return "#7a316f"; // Purple
|
||||
case clientStats.up + clientStats.down < clientStats.total - trafficDiff:
|
||||
return "#0e49b5"; // Blue
|
||||
case clientStats.up + clientStats.down < clientStats.total:
|
||||
return "#f37b24"; // Orange
|
||||
default:
|
||||
return "#e04141"; // red
|
||||
}
|
||||
}
|
||||
|
||||
function userExpiryColor(threshold, client, isDark = false) {
|
||||
if (!client.enable) {
|
||||
return isDark ? '#2c3950' : '#bcbcbc';
|
||||
return isDark ? '#2c3950' : '#bcbcbc'; // Gray
|
||||
}
|
||||
now = new Date().getTime(),
|
||||
expiry = client.expiryTime;
|
||||
switch (true) {
|
||||
case expiry === null:
|
||||
return "#389e0d";
|
||||
return "#7a316f"; // Purple
|
||||
case expiry < 0:
|
||||
return "#0e49b5";
|
||||
return "#0e49b5"; // Blue
|
||||
case expiry == 0:
|
||||
return "#7a316f";
|
||||
return "#7a316f"; // Purple
|
||||
case now < expiry - threshold:
|
||||
return "#0e49b5";
|
||||
return "#0e49b5"; // Blue
|
||||
case now < expiry:
|
||||
return "#ffa031";
|
||||
return "#f37b24"; // Orange
|
||||
default:
|
||||
return "#e04141";
|
||||
return "#e04141"; // red
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
class Msg {
|
||||
constructor(success, msg, obj) {
|
||||
this.success = false;
|
||||
this.msg = "";
|
||||
this.obj = null;
|
||||
|
||||
if (success != null) {
|
||||
this.success = success;
|
||||
}
|
||||
if (msg != null) {
|
||||
this.msg = msg;
|
||||
}
|
||||
if (obj != null) {
|
||||
this.obj = obj;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class HttpUtil {
|
||||
static _handleMsg(msg) {
|
||||
if (!(msg instanceof Msg)) {
|
||||
@@ -159,7 +177,7 @@ class ObjectUtil {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return obj.toString().toLowerCase().indexOf(key.toLowerCase()) >= 0;
|
||||
return this.isEmpty(obj) ? false : obj.toString().toLowerCase().indexOf(key.toLowerCase()) >= 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ func (a *APIController) initRouter(g *gin.RouterGroup) {
|
||||
g.POST("/resetAllClientTraffics/:id", a.resetAllClientTraffics)
|
||||
g.POST("/delDepletedClients/:id", a.delDepletedClients)
|
||||
g.GET("/createbackup", a.createBackup)
|
||||
g.POST("/onlines", a.onlines)
|
||||
|
||||
a.inboundController = NewInboundController(g)
|
||||
}
|
||||
@@ -95,3 +96,7 @@ func (a *APIController) delDepletedClients(c *gin.Context) {
|
||||
func (a *APIController) createBackup(c *gin.Context) {
|
||||
a.Tgbot.SendBackupToAdmins()
|
||||
}
|
||||
|
||||
func (a *APIController) onlines(c *gin.Context) {
|
||||
a.inboundController.onlines(c)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"x-ui/database/model"
|
||||
@@ -35,6 +36,7 @@ func (a *InboundController) initRouter(g *gin.RouterGroup) {
|
||||
g.POST("/resetAllTraffics", a.resetAllTraffics)
|
||||
g.POST("/resetAllClientTraffics/:id", a.resetAllClientTraffics)
|
||||
g.POST("/delDepletedClients/:id", a.delDepletedClients)
|
||||
g.POST("/import", a.importInbound)
|
||||
g.POST("/onlines", a.onlines)
|
||||
}
|
||||
|
||||
@@ -254,6 +256,31 @@ func (a *InboundController) delDepletedClients(c *gin.Context) {
|
||||
jsonMsg(c, "All delpeted clients are deleted", nil)
|
||||
}
|
||||
|
||||
func (a *InboundController) importInbound(c *gin.Context) {
|
||||
inbound := &model.Inbound{}
|
||||
err := json.Unmarshal([]byte(c.PostForm("data")), inbound)
|
||||
if err != nil {
|
||||
jsonMsg(c, "Something went wrong!", err)
|
||||
return
|
||||
}
|
||||
user := session.GetLoginUser(c)
|
||||
inbound.Id = 0
|
||||
inbound.UserId = user.Id
|
||||
inbound.Tag = fmt.Sprintf("inbound-%v", inbound.Port)
|
||||
|
||||
for index := range inbound.ClientStats {
|
||||
inbound.ClientStats[index].Id = 0
|
||||
inbound.ClientStats[index].Enable = true
|
||||
}
|
||||
|
||||
needRestart := false
|
||||
inbound, needRestart, err = a.inboundService.AddInbound(inbound)
|
||||
jsonMsgObj(c, I18nWeb(c, "pages.inbounds.create"), inbound, err)
|
||||
if err == nil && needRestart {
|
||||
a.xrayService.SetToNeedRestart()
|
||||
}
|
||||
}
|
||||
|
||||
func (a *InboundController) onlines(c *gin.Context) {
|
||||
jsonObj(c, a.inboundService.GetOnlineClinets(), nil)
|
||||
}
|
||||
|
||||
@@ -50,45 +50,11 @@ func (a *SettingController) getAllSetting(c *gin.Context) {
|
||||
}
|
||||
|
||||
func (a *SettingController) getDefaultSettings(c *gin.Context) {
|
||||
type settingFunc func() (interface{}, error)
|
||||
|
||||
settings := map[string]settingFunc{
|
||||
"expireDiff": func() (interface{}, error) { return a.settingService.GetExpireDiff() },
|
||||
"trafficDiff": func() (interface{}, error) { return a.settingService.GetTrafficDiff() },
|
||||
"defaultCert": func() (interface{}, error) { return a.settingService.GetCertFile() },
|
||||
"defaultKey": func() (interface{}, error) { return a.settingService.GetKeyFile() },
|
||||
"tgBotEnable": func() (interface{}, error) { return a.settingService.GetTgbotenabled() },
|
||||
"subEnable": func() (interface{}, error) { return a.settingService.GetSubEnable() },
|
||||
"subPort": func() (interface{}, error) { return a.settingService.GetSubPort() },
|
||||
"subPath": func() (interface{}, error) { return a.settingService.GetSubPath() },
|
||||
"subDomain": func() (interface{}, error) { return a.settingService.GetSubDomain() },
|
||||
"subKeyFile": func() (interface{}, error) { return a.settingService.GetSubKeyFile() },
|
||||
"subCertFile": func() (interface{}, error) { return a.settingService.GetSubCertFile() },
|
||||
"subEncrypt": func() (interface{}, error) { return a.settingService.GetSubEncrypt() },
|
||||
"subShowInfo": func() (interface{}, error) { return a.settingService.GetSubShowInfo() },
|
||||
"pageSize": func() (interface{}, error) { return a.settingService.GetPageSize() },
|
||||
result, err := a.settingService.GetDefaultSettings(c.Request.Host)
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
|
||||
return
|
||||
}
|
||||
|
||||
result := make(map[string]interface{})
|
||||
|
||||
for key, fn := range settings {
|
||||
value, err := fn()
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
|
||||
return
|
||||
}
|
||||
result[key] = value
|
||||
}
|
||||
|
||||
subTLS := false
|
||||
if result["subKeyFile"] != "" || result["subCertFile"] != "" {
|
||||
subTLS = true
|
||||
}
|
||||
result["subTLS"] = subTLS
|
||||
|
||||
delete(result, "subKeyFile")
|
||||
delete(result, "subCertFile")
|
||||
|
||||
jsonObj(c, result, nil)
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ import (
|
||||
type XraySettingController struct {
|
||||
XraySettingService service.XraySettingService
|
||||
SettingService service.SettingService
|
||||
InboundService service.InboundService
|
||||
XrayService service.XrayService
|
||||
}
|
||||
|
||||
func NewXraySettingController(g *gin.RouterGroup) *XraySettingController {
|
||||
@@ -22,6 +24,7 @@ func (a *XraySettingController) initRouter(g *gin.RouterGroup) {
|
||||
|
||||
g.POST("/", a.getXraySetting)
|
||||
g.POST("/update", a.updateSetting)
|
||||
g.GET("/getXrayResult", a.getXrayResult)
|
||||
g.GET("/getDefaultJsonConfig", a.getDefaultXrayConfig)
|
||||
}
|
||||
|
||||
@@ -31,7 +34,13 @@ func (a *XraySettingController) getXraySetting(c *gin.Context) {
|
||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
|
||||
return
|
||||
}
|
||||
jsonObj(c, xraySetting, nil)
|
||||
inboundTags, err := a.InboundService.GetInboundTags()
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
|
||||
return
|
||||
}
|
||||
xrayResponse := "{ \"xraySetting\": " + xraySetting + ", \"inboundTags\": " + inboundTags + " }"
|
||||
jsonObj(c, xrayResponse, nil)
|
||||
}
|
||||
|
||||
func (a *XraySettingController) updateSetting(c *gin.Context) {
|
||||
@@ -48,3 +57,7 @@ func (a *XraySettingController) getDefaultXrayConfig(c *gin.Context) {
|
||||
}
|
||||
jsonObj(c, defaultJsonConfig, nil)
|
||||
}
|
||||
|
||||
func (a *XraySettingController) getXrayResult(c *gin.Context) {
|
||||
jsonObj(c, a.XrayService.GetXrayResult(), nil)
|
||||
}
|
||||
@@ -25,6 +25,7 @@ type AllSetting struct {
|
||||
PageSize int `json:"pageSize" form:"pageSize"`
|
||||
ExpireDiff int `json:"expireDiff" form:"expireDiff"`
|
||||
TrafficDiff int `json:"trafficDiff" form:"trafficDiff"`
|
||||
RemarkModel string `json:"remarkModel" form:"remarkModel"`
|
||||
TgBotEnable bool `json:"tgBotEnable" form:"tgBotEnable"`
|
||||
TgBotToken string `json:"tgBotToken" form:"tgBotToken"`
|
||||
TgBotChatId string `json:"tgBotChatId" form:"tgBotChatId"`
|
||||
@@ -44,6 +45,7 @@ type AllSetting struct {
|
||||
SubUpdates int `json:"subUpdates" form:"subUpdates"`
|
||||
SubEncrypt bool `json:"subEncrypt" form:"subEncrypt"`
|
||||
SubShowInfo bool `json:"subShowInfo" form:"subShowInfo"`
|
||||
SubURI string `json:"subURI" form:"subURI"`
|
||||
}
|
||||
|
||||
func (s *AllSetting) CheckValid() error {
|
||||
@@ -94,6 +96,13 @@ func (s *AllSetting) CheckValid() error {
|
||||
s.WebBasePath += "/"
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(s.SubPath, "/") {
|
||||
s.SubPath = "/" + s.SubPath
|
||||
}
|
||||
if !strings.HasSuffix(s.SubPath, "/") {
|
||||
s.SubPath += "/"
|
||||
}
|
||||
|
||||
_, err := time.LoadLocation(s.TimeLocation)
|
||||
if err != nil {
|
||||
return common.NewError("time location not exist:", s.TimeLocation)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<meta name="renderer" content="webkit">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="{{ .base_path }}assets/ant-design-vue@1.7.2/antd.min.css">
|
||||
<link rel="stylesheet" href="{{ .base_path }}assets/ant-design-vue@1.7.8/antd.min.css">
|
||||
<link rel="stylesheet" href="{{ .base_path }}assets/element-ui@2.15.0/theme-chalk/display.css">
|
||||
<link rel="stylesheet" href="{{ .base_path }}assets/css/custom.css?{{ .cur_ver }}">
|
||||
<link rel=”icon” type=”image/x-icon” href="{{ .base_path }}assets/favicon.ico">
|
||||
|
||||
@@ -1,19 +1,13 @@
|
||||
{{define "js"}}
|
||||
<script src="{{ .base_path }}assets/vue@2.6.12/vue.min.js"></script>
|
||||
<script src="{{ .base_path }}assets/moment/moment.min.js"></script>
|
||||
<script src="{{ .base_path }}assets/ant-design-vue@1.7.2/antd.min.js"></script>
|
||||
<script src="{{ .base_path }}assets/base64/base64.min.js"></script>
|
||||
<script src="{{ .base_path }}assets/ant-design-vue@1.7.8/antd.min.js"></script>
|
||||
<script src="{{ .base_path }}assets/axios/axios.min.js"></script>
|
||||
<script src="{{ .base_path }}assets/qs/qs.min.js"></script>
|
||||
<script src="{{ .base_path }}assets/qrcode/qrious.min.js"></script>
|
||||
<script src="{{ .base_path }}assets/clipboard/clipboard.min.js"></script>
|
||||
<script src="{{ .base_path }}assets/uri/URI.min.js"></script>
|
||||
<script src="{{ .base_path }}assets/js/axios-init.js?{{ .cur_ver }}"></script>
|
||||
<script src="{{ .base_path }}assets/js/util/common.js?{{ .cur_ver }}"></script>
|
||||
<script src="{{ .base_path }}assets/js/util/date-util.js?{{ .cur_ver }}"></script>
|
||||
<script src="{{ .base_path }}assets/js/util/utils.js?{{ .cur_ver }}"></script>
|
||||
<script src="{{ .base_path }}assets/js/model/xray.js?{{ .cur_ver }}"></script>
|
||||
<script src="{{ .base_path }}assets/js/model/models.js?{{ .cur_ver }}"></script>
|
||||
<script src="{{ .base_path }}assets/js/langs.js"></script>
|
||||
<script>
|
||||
const basePath = '{{ .base_path }}';
|
||||
|
||||
@@ -19,39 +19,25 @@
|
||||
|
||||
const qrModal = {
|
||||
title: '',
|
||||
clientIndex: 0,
|
||||
inbound: new Inbound(),
|
||||
dbInbound: new DBInbound(),
|
||||
client: null,
|
||||
qrcodes: [],
|
||||
clipboard: null,
|
||||
visible: false,
|
||||
subId: '',
|
||||
show: function (title = '', dbInbound = new DBInbound(), clientIndex = 0) {
|
||||
show: function (title = '', dbInbound, client) {
|
||||
this.title = title;
|
||||
this.clientIndex = clientIndex;
|
||||
this.dbInbound = dbInbound;
|
||||
this.inbound = dbInbound.toInbound();
|
||||
settings = JSON.parse(this.inbound.settings);
|
||||
this.client = settings.clients[clientIndex];
|
||||
remark = [this.dbInbound.remark, ( this.client ? this.client.email : '')].filter(Boolean).join('-');
|
||||
address = this.dbInbound.address;
|
||||
this.client = client;
|
||||
this.subId = '';
|
||||
this.qrcodes = [];
|
||||
if (this.inbound.tls && !ObjectUtil.isArrEmpty(this.inbound.stream.tls.settings.domains)) {
|
||||
this.inbound.stream.tls.settings.domains.forEach((domain) => {
|
||||
remarkText = [remark, domain.remark].filter(Boolean).join('-');
|
||||
this.qrcodes.push({
|
||||
remark: remarkText,
|
||||
link: this.inbound.genLink(domain.domain, remarkText, clientIndex)
|
||||
});
|
||||
});
|
||||
} else {
|
||||
this.inbound.genAllLinks(this.dbInbound.remark, app.remarkModel, client).forEach(l => {
|
||||
this.qrcodes.push({
|
||||
remark: remark,
|
||||
link: this.inbound.genLink(address, remark, clientIndex)
|
||||
remark: l.remark,
|
||||
link: l.link
|
||||
});
|
||||
}
|
||||
});
|
||||
this.visible = true;
|
||||
},
|
||||
close: function () {
|
||||
@@ -83,8 +69,7 @@
|
||||
});
|
||||
},
|
||||
genSubLink(subID) {
|
||||
const { domain: host, port, tls: isTLS, path: base } = app.subSettings;
|
||||
return buildURL({ host, port, isTLS, base, path: subID+'?name='+subID });
|
||||
return app.subSettings.subURI+subID+'?name='+subID;
|
||||
}
|
||||
},
|
||||
updated() {
|
||||
|
||||
@@ -30,7 +30,10 @@
|
||||
this.clipboard = new ClipboardJS('#txt-modal-ok-btn', {
|
||||
text: () => this.content,
|
||||
});
|
||||
this.clipboard.on('success', () => app.$message.success('{{ i18n "copied" }}'));
|
||||
this.clipboard.on('success', () => {
|
||||
app.$message.success('{{ i18n "copied" }}')
|
||||
this.close();
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
@@ -2,112 +2,99 @@
|
||||
<html lang="en">
|
||||
{{template "head" .}}
|
||||
<style>
|
||||
h1 {
|
||||
text-align: center;
|
||||
margin: 20px 0 50px 0;
|
||||
}
|
||||
|
||||
.ant-btn, .ant-input {
|
||||
height: 50px;
|
||||
border-radius: 30px;
|
||||
}
|
||||
|
||||
.ant-input-group-addon {
|
||||
border-radius: 0 30px 30px 0;
|
||||
width: 50px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.ant-input-affix-wrapper .ant-input-prefix {
|
||||
left: 23px;
|
||||
}
|
||||
|
||||
.ant-input-affix-wrapper .ant-input:not(:first-child) {
|
||||
padding-left: 50px;
|
||||
}
|
||||
|
||||
.centered {
|
||||
display: flex;
|
||||
text-align: center;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#app {
|
||||
overflow: hidden;
|
||||
}
|
||||
#login {
|
||||
animation: charge .5s both;
|
||||
background-color: #fff;
|
||||
border-radius: 2rem;
|
||||
padding: 3rem;
|
||||
}
|
||||
#login:hover {
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,.09);
|
||||
}
|
||||
@keyframes charge {
|
||||
from {transform: translateY(5rem);opacity: 0}
|
||||
to {transform: translateY(0);opacity: 1}
|
||||
}
|
||||
@keyframes wave {
|
||||
from {transform: rotate(0deg);}
|
||||
to {transform: rotate(360deg);}
|
||||
}
|
||||
.wave {
|
||||
opacity: .6;
|
||||
position: absolute;
|
||||
bottom: 40%;
|
||||
left: 50%;
|
||||
width: 6000px;
|
||||
height: 6000px;
|
||||
background: #000;
|
||||
margin-left: -3000px;
|
||||
transform-origin: 50% 48%;
|
||||
border-radius: 46%;
|
||||
animation: wave 72s infinite linear;
|
||||
pointer-events: none;
|
||||
}
|
||||
.wave2 {
|
||||
animation: wave 88s infinite linear;
|
||||
opacity: .3;
|
||||
}
|
||||
.wave3 {
|
||||
animation: wave 80s infinite linear;
|
||||
opacity: .1;
|
||||
}
|
||||
.wave {
|
||||
background: #0e49b515;
|
||||
}
|
||||
.under {
|
||||
background-color: #dce9f5;
|
||||
}
|
||||
.dark .wave {
|
||||
background: rgb(14 73 181 / 20%);
|
||||
}
|
||||
.dark .under {
|
||||
background-color: #101828;
|
||||
}
|
||||
.dark #login {
|
||||
background-color: #151F31;
|
||||
}
|
||||
.dark h1 {
|
||||
color: rgb(255 255 255 / 85%);
|
||||
}
|
||||
h1 {
|
||||
text-align: center;
|
||||
margin: 20px 0 50px 0;
|
||||
}
|
||||
.ant-btn,
|
||||
.ant-input {
|
||||
height: 50px;
|
||||
border-radius: 30px;
|
||||
}
|
||||
.ant-input-group-addon {
|
||||
border-radius: 0 30px 30px 0;
|
||||
width: 50px;
|
||||
font-size: 18px;
|
||||
}
|
||||
.ant-input-affix-wrapper .ant-input-prefix {
|
||||
left: 23px;
|
||||
}
|
||||
.ant-input-affix-wrapper .ant-input:not(:first-child) {
|
||||
padding-left: 50px;
|
||||
}
|
||||
.centered {
|
||||
display: flex;
|
||||
text-align: center;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
.title {
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
}
|
||||
#app {
|
||||
overflow: hidden;
|
||||
}
|
||||
#login {
|
||||
background-color: #fff;
|
||||
border-radius: 2rem;
|
||||
padding: 3rem;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
#login:hover {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.09);
|
||||
}
|
||||
.wave {
|
||||
opacity: 0.6;
|
||||
position: absolute;
|
||||
bottom: 40%;
|
||||
left: 50%;
|
||||
width: 6000px;
|
||||
height: 6000px;
|
||||
background-color: rgba(0, 135, 113, 0.08);
|
||||
margin-left: -3000px;
|
||||
transform-origin: 50% 48%;
|
||||
border-radius: 46%;
|
||||
pointer-events: none;
|
||||
rotate: 125deg;
|
||||
}
|
||||
.wave2 {
|
||||
opacity: 0.4;
|
||||
rotate: 70deg;
|
||||
}
|
||||
.wave3 {
|
||||
opacity: 0.2;
|
||||
rotate: 90deg;
|
||||
}
|
||||
.under {
|
||||
background-color: #dce9f5;
|
||||
}
|
||||
.dark .wave {
|
||||
background: rgba(14, 73, 181, 0.2);
|
||||
}
|
||||
.dark .under {
|
||||
background-color: #101828;
|
||||
}
|
||||
.dark #login {
|
||||
background-color: #151f31;
|
||||
}
|
||||
.dark h1 {
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
}
|
||||
.ant-form-item {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||
<body>
|
||||
<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme">
|
||||
<transition name="list" appear>
|
||||
<a-layout-content class="under">
|
||||
<a-layout-content class="under" style="min-height: 0;">
|
||||
<div class='wave'></div>
|
||||
<div class='wave wave2'></div>
|
||||
<div class='wave wave3'></div>
|
||||
<a-row type="flex" justify="center" align="middle" style="height: 100%;">
|
||||
<a-col :xs="22" :sm="20" :md="14" :lg="10" :xl="6" id="login">
|
||||
<a-row type="flex" justify="center" align="middle" style="height: 100%; overflow: auto;">
|
||||
<a-col :xs="22" :sm="20" :md="14" :lg="10" :xl="8" :xxl="6" id="login" style="margin: 3rem 0;">
|
||||
<a-row type="flex" justify="center">
|
||||
<a-col>
|
||||
<h1 class="title">{{ i18n "pages.login.title" }}</h1>
|
||||
@@ -130,7 +117,7 @@
|
||||
<a-form-item>
|
||||
<a-row justify="center" class="centered">
|
||||
<a-button type="primary" :loading="loading" @click="login" :icon="loading ? 'poweroff' : undefined"
|
||||
:style="loading ? { width: '50px' } : { display: 'block', width: '100%' }">
|
||||
:style="loading ? { width: '50px' } : { display: 'inline-block', width: '100%' }">
|
||||
[[ loading ? '' : '{{ i18n "login" }}' ]]
|
||||
</a-button>
|
||||
</a-row>
|
||||
@@ -169,6 +156,13 @@
|
||||
{{template "component/themeSwitcher" .}}
|
||||
{{template "component/password" .}}
|
||||
<script>
|
||||
class User {
|
||||
constructor() {
|
||||
this.username = "";
|
||||
this.password = "";
|
||||
}
|
||||
}
|
||||
|
||||
const app = new Vue({
|
||||
delimiters: ['[[', ']]'],
|
||||
el: '#app',
|
||||
@@ -179,7 +173,6 @@
|
||||
lang: ""
|
||||
},
|
||||
created() {
|
||||
this.updateBackground();
|
||||
this.lang = getLang();
|
||||
},
|
||||
methods: {
|
||||
@@ -190,17 +183,9 @@
|
||||
if (msg.success) {
|
||||
location.href = basePath + 'xui/';
|
||||
}
|
||||
},
|
||||
updateBackground() {
|
||||
document.querySelector('#app').style.background = colors[this.themeSwitcher.currentTheme].bg;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
'themeSwitcher.isDarkTheme'(newVal, oldVal) {
|
||||
this.updateBackground();
|
||||
},
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -2,156 +2,105 @@
|
||||
<a-modal id="client-bulk-modal" v-model="clientsBulkModal.visible" :title="clientsBulkModal.title" @ok="clientsBulkModal.ok"
|
||||
:confirm-loading="clientsBulkModal.confirmLoading" :closable="true" :mask-closable="false"
|
||||
:ok-text="clientsBulkModal.okText" cancel-text='{{ i18n "close" }}' :class="themeSwitcher.currentTheme">
|
||||
<a-form layout="inline">
|
||||
<table width="100%" class="ant-table-tbody">
|
||||
<tr>
|
||||
<td>{{ i18n "pages.client.method" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-select v-model="clientsBulkModal.emailMethod" buttonStyle="solid" style="width: 250px" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option :value="0">Random</a-select-option>
|
||||
<a-select-option :value="1">Random+Prefix</a-select-option>
|
||||
<a-select-option :value="2">Random+Prefix+Num</a-select-option>
|
||||
<a-select-option :value="3">Random+Prefix+Num+Postfix</a-select-option>
|
||||
<a-select-option :value="4">Prefix+Num+Postfix</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="clientsBulkModal.emailMethod>1">
|
||||
<td>{{ i18n "pages.client.first" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input-number v-model="clientsBulkModal.firstNum" :min="1"></a-input-number>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="clientsBulkModal.emailMethod>1">
|
||||
<td>{{ i18n "pages.client.last" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input-number v-model="clientsBulkModal.lastNum" :min="clientsBulkModal.firstNum"></a-input-number>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="clientsBulkModal.emailMethod>0">
|
||||
<td>{{ i18n "pages.client.prefix" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model="clientsBulkModal.emailPrefix" style="width: 250px"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="clientsBulkModal.emailMethod>2">
|
||||
<td>{{ i18n "pages.client.postfix" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model="clientsBulkModal.emailPostfix" style="width: 250px"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="clientsBulkModal.emailMethod < 2">
|
||||
<td>{{ i18n "pages.client.clientCount" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input-number v-model="clientsBulkModal.quantity" :min="1" :max="100"></a-input-number>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="clientsBulkModal.inbound.canEnableTlsFlow()">
|
||||
<td>Flow</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-select v-model="clientsBulkModal.flow" style="width: 250px" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value="" selected>{{ i18n "none" }}</a-select-option>
|
||||
<a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="app.subSettings.enable">
|
||||
<td>Subscription</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="clientsBulkModal.subId" style="width: 250px"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="app.tgBotEnable">
|
||||
<td>Telegram Username</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="clientsBulkModal.tgId" style="width: 250px"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<span>{{ i18n "pages.inbounds.totalFlow" }}</span>(GB)
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input-number v-model="clientsBulkModal.totalGB" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "pages.client.delayedStart" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-switch v-model="clientsBulkModal.delayedStart" @click="clientsBulkModal.expiryTime=0"></a-switch>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="clientsBulkModal.delayedStart">
|
||||
<td>{{ i18n "pages.client.expireDays" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input-number v-model.number="delayedExpireDays" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-else>
|
||||
<td>
|
||||
<span>{{ i18n "pages.inbounds.expireDate" }}</span>
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme"
|
||||
v-model="clientsBulkModal.expiryTime" style="width: 250px;"></a-date-picker>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="clientsBulkModal.expiryTime != 0">
|
||||
<td>
|
||||
<span>{{ i18n "pages.client.renew" }}</span>
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.client.renewDesc" }}</span>
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input-number v-model.number="clientsBulkModal.reset" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label='{{ i18n "pages.client.method" }}'>
|
||||
<a-select v-model="clientsBulkModal.emailMethod" buttonStyle="solid" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option :value="0">Random</a-select-option>
|
||||
<a-select-option :value="1">Random+Prefix</a-select-option>
|
||||
<a-select-option :value="2">Random+Prefix+Num</a-select-option>
|
||||
<a-select-option :value="3">Random+Prefix+Num+Postfix</a-select-option>
|
||||
<a-select-option :value="4">Prefix+Num+Postfix</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.client.first" }}' v-if="clientsBulkModal.emailMethod>1">
|
||||
<a-input-number v-model="clientsBulkModal.firstNum" :min="1"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.client.last" }}' v-if="clientsBulkModal.emailMethod>1">
|
||||
<a-input-number v-model="clientsBulkModal.lastNum" :min="clientsBulkModal.firstNum"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.client.prefix" }}' v-if="clientsBulkModal.emailMethod>0">
|
||||
<a-input v-model="clientsBulkModal.emailPrefix"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.client.postfix" }}' v-if="clientsBulkModal.emailMethod>2">
|
||||
<a-input v-model="clientsBulkModal.emailPostfix"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.client.clientCount" }}' v-if="clientsBulkModal.emailMethod < 2">
|
||||
<a-input-number v-model="clientsBulkModal.quantity" :min="1" :max="100"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='Flow' v-if="clientsBulkModal.inbound.canEnableTlsFlow()">
|
||||
<a-select v-model="clientsBulkModal.flow" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value="" selected>{{ i18n "none" }}</a-select-option>
|
||||
<a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="app.subSettings.enable">
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.inbounds.subscriptionDesc" }}</span>
|
||||
</template>
|
||||
Subscription
|
||||
<a-icon @click="clientsBulkModal.subId = RandomUtil.randomLowerAndNum(16)" type="sync"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input v-model.trim="clientsBulkModal.subId"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="app.tgBotEnable">
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.inbounds.telegramDesc" }}</span>
|
||||
</template>
|
||||
Telegram ID
|
||||
<a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input v-model.trim="clientsBulkModal.tgId"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
|
||||
</template>
|
||||
{{ i18n "pages.inbounds.totalFlow" }} (GB)
|
||||
<a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input-number v-model="clientsBulkModal.totalGB" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.client.delayedStart" }}'>
|
||||
<a-switch v-model="clientsBulkModal.delayedStart" @click="clientsBulkModal.expiryTime=0"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.client.expireDays" }}' v-if="clientsBulkModal.delayedStart">
|
||||
<a-input-number v-model.number="delayedExpireDays" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item v-else>
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
|
||||
</template>
|
||||
{{ i18n "pages.inbounds.expireDate" }}
|
||||
<a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme"
|
||||
v-model="clientsBulkModal.expiryTime"></a-date-picker>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="clientsBulkModal.expiryTime != 0">
|
||||
<template slot="label">
|
||||
<span>{{ i18n "pages.client.renew" }}</span>
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.client.renewDesc" }}</span>
|
||||
</template>
|
||||
<a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input-number v-model.number="clientsBulkModal.reset" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
<script>
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
this.isEdit = isEdit;
|
||||
this.dbInbound = new DBInbound(dbInbound);
|
||||
this.inbound = dbInbound.toInbound();
|
||||
this.clients = this.getClients(this.inbound.protocol, this.inbound.settings);
|
||||
this.clients = this.inbound.clients;
|
||||
this.index = index === null ? this.clients.length : index;
|
||||
this.delayedStart = false;
|
||||
if (isEdit){
|
||||
@@ -50,15 +50,6 @@
|
||||
this.clientStats = this.dbInbound.clientStats.find(row => row.email === this.clients[this.index].email);
|
||||
this.confirm = confirm;
|
||||
},
|
||||
getClients(protocol, clientSettings) {
|
||||
switch(protocol){
|
||||
case Protocols.VMESS: return clientSettings.vmesses;
|
||||
case Protocols.VLESS: return clientSettings.vlesses;
|
||||
case Protocols.TROJAN: return clientSettings.trojans;
|
||||
case Protocols.SHADOWSOCKS: return clientSettings.shadowsockses;
|
||||
default: return null;
|
||||
}
|
||||
},
|
||||
getClientId(protocol, client) {
|
||||
switch(protocol){
|
||||
case Protocols.TROJAN: return client.password;
|
||||
@@ -110,9 +101,6 @@
|
||||
get isExpiry() {
|
||||
return this.clientModal.isEdit && this.client.expiryTime >0 ? (this.client.expiryTime < new Date().getTime()) : false;
|
||||
},
|
||||
get statsColor() {
|
||||
return usageColor(clientStats.up + clientStats.down, app.trafficDiff, this.client.totalGB);
|
||||
},
|
||||
get delayedStart() {
|
||||
return this.clientModal.delayedStart;
|
||||
},
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
</a-col>
|
||||
<a-col :lg="24" :xl="12">
|
||||
<template v-if="type === 'text'">
|
||||
<a-input :value="value" @input="$emit('input', $event.target.value)"></a-input>
|
||||
<a-input :value="value" @input="$emit('input', $event.target.value)" :placeholder="placeholder"></a-input>
|
||||
</template>
|
||||
<template v-else-if="type === 'number'">
|
||||
<a-input-number :value="value" @change="value => $emit('input', value)" :min="min" :step="step" style="width: 100%;"></a-input-number>
|
||||
@@ -28,7 +28,7 @@
|
||||
{{define "component/setting"}}
|
||||
<script>
|
||||
Vue.component('setting-list-item', {
|
||||
props: ["type", "title", "desc", "value", "min", "step"],
|
||||
props: ["type", "title", "desc", "value", "min", "step", "placeholder"],
|
||||
template: `{{template "component/settingListItem"}}`,
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,156 +1,125 @@
|
||||
{{define "form/client"}}
|
||||
<a-form layout="inline" v-if="client">
|
||||
<table width="100%" class="ant-table-tbody">
|
||||
<tr>
|
||||
<td>{{ i18n "pages.inbounds.enable" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-switch v-model="client.enable"></a-switch>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<span>{{ i18n "pages.inbounds.email" }}</span>
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.inbounds.emailDesc" }}</span>
|
||||
</template>
|
||||
<a-icon type="sync" @click="client.email = RandomUtil.randomLowerAndNum(9)"></a-icon>
|
||||
</a-tooltip>
|
||||
</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="client.email" style="width: 250px"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="inbound.protocol === Protocols.TROJAN || inbound.protocol === Protocols.SHADOWSOCKS">
|
||||
<td>password
|
||||
<a-icon v-if="inbound.protocol === Protocols.SHADOWSOCKS" @click="client.password = RandomUtil.randomShadowsocksPassword()" type="sync"> </a-icon>
|
||||
</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="client.password" style="width: 250px"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="inbound.protocol === Protocols.VMESS || inbound.protocol === Protocols.VLESS">
|
||||
<td>ID <a-icon @click="client.id = RandomUtil.randomUUID()" type="sync"></a-icon></td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="client.id" style="width: 250px"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="client.email && app.subSettings.enable">
|
||||
<td>Subscription <a-icon @click="client.subId = RandomUtil.randomLowerAndNum(16)" type="sync"></a-icon></td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="client.subId" style="width: 250px"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="client.email && app.tgBotEnable">
|
||||
<td>Telegram Username</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="client.tgId" style="width: 250px"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="inbound.canEnableTlsFlow()">
|
||||
<td>Flow</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-select v-model="client.flow" style="width: 250px" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value="" selected>{{ i18n "none" }}</a-select-option>
|
||||
<a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<span>{{ i18n "pages.inbounds.totalFlow" }}</span>(GB)
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input-number v-model="client._totalGB" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="isEdit && clientStats">
|
||||
<td>{{ i18n "usage" }}</td>
|
||||
<td>
|
||||
<a-tag :color="statsColor">
|
||||
[[ sizeFormat(clientStats.up) ]] /
|
||||
[[ sizeFormat(clientStats.down) ]]
|
||||
([[ sizeFormat(clientStats.up + clientStats.down) ]])
|
||||
</a-tag>
|
||||
<a-tooltip>
|
||||
<template slot="title">{{ i18n "pages.inbounds.resetTraffic" }}</template>
|
||||
<a-icon type="retweet" @click="resetClientTraffic(client.email,clientStats.inboundId,$event.target)" v-if="client.email.length > 0"></a-icon>
|
||||
</a-tooltip>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "pages.client.delayedStart" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-switch v-model="delayedStart" @click="client._expiryTime=0"></a-switch>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="delayedStart">
|
||||
<td>{{ i18n "pages.client.expireDays" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input-number v-model.number="delayedExpireDays" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-else>
|
||||
<td>
|
||||
<span>{{ i18n "pages.inbounds.expireDate" }}</span>
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme"
|
||||
v-model="client._expiryTime" style="width: 250px;"></a-date-picker>
|
||||
<a-tag color="red" v-if="isEdit && isExpiry">Expired</a-tag>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="client.expiryTime != 0">
|
||||
<td>
|
||||
<span>{{ i18n "pages.client.renew" }}</span>
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.client.renewDesc" }}</span>
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input-number v-model.number="client.reset" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<a-form layout="horizontal" v-if="client" :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label='{{ i18n "pages.inbounds.enable" }}'>
|
||||
<a-switch v-model="client.enable"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.inbounds.emailDesc" }}</span>
|
||||
</template>
|
||||
{{ i18n "pages.inbounds.email" }}
|
||||
<a-icon type="sync" @click="client.email = RandomUtil.randomLowerAndNum(9)"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input v-model.trim="client.email"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="inbound.protocol === Protocols.TROJAN || inbound.protocol === Protocols.SHADOWSOCKS">
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.client.renew" }}</span>
|
||||
</template>
|
||||
{{ i18n "password" }}
|
||||
<a-icon v-if="inbound.protocol === Protocols.SHADOWSOCKS" @click="client.password = RandomUtil.randomShadowsocksPassword()" type="sync"></a-icon>
|
||||
<a-icon v-if="inbound.protocol === Protocols.TROJAN" @click="client.password = RandomUtil.randomSeq(10)" type="sync"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input v-model.trim="client.password"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="inbound.protocol === Protocols.VMESS || inbound.protocol === Protocols.VLESS">
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.client.renew" }}</span>
|
||||
</template>
|
||||
ID <a-icon @click="client.id = RandomUtil.randomUUID()" type="sync"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input v-model.trim="client.id"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="client.email && app.subSettings.enable">
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.inbounds.subscriptionDesc" }}</span>
|
||||
</template>
|
||||
Subscription
|
||||
<a-icon @click="client.subId = RandomUtil.randomLowerAndNum(16)" type="sync"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input v-model.trim="client.subId"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="client.email && app.tgBotEnable">
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.inbounds.telegramDesc" }}</span>
|
||||
</template>
|
||||
Telegram ID
|
||||
<a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input v-model.trim="client.tgId"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="inbound.canEnableTlsFlow()" label='Flow'>
|
||||
<a-select v-model="client.flow" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value="" selected>{{ i18n "none" }}</a-select-option>
|
||||
<a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
|
||||
</template>
|
||||
{{ i18n "pages.inbounds.totalFlow" }}(GB)
|
||||
<a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input-number v-model="client._totalGB" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="isEdit && clientStats" label='{{ i18n "usage" }}'>
|
||||
<a-tag :color="clientUsageColor(clientStats, app.trafficDiff)">
|
||||
[[ sizeFormat(clientStats.up) ]] /
|
||||
[[ sizeFormat(clientStats.down) ]]
|
||||
([[ sizeFormat(clientStats.up + clientStats.down) ]])
|
||||
</a-tag>
|
||||
<a-tooltip>
|
||||
<template slot="title">{{ i18n "pages.inbounds.resetTraffic" }}</template>
|
||||
<a-icon type="retweet" @click="resetClientTraffic(client.email,clientStats.inboundId,$event.target)" v-if="client.email.length > 0"></a-icon>
|
||||
</a-tooltip>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.client.delayedStart" }}'>
|
||||
<a-switch v-model="delayedStart" @click="client._expiryTime=0"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="delayedStart" label='{{ i18n "pages.client.expireDays" }}'>
|
||||
<a-input-number v-model.number="delayedExpireDays" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item v-else>
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</template>
|
||||
{{ i18n "pages.inbounds.expireDate" }}
|
||||
<a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme"
|
||||
v-model="client._expiryTime"></a-date-picker>
|
||||
<a-tag color="red" v-if="isEdit && isExpiry">Expired</a-tag>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="client.expiryTime != 0">
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">{{ i18n "pages.client.renewDesc" }}</template>
|
||||
{{ i18n "pages.client.renew" }}
|
||||
<a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input-number v-model.number="client.reset" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
{{end}}
|
||||
@@ -1,91 +1,63 @@
|
||||
{{define "form/inbound"}}
|
||||
<!-- base -->
|
||||
<a-form layout="inline">
|
||||
<table width="100%" class="ant-table-tbody">
|
||||
<tr>
|
||||
<td>{{ i18n "enable" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-switch v-model="dbInbound.enable"></a-switch>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "remark" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="dbInbound.remark" style="width: 250px;"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "protocol" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-select v-model="inbound.protocol" style="width: 250px;" :disabled="isEdit" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="p in Protocols" :key="p" :value="p">[[ p ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "monitor" }}
|
||||
<a-tooltip>
|
||||
<a-form :colon="false" :label-col="{ md: {span:6} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label='{{ i18n "enable" }}'>
|
||||
<a-switch v-model="dbInbound.enable"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "remark" }}'>
|
||||
<a-input v-model.trim="dbInbound.remark"></a-input>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label='{{ i18n "protocol" }}'>
|
||||
<a-select v-model="inbound.protocol" :disabled="isEdit" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="p in Protocols" :key="p" :value="p">[[ p ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.inbounds.monitorDesc" }}</span>
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="inbound.listen" style="width: 250px;"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "pages.inbounds.port" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input-number v-model.number="inbound.port"></a-input-number>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<span>{{ i18n "pages.inbounds.totalFlow" }}</span>(GB)
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input-number v-model="dbInbound.totalGB" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<span>{{ i18n "pages.inbounds.expireDate" }}</span>
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme"
|
||||
v-model="dbInbound._expiryTime" style="width: 250px;"></a-date-picker>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{{ i18n "monitor" }}
|
||||
<a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input v-model.trim="inbound.listen"></a-input>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label='{{ i18n "pages.inbounds.port" }}'>
|
||||
<a-input-number v-model.number="inbound.port"></a-input-number>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
|
||||
</template>
|
||||
{{ i18n "pages.inbounds.totalFlow" }}(GB)
|
||||
<a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input-number v-model="dbInbound.totalGB" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
|
||||
</template>
|
||||
{{ i18n "pages.inbounds.expireDate" }}
|
||||
<a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme"
|
||||
v-model="dbInbound._expiryTime"></a-date-picker>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<!-- vmess settings -->
|
||||
@@ -126,6 +98,7 @@
|
||||
<!-- stream settings -->
|
||||
<template v-if="inbound.canEnableStream()">
|
||||
{{template "form/streamSettings"}}
|
||||
{{template "form/externalProxy" }}
|
||||
</template>
|
||||
|
||||
<!-- tls settings -->
|
||||
|
||||
307
web/html/xui/form/outbound.html
Normal file
307
web/html/xui/form/outbound.html
Normal file
@@ -0,0 +1,307 @@
|
||||
{{define "form/outbound"}}
|
||||
<!-- base -->
|
||||
<a-tabs :active-key="outModal.activeKey" style="padding: 0; background-color: transparent;" @change="(activeKey) => {outModal.toggleJson(activeKey == '2'); }">
|
||||
<a-tab-pane key="1" tab="Form">
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label='{{ i18n "protocol" }}'>
|
||||
<a-select v-model="outbound.protocol" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="x,y in Protocols" :value="x">[[ y ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.xray.outbound.tag" }}' has-feedback :validate-status="outModal.duplicateTag? 'warning' : 'success'">
|
||||
<a-input v-model.trim="outbound.tag" @change="outModal.check()" placeholder='{{ i18n "pages.xray.outbound.tagDesc" }}'></a-input>
|
||||
</a-form-item>
|
||||
|
||||
<!-- freedom settings-->
|
||||
<template v-if="outbound.protocol === Protocols.Freedom">
|
||||
<a-form-item label='Strategy'>
|
||||
<a-select
|
||||
v-model="outbound.settings.domainStrategy"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="s in outboundDomainStrategies" :value="s">[[ s ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='Fragment'>
|
||||
<a-switch
|
||||
:checked="Object.keys(outbound.settings.fragment).length >0"
|
||||
@change="checked => outbound.settings.fragment = checked ? new Outbound.FreedomSettings.Fragment() : {}">
|
||||
</a-switch>
|
||||
</a-form-item>
|
||||
<template v-if="Object.keys(outbound.settings.fragment).length >0">
|
||||
<a-form-item label='Packets'>
|
||||
<a-select
|
||||
v-model="outbound.settings.fragment.packets"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="s in ['1-3','tlshello']" :value="s">[[ s ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='Length'>
|
||||
<a-input v-model.trim="outbound.settings.fragment.length"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='Interval'>
|
||||
<a-input v-model.trim="outbound.settings.fragment.interval"></a-input>
|
||||
</a-form-item>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- blackhole settings -->
|
||||
<template v-if="outbound.protocol === Protocols.Blackhole">
|
||||
<a-form-item label='Response Type'>
|
||||
<a-select
|
||||
v-model="outbound.settings.type"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="s in ['', 'none','http']" :value="s">[[ s ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
<!-- dns settings -->
|
||||
<template v-if="outbound.protocol === Protocols.DNS">
|
||||
<a-form-item label='{{ i18n "pages.inbounds.network" }}'>
|
||||
<a-select
|
||||
v-model="outbound.settings.network"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="s in ['udp','tcp']" :value="s">[[ s ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
<!-- Address + Port -->
|
||||
<template v-if="outbound.hasAddressPort()">
|
||||
<a-form-item label='{{ i18n "pages.inbounds.address" }}'>
|
||||
<a-input v-model.trim="outbound.settings.address"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.port" }}'>
|
||||
<a-input-number v-model.number="outbound.settings.port" :min="1" :max="65532"></a-input-number>
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
<!-- Vnext (vless/vmess) settings -->
|
||||
<template v-if="[Protocols.VMess, Protocols.VLESS].includes(outbound.protocol)">
|
||||
<a-form-item label='ID'>
|
||||
<a-input v-model.trim="outbound.settings.id"></a-input>
|
||||
</a-form-item>
|
||||
<!-- vless settings -->
|
||||
<template v-if="outbound.canEnableTlsFlow()">
|
||||
<a-form-item label='Flow'>
|
||||
<a-select v-model="outbound.settings.flow" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value="" selected>{{ i18n "none" }}</a-select-option>
|
||||
<a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- Servers (trojan/shadowsocks/socks/http) settings -->
|
||||
<template v-if="outbound.hasServers()">
|
||||
<template v-if="outbound.hasUsername()">
|
||||
<a-form-item label='{{ i18n "username" }}'>
|
||||
<a-input v-model.trim="outbound.settings.user"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "password" }}'>
|
||||
<a-input v-model.trim="outbound.settings.password"></a-input>
|
||||
</a-form-item>
|
||||
</template>
|
||||
<!-- shadowsocks -->
|
||||
<template v-if="outbound.protocol === Protocols.Shadowsocks">
|
||||
<a-form-item label='{{ i18n "encryption" }}'>
|
||||
<a-select v-model="outbound.settings.method" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="method in SSMethods" :value="method">[[ method ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='UDP over TCP'>
|
||||
<a-switch v-model="outbound.settings.uot"></a-switch>
|
||||
</a-form-item>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- stream settings -->
|
||||
<template v-if="outbound.canEnableStream()">
|
||||
<a-form-item label='{{ i18n "transmission" }}'>
|
||||
<a-select v-model="outbound.stream.network" @change="streamNetworkChange"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value="tcp">TCP</a-select-option>
|
||||
<a-select-option value="kcp">KCP</a-select-option>
|
||||
<a-select-option value="ws">WebSocket</a-select-option>
|
||||
<a-select-option value="http">HTTP2</a-select-option>
|
||||
<a-select-option value="quic">QUIC</a-select-option>
|
||||
<a-select-option value="grpc">gRPC</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<template v-if="outbound.stream.network === 'tcp'">
|
||||
<a-form-item label='HTTP {{ i18n "camouflage" }}'>
|
||||
<a-switch
|
||||
:checked="outbound.stream.tcp.type === 'http'"
|
||||
@change="checked => outbound.stream.tcp.type = checked ? 'http' : 'none'">
|
||||
</a-switch>
|
||||
</a-form-item>
|
||||
<template v-if="outbound.stream.tcp.type == 'http'">
|
||||
<a-form-item label='{{ i18n "host" }}'>
|
||||
<a-input v-model.trim="outbound.stream.tcp.host"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "path" }}'>
|
||||
<a-input v-model.trim="outbound.stream.tcp.path"></a-input>
|
||||
</a-form-item>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- kcp -->
|
||||
<template v-if="outbound.stream.network === 'kcp'">
|
||||
<a-form-item label='{{ i18n "camouflage" }}'>
|
||||
<a-select v-model="outbound.stream.kcp.type" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value="none">none (not camouflage)</a-select-option>
|
||||
<a-select-option value="srtp">srtp (video call)</a-select-option>
|
||||
<a-select-option value="utp">utp (BT download)</a-select-option>
|
||||
<a-select-option value="wechat-video">wechat-video (WeChat video)</a-select-option>
|
||||
<a-select-option value="dtls">dtls (DTLS 1.2 packages)</a-select-option>
|
||||
<a-select-option value="wireguard">wireguard (wireguard packages)</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "password" }}'>
|
||||
<a-input v-model="outbound.stream.kcp.seed"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='MTU'>
|
||||
<a-input-number v-model.number="outbound.stream.kcp.mtu"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='TTI (ms)'>
|
||||
<a-input-number v-model.number="outbound.stream.kcp.tti"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='Uplink Capacity (Mb/s)'>
|
||||
<a-input-number v-model.number="outbound.stream.kcp.upCap"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='Downlink Capacity (Mb/s)'>
|
||||
<a-input-number v-model.number="outbound.stream.kcp.downCap"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='Congestion'>
|
||||
<a-switch v-model="outbound.stream.kcp.congestion"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label='Read Buffer Size (MB)'>
|
||||
<a-input-number v-model.number="outbound.stream.kcp.readBuffer"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='Write Buffer Size (MB)'>
|
||||
<a-input-number v-model.number="outbound.stream.kcp.writeBuffer"></a-input-number>
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
<!-- ws -->
|
||||
<template v-if="outbound.stream.network === 'ws'">
|
||||
<a-form-item label='{{ i18n "host" }}'>
|
||||
<a-input v-model="outbound.stream.ws.host"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "path" }}'>
|
||||
<a-form-item><a-input v-model.trim="outbound.stream.ws.path"></a-input>
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
<!-- http -->
|
||||
<template v-if="outbound.stream.network === 'http'">
|
||||
<a-form-item label='{{ i18n "host" }}'>
|
||||
<a-input v-model.trim="outbound.stream.http.host"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "path" }}'>
|
||||
<a-input v-model.trim="outbound.stream.http.path"></a-input>
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
<!-- quic -->
|
||||
<template v-if="outbound.stream.network === 'quic'">
|
||||
<a-form-item label='{{ i18n "pages.inbounds.stream.quic.encryption" }}'>
|
||||
<a-select v-model="outbound.stream.quic.security" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value="none">none</a-select-option>
|
||||
<a-select-option value="aes-128-gcm">aes-128-gcm</a-select-option>
|
||||
<a-select-option value="chacha20-poly1305">chacha20-poly1305</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "password" }}'>
|
||||
<a-input v-model.trim="outbound.stream.quic.key"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "camouflage" }}'>
|
||||
<a-select v-model="outbound.stream.quic.type" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value="none">none (not camouflage)</a-select-option>
|
||||
<a-select-option value="srtp">srtp (video call)</a-select-option>
|
||||
<a-select-option value="utp">utp (BT download)</a-select-option>
|
||||
<a-select-option value="wechat-video">wechat-video (WeChat video)</a-select-option>
|
||||
<a-select-option value="dtls">dtls (DTLS 1.2 packages)</a-select-option>
|
||||
<a-select-option value="wireguard">wireguard (wireguard packages)</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
<!-- grpc -->
|
||||
<template v-if="outbound.stream.network === 'grpc'">
|
||||
<a-form-item label='Service Name'>
|
||||
<a-input v-model.trim="outbound.stream.grpc.serviceName"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='MultiMode'>
|
||||
<a-switch v-model="outbound.stream.grpc.multiMode"></a-switch>
|
||||
</a-form-item>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- tls settings -->
|
||||
<template v-if="outbound.canEnableTls()">
|
||||
<a-form-item label='{{ i18n "security" }}'>
|
||||
<a-radio-group v-model="outbound.stream.security" button-style="solid">
|
||||
<a-radio-button value="none">{{ i18n "none" }}</a-radio-button>
|
||||
<a-radio-button value="tls">TLS</a-radio-button>
|
||||
<a-radio-button v-if="outbound.canEnableReality()" value="reality">Reality</a-radio-button>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<template v-if="outbound.stream.isTls">
|
||||
<a-form-item label="SNI" placeholder="Server Name Indication">
|
||||
<a-input v-model.trim="outbound.stream.tls.serverName"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="uTLS">
|
||||
<a-select v-model="outbound.stream.tls.fingerprint"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value=''>None</a-select-option>
|
||||
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="ALPN">
|
||||
<a-select
|
||||
mode="multiple"
|
||||
|
||||
:dropdown-class-name="themeSwitcher.currentTheme"
|
||||
v-model="outbound.stream.tls.alpn">
|
||||
<a-select-option v-for="alpn in ALPN_OPTION" :value="alpn">[[ alpn ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="Allow Insecure">
|
||||
<a-switch v-model="outbound.stream.tls.allowInsecure"></a-switch>
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
<!-- reality settings -->
|
||||
<template v-if="outbound.stream.isReality">
|
||||
<a-form-item label='{{ i18n "domainName" }}'>
|
||||
<a-input v-model.trim="outbound.stream.reality.serverName"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="uTLS">
|
||||
<a-select v-model="outbound.stream.reality.fingerprint"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="Short IDs">
|
||||
<a-input v-model.trim="outbound.stream.reality.shortId" style="width:250px"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="SpiderX">
|
||||
<a-input v-model.trim="outbound.stream.reality.spiderX" style="width:250px"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="Public Key">
|
||||
<a-input v-model.trim="outbound.stream.reality.publicKey"></a-input>
|
||||
</a-form-item>
|
||||
</template>
|
||||
</template>
|
||||
</a-form>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="2" tab="JSON" force-render="true">
|
||||
<a-form-item style="margin: 10px 0">
|
||||
Link: <a-input v-model.trim="outModal.link" style="width: 300px; margin-right: 5px;" placeholder="vmess:// vless:// trojan:// ss://"></a-input>
|
||||
<a-button @click="convertLink" type="primary"><a-icon type="form"></a-icon></a-button>
|
||||
</a-form-item>
|
||||
<textarea style="position:absolute; left: -800px;" id="outboundJson"></textarea>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
{{end}}
|
||||
@@ -1,42 +1,20 @@
|
||||
{{define "form/dokodemo"}}
|
||||
<a-form layout="inline">
|
||||
<table width="100%" class="ant-table-tbody">
|
||||
<tr>
|
||||
<td>{{ i18n "pages.inbounds.targetAddress"}}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="inbound.settings.address"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "pages.inbounds.destinationPort"}}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input-number v-model.number="inbound.settings.port"></a-input-number>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "pages.inbounds.network"}}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-select v-model="inbound.settings.network" style="width: 100px;" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value="tcp,udp">tcp+udp</a-select-option>
|
||||
<a-select-option value="tcp">tcp</a-select-option>
|
||||
<a-select-option value="udp">udp</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>FollowRedirect</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-switch v-model="inbound.settings.followRedirect"></a-switch>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<a-form :colon="false" :label-col="{ md: {span:6} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label='{{ i18n "pages.inbounds.targetAddress"}}'>
|
||||
<a-input v-model.trim="inbound.settings.address"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.destinationPort"}}'>
|
||||
<a-input-number v-model.number="inbound.settings.port"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.network"}}'>
|
||||
<a-select v-model="inbound.settings.network" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value="tcp,udp">TCP+UDP</a-select-option>
|
||||
<a-select-option value="tcp">TCP</a-select-option>
|
||||
<a-select-option value="udp">UDP</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='Follow Redirect'>
|
||||
<a-switch v-model="inbound.settings.followRedirect"></a-switch>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{{define "form/http"}}
|
||||
<a-form layout="inline">
|
||||
<a-form>
|
||||
<table style="width: 100%; text-align: center; margin-bottom: 10px;">
|
||||
<tr>
|
||||
<td width="45%">{{ i18n "username" }}</td>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
{{define "form/shadowsocks"}}
|
||||
<a-form layout="inline">
|
||||
<template v-if="inbound.isSSMultiUser">
|
||||
<template v-if="inbound.isSSMultiUser">
|
||||
<a-collapse activeKey="0" v-for="(client, index) in inbound.settings.shadowsockses.slice(0,1)" v-if="!isEdit">
|
||||
<a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'>
|
||||
{{template "form/client"}}
|
||||
@@ -20,40 +19,39 @@
|
||||
</table>
|
||||
</a-collapse-panel>
|
||||
</a-collapse>
|
||||
</template>
|
||||
<table width="100%" class="ant-table-tbody">
|
||||
<tr>
|
||||
<td>{{ i18n "encryption" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-select v-model="inbound.settings.method" style="width: 250px;" @change="SSMethodChange" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="method in SSMethods" :value="method">[[ method ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="inbound.isSS2022">
|
||||
<td>{{ i18n "password" }}
|
||||
<a-icon @click="inbound.settings.password = RandomUtil.randomShadowsocksPassword()" type="sync"> </a-icon>
|
||||
</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="inbound.settings.password" style="width: 250px"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "pages.inbounds.network" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-select v-model="inbound.settings.network" style="width: 100px;" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value="tcp,udp">tcp+udp</a-select-option>
|
||||
<a-select-option value="tcp">tcp</a-select-option>
|
||||
<a-select-option value="udp">udp</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</a-form>
|
||||
{{end}}
|
||||
</template>
|
||||
<table width="100%" class="ant-table-tbody">
|
||||
<tr>
|
||||
<td>{{ i18n "encryption" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-select v-model="inbound.settings.method" style="width: 250px;" @change="SSMethodChange" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="method in SSMethods" :value="method">[[ method ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="inbound.isSS2022">
|
||||
<td>{{ i18n "password" }}
|
||||
<a-icon @click="inbound.settings.password = RandomUtil.randomShadowsocksPassword()" type="sync"> </a-icon>
|
||||
</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="inbound.settings.password" style="width: 250px"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "pages.inbounds.network" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-select v-model="inbound.settings.network" style="width: 100px;" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value="tcp,udp">TCP+UDP</a-select-option>
|
||||
<a-select-option value="tcp">TCP</a-select-option>
|
||||
<a-select-option value="udp">UDP</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{{end}}
|
||||
|
||||
@@ -1,52 +1,33 @@
|
||||
{{define "form/socks"}}
|
||||
<a-form layout="inline">
|
||||
<table width="100%" class="ant-table-tbody">
|
||||
<tr>
|
||||
<td style="width: 30%;">{{ i18n "password" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-switch :checked="inbound.settings.auth === 'password'"
|
||||
@change="checked => inbound.settings.auth = checked ? 'password' : 'noauth'"></a-switch>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="inbound.settings.auth === 'password'">
|
||||
<td colspan="2">
|
||||
<table style="width: 100%; text-align: center; margin-bottom: 10px;">
|
||||
<tr>
|
||||
<td width="45%">{{ i18n "username" }}</td>
|
||||
<td width="45%">{{ i18n "password" }}</td>
|
||||
<td><a-button size="small" @click="inbound.settings.addAccount(new Inbound.SocksSettings.SocksAccount())">+</a-button></td>
|
||||
</tr>
|
||||
</table>
|
||||
<a-input-group compact v-for="(account, index) in inbound.settings.accounts" style="margin-bottom: 10px;">
|
||||
<a-input style="width: 50%" v-model.trim="account.user" placeholder='{{ i18n "username" }}'>
|
||||
<template slot="addonBefore" style="margin: 0;">[[ index+1 ]]</template>
|
||||
</a-input>
|
||||
<a-input style="width: 50%" v-model.trim="account.pass" placeholder='{{ i18n "password" }}'>
|
||||
<template slot="addonAfter">
|
||||
<a-button size="small" @click="inbound.settings.delAccount(index)">-</a-button>
|
||||
</template>
|
||||
</a-input>
|
||||
</a-input-group>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "pages.inbounds.enable" }} udp</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-switch v-model="inbound.settings.udp"></a-switch>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="inbound.settings.udp">
|
||||
<td>IP</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="inbound.settings.ip"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<a-form :colon="false" :label-col="{ md: {span:6} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label='{{ i18n "pages.inbounds.enable" }} UDP'>
|
||||
<a-switch v-model="inbound.settings.udp"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label="IP" v-if="inbound.settings.udp">
|
||||
<a-input v-model.trim="inbound.settings.ip"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "password" }}'>
|
||||
<a-switch :checked="inbound.settings.auth === 'password'"
|
||||
@change="checked => inbound.settings.auth = checked ? 'password' : 'noauth'"></a-switch>
|
||||
</a-form-item>
|
||||
<template v-if="inbound.settings.auth === 'password'">
|
||||
<table style="width: 100%; text-align: center; margin-bottom: 10px;">
|
||||
<tr>
|
||||
<td width="45%">{{ i18n "username" }}</td>
|
||||
<td width="45%">{{ i18n "password" }}</td>
|
||||
<td><a-button size="small" @click="inbound.settings.addAccount(new Inbound.SocksSettings.SocksAccount())">+</a-button></td>
|
||||
</tr>
|
||||
</table>
|
||||
<a-input-group compact v-for="(account, index) in inbound.settings.accounts" style="margin-bottom: 10px;">
|
||||
<a-input style="width: 50%" v-model.trim="account.user" placeholder='{{ i18n "username" }}'>
|
||||
<template slot="addonBefore" style="margin: 0;">[[ index+1 ]]</template>
|
||||
</a-input>
|
||||
<a-input style="width: 50%" v-model.trim="account.pass" placeholder='{{ i18n "password" }}'>
|
||||
<template slot="addonAfter">
|
||||
<a-button size="small" @click="inbound.settings.delAccount(index)">-</a-button>
|
||||
</template>
|
||||
</a-input>
|
||||
</a-input-group>
|
||||
</template>
|
||||
</a-form>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{{define "form/trojan"}}
|
||||
<a-form layout="inline">
|
||||
<a-collapse activeKey="0" v-for="(client, index) in inbound.settings.trojans.slice(0,1)" v-if="!isEdit">
|
||||
<a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'>
|
||||
{{template "form/client"}}
|
||||
@@ -19,12 +18,12 @@
|
||||
</table>
|
||||
</a-collapse-panel>
|
||||
</a-collapse>
|
||||
<template v-if="inbound.isTcp && inbound.tls">
|
||||
<template v-if="inbound.isTcp && !inbound.stream.isReality">
|
||||
<a-form layout="inline">
|
||||
<a-form-item label="Fallbacks">
|
||||
<a-row>
|
||||
<a-button type="primary" size="small"
|
||||
@click="inbound.settings.addTrojanFallback()">
|
||||
@click="inbound.settings.addFallback()">
|
||||
+
|
||||
</a-button>
|
||||
</a-row>
|
||||
@@ -32,28 +31,28 @@
|
||||
</a-form>
|
||||
|
||||
<!-- trojan fallbacks -->
|
||||
<a-form v-for="(fallback, index) in inbound.settings.fallbacks" layout="inline">
|
||||
<a-form v-for="(fallback, index) in inbound.settings.fallbacks" :colon="false" :label-col="{ md: {span:6} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-divider style="margin:0;">
|
||||
fallback[[ index + 1 ]]
|
||||
<a-icon type="delete" @click="() => inbound.settings.delTrojanFallback(index)"
|
||||
Fallback[[ index + 1 ]]
|
||||
<a-icon type="delete" @click="() => inbound.settings.delFallback(index)"
|
||||
style="color: rgb(255, 77, 79);cursor: pointer;"/>
|
||||
</a-divider>
|
||||
<a-form-item label="name">
|
||||
<a-form-item label='Name'>
|
||||
<a-input v-model="fallback.name"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="alpn">
|
||||
</a-form-item>
|
||||
<a-form-item label='ALPN'>
|
||||
<a-input v-model="fallback.alpn"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="path">
|
||||
<a-form-item label='Path'>
|
||||
<a-input v-model="fallback.path"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="dest">
|
||||
<a-form-item label='Dest'>
|
||||
<a-input v-model="fallback.dest"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="xver">
|
||||
<a-input-number v-model.number="fallback.xver"></a-input-number>
|
||||
<a-form-item label='xVer'>
|
||||
<a-input-number v-model="fallback.xver" :min="0" :max="2"></a-input-number>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<a-divider style="margin:0;"></a-divider>
|
||||
</template>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{{define "form/vless"}}
|
||||
<a-form layout="inline">
|
||||
<a-collapse activeKey="0" v-for="(client, index) in inbound.settings.vlesses.slice(0,1)" v-if="!isEdit">
|
||||
<a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'>
|
||||
{{template "form/client"}}
|
||||
@@ -21,7 +20,7 @@
|
||||
</table>
|
||||
</a-collapse-panel>
|
||||
</a-collapse>
|
||||
<template v-if="inbound.isTcp && inbound.tls">
|
||||
<template v-if="inbound.isTcp && !inbound.stream.isReality">
|
||||
<a-form layout="inline">
|
||||
<a-form-item label="Fallbacks">
|
||||
<a-row>
|
||||
@@ -34,26 +33,26 @@
|
||||
</a-form>
|
||||
|
||||
<!-- vless fallbacks -->
|
||||
<a-form v-for="(fallback, index) in inbound.settings.fallbacks" layout="inline">
|
||||
<a-form v-for="(fallback, index) in inbound.settings.fallbacks" :colon="false" :label-col="{ md: {span:6} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-divider style="margin:0;">
|
||||
fallback[[ index + 1 ]]
|
||||
Fallback[[ index + 1 ]]
|
||||
<a-icon type="delete" @click="() => inbound.settings.delFallback(index)"
|
||||
style="color: rgb(255, 77, 79);cursor: pointer;"/>
|
||||
</a-divider>
|
||||
<a-form-item label="name">
|
||||
<a-form-item label='Name'>
|
||||
<a-input v-model="fallback.name"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="alpn">
|
||||
</a-form-item>
|
||||
<a-form-item label='ALPN'>
|
||||
<a-input v-model="fallback.alpn"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="path">
|
||||
<a-form-item label='Path'>
|
||||
<a-input v-model="fallback.path"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="dest">
|
||||
<a-form-item label='Dest'>
|
||||
<a-input v-model="fallback.dest"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="xver">
|
||||
<a-input-number v-model.number="fallback.xver"></a-input-number>
|
||||
<a-form-item label='xVer'>
|
||||
<a-input-number v-model="fallback.xver" :min="0" :max="2"></a-input-number>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<a-divider style="margin:0;"></a-divider>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{{define "form/vmess"}}
|
||||
<a-form layout="inline">
|
||||
<a-collapse activeKey="0" v-for="(client, index) in inbound.settings.vmesses.slice(0,1)" v-if="!isEdit">
|
||||
<a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'>
|
||||
{{template "form/client"}}
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
{{define "form/sniffing"}}
|
||||
<a-divider style="margin:0;"></a-divider>
|
||||
<a-form layout="inline">
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item>
|
||||
<span slot="label">
|
||||
sniffing
|
||||
Sniffing
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.inbounds.noRecommendKeepDefault" }}</span>
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
<a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</span>
|
||||
<a-switch v-model="inbound.sniffing.enabled"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-form-item :wrapper-col="{span:24}">
|
||||
<a-checkbox-group v-model="inbound.sniffing.destOverride" v-if="inbound.sniffing.enabled">
|
||||
<a-checkbox v-for="key,value in SNIFFING_OPTION" :value="key">[[ value ]]</a-checkbox>
|
||||
</a-checkbox-group>
|
||||
|
||||
26
web/html/xui/form/stream/external_proxy.html
Normal file
26
web/html/xui/form/stream/external_proxy.html
Normal file
@@ -0,0 +1,26 @@
|
||||
{{define "form/externalProxy"}}
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-divider style="margin:0;"></a-divider>
|
||||
<a-form-item label="External Proxy">
|
||||
<a-switch v-model="externalProxy"></a-switch>
|
||||
<a-button v-if="externalProxy" type="primary" style="margin-left: 10px" size="small" @click="inbound.stream.externalProxy.push({forceTls: 'same', dest: '', port: 443, remark: ''})">+</a-button>
|
||||
</a-form-item>
|
||||
<a-input-group style="margin: 5px 0;" compact v-for="(row, index) in inbound.stream.externalProxy">
|
||||
<template>
|
||||
<a-tooltip title="Force TLS">
|
||||
<a-select v-model="row.forceTls" style="width:20%; margin: 0px" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value="same">{{ i18n "pages.inbounds.same" }}</a-select-option>
|
||||
<a-select-option value="none">{{ i18n "none" }}</a-select-option>
|
||||
<a-select-option value="tls">TLS</a-select-option>
|
||||
</a-select>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input style="width: 35%" v-model.trim="row.dest" placeholder='{{ i18n "host" }}'></a-input>
|
||||
<a-tooltip title='{{ i18n "pages.inbounds.port" }}'>
|
||||
<a-input-number style="width: 15%;" v-model.number="row.port" min="1" max="65531"></a-input-number>
|
||||
</a-tooltip>
|
||||
<a-input style="width: 20%" v-model.trim="row.remark" placeholder='{{ i18n "remark" }}'></a-input>
|
||||
<a-button style="width: 10%; margin: 0px" @click="inbound.stream.externalProxy.splice(index, 1)">-</a-button>
|
||||
</a-input-group>
|
||||
</a-form>
|
||||
{{end}}
|
||||
@@ -1,22 +1,10 @@
|
||||
{{define "form/streamGRPC"}}
|
||||
<a-form layout="inline">
|
||||
<table width="100%" class="ant-table-tbody">
|
||||
<tr>
|
||||
<td>serviceName</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="inbound.stream.grpc.serviceName" style="width: 250px;"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>MultiMode</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-switch v-model="inbound.stream.grpc.multiMode"></a-switch>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<a-form :colon="false" :label-col="{ md: {span:6} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label="Service Name">
|
||||
<a-input v-model.trim="inbound.stream.grpc.serviceName" style="width: 250px;"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="MultiMode">
|
||||
<a-switch v-model="inbound.stream.grpc.multiMode"></a-switch>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
{{end}}
|
||||
@@ -1,24 +1,19 @@
|
||||
{{define "form/streamHTTP"}}
|
||||
<a-form layout="inline">
|
||||
<table width="100%" class="ant-table-tbody">
|
||||
<tr>
|
||||
<td>{{ i18n "path" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="inbound.stream.http.path" style="width: 250px;"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>host</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-row v-for="(host, index) in inbound.stream.http.host">
|
||||
<a-input v-model.trim="inbound.stream.http.host[index]" style="width: 250px;"></a-input>
|
||||
</a-row>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<a-form :colon="false" :label-col="{ md: {span:6} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label='{{ i18n "path" }}'>
|
||||
<a-input v-model.trim="inbound.stream.http.path"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<template slot="label">{{ i18n "host" }}
|
||||
<a-button size="small" @click="inbound.stream.http.addHost()">+</a-button>
|
||||
</template>
|
||||
<template v-for="(host, index) in inbound.stream.http.host">
|
||||
<a-input v-model.trim="inbound.stream.http.host[index]">
|
||||
<a-button size="small" slot="addonAfter"
|
||||
@click="inbound.stream.http.removeHost(index)"
|
||||
v-if="inbound.stream.http.host.length>1">-</a-button>
|
||||
</a-input>
|
||||
</template>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
{{end}}
|
||||
@@ -1,85 +1,38 @@
|
||||
{{define "form/streamKCP"}}
|
||||
<a-form layout="inline">
|
||||
<table width="100%" class="ant-table-tbody">
|
||||
<tr>
|
||||
<td>{{ i18n "camouflage" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-select v-model="inbound.stream.kcp.type" style="width: 250px;" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value="none">none (not camouflage)</a-select-option>
|
||||
<a-select-option value="srtp">srtp (video call)</a-select-option>
|
||||
<a-select-option value="utp">utp (BT download)</a-select-option>
|
||||
<a-select-option value="wechat-video">wechat-video (WeChat video)</a-select-option>
|
||||
<a-select-option value="dtls">dtls (DTLS 1.2 packages)</a-select-option>
|
||||
<a-select-option value="wireguard">wireguard (wireguard packages)</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "password" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model="inbound.stream.kcp.seed" style="width: 250px;"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>mtu</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input-number v-model.number="inbound.stream.kcp.mtu"></a-input-number>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>tti (ms)</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input-number v-model.number="inbound.stream.kcp.tti"></a-input-number>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>uplink capacity (MB/S)</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input-number v-model.number="inbound.stream.kcp.upCap"></a-input-number>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>downlink capacity (MB/S)</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input-number v-model.number="inbound.stream.kcp.downCap"></a-input-number>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>congestion</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-switch v-model="inbound.stream.kcp.congestion"></a-switch>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>read buffer size (MB)</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input-number v-model.number="inbound.stream.kcp.readBuffer"></a-input-number>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>write buffer size (MB)</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input-number v-model.number="inbound.stream.kcp.writeBuffer"></a-input-number>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<a-form :colon="false" :label-col="{ md: {span:6} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label='{{ i18n "camouflage" }}'>
|
||||
<a-select v-model="inbound.stream.kcp.type" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value="none">none (not camouflage)</a-select-option>
|
||||
<a-select-option value="srtp">srtp (video call)</a-select-option>
|
||||
<a-select-option value="utp">utp (BT download)</a-select-option>
|
||||
<a-select-option value="wechat-video">wechat-video (WeChat video)</a-select-option>
|
||||
<a-select-option value="dtls">dtls (DTLS 1.2 packages)</a-select-option>
|
||||
<a-select-option value="wireguard">wireguard (wireguard packages)</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "password" }}'>
|
||||
<a-input v-model="inbound.stream.kcp.seed"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='MTU'>
|
||||
<a-input-number v-model.number="inbound.stream.kcp.mtu"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='TTI (ms)'>
|
||||
<a-input-number v-model.number="inbound.stream.kcp.tti"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='Uplink (Mb/s)'>
|
||||
<a-input-number v-model.number="inbound.stream.kcp.upCap"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='Downlink (Mb/s)'>
|
||||
<a-input-number v-model.number="inbound.stream.kcp.downCap"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='Congestion'>
|
||||
<a-switch v-model="inbound.stream.kcp.congestion"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label='Read buffer (MB)'>
|
||||
<a-input-number v-model.number="inbound.stream.kcp.readBuffer"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='Write buffer (MB)'>
|
||||
<a-input-number v-model.number="inbound.stream.kcp.writeBuffer"></a-input-number>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
@@ -1,41 +1,24 @@
|
||||
{{define "form/streamQUIC"}}
|
||||
<a-form layout="inline">
|
||||
<table width="100%" class="ant-table-tbody">
|
||||
<tr>
|
||||
<td>{{ i18n "pages.inbounds.stream.quic.encryption" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-select v-model="inbound.stream.quic.security" style="width: 250px;" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value="none">none</a-select-option>
|
||||
<a-select-option value="aes-128-gcm">aes-128-gcm</a-select-option>
|
||||
<a-select-option value="chacha20-poly1305">chacha20-poly1305</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "password" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="inbound.stream.quic.key" style="width: 250px;"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "camouflage" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-select v-model="inbound.stream.quic.type" style="width: 250px;" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value="none">none (not camouflage)</a-select-option>
|
||||
<a-select-option value="srtp">srtp (video call)</a-select-option>
|
||||
<a-select-option value="utp">utp (BT download)</a-select-option>
|
||||
<a-select-option value="wechat-video">wechat-video (WeChat video)</a-select-option>
|
||||
<a-select-option value="dtls">dtls (DTLS 1.2 packages)</a-select-option>
|
||||
<a-select-option value="wireguard">wireguard (wireguard packages)</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<a-form :colon="false" :label-col="{ md: {span:6} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label='{{ i18n "pages.inbounds.stream.quic.encryption" }}'>
|
||||
<a-select v-model="inbound.stream.quic.security" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value="none">none</a-select-option>
|
||||
<a-select-option value="aes-128-gcm">aes-128-gcm</a-select-option>
|
||||
<a-select-option value="chacha20-poly1305">chacha20-poly1305</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "password" }}'>
|
||||
<a-input v-model.trim="inbound.stream.quic.key"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "camouflage" }}'>
|
||||
<a-select v-model="inbound.stream.quic.type" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value="none">none (not camouflage)</a-select-option>
|
||||
<a-select-option value="srtp">srtp (video call)</a-select-option>
|
||||
<a-select-option value="utp">utp (BT download)</a-select-option>
|
||||
<a-select-option value="wechat-video">wechat-video (WeChat video)</a-select-option>
|
||||
<a-select-option value="dtls">dtls (DTLS 1.2 packages)</a-select-option>
|
||||
<a-select-option value="wireguard">wireguard (wireguard packages)</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
{{end}}
|
||||
@@ -1,9 +1,9 @@
|
||||
{{define "form/streamSettings"}}
|
||||
<!-- select stream network -->
|
||||
<a-form layout="inline">
|
||||
<a-form :colon="false" :label-col="{ md: {span:6} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label="{{ i18n "transmission" }}">
|
||||
<a-select v-model="inbound.stream.network" @change="streamNetworkChange"
|
||||
style="width: 150px;" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value="tcp">TCP</a-select-option>
|
||||
<a-select-option value="kcp">KCP</a-select-option>
|
||||
<a-select-option value="ws">WebSocket</a-select-option>
|
||||
|
||||
@@ -1,46 +1,26 @@
|
||||
{{define "form/streamSockopt"}}
|
||||
<a-divider style="margin:0;"></a-divider>
|
||||
<a-form layout="inline">
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label="Transparent Proxy">
|
||||
<a-switch v-model="inbound.stream.sockoptSwitch"></a-switch>
|
||||
</a-form-item>
|
||||
<table width="100%" class="ant-table-tbody" v-if="inbound.stream.sockoptSwitch">
|
||||
<tr>
|
||||
<td>Accept Proxy Protocol</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-switch v-model="inbound.stream.sockopt.acceptProxyProtocol"></a-switch>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>TCP FastOpen</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-switch v-model.trim="inbound.stream.sockopt.tcpFastOpen"></a-switch>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Route Mark</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input-number v-model="inbound.stream.sockopt.mark" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>T-Proxy</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-select v-model="inbound.stream.sockopt.tproxy" style="width: 250px;" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value="off">OFF</a-select-option>
|
||||
<a-select-option value="redirect">Redirect</a-select-option>
|
||||
<a-select-option value="tproxy">T-Proxy</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<template v-if="inbound.stream.sockoptSwitch">
|
||||
<a-form-item label="Accept Proxy Protocol">
|
||||
<a-switch v-model="inbound.stream.sockopt.acceptProxyProtocol"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label="TCP FastOpen">
|
||||
<a-switch v-model.trim="inbound.stream.sockopt.tcpFastOpen"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label="Route Mark">
|
||||
<a-input-number v-model="inbound.stream.sockopt.mark" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label="T-Proxy">
|
||||
<a-select v-model="inbound.stream.sockopt.tproxy" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value="off">OFF</a-select-option>
|
||||
<a-select-option value="redirect">Redirect</a-select-option>
|
||||
<a-select-option value="tproxy">T-Proxy</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</template>
|
||||
</a-form>
|
||||
{{end}}
|
||||
@@ -1,107 +1,76 @@
|
||||
{{define "form/streamTCP"}}
|
||||
<!-- tcp type -->
|
||||
<a-form layout="inline">
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label="Accept Proxy Protocol" v-if="inbound.canEnableTls()">
|
||||
<a-switch v-model="inbound.stream.tcp.acceptProxyProtocol"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label="http {{ i18n "camouflage" }}">
|
||||
<a-form-item label="HTTP {{ i18n "camouflage" }}">
|
||||
<a-switch
|
||||
:checked="inbound.stream.tcp.type === 'http'"
|
||||
@change="checked => inbound.stream.tcp.type = checked ? 'http' : 'none'">
|
||||
:checked="inbound.stream.tcp.type === 'http'"
|
||||
@change="checked => inbound.stream.tcp.type = checked ? 'http' : 'none'">
|
||||
</a-switch>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<!-- tcp request -->
|
||||
<a-form v-if="inbound.stream.tcp.type === 'http'" layout="inline">
|
||||
<table width="100%" class="ant-table-tbody">
|
||||
<tr>
|
||||
<td>{{ i18n "pages.inbounds.stream.tcp.requestVersion" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="inbound.stream.tcp.request.version" style="width: 200px;"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "pages.inbounds.stream.tcp.requestMethod" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="inbound.stream.tcp.request.method" style="width: 200px;"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "pages.inbounds.stream.tcp.requestPath" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-row v-for="(path, index) in inbound.stream.tcp.request.path">
|
||||
<a-input v-model.trim="inbound.stream.tcp.request.path[index]" style="width: 200px;"></a-input>
|
||||
</a-row>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" width="100%">
|
||||
<a-form-item>
|
||||
<span>{{ i18n "pages.inbounds.stream.general.requestHeader" }}:</span>
|
||||
<a-button size="small" style="margin-left: 10px" @click="inbound.stream.tcp.request.addHeader('', '')">+</a-button>
|
||||
<a-input-group compact v-for="(header, index) in inbound.stream.tcp.request.headers">
|
||||
<a-input style="width: 50%" v-model.trim="header.name" placeholder='{{ i18n "pages.inbounds.stream.general.name" }}'>
|
||||
<template slot="addonBefore" style="margin: 0;">[[ index+1 ]]</template>
|
||||
</a-input>
|
||||
<a-input style="width: 50%" v-model.trim="header.value" placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'>
|
||||
<a-button slot="addonAfter" size="small" @click="inbound.stream.tcp.request.removeHeader(index)">-</a-button>
|
||||
</a-input>
|
||||
</a-input-group>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<a-form v-if="inbound.stream.tcp.type === 'http'" :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestVersion" }}'>
|
||||
<a-input v-model.trim="inbound.stream.tcp.request.version"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestMethod" }}'>
|
||||
<a-input v-model.trim="inbound.stream.tcp.request.method"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<template slot="label">{{ i18n "pages.inbounds.stream.tcp.requestPath" }}
|
||||
<a-button size="small" @click="inbound.stream.tcp.request.addPath('/')">+</a-button>
|
||||
</template>
|
||||
<template v-for="(path, index) in inbound.stream.tcp.request.path">
|
||||
<a-input v-model.trim="inbound.stream.tcp.request.path[index]">
|
||||
<a-button size="small" slot="addonAfter"
|
||||
@click="inbound.stream.tcp.request.removePath(index)"
|
||||
v-if="inbound.stream.tcp.request.path.length>1">-</a-button>
|
||||
</a-input>
|
||||
</template>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.stream.general.requestHeader" }}'>
|
||||
<a-button size="small" @click="inbound.stream.tcp.request.addHeader('', '')">+</a-button>
|
||||
</a-form-item>
|
||||
<a-form-item :wrapper-col="{span:24}">
|
||||
<a-input-group compact v-for="(header, index) in inbound.stream.tcp.request.headers">
|
||||
<a-input style="width: 50%" v-model.trim="header.name" placeholder='{{ i18n "pages.inbounds.stream.general.name" }}'>
|
||||
<template slot="addonBefore" style="margin: 0;">[[ index+1 ]]</template>
|
||||
</a-input>
|
||||
<a-input style="width: 50%" v-model.trim="header.value" placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'>
|
||||
<a-button slot="addonAfter" size="small" @click="inbound.stream.tcp.request.removeHeader(index)">-</a-button>
|
||||
</a-input>
|
||||
</a-input-group>
|
||||
</a-form-item>
|
||||
<!-- tcp response -->
|
||||
<tr>
|
||||
<td>{{ i18n "pages.inbounds.stream.tcp.responseVersion" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="inbound.stream.tcp.response.version" style="width: 200px;"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "pages.inbounds.stream.tcp.responseStatus" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="inbound.stream.tcp.response.status" style="width: 200px;"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "pages.inbounds.stream.tcp.responseStatusDescription" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="inbound.stream.tcp.response.reason" style="width: 200px;"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" width="100%">
|
||||
<a-form-item>
|
||||
<span>{{ i18n "pages.inbounds.stream.tcp.responseHeader" }}:</span>
|
||||
<a-button size="small" style="margin-left: 10px"
|
||||
@click="inbound.stream.tcp.response.addHeader('Content-Type', 'application/octet-stream')">+</a-button>
|
||||
<a-input-group compact v-for="(header, index) in inbound.stream.tcp.response.headers">
|
||||
<a-input style="width: 50%" v-model.trim="header.name" placeholder='{{ i18n "pages.inbounds.stream.general.name" }}'>
|
||||
<template slot="addonBefore" style="margin: 0;">[[ index+1 ]]</template>
|
||||
</a-input>
|
||||
<a-input style="width: 50%" v-model.trim="header.value"
|
||||
placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'>
|
||||
<template slot="addonAfter">
|
||||
<a-button size="small" @click="inbound.stream.tcp.response.removeHeader(index)">-</a-button>
|
||||
</template>
|
||||
</a-input>
|
||||
</a-input-group>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.responseVersion" }}'>
|
||||
<a-input v-model.trim="inbound.stream.tcp.response.version"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.responseStatus" }}'>
|
||||
<a-input v-model.trim="inbound.stream.tcp.response.status"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.responseStatusDescription" }}'>
|
||||
<a-input v-model.trim="inbound.stream.tcp.response.reason"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.responseHeader" }}'>
|
||||
<a-button size="small"
|
||||
@click="inbound.stream.tcp.response.addHeader('Content-Type', 'application/octet-stream')">+</a-button>
|
||||
</a-form-item>
|
||||
<a-form-item :wrapper-col="{span:24}">
|
||||
<a-input-group compact v-for="(header, index) in inbound.stream.tcp.response.headers">
|
||||
<a-input style="width: 50%" v-model.trim="header.name" placeholder='{{ i18n "pages.inbounds.stream.general.name" }}'>
|
||||
<template slot="addonBefore" style="margin: 0;">[[ index+1 ]]</template>
|
||||
</a-input>
|
||||
<a-input style="width: 50%" v-model.trim="header.value"
|
||||
placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'>
|
||||
<template slot="addonAfter">
|
||||
<a-button size="small" @click="inbound.stream.tcp.response.removeHeader(index)">-</a-button>
|
||||
</template>
|
||||
</a-input>
|
||||
</a-input-group>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
{{define "form/streamWS"}}
|
||||
<a-form layout="inline">
|
||||
<a-form-item label="AcceptProxyProtocol">
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label="Accept Proxy Protocol">
|
||||
<a-switch v-model="inbound.stream.ws.acceptProxyProtocol"></a-switch>
|
||||
</a-form-item>
|
||||
<br>
|
||||
<a-form-item label='{{ i18n "path" }}'>
|
||||
<a-input v-model.trim="inbound.stream.ws.path"></a-input>
|
||||
</a-form-item>
|
||||
<br>
|
||||
<a-form-item style="width: 100%;">
|
||||
<span>{{ i18n "pages.inbounds.stream.general.requestHeader" }}:</span>
|
||||
<a-button size="small" style="margin-left: 10px" @click="inbound.stream.ws.addHeader()">+</a-button>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.stream.general.requestHeader" }}'>
|
||||
<a-button size="small" @click="inbound.stream.ws.addHeader()">+</a-button>
|
||||
</a-form-item>
|
||||
<a-form-item :wrapper-col="{span:24}">
|
||||
<a-input-group compact v-for="(header, index) in inbound.stream.ws.headers">
|
||||
<a-input style="width: 50%" v-model.trim="header.name" placeholder='{{ i18n "pages.inbounds.stream.general.name"}}'>
|
||||
<template slot="addonBefore" style="margin: 0;">[[ index+1 ]]</template>
|
||||
@@ -21,4 +20,4 @@
|
||||
</a-input-group>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
@@ -1,282 +1,135 @@
|
||||
{{define "form/tlsSettings"}}
|
||||
<!-- tls enable -->
|
||||
<a-form v-if="inbound.canSetTls()" layout="inline">
|
||||
<a-form v-if="inbound.canEnableTls()" :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-divider style="margin:0;"></a-divider>
|
||||
<a-form-item label="TLS">
|
||||
<a-switch v-model="inbound.tls">
|
||||
</a-switch>
|
||||
<a-form-item label='{{ i18n "security" }}'>
|
||||
<a-radio-group v-model="inbound.stream.security" button-style="solid">
|
||||
<a-radio-button value="none">{{ i18n "none" }}</a-radio-button>
|
||||
<a-radio-button value="tls">TLS</a-radio-button>
|
||||
<a-radio-button v-if="inbound.canEnableReality()" value="reality">Reality</a-radio-button>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<a-form-item label="Reality" v-if="inbound.canEnableReality()">
|
||||
<a-switch v-model="inbound.reality"></a-switch>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<!-- tls settings -->
|
||||
<a-form v-if="inbound.tls" layout="inline">
|
||||
<table width="100%" class="ant-table-tbody">
|
||||
<tr>
|
||||
<td>SNI</td>
|
||||
<td>
|
||||
<a-form-item placeholder="Server Name Indication" v-if="inbound.tls">
|
||||
<a-input v-model.trim="inbound.stream.tls.settings.serverName" style="width: 250px"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>CipherSuites</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-select v-model="inbound.stream.tls.cipherSuites" style="width: 250px" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value="">auto</a-select-option>
|
||||
<a-select-option v-for="key,value in TLS_CIPHER_OPTION" :value="key">[[ value ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Min/Max Version</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input-group compact>
|
||||
<a-select style="width: 125px" v-model="inbound.stream.tls.minVersion" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
<a-select style="width: 125px" v-model="inbound.stream.tls.maxVersion" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-input-group>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>uTLS</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-select v-model="inbound.stream.tls.settings.fingerprint"
|
||||
style="width: 250px" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value=''>None</a-select-option>
|
||||
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="line-height: 40px;">
|
||||
<td>Multi Domain</td>
|
||||
<td>
|
||||
<a-switch v-model="multiDomain"></a-switch>
|
||||
<a-button v-if="multiDomain" style="margin-left: 10px" size="small" @click="inbound.stream.tls.settings.domains.push({remark: '', domain: ''})">+</a-button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="multiDomain" style="line-height: 40px;">
|
||||
<td colspan="2" width="100%">
|
||||
<a-input-group style="margin-top:5px;" compact v-for="(row, index) in inbound.stream.tls.settings.domains">
|
||||
<a-input style="width: 50%" v-model.trim="row.remark" placeholder='{{ i18n "remark" }}'>
|
||||
<template slot="addonBefore" style="margin: 0;">[[ index+1 ]]</template>
|
||||
</a-input>
|
||||
<a-input style="width: 50%" v-model.trim="row.domain" placeholder='{{ i18n "host" }}'>
|
||||
<a-button slot="addonAfter" size="small" style="margin: 0px" @click="inbound.stream.tls.settings.domains.splice(index, 1)">-</a-button>
|
||||
</a-input>
|
||||
</a-input-group>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-else>
|
||||
<td>{{ i18n "domainName" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="inbound.stream.tls.server" style="width: 250px"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>ALPN</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-select
|
||||
mode="multiple"
|
||||
style="width: 250px"
|
||||
v-model="inbound.stream.tls.alpn">
|
||||
<a-select-option v-for="alpn in ALPN_OPTION" :value="alpn">[[ alpn ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Allow insecure</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-switch v-model="inbound.stream.tls.settings.allowInsecure"></a-switch>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Reject Unknown SNI</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-switch v-model="inbound.stream.tls.rejectUnknownSni"></a-switch>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<template v-for="cert,index in inbound.stream.tls.certs">
|
||||
<tr>
|
||||
<td colspan="2" width="100%">
|
||||
<a-form-item label='{{ i18n "certificate" }}'>
|
||||
<a-radio-group v-model="cert.useFile" button-style="solid">
|
||||
<a-radio-button :value="true">{{ i18n "pages.inbounds.certificatePath" }}</a-radio-button>
|
||||
<a-radio-button :value="false">{{ i18n "pages.inbounds.certificateContent" }}</a-radio-button>
|
||||
</a-radio-group>
|
||||
<a-button v-if="index === 0" type="primary" size="small" @click="inbound.stream.tls.addCert()" style="margin-left: 10px">+</a-button>
|
||||
<a-button v-if="inbound.stream.tls.certs.length>1" type="primary" size="small" @click="inbound.stream.tls.removeCert(index)" style="margin-left: 10px">-</a-button>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<template v-if="cert.useFile">
|
||||
<tr>
|
||||
<td>{{ i18n "pages.inbounds.publicKeyPath" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="cert.certFile" style="width:250px;"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "pages.inbounds.keyPath" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="cert.keyFile" style="width:250px;"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>
|
||||
<a-button type="primary" icon="import" @click="setDefaultCertData(index)">{{ i18n "pages.inbounds.setDefaultCert" }}</a-button>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
<template v-else>
|
||||
<tr>
|
||||
<td>{{ i18n "pages.inbounds.publicKeyContent" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input type="textarea" :rows="3" style="width:250px;" v-model="cert.cert"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "pages.inbounds.keyContent" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input type="textarea" :rows="3" style="width:250px;" v-model="cert.key"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
<tr>
|
||||
<td>ocspStapling</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input-number v-model.number="cert.ocspStapling" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<template v-if="inbound.stream.isTls">
|
||||
<a-form-item label="SNI" placeholder="Server Name Indication">
|
||||
<a-input v-model.trim="inbound.stream.tls.sni"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="CipherSuites">
|
||||
<a-select v-model="inbound.stream.tls.cipherSuites" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value="">auto</a-select-option>
|
||||
<a-select-option v-for="key,value in TLS_CIPHER_OPTION" :value="key">[[ value ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="Min/Max Version">
|
||||
<a-input-group compact>
|
||||
<a-select v-model="inbound.stream.tls.minVersion" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
<a-select v-model="inbound.stream.tls.maxVersion" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-input-group>
|
||||
</a-form-item>
|
||||
<a-form-item label="uTLS">
|
||||
<a-select v-model="inbound.stream.tls.settings.fingerprint"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value=''>None</a-select-option>
|
||||
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="ALPN">
|
||||
<a-select
|
||||
mode="multiple"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme"
|
||||
v-model="inbound.stream.tls.alpn">
|
||||
<a-select-option v-for="alpn in ALPN_OPTION" :value="alpn">[[ alpn ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="Allow Insecure">
|
||||
<a-switch v-model="inbound.stream.tls.settings.allowInsecure"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label="Reject Unknown SNI">
|
||||
<a-switch v-model="inbound.stream.tls.rejectUnknownSni"></a-switch>
|
||||
</a-form-item>
|
||||
<template v-for="cert,index in inbound.stream.tls.certs">
|
||||
<a-form-item label='{{ i18n "certificate" }}'>
|
||||
<a-radio-group v-model="cert.useFile" button-style="solid">
|
||||
<a-radio-button :value="true">{{ i18n "pages.inbounds.certificatePath" }}</a-radio-button>
|
||||
<a-radio-button :value="false">{{ i18n "pages.inbounds.certificateContent" }}</a-radio-button>
|
||||
</a-radio-group>
|
||||
<a-button v-if="index === 0" type="primary" size="small" @click="inbound.stream.tls.addCert()" style="margin-left: 10px">+</a-button>
|
||||
<a-button v-if="inbound.stream.tls.certs.length>1" type="primary" size="small" @click="inbound.stream.tls.removeCert(index)" style="margin-left: 10px">-</a-button>
|
||||
</a-form-item>
|
||||
<template v-if="cert.useFile">
|
||||
<a-form-item label='{{ i18n "pages.inbounds.publicKeyPath" }}'>
|
||||
<a-input v-model.trim="cert.certFile" style="width:250px;"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.keyPath" }}'>
|
||||
<a-input v-model.trim="cert.keyFile" style="width:250px;"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label=" ">
|
||||
<a-button type="primary" icon="import" @click="setDefaultCertData(index)">{{ i18n "pages.inbounds.setDefaultCert" }}</a-button>
|
||||
</a-form-item>
|
||||
</template>
|
||||
</table>
|
||||
</a-form>
|
||||
<template v-else>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.publicKeyContent" }}'>
|
||||
<a-input type="textarea" :rows="3" style="width:250px;" v-model="cert.cert"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.keyContent" }}'>
|
||||
<a-input type="textarea" :rows="3" style="width:250px;" v-model="cert.key"></a-input>
|
||||
</a-form-item>
|
||||
</template>
|
||||
<a-form-item label='ocspStapling'>
|
||||
<a-input-number v-model.number="cert.ocspStapling" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- reality settings -->
|
||||
<a-form v-if="inbound.reality" layout="inline">
|
||||
<table width="100%" class="ant-table-tbody">
|
||||
<tr>
|
||||
<td>{{ i18n "domainName" }}</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="inbound.stream.reality.settings.serverName" style="width: 250px"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Show</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-switch v-model="inbound.stream.reality.show"></a-switch>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Xver</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input-number v-model.number="inbound.stream.reality.xver" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>uTLS</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-select v-model="inbound.stream.reality.settings.fingerprint"
|
||||
style="width: 250px" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Dest</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="inbound.stream.reality.dest" style="width: 250px"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Server Names</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="inbound.stream.reality.serverNames" style="width: 250px"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Short Ids
|
||||
<template v-if="inbound.stream.isReality">
|
||||
<a-form-item label='Show'>
|
||||
<a-switch v-model="inbound.stream.reality.show"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label='Xver'>
|
||||
<a-input-number v-model.number="inbound.stream.reality.xver" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='uTLS'>
|
||||
<a-select v-model="inbound.stream.reality.settings.fingerprint"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='Dest'>
|
||||
<a-input v-model.trim="inbound.stream.reality.dest"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='Server Names'>
|
||||
<a-input v-model.trim="inbound.stream.reality.serverNames"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.client.renew" }}</span>
|
||||
</template>
|
||||
Short IDs
|
||||
<a-icon @click="inbound.stream.reality.shortIds = RandomUtil.randomShortId().join(',')" type="sync">
|
||||
</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="inbound.stream.reality.shortIds" style="width:250px"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>SpiderX</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="inbound.stream.reality.settings.spiderX" style="width:250px"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Private Key</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="inbound.stream.reality.privateKey" style="width: 250px"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Public Key</td>
|
||||
<td>
|
||||
<a-form-item>
|
||||
<a-input v-model.trim="inbound.stream.reality.settings.publicKey" style="width: 250px"></a-input>
|
||||
</a-form-item>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>
|
||||
<a-button type="primary" icon="import" @click="getNewX25519Cert">Get new cert</a-button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</a-icon>
|
||||
</template>
|
||||
<a-input v-model.trim="inbound.stream.reality.shortIds" style="width:250px"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='SpiderX'>
|
||||
<a-input v-model.trim="inbound.stream.reality.settings.spiderX" style="width:250px"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='Private Key'>
|
||||
<a-input v-model.trim="inbound.stream.reality.privateKey"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='Public Key'>
|
||||
<a-input v-model.trim="inbound.stream.reality.settings.publicKey"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label=" ">
|
||||
<a-button type="primary" icon="import" @click="getNewX25519Cert">Get new cert</a-button>
|
||||
</a-form-item>
|
||||
</template>
|
||||
</a-form>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
@@ -19,8 +19,8 @@
|
||||
:overlay-class-name="themeSwitcher.currentTheme"
|
||||
ok-text='{{ i18n "reset"}}'
|
||||
cancel-text='{{ i18n "cancel"}}'>
|
||||
<a-icon slot="icon" type="question-circle-o" style="color: blue"></a-icon>
|
||||
<a-icon style="font-size: 24px;" class="normal-icon" type="retweet" v-if="client.email.length > 0"></a-icon>
|
||||
<a-icon slot="icon" type="question-circle-o" :style="themeSwitcher.isDarkTheme ? 'color: #3c89e8' : 'color: blue'"></a-icon>
|
||||
<a-icon style="font-size: 24px; cursor: pointer;" class="normal-icon" type="retweet" v-if="client.email.length > 0"></a-icon>
|
||||
</a-popconfirm>
|
||||
</a-tooltip>
|
||||
<a-tooltip>
|
||||
@@ -32,7 +32,7 @@
|
||||
ok-type="danger"
|
||||
cancel-text='{{ i18n "cancel"}}'>
|
||||
<a-icon slot="icon" type="question-circle-o" style="color: #e04141"></a-icon>
|
||||
<a-icon style="font-size: 24px" class="delete-icon" type="delete" v-if="isRemovable(record.id)"></a-icon>
|
||||
<a-icon style="font-size: 24px; cursor: pointer;" class="delete-icon" type="delete" v-if="isRemovable(record.id)"></a-icon>
|
||||
</a-popconfirm>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
@@ -84,7 +84,7 @@
|
||||
:percent="statsProgress(record, client.email)"/>
|
||||
</td>
|
||||
<td width="120px" v-else-if="client.totalGB > 0">
|
||||
<a-progress :stroke-color="statsColor(record, client.email)"
|
||||
<a-progress :stroke-color="clientStatsColor(record, client.email)"
|
||||
:show-info="false"
|
||||
:status="isClientOnline(client.email)? 'active' : isClientEnabled(record, client.email)? 'exception' : ''"
|
||||
:percent="statsProgress(record, client.email)"/>
|
||||
@@ -135,7 +135,7 @@
|
||||
[[ remainedDays(client.expiryTime) ]]
|
||||
</a-tag>
|
||||
</a-popover>
|
||||
<a-tag v-else :color="userExpiryColor(app.expireDiff, client, themeSwitcher.isDarkTheme)" style="border: 0;" class="infinite-tag">∞</a-tag>
|
||||
<a-tag v-else :color="userExpiryColor(app.expireDiff, client, themeSwitcher.isDarkTheme)" style="border: none;" class="infinite-tag">∞</a-tag>
|
||||
</template>
|
||||
</template>
|
||||
<template slot="actionMenu" slot-scope="text, client, index">
|
||||
@@ -200,7 +200,7 @@
|
||||
</tr>
|
||||
</table>
|
||||
</template>
|
||||
<a-progress :stroke-color="statsColor(record, client.email)"
|
||||
<a-progress :stroke-color="clientStatsColor(record, client.email)"
|
||||
:show-info="false"
|
||||
:status="isClientOnline(client.email)? 'active' : isClientEnabled(record, client.email)? 'exception' : ''"
|
||||
:percent="statsProgress(record, client.email)"/>
|
||||
@@ -265,4 +265,4 @@
|
||||
</a-badge>
|
||||
</a-popover>
|
||||
</template>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
@@ -7,66 +7,88 @@
|
||||
width="600px"
|
||||
:class="themeSwitcher.currentTheme"
|
||||
>
|
||||
<table style="margin-bottom: 10px; width: 100%;">
|
||||
<tr><td>
|
||||
<a-row>
|
||||
<a-col :xs="24" :md="12">
|
||||
<table>
|
||||
<tr><td>{{ i18n "protocol" }}</td><td><a-tag color="green">[[ dbInbound.protocol ]]</a-tag></td></tr>
|
||||
<tr><td>{{ i18n "pages.inbounds.address" }}</td><td><a-tag color="blue">[[ dbInbound.address ]]</a-tag></td></tr>
|
||||
<tr><td>{{ i18n "pages.inbounds.port" }}</td><td><a-tag color="green">[[ dbInbound.port ]]</a-tag></td></tr>
|
||||
<tr><td>{{ i18n "protocol" }}</td><td><a-tag color="purple">[[ dbInbound.protocol ]]</a-tag></td></tr>
|
||||
<tr><td>{{ i18n "pages.inbounds.address" }}</td><td>
|
||||
<a-tooltip :title="[[ dbInbound.address ]]">
|
||||
<a-tag class="info-large-tag">[[ dbInbound.address ]]</a-tag>
|
||||
</a-tooltip>
|
||||
</td></tr>
|
||||
<tr><td>{{ i18n "pages.inbounds.port" }}</td><td><a-tag>[[ dbInbound.port ]]</a-tag></td></tr>
|
||||
</table>
|
||||
</td>
|
||||
<td v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS">
|
||||
</a-col>
|
||||
<a-col :xs="24" :md="12">
|
||||
<template v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS">
|
||||
<table>
|
||||
<tr>
|
||||
<td>{{ i18n "transmission" }}</td><td><a-tag color="green">[[ inbound.network ]]</a-tag></td>
|
||||
<td>{{ i18n "transmission" }}</td><td><a-tag color="blue">[[ inbound.network ]]</a-tag></td>
|
||||
</tr>
|
||||
<template v-if="inbound.isTcp || inbound.isWs || inbound.isH2">
|
||||
<tr v-if="inbound.host"><td>{{ i18n "host" }}</td><td><a-tag color="green">[[ inbound.host ]]</a-tag></td></tr>
|
||||
<tr v-else><td>{{ i18n "host" }}</td><td><a-tag color="orange">{{ i18n "none" }}</a-tag></td></tr>
|
||||
|
||||
<tr v-if="inbound.path"><td>{{ i18n "path" }}</td><td><a-tag color="green">[[ inbound.path ]]</a-tag></td></tr>
|
||||
<tr v-else><td>{{ i18n "path" }}</td><td><a-tag color="orange">{{ i18n "none" }}</a-tag></td></tr>
|
||||
<tr>
|
||||
<td>{{ i18n "host" }}</td>
|
||||
<td v-if="inbound.host">
|
||||
<a-tooltip :title="[[ inbound.host ]]">
|
||||
<a-tag class="info-large-tag">[[ inbound.host ]]</a-tag>
|
||||
</a-tooltip>
|
||||
</td>
|
||||
<td v-else><a-tag color="orange">{{ i18n "none" }}</a-tag></td></tr>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "path" }}</td>
|
||||
<td v-if="inbound.path">
|
||||
<a-tooltip :title="[[ inbound.path ]]">
|
||||
<a-tag class="info-large-tag">[[ inbound.path ]]</a-tag>
|
||||
</a-tooltip>
|
||||
<td v-else><a-tag color="orange">{{ i18n "none" }}</a-tag></td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<template v-if="inbound.isQuic">
|
||||
<tr><td>quic {{ i18n "encryption" }}</td><td><a-tag color="green">[[ inbound.quicSecurity ]]</a-tag></td></tr>
|
||||
<tr><td>quic {{ i18n "password" }}</td><td><a-tag color="green">[[ inbound.quicKey ]]</a-tag></td></tr>
|
||||
<tr><td>quic {{ i18n "camouflage" }}</td><td><a-tag color="green">[[ inbound.quicType ]]</a-tag></td></tr>
|
||||
<tr><td>quic {{ i18n "encryption" }}</td><td><a-tag>[[ inbound.quicSecurity ]]</a-tag></td></tr>
|
||||
<tr><td>quic {{ i18n "password" }}</td><td><a-tag>[[ inbound.quicKey ]]</a-tag></td></tr>
|
||||
<tr><td>quic {{ i18n "camouflage" }}</td><td><a-tag>[[ inbound.quicType ]]</a-tag></td></tr>
|
||||
</template>
|
||||
|
||||
<template v-if="inbound.isKcp">
|
||||
<tr><td>kcp {{ i18n "encryption" }}</td><td><a-tag color="green">[[ inbound.kcpType ]]</a-tag></td></tr>
|
||||
<tr><td>kcp {{ i18n "password" }}</td><td><a-tag color="green">[[ inbound.kcpSeed ]]</a-tag></td></tr>
|
||||
<tr><td>kcp {{ i18n "encryption" }}</td><td><a-tag>[[ inbound.kcpType ]]</a-tag></td></tr>
|
||||
<tr><td>kcp {{ i18n "password" }}</td><td><a-tag>[[ inbound.kcpSeed ]]</a-tag></td></tr>
|
||||
</template>
|
||||
|
||||
<template v-if="inbound.isGrpc">
|
||||
<tr><td>grpc serviceName</td><td><a-tag color="green">[[ inbound.serviceName ]]</a-tag></td></tr>
|
||||
<tr><td>grpc multiMode</td><td><a-tag color="green">[[ inbound.stream.grpc.multiMode ]]</a-tag></td></tr>
|
||||
<tr><td>grpc serviceName</td><td>
|
||||
<a-tooltip :title="[[ inbound.serviceName ]]">
|
||||
<a-tag class="info-large-tag">[[ inbound.serviceName ]]</a-tag>
|
||||
</a-tooltip>
|
||||
<tr><td>grpc multiMode</td><td><a-tag>[[ inbound.stream.grpc.multiMode ]]</a-tag></td></tr>
|
||||
</template>
|
||||
</table>
|
||||
</td></tr>
|
||||
<tr colspan="2" v-if="dbInbound.hasLink()">
|
||||
<td v-if="inbound.tls">
|
||||
tls: <a-tag color="green">{{ i18n "enabled" }}</a-tag><br />
|
||||
tls {{ i18n "domainName" }}: <a-tag :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : '' ]]</a-tag>
|
||||
</td>
|
||||
<td v-else-if="inbound.reality">
|
||||
reality: <a-tag color="green">{{ i18n "enabled" }}</a-tag><br />
|
||||
reality Destination: <a-tag :color="inbound.stream.reality.dest ? 'green' : 'orange'">[[ inbound.stream.reality.dest ]]</a-tag>
|
||||
</td>
|
||||
<td v-else>tls: <a-tag color="red">{{ i18n "disabled" }}</a-tag></td>
|
||||
</tr>
|
||||
</table>
|
||||
</template>
|
||||
</a-col>
|
||||
<template v-if="dbInbound.hasLink()">
|
||||
{{ i18n "security" }}
|
||||
<a-tag :color="inbound.stream.security == 'none' ? 'red' : 'green'">[[ inbound.stream.security ]]</a-tag>
|
||||
<br />
|
||||
<template v-if="inbound.stream.security != 'none'">
|
||||
{{ i18n "domainName" }}
|
||||
<a-tag :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : '' ]]</a-tag>
|
||||
</template>
|
||||
</template>
|
||||
<table v-if="dbInbound.isSS" style="margin-bottom: 10px; width: 100%;">
|
||||
<tr>
|
||||
<td>{{ i18n "encryption" }}</td>
|
||||
<td><a-tag color="green">[[ inbound.settings.method ]]</a-tag></td>
|
||||
<td><a-tag color="blue">[[ inbound.settings.method ]]</a-tag></td>
|
||||
</tr><tr v-if="inbound.isSS2022">
|
||||
<td>{{ i18n "password" }}</td>
|
||||
<td><a-tag color="blue">[[ inbound.settings.password ]]</a-tag></td>
|
||||
<td>
|
||||
<a-tooltip :title="[[ inbound.settings.password ]]">
|
||||
<a-tag class="info-large-tag">[[ inbound.settings.password ]]</a-tag>
|
||||
</a-tooltip>
|
||||
</td>
|
||||
</tr><tr>
|
||||
<td>{{ i18n "pages.inbounds.network" }}</td>
|
||||
<td><a-tag color="green">[[ inbound.settings.network ]]</a-tag></td>
|
||||
<td><a-tag color="blue">[[ inbound.settings.network ]]</a-tag></td>
|
||||
</tr>
|
||||
</table>
|
||||
<template v-if="infoModal.clientSettings">
|
||||
@@ -74,37 +96,41 @@
|
||||
<table style="margin-bottom: 10px;">
|
||||
<tr>
|
||||
<td>{{ i18n "pages.inbounds.email" }}</td>
|
||||
<td><a-tag color="green">[[ infoModal.clientSettings.email ]]</a-tag></td>
|
||||
<td><a-tag color="blue">[[ infoModal.clientSettings.email ]]</a-tag></td>
|
||||
</tr>
|
||||
<tr v-if="infoModal.clientSettings.id">
|
||||
<td>ID</td>
|
||||
<td><a-tag color="green">[[ infoModal.clientSettings.id ]]</a-tag></td>
|
||||
<td><a-tag>[[ infoModal.clientSettings.id ]]</a-tag></td>
|
||||
</tr>
|
||||
<tr v-if="infoModal.inbound.canEnableTlsFlow()">
|
||||
<td>Flow</td>
|
||||
<td><a-tag color="green">[[ infoModal.clientSettings.flow ]]</a-tag></td>
|
||||
<td><a-tag>[[ infoModal.clientSettings.flow ]]</a-tag></td>
|
||||
</tr>
|
||||
<tr v-if="infoModal.clientSettings.password">
|
||||
<td>Password</td>
|
||||
<td><a-tag color="green">[[ infoModal.clientSettings.password ]]</a-tag></td>
|
||||
<td>{{ i18n "password" }}</td>
|
||||
<td>
|
||||
<a-tooltip :title="[[ infoModal.clientSettings.password ]]">
|
||||
<a-tag class="info-large-tag">[[ infoModal.clientSettings.password ]]</a-tag>
|
||||
</a-tooltip>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "status" }}</td>
|
||||
<td>
|
||||
<a-tag v-if="isEnable" color="blue">{{ i18n "enabled" }}</a-tag>
|
||||
<a-tag v-else color="red">{{ i18n "disabled" }}</a-tag>
|
||||
<a-tag v-else>{{ i18n "disabled" }}</a-tag>
|
||||
<a-tag v-if="!isActive" color="red">{{ i18n "depleted" }}</a-tag>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="infoModal.clientStats">
|
||||
<td>{{ i18n "usage" }}</td>
|
||||
<td>
|
||||
<a-tag color="green">[[ sizeFormat(infoModal.clientStats.up + infoModal.clientStats.down) ]]</a-tag>
|
||||
<a-tag color="blue">↑ [[ sizeFormat(infoModal.clientStats.up) ]] / [[ sizeFormat(infoModal.clientStats.down) ]] ↓</a-tag>
|
||||
<a-tag color="blue">[[ sizeFormat(infoModal.clientStats.up + infoModal.clientStats.down) ]]</a-tag>
|
||||
<a-tag>↑ [[ sizeFormat(infoModal.clientStats.up) ]] / [[ sizeFormat(infoModal.clientStats.down) ]] ↓</a-tag>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table style="margin-bottom: 10px; width: 100%;">
|
||||
<table style="margin-bottom: 10px; width: 100%; text-align: center;">
|
||||
<tr>
|
||||
<th>{{ i18n "remained" }}</th>
|
||||
<th>{{ i18n "pages.inbounds.totalFlow" }}</th>
|
||||
@@ -120,7 +146,7 @@
|
||||
<a-tag v-if="infoModal.clientSettings.totalGB > 0" :color="statsColor(infoModal.clientStats)">
|
||||
[[ sizeFormat(infoModal.clientSettings.totalGB) ]]
|
||||
</a-tag>
|
||||
<a-tag v-else color="green">{{ i18n "indefinite" }}</a-tag>
|
||||
<a-tag v-else color="purple" class="infinite-tag">∞</a-tag>
|
||||
</td>
|
||||
<td>
|
||||
<template v-if="infoModal.clientSettings.expiryTime > 0">
|
||||
@@ -128,16 +154,16 @@
|
||||
[[ DateUtil.formatMillis(infoModal.clientSettings.expiryTime) ]]
|
||||
</a-tag>
|
||||
</template>
|
||||
<a-tag v-else-if="infoModal.clientSettings.expiryTime < 0" color="cyan">[[ infoModal.clientSettings.expiryTime / -86400000 ]] {{ i18n "pages.client.days" }}</a-tag>
|
||||
<a-tag v-else color="green">{{ i18n "indefinite" }}</a-tag>
|
||||
<a-tag v-else-if="infoModal.clientSettings.expiryTime < 0" color="blue">[[ infoModal.clientSettings.expiryTime / -86400000 ]] {{ i18n "pages.client.days" }}</a-tag>
|
||||
<a-tag v-else color="purple" class="infinite-tag">∞</a-tag>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<template v-if="app.subSettings.enable && infoModal.clientSettings.subId">
|
||||
<a-divider>Subscription link</a-divider>
|
||||
<a-row>
|
||||
<a-col :span="22"><a :href="[[ infoModal.subLink ]]" target="_blank">[[ infoModal.subLink ]]</a></a-col>
|
||||
<a-col :span="2">
|
||||
<a-col :sx="24" :md="22"><a :href="[[ infoModal.subLink ]]" target="_blank">[[ infoModal.subLink ]]</a></a-col>
|
||||
<a-col :sx="24" :md="2" style="text-align: right;">
|
||||
<a-tooltip title='{{ i18n "copy" }}'>
|
||||
<button class="ant-btn ant-btn-primary" id="copy-sub-link" @click="copyToClipboard('copy-sub-link', infoModal.subLink)">
|
||||
<a-icon type="snippets"></a-icon>
|
||||
@@ -147,10 +173,10 @@
|
||||
</a-row>
|
||||
</template>
|
||||
<template v-if="app.tgBotEnable && infoModal.clientSettings.tgId">
|
||||
<a-divider>Telegram Username</a-divider>
|
||||
<a-divider>Telegram ID</a-divider>
|
||||
<a-row>
|
||||
<a-col :span="22"><a :href="[[ infoModal.tgLink ]]" target="_blank">@[[ infoModal.clientSettings.tgId ]]</a></a-col>
|
||||
<a-col :span="2">
|
||||
<a-col :sx="24" :md="22"><a :href="[[ infoModal.tgLink ]]" target="_blank">@[[ infoModal.clientSettings.tgId ]]</a></a-col>
|
||||
<a-col :sx="24" :md="2" style="text-align: right;">
|
||||
<a-tooltip title='{{ i18n "copy" }}'>
|
||||
<button class="ant-btn ant-btn-primary" id="copy-tg-link" @click="copyToClipboard('copy-tg-link', '@' + infoModal.clientSettings.tgId)">
|
||||
<a-icon type="snippets"></a-icon>
|
||||
@@ -162,8 +188,8 @@
|
||||
<template v-if="dbInbound.hasLink()">
|
||||
<a-divider>URL</a-divider>
|
||||
<a-row v-for="(link,index) in infoModal.links">
|
||||
<a-col :span="22"><a-tag color="cyan">[[ link.remark ]]</a-tag><br />[[ link.link ]]</a-col>
|
||||
<a-col :span="2" style="text-align: right;">
|
||||
<a-col :sx="24" :md="22"><a-tag color="blue">[[ link.remark ]]</a-tag><br />[[ link.link ]]</a-col>
|
||||
<a-col :sx="24" :md="2" style="text-align: right;">
|
||||
<a-tooltip title='{{ i18n "copy" }}'>
|
||||
<button class="ant-btn ant-btn-primary" :id="'copy-url-link-'+index" @click="copyToClipboard('copy-url-link-'+index, link.link)">
|
||||
<a-icon type="snippets"></a-icon>
|
||||
@@ -177,7 +203,7 @@
|
||||
<template v-if="dbInbound.isSS && !inbound.isSSMultiUser">
|
||||
<a-divider>URL</a-divider>
|
||||
<a-row v-for="(link,index) in infoModal.links">
|
||||
<a-col :span="22"><a-tag color="cyan">[[ link.remark ]]</a-tag><br />[[ link.link ]]</a-col>
|
||||
<a-col :span="22"><a-tag color="blue">[[ link.remark ]]</a-tag><br />[[ link.link ]]</a-col>
|
||||
<a-col :span="2" style="text-align: right;">
|
||||
<a-tooltip title='{{ i18n "copy" }}'>
|
||||
<button class="ant-btn ant-btn-primary" :id="'copy-url-link-'+index" @click="copyToClipboard('copy-url-link-'+index, link.link)">
|
||||
@@ -194,9 +220,9 @@
|
||||
<th>{{ i18n "pages.inbounds.network" }}</th>
|
||||
<th>FollowRedirect</th>
|
||||
</tr><tr>
|
||||
<td><a-tag color="green">[[ inbound.settings.address ]]</a-tag></td>
|
||||
<td><a-tag color="blue">[[ inbound.settings.address ]]</a-tag></td>
|
||||
<td><a-tag color="blue">[[ inbound.settings.port ]]</a-tag></td>
|
||||
<td><a-tag color="green">[[ inbound.settings.network ]]</a-tag></td>
|
||||
<td><a-tag color="blue">[[ inbound.settings.network ]]</a-tag></td>
|
||||
<td><a-tag color="blue">[[ inbound.settings.followRedirect ]]</a-tag></td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -208,7 +234,7 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a-tag color="green">[[ inbound.settings.auth ]]</a-tag></td>
|
||||
<td><a-tag color="blue">[[ inbound.settings.udp]]</a-tag></td>
|
||||
<td><a-tag color="green">[[ inbound.settings.udp]]</a-tag></td>
|
||||
<td><a-tag color="green">[[ inbound.settings.ip ]]</a-tag></td>
|
||||
</tr>
|
||||
<template v-if="inbound.settings.auth == 'password'">
|
||||
@@ -217,9 +243,9 @@
|
||||
<td>{{ i18n "username" }}</td>
|
||||
<td>{{ i18n "password" }}</td>
|
||||
</tr><tr v-for="account,index in inbound.settings.accounts">
|
||||
<td><a-tag color="green">[[ index ]]</a-tag></td>
|
||||
<td>[[ index ]]</td>
|
||||
<td><a-tag color="blue">[[ account.user ]]</a-tag></td>
|
||||
<td><a-tag color="green">[[ account.pass ]]</a-tag></td>
|
||||
<td><a-tag color="blue">[[ account.pass ]]</a-tag></td>
|
||||
</tr>
|
||||
</template>
|
||||
</table>
|
||||
@@ -229,9 +255,9 @@
|
||||
<th>{{ i18n "username" }}</th>
|
||||
<th>{{ i18n "password" }}</th>
|
||||
</tr><tr v-for="account,index in inbound.settings.accounts">
|
||||
<td><a-tag color="green">[[ index ]]</a-tag></td>
|
||||
<td>[[ index ]]</td>
|
||||
<td><a-tag color="blue">[[ account.user ]]</a-tag></td>
|
||||
<td><a-tag color="green">[[ account.pass ]]</a-tag></td>
|
||||
<td><a-tag color="blue">[[ account.pass ]]</a-tag></td>
|
||||
</tr>
|
||||
</table>
|
||||
</template>
|
||||
@@ -241,7 +267,6 @@
|
||||
visible: false,
|
||||
inbound: new Inbound(),
|
||||
dbInbound: new DBInbound(),
|
||||
settings: null,
|
||||
clientSettings: null,
|
||||
clientStats: [],
|
||||
upStats: 0,
|
||||
@@ -256,27 +281,10 @@
|
||||
this.index = index;
|
||||
this.inbound = dbInbound.toInbound();
|
||||
this.dbInbound = new DBInbound(dbInbound);
|
||||
this.settings = JSON.parse(this.inbound.settings);
|
||||
this.clientSettings = this.settings.clients ? Object.values(this.settings.clients)[index] : null;
|
||||
this.isExpired = this.inbound.isExpiry(index);
|
||||
this.clientStats = this.settings.clients ? this.dbInbound.clientStats.find(row => row.email === this.clientSettings.email) : [];
|
||||
remark = [this.dbInbound.remark, ( this.clientSettings ? this.clientSettings.email : '')].filter(Boolean).join('-');
|
||||
address = this.dbInbound.address;
|
||||
this.links = [];
|
||||
if (this.inbound.tls && !ObjectUtil.isArrEmpty(this.inbound.stream.tls.settings.domains)) {
|
||||
this.inbound.stream.tls.settings.domains.forEach((domain) => {
|
||||
remarkText = [remark, domain.remark].filter(Boolean).join('-');
|
||||
this.links.push({
|
||||
remark: remarkText,
|
||||
link: this.inbound.genLink(domain.domain, remarkText, index)
|
||||
});
|
||||
});
|
||||
} else {
|
||||
this.links.push({
|
||||
remark: remark,
|
||||
link: this.inbound.genLink(address, remark, index)
|
||||
});
|
||||
}
|
||||
this.clientSettings = this.inbound.clients ? this.inbound.clients[index] : null;
|
||||
this.isExpired = this.inbound.clients ? this.inbound.isExpiry(index): this.dbInbound.isExpiry;
|
||||
this.clientStats = this.inbound.clients ? this.dbInbound.clientStats.find(row => row.email === this.clientSettings.email) : [];
|
||||
this.links = this.inbound.genAllLinks(this.dbInbound.remark, app.remarkModel, this.clientSettings);
|
||||
if (this.clientSettings) {
|
||||
if (this.clientSettings.subId) {
|
||||
this.subLink = this.genSubLink(this.clientSettings.subId);
|
||||
@@ -291,8 +299,7 @@
|
||||
infoModal.visible = false;
|
||||
},
|
||||
genSubLink(subID) {
|
||||
const { domain: host, port, tls: isTLS, path: base } = app.subSettings;
|
||||
return buildURL({ host, port, isTLS, base, path: subID+'?name='+subID });
|
||||
return app.subSettings.subURI+subID+'?name='+subID;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -42,15 +42,6 @@
|
||||
loading(loading) {
|
||||
inModal.confirmLoading = loading;
|
||||
},
|
||||
getClients(protocol, clientSettings) {
|
||||
switch(protocol){
|
||||
case Protocols.VMESS: return clientSettings.vmesses;
|
||||
case Protocols.VLESS: return clientSettings.vlesses;
|
||||
case Protocols.TROJAN: return clientSettings.trojans;
|
||||
case Protocols.SHADOWSOCKS: return clientSettings.shadowsockses;
|
||||
default: return null;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
new Vue({
|
||||
@@ -69,7 +60,7 @@
|
||||
return inModal.isEdit;
|
||||
},
|
||||
get client() {
|
||||
return inModal.getClients(this.inbound.protocol, this.inbound.settings)[0];
|
||||
return inModal.inbound.clients[0];
|
||||
},
|
||||
get delayedExpireDays() {
|
||||
return this.client && this.client.expiryTime < 0 ? this.client.expiryTime / -86400000 : 0;
|
||||
@@ -77,26 +68,29 @@
|
||||
set delayedExpireDays(days){
|
||||
this.client.expiryTime = -86400000 * days;
|
||||
},
|
||||
get multiDomain() {
|
||||
return this.inbound.stream.tls.settings.domains.length > 0;
|
||||
get externalProxy() {
|
||||
return this.inbound.stream.externalProxy.length > 0;
|
||||
},
|
||||
set multiDomain(value) {
|
||||
set externalProxy(value) {
|
||||
if (value) {
|
||||
inModal.inbound.stream.tls.server = "";
|
||||
inModal.inbound.stream.tls.settings.domains = [{ remark: "", domain: window.location.hostname }];
|
||||
inModal.inbound.stream.externalProxy = [{
|
||||
forceTls: "same",
|
||||
dest: window.location.hostname,
|
||||
port: inModal.inbound.port,
|
||||
remark: ""
|
||||
}];
|
||||
} else {
|
||||
inModal.inbound.stream.tls.server = "";
|
||||
inModal.inbound.stream.tls.settings.domains = [];
|
||||
inModal.inbound.stream.externalProxy = [];
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
streamNetworkChange() {
|
||||
if (!inModal.inbound.canSetTls()) {
|
||||
if (!inModal.inbound.canEnableTls()) {
|
||||
this.inModal.inbound.stream.security = 'none';
|
||||
}
|
||||
if (!inModal.inbound.canEnableReality()) {
|
||||
this.inModal.inbound.reality = false;
|
||||
this.inModal.inbound.stream.isReality = false;
|
||||
}
|
||||
if (this.inModal.inbound.protocol == Protocols.VLESS && !inModal.inbound.canEnableTlsFlow()) {
|
||||
this.inModal.inbound.settings.vlesses.forEach(client => {
|
||||
|
||||
@@ -43,6 +43,10 @@
|
||||
0%, 50%, 100% { transform: scale(1); opacity: 1; }
|
||||
10% { transform: scale(1.5); opacity: .2; }
|
||||
}
|
||||
.info-large-tag {
|
||||
max-width: 200px;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
|
||||
<body>
|
||||
@@ -76,6 +80,10 @@
|
||||
<a-tag color="blue">[[ dbInbounds.length ]]</a-tag>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="24" :lg="12">
|
||||
<template>
|
||||
<div>
|
||||
<a-back-top :target="() => document.getElementById('content-layout')" visibility-height="200">
|
||||
</a-back-top>
|
||||
{{ i18n "clients" }}:
|
||||
<a-tag color="blue">[[ total.clients ]]</a-tag>
|
||||
<a-popover title='{{ i18n "disabled" }}' :overlay-class-name="themeSwitcher.currentTheme">
|
||||
@@ -102,6 +110,8 @@
|
||||
</template>
|
||||
<a-tag color="green" v-if="onlineClients.length">[[ onlineClients.length ]]</a-tag>
|
||||
</a-popover>
|
||||
</div>
|
||||
</template>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
@@ -119,6 +129,10 @@
|
||||
<template v-if="!isMobile">{{ i18n "pages.inbounds.generalActions" }}</template>
|
||||
</a-button>
|
||||
<a-menu slot="overlay" @click="a => generalActions(a)" :theme="themeSwitcher.currentTheme">
|
||||
<a-menu-item key="import">
|
||||
<a-icon type="import"></a-icon>
|
||||
{{ i18n "pages.inbounds.importInbound" }}
|
||||
</a-menu-item>
|
||||
<a-menu-item key="export">
|
||||
<a-icon type="export"></a-icon>
|
||||
{{ i18n "pages.inbounds.export" }}
|
||||
@@ -150,9 +164,9 @@
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; justify-content: flex-start;">
|
||||
<div :style="isMobile ? '' : 'display: flex; align-items: center; justify-content: flex-start;'">
|
||||
<a-switch v-model="enableFilter"
|
||||
style="margin-right: .5rem;"
|
||||
:style="isMobile ? 'margin-bottom: .5rem; display: flex;' : 'margin-right: .5rem;'"
|
||||
@change="toggleFilter">
|
||||
<a-icon slot="checkedChildren" type="search"></a-icon>
|
||||
<a-icon slot="unCheckedChildren" type="filter"></a-icon>
|
||||
@@ -166,6 +180,7 @@
|
||||
<a-radio-button value="online">{{ i18n "online" }}</a-radio-button>
|
||||
</a-radio-group>
|
||||
</div>
|
||||
<a-back-top></a-back-top>
|
||||
<a-table :columns="isMobile ? mobileColums : columns" :row-key="dbInbound => dbInbound.id"
|
||||
:data-source="searchedInbounds"
|
||||
:scroll="isMobile ? {} : { x: 1000 }"
|
||||
@@ -219,6 +234,10 @@
|
||||
<a-menu-item key="resetTraffic">
|
||||
<a-icon type="retweet"></a-icon> {{ i18n "pages.inbounds.resetTraffic" }}
|
||||
</a-menu-item>
|
||||
<a-menu-item key="clipboard">
|
||||
<a-icon type="copy"></a-icon>
|
||||
{{ i18n "pages.inbounds.copyToClipboard" }}
|
||||
</a-menu-item>
|
||||
<a-menu-item key="clone">
|
||||
<a-icon type="block"></a-icon> {{ i18n "pages.inbounds.clone"}}
|
||||
</a-menu-item>
|
||||
@@ -408,7 +427,7 @@
|
||||
:columns="isMobile ? innerMobileColumns : innerColumns"
|
||||
:data-source="getInboundClients(record)"
|
||||
:pagination=pagination(getInboundClients(record))
|
||||
:style="isMobile ? 'margin: -16px -5px -17px;' : 'margin-left: 10px;'">
|
||||
style="margin: -12px -6px -13px;">
|
||||
{{template "client_table"}}
|
||||
</a-table>
|
||||
</template>
|
||||
@@ -420,6 +439,12 @@
|
||||
</a-layout>
|
||||
</a-layout>
|
||||
{{template "js" .}}
|
||||
<script src="{{ .base_path }}assets/base64/base64.min.js"></script>
|
||||
<script src="{{ .base_path }}assets/qrcode/qrious.min.js"></script>
|
||||
<script src="{{ .base_path }}assets/clipboard/clipboard.min.js"></script>
|
||||
<script src="{{ .base_path }}assets/uri/URI.min.js"></script>
|
||||
<script src="{{ .base_path }}assets/js/model/xray.js?{{ .cur_ver }}"></script>
|
||||
<script src="{{ .base_path }}assets/js/model/dbinbound.js?{{ .cur_ver }}"></script>
|
||||
{{template "component/themeSwitcher" .}}
|
||||
<script>
|
||||
const columns = [{
|
||||
@@ -496,9 +521,9 @@
|
||||
{ title: '{{ i18n "pages.inbounds.operate" }}', width: 50, scopedSlots: { customRender: 'actions' } },
|
||||
{ title: '{{ i18n "pages.inbounds.enable" }}', width: 20, scopedSlots: { customRender: 'enable' } },
|
||||
{ title: '{{ i18n "online" }}', width: 20, scopedSlots: { customRender: 'online' } },
|
||||
{ title: '{{ i18n "pages.inbounds.client" }}', width: 80, scopedSlots: { customRender: 'client' } },
|
||||
{ title: '{{ i18n "pages.inbounds.client" }}', width: 70, scopedSlots: { customRender: 'client' } },
|
||||
{ title: '{{ i18n "pages.inbounds.traffic" }}', width: 80, align: 'center', scopedSlots: { customRender: 'traffic' } },
|
||||
{ title: '{{ i18n "pages.inbounds.expireDate" }}', width: 80, align: 'center', scopedSlots: { customRender: 'expiryTime' } },
|
||||
{ title: '{{ i18n "pages.inbounds.expireDate" }}', width: 70, align: 'center', scopedSlots: { customRender: 'expiryTime' } },
|
||||
];
|
||||
|
||||
const innerMobileColumns = [
|
||||
@@ -531,11 +556,9 @@
|
||||
refreshInterval: Number(localStorage.getItem("refreshInterval")) || 5000,
|
||||
subSettings: {
|
||||
enable : false,
|
||||
port: 0,
|
||||
path: '',
|
||||
domain: '',
|
||||
tls: false
|
||||
subURI : ''
|
||||
},
|
||||
remarkModel: '-ieo',
|
||||
tgBotEnable: false,
|
||||
showAlert: false,
|
||||
pageSize: 0,
|
||||
@@ -578,12 +601,10 @@
|
||||
this.tgBotEnable = tgBotEnable;
|
||||
this.subSettings = {
|
||||
enable : subEnable,
|
||||
port: subPort,
|
||||
path: subPath,
|
||||
domain: subDomain,
|
||||
tls: subTLS
|
||||
subURI: subURI
|
||||
};
|
||||
this.pageSize = pageSize;
|
||||
this.remarkModel = remarkModel;
|
||||
}
|
||||
},
|
||||
setInbounds(dbInbounds) {
|
||||
@@ -610,7 +631,7 @@
|
||||
},
|
||||
getClientCounts(dbInbound, inbound) {
|
||||
let clientCount = 0, active = [], deactive = [], depleted = [], expiring = [], online = [];
|
||||
clients = this.getClients(dbInbound.protocol, inbound.settings);
|
||||
clients = inbound.clients;
|
||||
clientStats = dbInbound.clientStats
|
||||
now = new Date().getTime()
|
||||
if (clients) {
|
||||
@@ -700,6 +721,9 @@
|
||||
},
|
||||
generalActions(action) {
|
||||
switch (action.key) {
|
||||
case "import":
|
||||
this.importInbound();
|
||||
break;
|
||||
case "export":
|
||||
this.exportAllLinks();
|
||||
break;
|
||||
@@ -734,6 +758,9 @@
|
||||
case "export":
|
||||
this.inboundLinks(dbInbound.id);
|
||||
break;
|
||||
case "clipboard":
|
||||
this.copyToClipboard(dbInbound.id);
|
||||
break;
|
||||
case "resetTraffic":
|
||||
this.resetTraffic(dbInbound.id);
|
||||
break;
|
||||
@@ -785,7 +812,7 @@
|
||||
this.$confirm({
|
||||
title: '{{ i18n "pages.inbounds.cloneInbound"}} \"' + dbInbound.remark + '\"',
|
||||
content: '{{ i18n "pages.inbounds.cloneInboundContent"}}',
|
||||
okText: '{{ i18n "pages.inbounds.update"}}',
|
||||
okText: '{{ i18n "pages.inbounds.clone"}}',
|
||||
class: themeSwitcher.currentTheme,
|
||||
cancelText: '{{ i18n "cancel" }}',
|
||||
onOk: () => {
|
||||
@@ -921,7 +948,7 @@
|
||||
resetTraffic(dbInboundId) {
|
||||
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
|
||||
this.$confirm({
|
||||
title: '{{ i18n "pages.inbounds.resetTraffic"}}',
|
||||
title: '{{ i18n "pages.inbounds.resetTraffic"}}' + ' #' + dbInboundId,
|
||||
content: '{{ i18n "pages.inbounds.resetTrafficContent"}}',
|
||||
class: themeSwitcher.currentTheme,
|
||||
okText: '{{ i18n "reset"}}',
|
||||
@@ -936,7 +963,7 @@
|
||||
},
|
||||
delInbound(dbInboundId) {
|
||||
this.$confirm({
|
||||
title: '{{ i18n "pages.inbounds.deleteInbound"}}',
|
||||
title: '{{ i18n "pages.inbounds.deleteInbound"}}' + ' #' + dbInboundId,
|
||||
content: '{{ i18n "pages.inbounds.deleteInboundContent"}}',
|
||||
class: themeSwitcher.currentTheme,
|
||||
okText: '{{ i18n "delete"}}',
|
||||
@@ -949,7 +976,7 @@
|
||||
clientId = this.getClientId(dbInbound.protocol, client);
|
||||
if (confirmation){
|
||||
this.$confirm({
|
||||
title: '{{ i18n "pages.inbounds.deleteClient"}}',
|
||||
title: '{{ i18n "pages.inbounds.deleteClient"}}' + ' ' + client.email,
|
||||
content: '{{ i18n "pages.inbounds.deleteClientContent"}}',
|
||||
class: themeSwitcher.currentTheme,
|
||||
okText: '{{ i18n "delete"}}',
|
||||
@@ -960,15 +987,6 @@
|
||||
this.submit(`/xui/inbound/${dbInboundId}/delClient/${clientId}`);
|
||||
}
|
||||
},
|
||||
getClients(protocol, clientSettings) {
|
||||
switch (protocol) {
|
||||
case Protocols.VMESS: return clientSettings.vmesses;
|
||||
case Protocols.VLESS: return clientSettings.vlesses;
|
||||
case Protocols.TROJAN: return clientSettings.trojans;
|
||||
case Protocols.SHADOWSOCKS: return clientSettings.shadowsockses;
|
||||
default: return null;
|
||||
}
|
||||
},
|
||||
getClientId(protocol, client) {
|
||||
switch (protocol) {
|
||||
case Protocols.TROJAN: return client.password;
|
||||
@@ -980,7 +998,7 @@
|
||||
newDbInbound = new DBInbound(dbInbound);
|
||||
if (dbInbound.listen.startsWith("@")){
|
||||
rootInbound = this.inbounds.find((i) =>
|
||||
i.tls &&
|
||||
i.isTcp &&
|
||||
['trojan','vless'].includes(i.protocol) &&
|
||||
i.settings.fallbacks.find(f => f.dest === dbInbound.listen)
|
||||
);
|
||||
@@ -988,8 +1006,9 @@
|
||||
newDbInbound.listen = rootInbound.listen;
|
||||
newDbInbound.port = rootInbound.port;
|
||||
newInbound = newDbInbound.toInbound();
|
||||
newInbound.stream.security = 'tls';
|
||||
newInbound.stream.security = rootInbound.stream.security;
|
||||
newInbound.stream.tls = rootInbound.stream.tls;
|
||||
newInbound.stream.externalProxy = rootInbound.stream.externalProxy;
|
||||
newDbInbound.streamSettings = newInbound.stream.toString();
|
||||
}
|
||||
}
|
||||
@@ -997,18 +1016,15 @@
|
||||
},
|
||||
showQrcode(dbInboundId, client) {
|
||||
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
|
||||
inbound = dbInbound.toInbound();
|
||||
clients = this.getClients(dbInbound.protocol, inbound.settings);
|
||||
index = this.findIndexOfClient(dbInbound.protocol, clients, client);
|
||||
newDbInbound = this.checkFallback(dbInbound);
|
||||
qrModal.show('{{ i18n "qrCode"}}', newDbInbound, index);
|
||||
qrModal.show('{{ i18n "qrCode"}}', newDbInbound, client);
|
||||
},
|
||||
showInfo(dbInboundId, client) {
|
||||
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
|
||||
index=0;
|
||||
if (dbInbound.isMultiUser()){
|
||||
inbound = dbInbound.toInbound();
|
||||
clients = this.getClients(dbInbound.protocol, inbound.settings);
|
||||
clients = inbound.clients;
|
||||
index = this.findIndexOfClient(dbInbound.protocol, clients, client);
|
||||
}
|
||||
newDbInbound = this.checkFallback(dbInbound);
|
||||
@@ -1022,7 +1038,7 @@
|
||||
this.loading()
|
||||
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
|
||||
inbound = dbInbound.toInbound();
|
||||
clients = this.getClients(dbInbound.protocol, inbound.settings);
|
||||
clients = inbound.clients;
|
||||
index = this.findIndexOfClient(dbInbound.protocol, clients, client);
|
||||
clients[index].enable = !clients[index].enable;
|
||||
clientId = this.getClientId(dbInbound.protocol, clients[index]);
|
||||
@@ -1036,20 +1052,12 @@
|
||||
}
|
||||
},
|
||||
getInboundClients(dbInbound) {
|
||||
if (dbInbound.protocol == Protocols.VLESS) {
|
||||
return dbInbound.toInbound().settings.vlesses;
|
||||
} else if (dbInbound.protocol == Protocols.VMESS) {
|
||||
return dbInbound.toInbound().settings.vmesses;
|
||||
} else if (dbInbound.protocol == Protocols.TROJAN) {
|
||||
return dbInbound.toInbound().settings.trojans;
|
||||
} else if (dbInbound.protocol == Protocols.SHADOWSOCKS) {
|
||||
return dbInbound.toInbound().settings.shadowsockses;
|
||||
}
|
||||
return dbInbound.toInbound().clients;
|
||||
},
|
||||
resetClientTraffic(client, dbInboundId, confirmation = true) {
|
||||
if (confirmation){
|
||||
this.$confirm({
|
||||
title: '{{ i18n "pages.inbounds.resetTraffic"}}',
|
||||
title: '{{ i18n "pages.inbounds.resetTraffic"}}' + ' ' + client.email,
|
||||
content: '{{ i18n "pages.inbounds.resetTrafficContent"}}',
|
||||
class: themeSwitcher.currentTheme,
|
||||
okText: '{{ i18n "reset"}}',
|
||||
@@ -1085,7 +1093,7 @@
|
||||
title: '{{ i18n "pages.inbounds.delDepletedClientsTitle"}}',
|
||||
content: '{{ i18n "pages.inbounds.delDepletedClientsContent"}}',
|
||||
class: themeSwitcher.currentTheme,
|
||||
okText: '{{ i18n "reset"}}',
|
||||
okText: '{{ i18n "delete"}}',
|
||||
cancelText: '{{ i18n "cancel"}}',
|
||||
onOk: () => this.submit('/xui/inbound/delDepletedClients/' + dbInboundId),
|
||||
})
|
||||
@@ -1112,22 +1120,13 @@
|
||||
if (email.length == 0) return 0;
|
||||
clientStats = dbInbound.clientStats.find(stats => stats.email === email);
|
||||
if (!clientStats) return 0;
|
||||
remained = clientStats.totalGB - (clientStats.up + clientStats.down);
|
||||
remained = clientStats.total - (clientStats.up + clientStats.down);
|
||||
return remained>0 ? remained : 0;
|
||||
},
|
||||
statsColor(dbInbound, email) {
|
||||
if (email.length == 0) return '#0e49b5';
|
||||
clientStatsColor(dbInbound, email) {
|
||||
if (email.length == 0) return clientUsageColor();
|
||||
clientStats = dbInbound.clientStats.find(stats => stats.email === email);
|
||||
switch (true) {
|
||||
case !clientStats:
|
||||
return "#0e49b5";
|
||||
case clientStats.up + clientStats.down < clientStats.total - app.trafficDiff:
|
||||
return "#0e49b5";
|
||||
case clientStats.up + clientStats.down < clientStats.total:
|
||||
return "#FFA031";
|
||||
default:
|
||||
return "#E04141";
|
||||
}
|
||||
return clientUsageColor(clientStats, app.trafficDiff)
|
||||
},
|
||||
statsProgress(dbInbound, email) {
|
||||
if (email.length == 0) return 100;
|
||||
@@ -1180,14 +1179,30 @@
|
||||
inboundLinks(dbInboundId) {
|
||||
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
|
||||
newDbInbound = this.checkFallback(dbInbound);
|
||||
txtModal.show('{{ i18n "pages.inbounds.export"}}', newDbInbound.genInboundLinks, newDbInbound.remark);
|
||||
txtModal.show('{{ i18n "pages.inbounds.export"}}', newDbInbound.genInboundLinks(this.remarkModel), newDbInbound.remark);
|
||||
},
|
||||
importInbound() {
|
||||
promptModal.open({
|
||||
title: '{{ i18n "pages.inbounds.importInbound" }}',
|
||||
type: 'textarea',
|
||||
value: '',
|
||||
okText: '{{ i18n "pages.inbounds.import" }}',
|
||||
confirm: async (dbInboundText) => {
|
||||
await this.submit('/xui/inbound/import', {data: dbInboundText}, promptModal);
|
||||
promptModal.close();
|
||||
},
|
||||
});
|
||||
},
|
||||
exportAllLinks() {
|
||||
let copyText = '';
|
||||
let copyText = [];
|
||||
for (const dbInbound of this.dbInbounds) {
|
||||
copyText += dbInbound.genInboundLinks;
|
||||
copyText.push(dbInbound.genInboundLinks(this.remarkModel));
|
||||
}
|
||||
txtModal.show('{{ i18n "pages.inbounds.export"}}', copyText, 'All-Inbounds');
|
||||
txtModal.show('{{ i18n "pages.inbounds.export"}}', copyText.join('\r\n'), 'All-Inbounds');
|
||||
},
|
||||
copyToClipboard(dbInboundId) {
|
||||
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
|
||||
txtModal.show('{{ i18n "pages.inbounds.inboundData" }}', JSON.stringify(dbInbound, null, 2));
|
||||
},
|
||||
async startDataRefreshLoop() {
|
||||
while (this.isRefreshEnabled) {
|
||||
|
||||
@@ -85,7 +85,10 @@
|
||||
<a-col :sm="24" :md="12">
|
||||
<a-card hoverable>
|
||||
X-UI: <a href="https://github.com/alireza0/x-ui/releases" target="_blank"><a-tag color="blue">{{ .cur_ver }}</a-tag></a>
|
||||
Xray: <a-tag color="blue" style="cursor: pointer;" @click="openSelectV2rayVersion">[[ status.xray.version ]]</a-tag>
|
||||
Xray:
|
||||
<a-tooltip title='{{ i18n "pages.index.xraySwitch" }}'>
|
||||
<a-tag color="blue" style="cursor: pointer;" @click="openSelectV2rayVersion">[[ status.xray.version ]]</a-tag>
|
||||
</a-tooltip>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :sm="24" :md="12">
|
||||
@@ -98,7 +101,7 @@
|
||||
<template slot="title">
|
||||
{{ i18n "pages.index.operationHoursDesc" }}
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
<a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
<a-tag color="blue">[[ formatSecond(status.uptime) ]]</a-tag>
|
||||
</a-card>
|
||||
@@ -107,15 +110,18 @@
|
||||
<a-card hoverable>
|
||||
{{ i18n "pages.index.xrayStatus" }}:
|
||||
<a-tag :color="status.xray.color">[[ status.xray.state ]]</a-tag>
|
||||
<a-tooltip v-if="status.xray.state === State.Error">
|
||||
<template slot="title">
|
||||
<p v-for="line in status.xray.errorMsg.split('\n')">[[ line ]]</p>
|
||||
<a-popover v-if="status.xray.state === State.Error"
|
||||
:overlay-class-name="themeSwitcher.currentTheme">
|
||||
<span slot="title" style="font-size: 12pt">Error in running xray-core
|
||||
<a-tag color="purple" style="cursor: pointer; float: right;" @click="openLogs()">{{ i18n "pages.index.logs" }}</a-tag>
|
||||
</span>
|
||||
<template slot="content">
|
||||
<p style="max-width: 400px" v-for="line in status.xray.errorMsg.split('\n')">[[ line ]]</p>
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
<a-icon type="question-circle"></a-icon>
|
||||
</a-popover>
|
||||
<a-tag color="purple" style="cursor: pointer;" @click="stopXrayService">{{ i18n "pages.index.stopXray" }}</a-tag>
|
||||
<a-tag color="purple" style="cursor: pointer;" @click="restartXrayService">{{ i18n "pages.index.restartXray" }}</a-tag>
|
||||
<a-tag color="purple" style="cursor: pointer;" @click="openSelectV2rayVersion">{{ i18n "pages.index.xraySwitch" }}</a-tag>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :sm="24" :md="12">
|
||||
@@ -134,7 +140,7 @@
|
||||
<a-col :sm="24" :md="12">
|
||||
<a-card hoverable>
|
||||
{{ i18n "usage"}}:
|
||||
Memory: [[ sizeFormat(status.appStats.mem) ]] -
|
||||
RAM: [[ sizeFormat(status.appStats.mem) ]] -
|
||||
Threads: [[ status.appStats.threads ]]
|
||||
</a-tooltip>
|
||||
</a-card>
|
||||
@@ -147,7 +153,7 @@
|
||||
<template slot="title">
|
||||
[[ status.hostInfo.ipv4 ]]
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
<a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<template v-if="status.hostInfo.ipv6">IPv6:
|
||||
@@ -155,20 +161,33 @@
|
||||
<template slot="title">
|
||||
[[ status.hostInfo.ipv6 ]]
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
<a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :sm="24" :md="12">
|
||||
<a-card hoverable>
|
||||
{{ i18n "pages.index.connectionCount" }}: TCP: [[ status.tcpCount ]] UDP: [[ status.udpCount ]]
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
{{ i18n "pages.index.connectionCountDesc" }}
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
<a-row>
|
||||
<a-col :span="12">
|
||||
TCP: [[ status.tcpCount ]]
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
{{ i18n "pages.index.connectionTcpCountDesc" }}
|
||||
</template>
|
||||
<a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
UDP: [[ status.udpCount ]]
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
{{ i18n "pages.index.connectionUdpCountDesc" }}
|
||||
</template>
|
||||
<a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :sm="24" :md="12">
|
||||
@@ -176,22 +195,22 @@
|
||||
<a-row>
|
||||
<a-col :span="12">
|
||||
<a-icon type="arrow-up"></a-icon>
|
||||
[[ sizeFormat(status.netIO.up) ]] / S
|
||||
[[ sizeFormat(status.netIO.up) ]]/s
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
{{ i18n "pages.index.upSpeed" }}
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
<a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-icon type="arrow-down"></a-icon>
|
||||
[[ sizeFormat(status.netIO.down) ]] / S
|
||||
[[ sizeFormat(status.netIO.down) ]]/s
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
{{ i18n "pages.index.downSpeed" }}
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
<a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</a-col>
|
||||
</a-row>
|
||||
@@ -207,7 +226,7 @@
|
||||
<template slot="title">
|
||||
{{ i18n "pages.index.totalSent" }}
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
<a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
@@ -217,7 +236,7 @@
|
||||
<template slot="title">
|
||||
{{ i18n "pages.index.totalReceive" }}
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
<a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</a-col>
|
||||
</a-row>
|
||||
@@ -232,8 +251,10 @@
|
||||
:closable="true" @ok="() => versionModal.visible = false"
|
||||
:class="themeSwitcher.currentTheme"
|
||||
footer="">
|
||||
<h2>{{ i18n "pages.index.xraySwitchClick"}}</h2>
|
||||
<h2>{{ i18n "pages.index.xraySwitchClickDesk"}}</h2>
|
||||
<a-alert type="warning" style="margin-bottom: 12px; width: fit-content"
|
||||
message='{{ i18n "pages.index.xraySwitchClickDesk" }}'
|
||||
show-icon
|
||||
></a-alert>
|
||||
<template v-for="version, index in versionModal.versions">
|
||||
<a-tag :color="index % 2 == 0 ? 'purple' : 'blue'"
|
||||
style="margin: 10px" @click="switchV2rayVersion(version)">
|
||||
@@ -242,7 +263,7 @@
|
||||
</template>
|
||||
</a-modal>
|
||||
|
||||
<a-modal id="log-modal" v-model="logModal.visible" title="X-UI logs"
|
||||
<a-modal id="log-modal" v-model="logModal.visible" title="Logs"
|
||||
:closable="true" @ok="() => logModal.visible = false" @cancel="() => logModal.visible = false"
|
||||
:class="themeSwitcher.currentTheme"
|
||||
width="800px"
|
||||
@@ -272,7 +293,7 @@
|
||||
<a-checkbox v-model="logModal.syslog" @change="openLogs()"></a-checkbox>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<button class="ant-btn ant-btn-primary" @click="openLogs()"><a-icon type="sync"></a-icon> Reload</button>
|
||||
<a-button class="ant-btn ant-btn-primary" :loading="logModal.loading" @click="openLogs()"><a-icon :spin="logModal.loading" type="sync"></a-icon> Reload</a-button>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button type="primary" style="margin-bottom: 10px;"
|
||||
@@ -281,8 +302,7 @@
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<a-input type="textarea" v-model="logModal.logs" disabled="true"
|
||||
:autosize="{ minRows: 10, maxRows: 22}"></a-input>
|
||||
<div class="ant-input" style="height: auto; max-height: 500px; overflow: auto;" v-html="logModal.logs"></div>
|
||||
</a-modal>
|
||||
|
||||
<a-modal id="backup-modal" v-model="backupModal.visible" :title="backupModal.title"
|
||||
@@ -305,10 +325,10 @@
|
||||
|
||||
</a-layout>
|
||||
{{template "js" .}}
|
||||
<script src="{{ .base_path }}assets/clipboard/clipboard.min.js"></script>
|
||||
{{template "component/themeSwitcher" .}}
|
||||
{{template "textModal"}}
|
||||
<script>
|
||||
|
||||
const State = {
|
||||
Running: "running",
|
||||
Stop: "stop",
|
||||
@@ -412,9 +432,46 @@
|
||||
rows: 20,
|
||||
level: 'info',
|
||||
syslog: false,
|
||||
loading: false,
|
||||
show(logs) {
|
||||
this.visible = true;
|
||||
this.logs = logs? logs.join("\n"): "No Record...";
|
||||
this.logs = logs? this.formatLogs(logs) : "No Record...";
|
||||
},
|
||||
formatLogs(logs) {
|
||||
let formattedLogs = '';
|
||||
const levels = ["DEBUG","INFO","WARNING","ERROR"];
|
||||
const levelColors = ["#3c89e8","#008771","#f37b24","#e04141","#bcbcbc"];
|
||||
|
||||
logs.forEach((log, index) => {
|
||||
let [data, message] = log.split(" - ",2);
|
||||
const parts = data.split(" ")
|
||||
if(index>0) formattedLogs += '<br>';
|
||||
|
||||
if (parts.length === 3) {
|
||||
const d = parts[0];
|
||||
const t = parts[1];
|
||||
const level = parts[2];
|
||||
const levelIndex = levels.indexOf(level,levels) || 4;
|
||||
|
||||
//formattedLogs += `<span style="color: gray;">${index + 1}.</span>`;
|
||||
formattedLogs += `<span style="color: ${levelColors[0]};">${d} ${t}</span> `;
|
||||
formattedLogs += `<span style="color: ${levelColors[levelIndex]}">${level}</span>`;
|
||||
} else {
|
||||
const levelIndex = levels.indexOf(data,levels) || 4;
|
||||
formattedLogs += `<span style="color: ${levelColors[levelIndex]}">${data}</span>`;
|
||||
}
|
||||
|
||||
if(message){
|
||||
if(message.startsWith("XRAY:"))
|
||||
message = "<b>XRAY: </b>" + message.substring(5);
|
||||
else
|
||||
message = "<b>X-UI: </b>" + message;
|
||||
}
|
||||
|
||||
formattedLogs += message ? ' - ' + message : '';
|
||||
});
|
||||
|
||||
return formattedLogs;
|
||||
},
|
||||
hide() {
|
||||
this.visible = false;
|
||||
@@ -513,13 +570,14 @@
|
||||
}
|
||||
},
|
||||
async openLogs(){
|
||||
this.loading(true);
|
||||
logModal.loading = true;
|
||||
const msg = await HttpUtil.post('server/logs/'+logModal.rows,{level: logModal.level, syslog: logModal.syslog});
|
||||
this.loading(false);
|
||||
if (!msg.success) {
|
||||
return;
|
||||
}
|
||||
logModal.show(msg.obj);
|
||||
await PromiseUtil.sleep(500);
|
||||
logModal.loading = false;
|
||||
},
|
||||
async openConfig() {
|
||||
this.loading(true);
|
||||
|
||||
@@ -61,16 +61,44 @@
|
||||
</a-space>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="16">
|
||||
<a-alert type="warning" style="float: right; width: fit-content"
|
||||
message='{{ i18n "pages.settings.infoDesc" }}'
|
||||
show-icon
|
||||
>
|
||||
<template>
|
||||
<div>
|
||||
<a-back-top :target="() => document.getElementById('content-layout')" visibility-height="200">
|
||||
</a-back-top>
|
||||
<a-alert type="warning" style="float: right; width: fit-content"
|
||||
message='{{ i18n "pages.settings.infoDesc" }}'
|
||||
show-icon
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
<a-tabs default-active-key="1">
|
||||
<a-tab-pane key="1" tab='{{ i18n "pages.settings.panelConfig"}}'>
|
||||
<a-list item-layout="horizontal">
|
||||
<a-list-item>
|
||||
<a-row style="padding: 20px">
|
||||
<a-col :lg="24" :xl="12">
|
||||
<a-list-item-meta title='{{ i18n "pages.settings.remarkModel"}}'>
|
||||
<template slot="description">{{ i18n "pages.settings.sampleRemark"}}: <i>#[[ remarkSample ]]</i></template>
|
||||
</a-list-item-meta>
|
||||
</a-col>
|
||||
<a-col :lg="24" :xl="12">
|
||||
<a-input-group style="width: 100%;">
|
||||
<a-select style="padding-right: .5rem; min-width: 80%; width: auto;"
|
||||
mode="multiple"
|
||||
v-model="remarkModel"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="(value, key) in remarkModels" :value="key">[[ value ]]</a-select-option>
|
||||
</a-select>
|
||||
<a-select style="width: 20%;" v-model="remarkSeparator" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="key in remarkSeparators" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-input-group>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.settings.panelListeningIP"}}' desc='{{ i18n "pages.settings.panelListeningIPDesc"}}' v-model="allSetting.webListen"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.settings.panelListeningDomain"}}' desc='{{ i18n "pages.settings.panelListeningDomainDesc"}}' v-model="allSetting.webDomain"></setting-list-item>
|
||||
<setting-list-item type="number" title='{{ i18n "pages.settings.panelPort"}}' desc='{{ i18n "pages.settings.panelPortDesc"}}' v-model.number="allSetting.webPort"></setting-list-item>
|
||||
@@ -87,7 +115,6 @@
|
||||
<a-col :lg="24" :xl="12">
|
||||
<a-list-item-meta title="Language"/>
|
||||
</a-col>
|
||||
|
||||
<a-col :lg="24" :xl="12">
|
||||
<template>
|
||||
<a-select
|
||||
@@ -107,22 +134,40 @@
|
||||
</a-list>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="2" tab='{{ i18n "pages.settings.userSettings"}}'>
|
||||
<a-form style="padding: 20px;">
|
||||
<a-form-item label='{{ i18n "pages.settings.oldUsername"}}'>
|
||||
<a-input v-model="user.oldUsername" style="max-width: 300px"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.settings.currentPassword"}}'>
|
||||
<password-input v-model="user.oldPassword" style="max-width: 300px"></password-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.settings.newUsername"}}'>
|
||||
<a-input v-model="user.newUsername" style="max-width: 300px"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.settings.newPassword"}}'>
|
||||
<password-input v-model="user.newPassword" style="max-width: 300px"></password-input>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button type="primary" @click="updateUser">{{ i18n "confirm" }}</a-button>
|
||||
</a-form-item>
|
||||
<a-form style="padding: 20px;" layout="inline">
|
||||
<a-row>
|
||||
<a-col :xs="24" :md="8" style="margin-top: 8px;">{{ i18n "pages.settings.oldUsername"}}:</a-col>
|
||||
<a-col :xs="24" :md="8">
|
||||
<a-form-item>
|
||||
<a-input v-model="user.oldUsername" style="width: 200px"></a-input>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row>
|
||||
<a-col :xs="24" :md="8" style="margin-top: 8px;">{{ i18n "pages.settings.currentPassword"}}:</a-col>
|
||||
<a-col :xs="24" :md="8">
|
||||
<a-form-item>
|
||||
<password-input v-model="user.oldPassword" style="width: 200px"></password-input>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row>
|
||||
<a-col :xs="24" :md="8" style="margin-top: 8px;">{{ i18n "pages.settings.newUsername"}}:</a-col>
|
||||
<a-col :xs="24" :md="8">
|
||||
<a-form-item>
|
||||
<a-input v-model="user.newUsername" style="width: 200px"></a-input>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row>
|
||||
<a-col :xs="24" :md="8" style="margin-top: 8px;">{{ i18n "pages.settings.newPassword"}}:</a-col>
|
||||
<a-col :xs="24" :md="8">
|
||||
<a-form-item>
|
||||
<password-input v-model="user.newPassword" style="width: 200px"></password-input>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-button type="primary" @click="updateUser">{{ i18n "confirm" }}</a-button>
|
||||
</a-form>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="3" tab='{{ i18n "pages.settings.TGBotSettings"}}'>
|
||||
@@ -146,8 +191,7 @@
|
||||
ref="selectBotLang"
|
||||
v-model="allSetting.tgLang"
|
||||
style="width: 100%"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme"
|
||||
>
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option :value="l.value" :label="l.value" v-for="l in supportLangs">
|
||||
<span role="img" aria-label="l.name" v-text="l.icon"></span>
|
||||
<span v-text="l.name"></span>
|
||||
@@ -170,6 +214,7 @@
|
||||
<setting-list-item type="text" title='{{ i18n "pages.settings.subPath"}}' desc='{{ i18n "pages.settings.subPathDesc"}}' v-model="allSetting.subPath"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.settings.subCertPath"}}' desc='{{ i18n "pages.settings.subCertPathDesc"}}' v-model="allSetting.subCertFile"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.settings.subKeyPath"}}' desc='{{ i18n "pages.settings.subKeyPathDesc"}}' v-model="allSetting.subKeyFile"></setting-list-item>
|
||||
<setting-list-item type="text" title='{{ i18n "pages.settings.subURI"}}' desc='{{ i18n "pages.settings.subURIDesc"}}' v-model="allSetting.subURI" placeholder="(http|https)://domain[:port]/path/"></setting-list-item>
|
||||
<setting-list-item type="number" title='{{ i18n "pages.settings.subUpdates"}}' desc='{{ i18n "pages.settings.subUpdatesDesc"}}' v-model="allSetting.subUpdates"></setting-list-item>
|
||||
</a-list>
|
||||
</a-tab-pane>
|
||||
@@ -180,6 +225,7 @@
|
||||
</a-layout>
|
||||
</a-layout>
|
||||
{{template "js" .}}
|
||||
<script src="{{ .base_path }}assets/js/model/setting.js?{{ .cur_ver }}"></script>
|
||||
{{template "component/themeSwitcher" .}}
|
||||
{{template "component/password" .}}
|
||||
{{template "component/setting"}}
|
||||
@@ -197,7 +243,31 @@
|
||||
saveBtnDisable: true,
|
||||
user: {},
|
||||
lang: getLang(),
|
||||
showAlert: false
|
||||
showAlert: false,
|
||||
remarkModels: {i:'Inbound',e:'Email',o:'Other'},
|
||||
remarkSeparators: [' ','-','_','@',':','~','|',',','.','/'],
|
||||
remarkSample: '',
|
||||
get remarkModel() {
|
||||
rm = this.allSetting.remarkModel;
|
||||
return rm.length>1 ? rm.substring(1).split('') : [];
|
||||
},
|
||||
set remarkModel(value) {
|
||||
rs = this.allSetting.remarkModel[0];
|
||||
this.allSetting.remarkModel = rs + value.join('');
|
||||
this.changeRemarkSample();
|
||||
},
|
||||
get remarkSeparator() {
|
||||
return this.allSetting.remarkModel.length > 1 ? this.allSetting.remarkModel.charAt(0) : '-';
|
||||
},
|
||||
set remarkSeparator(value) {
|
||||
this.allSetting.remarkModel = value + this.allSetting.remarkModel.substring(1);
|
||||
this.changeRemarkSample();
|
||||
},
|
||||
changeRemarkSample(){
|
||||
sample = []
|
||||
this.remarkModel.forEach(r => sample.push(this.remarkModels[r]));
|
||||
this.remarkSample = sample.length == 0 ? '' : sample.join(this.remarkSeparator);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
loading(spinning = true) {
|
||||
@@ -210,6 +280,7 @@
|
||||
if (msg.success) {
|
||||
this.oldAllSetting = new AllSetting(msg.obj);
|
||||
this.allSetting = new AllSetting(msg.obj);
|
||||
app.changeRemarkSample();
|
||||
this.saveBtnDisable = true;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,6 +1,23 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
{{template "head" .}}
|
||||
<link rel="stylesheet" href="{{ .base_path }}assets/codemirror/codemirror.css">
|
||||
<link rel="stylesheet" href="{{ .base_path }}assets/codemirror/fold/foldgutter.css">
|
||||
<link rel="stylesheet" href="{{ .base_path }}assets/codemirror/xq.css?{{ .cur_ver }}">
|
||||
<link rel="stylesheet" href="{{ .base_path }}assets/codemirror/lint/lint.css">
|
||||
|
||||
<script src="{{ .base_path }}assets/base64/base64.min.js"></script>
|
||||
<script src="{{ .base_path }}assets/js/model/outbound.js"></script>
|
||||
<script src="{{ .base_path }}assets/codemirror/codemirror.js"></script>
|
||||
<script src="{{ .base_path }}assets/codemirror/javascript.js"></script>
|
||||
<script src="{{ .base_path }}assets/codemirror/jshint.js"></script>
|
||||
<script src="{{ .base_path }}assets/codemirror/jsonlint.js"></script>
|
||||
<script src="{{ .base_path }}assets/codemirror/lint/lint.js"></script>
|
||||
<script src="{{ .base_path }}assets/codemirror/lint/javascript-lint.js"></script>
|
||||
<script src="{{ .base_path }}assets/codemirror/hint/javascript-hint.js"></script>
|
||||
<script src="{{ .base_path }}assets/codemirror/fold/foldcode.js"></script>
|
||||
<script src="{{ .base_path }}assets/codemirror/fold/foldgutter.js"></script>
|
||||
<script src="{{ .base_path }}assets/codemirror/fold/brace-fold.js"></script>
|
||||
<style>
|
||||
@media (min-width: 769px) {
|
||||
.ant-layout-content {
|
||||
@@ -13,6 +30,10 @@
|
||||
margin: 0;
|
||||
padding: 12px .5rem;
|
||||
}
|
||||
.ant-table-thead > tr > th,
|
||||
.ant-table-tbody > tr > td {
|
||||
padding: 10px 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-tabs-bar {
|
||||
@@ -56,19 +77,33 @@
|
||||
<a-row>
|
||||
<a-col :xs="24" :sm="8" style="padding: 4px;">
|
||||
<a-space direction="horizontal">
|
||||
<a-button type="primary" :disabled="saveBtnDisable" @click="updateXraySetting">{{ i18n "pages.settings.save" }}</a-button>
|
||||
<a-button type="danger" :disabled="!saveBtnDisable" @click="restartPanel">{{ i18n "pages.settings.restartPanel" }}</a-button>
|
||||
<a-button type="primary" :disabled="saveBtnDisable" @click="updateXraySetting">{{ i18n "pages.xray.save" }}</a-button>
|
||||
<a-button type="danger" :disabled="!saveBtnDisable" @click="restartXray">{{ i18n "pages.xray.restart" }}</a-button>
|
||||
<a-popover v-if="restartResult"
|
||||
:overlay-class-name="themeSwitcher.currentTheme">
|
||||
<span slot="title" style="font-size: 12pt">Error in running xray-core</span>
|
||||
<template slot="content">
|
||||
<p style="max-width: 400px" v-for="line in restartResult.split('\n')">[[ line ]]</p>
|
||||
</template>
|
||||
<a-icon type="question-circle"></a-icon>
|
||||
</a-popover>
|
||||
</a-space>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="16">
|
||||
<a-alert type="warning" style="float: right; width: fit-content"
|
||||
message='{{ i18n "pages.settings.infoDesc" }}'
|
||||
show-icon
|
||||
>
|
||||
<template>
|
||||
<div>
|
||||
<a-back-top :target="() => document.getElementById('content-layout')" visibility-height="200">
|
||||
</a-back-top>
|
||||
<a-alert type="warning" style="float: right; width: fit-content"
|
||||
message='{{ i18n "pages.settings.infoDesc" }}'
|
||||
show-icon
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
<a-tabs default-active-key="1">
|
||||
<a-tabs default-active-key="1" @change="(activeKey) => { if(activeKey == 'tpl-4') this.changeCode(); }">
|
||||
<a-tab-pane key="tpl-1" tab='{{ i18n "pages.xray.basicTemplate"}}' style="padding-top: 20px;">
|
||||
<a-collapse>
|
||||
<a-collapse-panel header='{{ i18n "pages.xray.generalConfigs"}}'>
|
||||
@@ -121,7 +156,7 @@
|
||||
<a-alert type="warning" style="text-align: center;">
|
||||
<template slot="message">
|
||||
<a-icon type="exclamation-circle" theme="filled" style="color: #FFA031"></a-icon>
|
||||
{{ i18n "pages.xray.generalConfigsDesc" }}
|
||||
{{ i18n "pages.xray.blockConfigsDesc" }}
|
||||
</template>
|
||||
</a-alert>
|
||||
</a-row>
|
||||
@@ -181,48 +216,196 @@
|
||||
</a-collapse-panel>
|
||||
</a-collapse>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="tpl-2" tab='{{ i18n "pages.xray.manualLists"}}' style="padding-top: 20px;">
|
||||
<a-row :xs="24" :sm="24" :lg="12" style="margin-bottom: 10px;">
|
||||
<a-alert type="warning" style="float: left; width: fit-content">
|
||||
<template slot="message">
|
||||
<a-icon type="exclamation-circle" theme="filled" style="color: #FFA031"></a-icon>
|
||||
{{ i18n "pages.xray.manualListsDesc" }}
|
||||
</template>
|
||||
</a-alert>
|
||||
<a-tab-pane key="tpl-2" tab='{{ i18n "pages.xray.Routings"}}' style="padding-top: 20px;">
|
||||
<a-alert type="warning" style="margin-bottom: 10px; width: fit-content"
|
||||
message='{{ i18n "pages.xray.RoutingsDesc"}}' show-icon></a-alert>
|
||||
<a-button type="primary" icon="plus" @click="addRule">{{ i18n "pages.xray.rules.add" }}</a-button>
|
||||
<a-table :columns="isMobile ? rulesMobileColumns : rulesColumns" bordered
|
||||
:row-key="r => r.key"
|
||||
:data-source="routingRuleData"
|
||||
:scroll="isMobile ? {} : { x: 1000 }"
|
||||
:pagination="false"
|
||||
:indent-size="0"
|
||||
:style="isMobile ? 'padding: 5px 0' : 'margin-top: 10px;'">
|
||||
<template slot="action" slot-scope="text, rule, index">
|
||||
[[ index+1 ]]
|
||||
<a-dropdown :trigger="['click']">
|
||||
<a-icon @click="e => e.preventDefault()" type="more" style="font-size: 16px; text-decoration: bold;"></a-icon>
|
||||
<a-menu slot="overlay" :theme="themeSwitcher.currentTheme">
|
||||
<a-menu-item v-if="index>0" @click="replaceRule(index,0)">
|
||||
<a-icon type="vertical-align-top"></a-icon>
|
||||
{{ i18n "pages.xray.rules.first"}}
|
||||
</a-menu-item>
|
||||
<a-menu-item v-if="index>0" @click="replaceRule(index,index-1)">
|
||||
<a-icon type="arrow-up"></a-icon>
|
||||
{{ i18n "pages.xray.rules.up"}}
|
||||
</a-menu-item>
|
||||
<a-menu-item v-if="index<routingRuleData.length-1" @click="replaceRule(index,index+1)">
|
||||
<a-icon type="arrow-down"></a-icon>
|
||||
{{ i18n "pages.xray.rules.down"}}
|
||||
</a-menu-item>
|
||||
<a-menu-item v-if="index<routingRuleData.length-1" @click="replaceRule(index,routingRuleData.length-1)">
|
||||
<a-icon type="vertical-align-bottom"></a-icon>
|
||||
{{ i18n "pages.xray.rules.last"}}
|
||||
</a-menu-item>
|
||||
<a-menu-item @click="editRule(index)">
|
||||
<a-icon type="edit"></a-icon>
|
||||
{{ i18n "edit" }}
|
||||
</a-menu-item>
|
||||
<a-menu-item @click="deleteRule(index)">
|
||||
<span style="color: #FF4D4F">
|
||||
<a-icon type="delete"></a-icon> {{ i18n "delete"}}
|
||||
</span>
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</a-dropdown>
|
||||
</template>
|
||||
<template slot="inbound" slot-scope="text, rule, index">
|
||||
<a-popover :overlay-class-name="themeSwitcher.currentTheme">
|
||||
<template slot="content">
|
||||
<p v-if="rule.inboundTag">Inbound Tag: [[ rule.inboundTag ]]</p>
|
||||
<p v-if="rule.user">User email: [[ rule.user ]]</p>
|
||||
</template>
|
||||
[[ [rule.inboundTag,rule.user].join('\n') ]]
|
||||
</a-popover>
|
||||
</template>
|
||||
<template slot="outbound" slot-scope="text, rule, index">
|
||||
<a-popover :overlay-class-name="themeSwitcher.currentTheme">
|
||||
<template slot="content">
|
||||
<p v-if="rule.outboundTag">Outbound Tag: [[ rule.outboundTag ]]</p>
|
||||
</template>
|
||||
[[ rule.outboundTag ]]
|
||||
</a-popover>
|
||||
</template>
|
||||
<template slot="info" slot-scope="text, rule, index">
|
||||
<a-popover placement="bottomRight"
|
||||
v-if="(rule.source+rule.sourcePort+rule.network+rule.protocol+rule.attrs+rule.ip+rule.domain+rule.port).length>0"
|
||||
:overlay-class-name="themeSwitcher.currentTheme" trigger="click">
|
||||
<template slot="content">
|
||||
<table cellpadding="2" style="max-width: 300px;">
|
||||
<tr v-if="rule.source">
|
||||
<td>Source</td>
|
||||
<td><a-tag color="blue" v-for="r in rule.source.split(',')">[[ r ]]</a-tag></td>
|
||||
</tr>
|
||||
<tr v-if="rule.sourcePort">
|
||||
<td>Source Port</td>
|
||||
<td><a-tag color="green" v-for="r in rule.sourcePort.split(',')">[[ r ]]</a-tag></td>
|
||||
</tr>
|
||||
<tr v-if="rule.network">
|
||||
<td>Network</td>
|
||||
<td><a-tag color="blue" v-for="r in rule.network.split(',')">[[ r ]]</a-tag></td>
|
||||
</tr>
|
||||
<tr v-if="rule.protocol">
|
||||
<td>Protocol</td>
|
||||
<td><a-tag color="green" v-for="r in rule.protocol.split(',')">[[ r ]]</a-tag></td>
|
||||
</tr>
|
||||
<tr v-if="rule.attrs">
|
||||
<td>Attrs</td>
|
||||
<td><a-tag color="blue" v-for="r in rule.attrs.split(',')">[[ r ]]</a-tag></td>
|
||||
</tr>
|
||||
<tr v-if="rule.ip">
|
||||
<td>IP</td>
|
||||
<td><a-tag color="green" v-for="r in rule.ip.split(',')">[[ r ]]</a-tag></td>
|
||||
</tr>
|
||||
<tr v-if="rule.domain">
|
||||
<td>Domain</td>
|
||||
<td><a-tag color="blue" v-for="r in rule.domain.split(',')">[[ r ]]</a-tag></td>
|
||||
</tr>
|
||||
<tr v-if="rule.port">
|
||||
<td>Port</td>
|
||||
<td><a-tag color="green" v-for="r in rule.port.split(',')">[[ r ]]</a-tag></td>
|
||||
</tr>
|
||||
</table>
|
||||
</template>
|
||||
<a-button shape="round" size="small" style="font-size: 14px; padding: 0 10px;">
|
||||
<a-icon type="info"></a-icon>
|
||||
</a-button>
|
||||
</a-popover>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="tpl-3" tab='{{ i18n "pages.xray.Outbounds"}}' style="padding-top: 20px;" force-render="true">
|
||||
<a-button type="primary" icon="plus" @click="addOutbound()">{{ i18n "pages.xray.outbound.addOutbound" }}</a-button>
|
||||
<a-button type="primary" icon="plus" @click="addReverse()">{{ i18n "pages.xray.outbound.addReverse" }}</a-button>
|
||||
<a-row>
|
||||
<a-col :sm="24" :md="12">
|
||||
<p style="margin: 10px;">{{ i18n "pages.xray.Outbounds"}}</p>
|
||||
<a-table :columns="outboundColumns" bordered
|
||||
:row-key="r => r.key"
|
||||
:data-source="outboundData"
|
||||
:scroll="isMobile ? {} : { x: 200 }"
|
||||
:pagination="false"
|
||||
:indent-size="0"
|
||||
:style="isMobile ? 'padding: 5px 5px' : 'margin-right: 1px;'">
|
||||
<template slot="action" slot-scope="text, outbound, index">
|
||||
[[ index+1 ]]
|
||||
<a-dropdown :trigger="['click']">
|
||||
<a-icon @click="e => e.preventDefault()" type="more" style="font-size: 16px; text-decoration: bold;"></a-icon>
|
||||
<a-menu slot="overlay" :theme="themeSwitcher.currentTheme">
|
||||
<a-menu-item @click="editOutbound(index)">
|
||||
<a-icon type="edit"></a-icon>
|
||||
{{ i18n "edit" }}
|
||||
</a-menu-item>
|
||||
<a-menu-item @click="deleteOutbound(index)">
|
||||
<span style="color: #FF4D4F">
|
||||
<a-icon type="delete"></a-icon> {{ i18n "delete"}}
|
||||
</span>
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</a-dropdown>
|
||||
</template>
|
||||
<template slot="address" slot-scope="text, outbound, index">
|
||||
<p style="margin: 0 5px;" v-for="addr in findOutboundAddress(outbound)">[[ addr ]]</p>
|
||||
</template>
|
||||
<template slot="protocol" slot-scope="text, outbound, index">
|
||||
<a-tag style="margin:0;" color="purple">[[ outbound.protocol ]]</a-tag>
|
||||
<template v-if="[Protocols.VMess, Protocols.VLESS, Protocols.Trojan, Protocols.Shadowsocks].includes(outbound.protocol)">
|
||||
<a-tag style="margin:0;" color="blue">[[ outbound.streamSettings.network ]]</a-tag>
|
||||
<a-tag style="margin:0;" v-if="outbound.streamSettings.security=='tls'" color="green">tls</a-tag>
|
||||
<a-tag style="margin:0;" v-if="outbound.streamSettings.security=='reality'" color="green">reality</a-tag>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-col>
|
||||
<a-col :sm="24" :md="12" v-if="reverseData.length>0">
|
||||
<p style="margin: 10px;">{{ i18n "pages.xray.outbound.reverse"}}</p>
|
||||
<a-table :columns="reverseColumns" bordered
|
||||
:row-key="r => r.key"
|
||||
:data-source="reverseData"
|
||||
:scroll="isMobile ? {} : { x: 200 }"
|
||||
:pagination="false"
|
||||
:indent-size="0"
|
||||
:style="isMobile ? 'padding: 5px 0' : 'margin-left: 1px;'">
|
||||
<template slot="action" slot-scope="text, reverse, index">
|
||||
[[ index+1 ]]
|
||||
<a-dropdown :trigger="['click']">
|
||||
<a-icon @click="e => e.preventDefault()" type="more" style="font-size: 16px; text-decoration: bold;"></a-icon>
|
||||
<a-menu slot="overlay" :theme="themeSwitcher.currentTheme">
|
||||
<a-menu-item @click="editReverse(index)">
|
||||
<a-icon type="edit"></a-icon>
|
||||
{{ i18n "edit" }}
|
||||
</a-menu-item>
|
||||
<a-menu-item @click="deleteReverse(index)">
|
||||
<span style="color: #FF4D4F">
|
||||
<a-icon type="delete"></a-icon> {{ i18n "delete"}}
|
||||
</span>
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</a-dropdown>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-collapse>
|
||||
<a-collapse-panel header='{{ i18n "pages.xray.manualBlockedIPs"}}'>
|
||||
<setting-list-item type="textarea" v-model="manualBlockedIPs"></setting-list-item>
|
||||
</a-collapse-panel>
|
||||
<a-collapse-panel header='{{ i18n "pages.xray.manualBlockedDomains"}}'>
|
||||
<setting-list-item type="textarea" v-model="manualBlockedDomains"></setting-list-item>
|
||||
</a-collapse-panel>
|
||||
<a-collapse-panel header='{{ i18n "pages.xray.manualDirectIPs"}}'>
|
||||
<setting-list-item type="textarea" v-model="manualDirectIPs"></setting-list-item>
|
||||
</a-collapse-panel>
|
||||
<a-collapse-panel header='{{ i18n "pages.xray.manualDirectDomains"}}'>
|
||||
<setting-list-item type="textarea" v-model="manualDirectDomains"></setting-list-item>
|
||||
</a-collapse-panel>
|
||||
<a-collapse-panel header='{{ i18n "pages.xray.manualIPv4Domains"}}'>
|
||||
<setting-list-item type="textarea" v-model="manualIPv4Domains"></setting-list-item>
|
||||
</a-collapse-panel>
|
||||
</a-collapse>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="tpl-3" tab='{{ i18n "pages.xray.advancedTemplate"}}' style="padding-top: 20px;">
|
||||
<a-collapse>
|
||||
<a-collapse-panel header='{{ i18n "pages.xray.Inbounds"}}'>
|
||||
<setting-list-item type="textarea" title='{{ i18n "pages.xray.Inbounds"}}' desc='{{ i18n "pages.xray.InboundsDesc"}}' v-model="inboundSettings"></setting-list-item>
|
||||
</a-collapse-panel>
|
||||
<a-collapse-panel header='{{ i18n "pages.xray.Outbounds"}}'>
|
||||
<setting-list-item type="textarea" title='{{ i18n "pages.xray.Outbounds"}}' desc='{{ i18n "pages.xray.OutboundsDesc"}}' v-model="outboundSettings"></setting-list-item>
|
||||
</a-collapse-panel>
|
||||
<a-collapse-panel header='{{ i18n "pages.xray.Routings"}}'>
|
||||
<setting-list-item type="textarea" title='{{ i18n "pages.xray.Routings"}}' desc='{{ i18n "pages.xray.RoutingsDesc"}}' v-model="routingRuleSettings"></setting-list-item>
|
||||
</a-collapse-panel>
|
||||
</a-collapse>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="tpl-4" tab='{{ i18n "pages.xray.completeTemplate"}}' style="padding-top: 20px;">
|
||||
<setting-list-item type="textarea" title='{{ i18n "pages.xray.Template"}}' desc='{{ i18n "pages.xray.TemplateDesc"}}' v-model="xraySetting"></setting-list-item>
|
||||
<a-tab-pane key="tpl-4" tab='{{ i18n "pages.xray.advancedTemplate"}}' style="padding-top: 20px;" force-render="true">
|
||||
<a-list-item-meta title='{{ i18n "pages.xray.Template"}}' description='{{ i18n "pages.xray.TemplateDesc"}}'></a-list-item-meta>
|
||||
<a-radio-group v-model="advSettings" @change="changeCode" button-style="solid" style="margin: 10px 0;" :size="isMobile ? 'small' : ''">
|
||||
<a-radio-button value="xraySetting">{{ i18n "pages.xray.completeTemplate"}}</a-radio-button>
|
||||
<a-radio-button value="inboundSettings">{{ i18n "pages.xray.Inbounds" }}</a-radio-button>
|
||||
<a-radio-button value="outboundSettings">{{ i18n "pages.xray.Outbounds" }}</a-radio-button>
|
||||
<a-radio-button value="routingRuleSettings">{{ i18n "pages.xray.Routings" }}</a-radio-button>
|
||||
</a-radio-group>
|
||||
<textarea style="position:absolute; left: -800px;" id="xraySetting"></textarea>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</a-space>
|
||||
@@ -233,7 +416,49 @@
|
||||
{{template "js" .}}
|
||||
{{template "component/themeSwitcher" .}}
|
||||
{{template "component/setting"}}
|
||||
{{template "ruleModal"}}
|
||||
{{template "outModal"}}
|
||||
{{template "reverseModal"}}
|
||||
<script>
|
||||
const rulesColumns = [
|
||||
{ title: "#", align: 'center', width: 15, scopedSlots: { customRender: 'action' } },
|
||||
{ title: '{{ i18n "pages.xray.rules.source"}}', children: [
|
||||
{ title: 'IP', dataIndex: "source", align: 'center', width: 20, ellipsis: true },
|
||||
{ title: 'Port', dataIndex: 'sourcePort', align: 'center', width: 10, ellipsis: true } ]},
|
||||
{ title: '{{ i18n "pages.inbounds.network"}}', children: [
|
||||
{ title: 'L4', dataIndex: 'network', align: 'center', width: 10 },
|
||||
{ title: 'Protocol', dataIndex: 'protocol', align: 'center', width: 10, ellipsis: true },
|
||||
{ title: 'Attrs', dataIndex: 'attrs', align: 'center', width: 20, ellipsis: true } ]},
|
||||
{ title: '{{ i18n "pages.xray.rules.dest"}}', children: [
|
||||
{ title: 'IP', dataIndex: 'ip', align: 'center', width: 20, ellipsis: true },
|
||||
{ title: 'Domain', dataIndex: 'domain', align: 'center', width: 20, ellipsis: true },
|
||||
{ title: 'Port', dataIndex: 'port', align: 'center', width: 10, ellipsis: true }]},
|
||||
{ title: '{{ i18n "pages.xray.rules.inbound"}}', children: [
|
||||
{ title: 'Inbound Tag', dataIndex: 'inboundTag', align: 'center', width: 20, ellipsis: true },
|
||||
{ title: 'Client Email', dataIndex: 'user', align: 'center', width: 20, ellipsis: true }]},
|
||||
{ title: '{{ i18n "pages.xray.rules.outbound"}}', dataIndex: 'outboundTag', align: 'center', width: 20 },
|
||||
];
|
||||
|
||||
const rulesMobileColumns = [
|
||||
{ title: "#", align: 'center', width: 20, scopedSlots: { customRender: 'action' } },
|
||||
{ title: '{{ i18n "pages.xray.rules.inbound"}}', align: 'center', width: 50, ellipsis: true, scopedSlots: { customRender: 'inbound' } },
|
||||
{ title: '{{ i18n "pages.xray.rules.outbound"}}', align: 'center', width: 50, ellipsis: true, scopedSlots: { customRender: 'outbound' } },
|
||||
{ title: '{{ i18n "pages.xray.rules.info"}}', align: 'center', width: 50, ellipsis: true, scopedSlots: { customRender: 'info' } },
|
||||
];
|
||||
|
||||
const outboundColumns = [
|
||||
{ title: "#", align: 'center', width: 20, scopedSlots: { customRender: 'action' } },
|
||||
{ title: '{{ i18n "pages.xray.outbound.tag"}}', dataIndex: 'tag', align: 'center', width: 50 },
|
||||
{ title: '{{ i18n "protocol"}}', align: 'center', width: 50, scopedSlots: { customRender: 'protocol' } },
|
||||
{ title: '{{ i18n "pages.xray.outbound.address"}}', align: 'center', width: 50, scopedSlots: { customRender: 'address' } },
|
||||
];
|
||||
|
||||
const reverseColumns = [
|
||||
{ title: "#", align: 'center', width: 20, scopedSlots: { customRender: 'action' } },
|
||||
{ title: '{{ i18n "pages.xray.outbound.type"}}', dataIndex: 'type', align: 'center', width: 50 },
|
||||
{ title: '{{ i18n "pages.xray.outbound.tag"}}', dataIndex: 'tag', align: 'center', width: 50 },
|
||||
{ title: '{{ i18n "pages.xray.outbound.domain"}}', dataIndex: 'domain', align: 'center', width: 50 },
|
||||
];
|
||||
|
||||
const app = new Vue({
|
||||
delimiters: ['[[', ']]'],
|
||||
@@ -241,11 +466,38 @@
|
||||
data: {
|
||||
siderDrawer,
|
||||
themeSwitcher,
|
||||
isDarkTheme: themeSwitcher.isDarkTheme,
|
||||
spinning: false,
|
||||
oldXraySetting: '',
|
||||
xraySetting: '',
|
||||
inboundTags: [],
|
||||
saveBtnDisable: true,
|
||||
restartResult: '',
|
||||
showAlert: false,
|
||||
isMobile: window.innerWidth <= 768,
|
||||
advSettings: 'xraySetting',
|
||||
cm: null,
|
||||
cmOptions: {
|
||||
lineNumbers: true,
|
||||
mode: "application/json",
|
||||
lint: true,
|
||||
styleActiveLine: true,
|
||||
matchBrackets: true,
|
||||
theme: "xq",
|
||||
autoCloseTags: true,
|
||||
lineWrapping: true,
|
||||
indentUnit: 2,
|
||||
indentWithTabs: true,
|
||||
smartIndent: true,
|
||||
tabSize: 2,
|
||||
lineWiseCopyCut: false,
|
||||
foldGutter: true,
|
||||
gutters: [
|
||||
"CodeMirror-lint-markers",
|
||||
"CodeMirror-linenumbers",
|
||||
"CodeMirror-foldgutter",
|
||||
],
|
||||
},
|
||||
ipv4Settings: {
|
||||
tag: "IPv4",
|
||||
protocol: "freedom",
|
||||
@@ -257,7 +509,6 @@
|
||||
tag: "direct",
|
||||
protocol: "freedom"
|
||||
},
|
||||
outboundDomainStrategies: ["AsIs", "UseIP", "UseIPv4", "UseIPv6"],
|
||||
routingDomainStrategies: ["AsIs", "IPIfNonMatch", "IPOnDemand"],
|
||||
settingsData: {
|
||||
protocols: {
|
||||
@@ -266,13 +517,13 @@
|
||||
ips: {
|
||||
local: ["geoip:private"],
|
||||
cn: ["geoip:cn"],
|
||||
ir: ["geoip:ir"],
|
||||
ir: ["ext:geoip_IR.dat:ir","ext:geoip_IR.dat:arvancloud","ext:geoip_IR.dat:derakcloud","ext:geoip_IR.dat:iranserver"],
|
||||
ru: ["geoip:ru"],
|
||||
},
|
||||
domains: {
|
||||
ads: [
|
||||
"geosite:category-ads-all",
|
||||
"ext:iran.dat:ads"
|
||||
"ext:geosite_IR.dat:category-ads-all"
|
||||
],
|
||||
google: ["geosite:google"],
|
||||
netflix: ["geosite:netflix"],
|
||||
@@ -286,9 +537,8 @@
|
||||
],
|
||||
ir: [
|
||||
"regexp:.*\\.ir$",
|
||||
"ext:iran.dat:ir",
|
||||
"ext:iran.dat:other",
|
||||
"geosite:category-ir"
|
||||
"regexp:.*\\.xn--mgba3a4f16a$", // .ایران
|
||||
"ext:geosite_IR.dat:ir" // have rules to bypass all .ir domains.
|
||||
]
|
||||
},
|
||||
familyProtectDNS: {
|
||||
@@ -311,8 +561,11 @@
|
||||
const msg = await HttpUtil.post("/xui/xray/");
|
||||
this.loading(false);
|
||||
if (msg.success) {
|
||||
this.oldXraySetting = msg.obj;
|
||||
this.xraySetting = msg.obj;
|
||||
result = JSON.parse(msg.obj);
|
||||
xs = JSON.stringify(result.xraySetting, null, 2);
|
||||
this.oldXraySetting = xs;
|
||||
this.xraySetting = xs;
|
||||
this.inboundTags = result.inboundTags;
|
||||
this.saveBtnDisable = true;
|
||||
}
|
||||
},
|
||||
@@ -324,21 +577,22 @@
|
||||
await this.getXraySetting();
|
||||
}
|
||||
},
|
||||
async restartPanel() {
|
||||
await new Promise(resolve => {
|
||||
this.$confirm({
|
||||
title: '{{ i18n "pages.settings.restartPanel" }}',
|
||||
content: '{{ i18n "pages.settings.restartPanelDesc" }}',
|
||||
class: themeSwitcher.currentTheme,
|
||||
okText: '{{ i18n "sure" }}',
|
||||
cancelText: '{{ i18n "cancel" }}',
|
||||
onOk: () => resolve(),
|
||||
});
|
||||
});
|
||||
async restartXray() {
|
||||
this.loading(true);
|
||||
const msg = await HttpUtil.post("/xui/setting/restartPanel");
|
||||
const msg = await HttpUtil.post("server/restartXrayService");
|
||||
if (msg.success) {
|
||||
await PromiseUtil.sleep(500);
|
||||
await this.getXrayResult();
|
||||
}
|
||||
this.loading(false);
|
||||
},
|
||||
async getXrayResult() {
|
||||
const msg = await HttpUtil.get("/xui/xray/getXrayResult");
|
||||
if(msg.success){
|
||||
this.restartResult=msg.obj;
|
||||
if(msg.obj.length > 1) Vue.prototype.$message.error(msg.obj);
|
||||
}
|
||||
},
|
||||
async resetXrayConfigToDefault() {
|
||||
this.loading(true);
|
||||
const msg = await HttpUtil.get("/xui/xray/getDefaultJsonConfig");
|
||||
@@ -415,6 +669,209 @@
|
||||
newTemplateSettings.routing.rules = newRules;
|
||||
}
|
||||
this.templateSettings = newTemplateSettings;
|
||||
},
|
||||
changeCode() {
|
||||
if(this.cm != null) {
|
||||
this.cm.toTextArea();
|
||||
}
|
||||
textAreaObj = document.getElementById('xraySetting');
|
||||
textAreaObj.value = this[this.advSettings];
|
||||
this.cm = CodeMirror.fromTextArea(textAreaObj, this.cmOptions);
|
||||
this.cm.on('change',editor => {
|
||||
value = editor.getValue();
|
||||
if(this.isJsonString(value)){
|
||||
this[this.advSettings] = value;
|
||||
}
|
||||
});
|
||||
},
|
||||
isJsonString(str) {
|
||||
try {
|
||||
JSON.parse(str);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
findOutboundAddress(o) {
|
||||
serverObj = null;
|
||||
switch(o.protocol){
|
||||
case Protocols.VMess:
|
||||
case Protocols.VLESS:
|
||||
serverObj = o.settings.vnext;
|
||||
break;
|
||||
case Protocols.HTTP:
|
||||
case Protocols.Socks:
|
||||
case Protocols.Shadowsocks:
|
||||
case Protocols.Trojan:
|
||||
serverObj = o.settings.servers;
|
||||
break;
|
||||
case Protocols.DNS:
|
||||
return [o.settings.address + ':' + o.settings.port];
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
return serverObj ? serverObj.map(obj => obj.address + ':' + obj.port) : null;
|
||||
},
|
||||
addOutbound(){
|
||||
outModal.show({
|
||||
title: '{{ i18n "pages.xray.outbound.addOutbound"}}',
|
||||
okText: '{{ i18n "pages.xray.outbound.addOutbound" }}',
|
||||
confirm: (outbound) => {
|
||||
outModal.loading();
|
||||
if(outbound.tag.length > 0){
|
||||
this.templateSettings.outbounds.push(outbound);
|
||||
this.outboundSettings = JSON.stringify(this.templateSettings.outbounds);
|
||||
}
|
||||
outModal.close();
|
||||
},
|
||||
isEdit: false,
|
||||
tags: this.templateSettings.outbounds.map(obj => obj.tag)
|
||||
});
|
||||
},
|
||||
editOutbound(index){
|
||||
outModal.show({
|
||||
title: '{{ i18n "pages.xray.outbound.editOutbound"}} ' + (index+1),
|
||||
outbound: app.templateSettings.outbounds[index],
|
||||
confirm: (outbound) => {
|
||||
outModal.loading();
|
||||
this.templateSettings.outbounds[index] = outbound;
|
||||
this.outboundSettings = JSON.stringify(this.templateSettings.outbounds);
|
||||
outModal.close();
|
||||
},
|
||||
isEdit: true,
|
||||
tags: this.outboundData.filter((o) => o.key != index ).map(obj => obj.tag)
|
||||
});
|
||||
},
|
||||
deleteOutbound(index){
|
||||
outbounds = this.templateSettings.outbounds;
|
||||
outbounds.splice(index,1);
|
||||
this.outboundSettings = JSON.stringify(outbounds);
|
||||
},
|
||||
addReverse(){
|
||||
reverseModal.show({
|
||||
title: '{{ i18n "pages.xray.outbound.addReverse"}}',
|
||||
okText: '{{ i18n "pages.xray.outbound.addReverse" }}',
|
||||
confirm: (reverse, rules) => {
|
||||
reverseModal.loading();
|
||||
if(reverse.tag.length > 0){
|
||||
newTemplateSettings = this.templateSettings;
|
||||
if(newTemplateSettings.reverse == undefined) newTemplateSettings.reverse = {};
|
||||
if(newTemplateSettings.reverse[reverse.type+'s'] == undefined) newTemplateSettings.reverse[reverse.type+'s'] = [];
|
||||
newTemplateSettings.reverse[reverse.type+'s'].push({ tag: reverse.tag, domain: reverse.domain });
|
||||
this.templateSettings = newTemplateSettings;
|
||||
|
||||
// Add related rules
|
||||
this.templateSettings.routing.rules.push(...rules);
|
||||
this.routingRuleSettings = JSON.stringify(this.templateSettings.routing.rules);
|
||||
}
|
||||
reverseModal.close();
|
||||
},
|
||||
isEdit: false
|
||||
});
|
||||
},
|
||||
editReverse(index){
|
||||
if(this.reverseData[index].type == "bridge") {
|
||||
oldRules = this.templateSettings.routing.rules.filter(r => r.inboundTag && r.inboundTag[0] == this.reverseData[index].tag);
|
||||
} else {
|
||||
oldRules = this.templateSettings.routing.rules.filter(r => r.outboundTag && r.outboundTag == this.reverseData[index].tag);
|
||||
}
|
||||
reverseModal.show({
|
||||
title: '{{ i18n "pages.xray.outbound.editReverse"}} ' + (index+1),
|
||||
reverse: this.reverseData[index],
|
||||
rules: oldRules,
|
||||
confirm: (reverse, rules) => {
|
||||
reverseModal.loading();
|
||||
if(reverse.tag.length > 0){
|
||||
oldData = this.reverseData[index];
|
||||
newTemplateSettings = this.templateSettings;
|
||||
oldReverseIndex = newTemplateSettings.reverse[oldData.type+'s'].findIndex(rs => rs.tag == oldData.tag);
|
||||
oldRuleIndex0 = oldRules.length>0 ? newTemplateSettings.routing.rules.findIndex(r => JSON.stringify(r) == JSON.stringify(oldRules[0])) : -1;
|
||||
oldRuleIndex1 = oldRules.length==2 ? newTemplateSettings.routing.rules.findIndex(r => JSON.stringify(r) == JSON.stringify(oldRules[1])) : -1;
|
||||
if(oldData.type == reverse.type){
|
||||
newTemplateSettings.reverse[oldData.type + 's'][oldReverseIndex] = { tag: reverse.tag, domain: reverse.domain };
|
||||
} else {
|
||||
newTemplateSettings.reverse[oldData.type+'s'].splice(oldReverseIndex,1);
|
||||
// delete empty object
|
||||
if(newTemplateSettings.reverse[oldData.type+'s'].length == 0) Reflect.deleteProperty(newTemplateSettings.reverse, oldData.type+'s');
|
||||
// add other type of reverse if it is not exist
|
||||
if(!newTemplateSettings.reverse[reverse.type+'s']) newTemplateSettings.reverse[reverse.type+'s'] = [];
|
||||
newTemplateSettings.reverse[reverse.type+'s'].push({ tag: reverse.tag, domain: reverse.domain });
|
||||
}
|
||||
this.templateSettings = newTemplateSettings;
|
||||
|
||||
// Adjust Rules
|
||||
newRules = this.templateSettings.routing.rules;
|
||||
oldRuleIndex0 != -1 ? newRules[oldRuleIndex0] = rules[0] : newRules.push(rules[0]);
|
||||
oldRuleIndex1 != -1 ? newRules[oldRuleIndex1] = rules[1] : newRules.push(rules[1]);
|
||||
this.routingRuleSettings = JSON.stringify(newRules);
|
||||
}
|
||||
reverseModal.close();
|
||||
},
|
||||
isEdit: true
|
||||
});
|
||||
},
|
||||
deleteReverse(index){
|
||||
oldData = this.reverseData[index];
|
||||
newTemplateSettings = this.templateSettings;
|
||||
reverseTypeObj = newTemplateSettings.reverse[oldData.type+'s'];
|
||||
realIndex = reverseTypeObj.findIndex(r => r.tag==oldData.tag && r.domain==oldData.domain);
|
||||
newTemplateSettings.reverse[oldData.type+'s'].splice(realIndex,1);
|
||||
|
||||
// delete empty objects
|
||||
if(reverseTypeObj.length == 0) Reflect.deleteProperty(newTemplateSettings.reverse, oldData.type+'s');
|
||||
if(Object.keys(newTemplateSettings.reverse).length === 0) Reflect.deleteProperty(newTemplateSettings, 'reverse');
|
||||
|
||||
// delete related routing rules
|
||||
newRules = newTemplateSettings.routing.rules;
|
||||
if(oldData.type == "bridge"){
|
||||
newRules = newTemplateSettings.routing.rules.filter(r => !( r.inboundTag && r.inboundTag.length == 1 && r.inboundTag[0] == oldData.tag));
|
||||
} else if(oldData.type == "portal"){
|
||||
newRules = newTemplateSettings.routing.rules.filter(r => r.outboundTag != oldData.tag);
|
||||
}
|
||||
newTemplateSettings.routing.rules = newRules;
|
||||
|
||||
this.templateSettings = newTemplateSettings;
|
||||
},
|
||||
addRule(){
|
||||
ruleModal.show({
|
||||
title: '{{ i18n "pages.xray.rules.add"}}',
|
||||
okText: '{{ i18n "pages.xray.rules.add" }}',
|
||||
confirm: (rule) => {
|
||||
ruleModal.loading();
|
||||
if(JSON.stringify(rule).length > 3){
|
||||
this.templateSettings.routing.rules.push(rule);
|
||||
this.routingRuleSettings = JSON.stringify(this.templateSettings.routing.rules);
|
||||
}
|
||||
ruleModal.close();
|
||||
},
|
||||
isEdit: false
|
||||
});
|
||||
},
|
||||
editRule(index){
|
||||
ruleModal.show({
|
||||
title: '{{ i18n "pages.xray.rules.edit"}} ' + (index+1),
|
||||
rule: app.templateSettings.routing.rules[index],
|
||||
confirm: (rule) => {
|
||||
ruleModal.loading();
|
||||
if(JSON.stringify(rule).length > 3){
|
||||
this.templateSettings.routing.rules[index] = rule;
|
||||
this.routingRuleSettings = JSON.stringify(this.templateSettings.routing.rules);
|
||||
}
|
||||
ruleModal.close();
|
||||
},
|
||||
isEdit: true
|
||||
});
|
||||
},
|
||||
replaceRule(old_index,new_index){
|
||||
rules = this.templateSettings.routing.rules;
|
||||
if (new_index >= rules.length) rules.push(undefined);
|
||||
rules.splice(new_index, 0, rules.splice(old_index, 1)[0]);
|
||||
this.routingRuleSettings = JSON.stringify(rules);
|
||||
},
|
||||
deleteRule(index){
|
||||
rules = this.templateSettings.routing.rules;
|
||||
rules.splice(index,1);
|
||||
this.routingRuleSettings = JSON.stringify(rules);
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
@@ -422,6 +879,7 @@
|
||||
this.showAlert = true;
|
||||
}
|
||||
await this.getXraySetting();
|
||||
await this.getXrayResult();
|
||||
while (true) {
|
||||
await PromiseUtil.sleep(1000);
|
||||
this.saveBtnDisable = this.oldXraySetting === this.xraySetting;
|
||||
@@ -448,6 +906,35 @@
|
||||
this.templateSettings = newTemplateSettings;
|
||||
},
|
||||
},
|
||||
outboundData: {
|
||||
get: function () {
|
||||
data = []
|
||||
if (this.templateSettings != null) {
|
||||
this.templateSettings.outbounds.forEach((o, index) => {
|
||||
data.push({'key': index, ...o});
|
||||
});
|
||||
}
|
||||
return data;
|
||||
},
|
||||
},
|
||||
reverseData: {
|
||||
get: function () {
|
||||
data = []
|
||||
if (this.templateSettings != null && this.templateSettings.reverse != null) {
|
||||
if(this.templateSettings.reverse.bridges) {
|
||||
this.templateSettings.reverse.bridges.forEach((o, index) => {
|
||||
data.push({'key': index, 'type':'bridge', ...o});
|
||||
});
|
||||
}
|
||||
if(this.templateSettings.reverse.portals){
|
||||
this.templateSettings.reverse.portals.forEach((o, index) => {
|
||||
data.push({'key': index, 'type':'portal', ...o});
|
||||
});
|
||||
}
|
||||
}
|
||||
return data;
|
||||
},
|
||||
},
|
||||
routingRuleSettings: {
|
||||
get: function () { return this.templateSettings ? JSON.stringify(this.templateSettings.routing.rules, null, 2) : null; },
|
||||
set: function (newValue) {
|
||||
@@ -456,18 +943,41 @@
|
||||
this.templateSettings = newTemplateSettings;
|
||||
},
|
||||
},
|
||||
routingRuleData: {
|
||||
get: function () {
|
||||
data = [];
|
||||
if (this.templateSettings != null) {
|
||||
this.templateSettings.routing.rules.forEach((r, index) => {
|
||||
data.push({'key': index, ...r});
|
||||
});
|
||||
// Make rules readable
|
||||
data.forEach(r => {
|
||||
if(r.domain) r.domain = r.domain.join(',')
|
||||
if(r.ip) r.ip = r.ip.join(',')
|
||||
if(r.source) r.source = r.source.join(',');
|
||||
if(r.user) r.user = r.user.join(',')
|
||||
if(r.inboundTag) r.inboundTag = r.inboundTag.join(',')
|
||||
if(r.protocol) r.protocol = r.protocol.join(',')
|
||||
if(r.attrs) r.attrs = JSON.stringify(r.attrs, null, 2)
|
||||
});
|
||||
}
|
||||
return data;
|
||||
}
|
||||
},
|
||||
freedomStrategy: {
|
||||
get: function () {
|
||||
if (!this.templateSettings) return "AsIs";
|
||||
freedomOutbound = this.templateSettings.outbounds.find((o) => o.protocol === "freedom" && !o.tag);
|
||||
freedomOutbound = this.templateSettings.outbounds.find((o) => o.protocol === "freedom" && o.tag == "direct");
|
||||
if (!freedomOutbound) return "AsIs";
|
||||
if (!freedomOutbound.settings || !freedomOutbound.settings.domainStrategy) return "AsIs";
|
||||
return freedomOutbound.settings.domainStrategy;
|
||||
},
|
||||
set: function (newValue) {
|
||||
newTemplateSettings = this.templateSettings;
|
||||
freedomOutboundIndex = newTemplateSettings.outbounds.findIndex((o) => o.protocol === "freedom" && !o.tag);
|
||||
if (!newTemplateSettings.outbounds[freedomOutboundIndex].settings) {
|
||||
freedomOutboundIndex = newTemplateSettings.outbounds.findIndex((o) => o.protocol === "freedom" && o.tag == "direct");
|
||||
if(freedomOutboundIndex == -1){
|
||||
newTemplateSettings.outbounds.push({protocol: "freedom", tag: "direct", settings: { "domainStrategy": newValue }});
|
||||
} else if (!newTemplateSettings.outbounds[freedomOutboundIndex].settings) {
|
||||
newTemplateSettings.outbounds[freedomOutboundIndex].settings = { "domainStrategy": newValue };
|
||||
} else {
|
||||
newTemplateSettings.outbounds[freedomOutboundIndex].settings.domainStrategy = newValue;
|
||||
@@ -537,26 +1047,6 @@
|
||||
this.syncRulesWithOutbound("IPv4", this.ipv4Settings);
|
||||
}
|
||||
},
|
||||
manualBlockedIPs: {
|
||||
get: function () { return JSON.stringify(this.blockedIPs, null, 2); },
|
||||
set: debounce(function (value) { this.blockedIPs = JSON.parse(value); }, 1000)
|
||||
},
|
||||
manualBlockedDomains: {
|
||||
get: function () { return JSON.stringify(this.blockedDomains, null, 2); },
|
||||
set: debounce(function (value) { this.blockedDomains = JSON.parse(value); }, 1000)
|
||||
},
|
||||
manualDirectIPs: {
|
||||
get: function () { return JSON.stringify(this.directIPs, null, 2); },
|
||||
set: debounce(function (value) { this.directIPs = JSON.parse(value); }, 1000)
|
||||
},
|
||||
manualDirectDomains: {
|
||||
get: function () { return JSON.stringify(this.directDomains, null, 2); },
|
||||
set: debounce(function (value) { this.directDomains = JSON.parse(value); }, 1000)
|
||||
},
|
||||
manualIPv4Domains: {
|
||||
get: function () { return JSON.stringify(this.ipv4Domains, null, 2); },
|
||||
set: debounce(function (value) { this.ipv4Domains = JSON.parse(value); }, 1000)
|
||||
},
|
||||
torrentSettings: {
|
||||
get: function () {
|
||||
return doAllItemsExist(this.settingsData.protocols.bittorrent, this.blockedProtocols);
|
||||
@@ -777,7 +1267,7 @@
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
127
web/html/xui/xray_outbound_modal.html
Normal file
127
web/html/xui/xray_outbound_modal.html
Normal file
@@ -0,0 +1,127 @@
|
||||
{{define "outModal"}}
|
||||
<a-modal id="out-modal" v-model="outModal.visible" :title="outModal.title" @ok="outModal.ok"
|
||||
:confirm-loading="outModal.confirmLoading" :closable="true" :mask-closable="false"
|
||||
:ok-button-props="{ props: { disabled: !outModal.isValid } }" style="overflow: hidden;"
|
||||
:ok-text="outModal.okText" cancel-text='{{ i18n "close" }}' :class="themeSwitcher.currentTheme">
|
||||
{{template "form/outbound"}}
|
||||
</a-modal>
|
||||
<script>
|
||||
|
||||
const outModal = {
|
||||
title: '',
|
||||
visible: false,
|
||||
confirmLoading: false,
|
||||
okText: '{{ i18n "sure" }}',
|
||||
isEdit: false,
|
||||
confirm: null,
|
||||
outbound: new Outbound(),
|
||||
jsonMode: false,
|
||||
link: '',
|
||||
cm: null,
|
||||
duplicateTag: false,
|
||||
isValid: true,
|
||||
activeKey: '1',
|
||||
tags: [],
|
||||
ok() {
|
||||
ObjectUtil.execute(outModal.confirm, outModal.outbound.toJson());
|
||||
},
|
||||
show({ title='', okText='{{ i18n "sure" }}', outbound, confirm=(outbound)=>{}, isEdit=false, tags=[] }) {
|
||||
this.title = title;
|
||||
this.okText = okText;
|
||||
this.confirm = confirm;
|
||||
this.jsonMode = false;
|
||||
this.link = '';
|
||||
this.activeKey = '1';
|
||||
this.visible = true;
|
||||
this.outbound = isEdit ? Outbound.fromJson(outbound) : new Outbound();
|
||||
this.isEdit = isEdit;
|
||||
this.tags = tags;
|
||||
this.check()
|
||||
},
|
||||
close() {
|
||||
outModal.visible = false;
|
||||
outModal.loading(false);
|
||||
},
|
||||
loading(loading) {
|
||||
outModal.confirmLoading = loading;
|
||||
},
|
||||
check(){
|
||||
if(outModal.outbound.tag == '' || outModal.tags.includes(outModal.outbound.tag)){
|
||||
this.duplicateTag = true;
|
||||
this.isValid = false;
|
||||
} else {
|
||||
this.duplicateTag = false;
|
||||
this.isValid = true;
|
||||
}
|
||||
},
|
||||
toggleJson(jsonTab) {
|
||||
textAreaObj = document.getElementById('outboundJson');
|
||||
if(jsonTab){
|
||||
if(this.cm != null) {
|
||||
this.cm.toTextArea();
|
||||
this.cm=null;
|
||||
}
|
||||
textAreaObj.value = JSON.stringify(this.outbound.toJson(), null, 2);
|
||||
this.cm = CodeMirror.fromTextArea(textAreaObj, app.cmOptions);
|
||||
this.cm.on('change',editor => {
|
||||
value = editor.getValue();
|
||||
if(this.isJsonString(value)){
|
||||
this.outbound = Outbound.fromJson(JSON.parse(value));
|
||||
this.check();
|
||||
}
|
||||
});
|
||||
this.activeKey = '2';
|
||||
} else {
|
||||
if(this.cm != null) {
|
||||
this.cm.toTextArea();
|
||||
this.cm=null;
|
||||
}
|
||||
this.activeKey = '1';
|
||||
}
|
||||
},
|
||||
isJsonString(str) {
|
||||
try {
|
||||
JSON.parse(str);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
new Vue({
|
||||
delimiters: ['[[', ']]'],
|
||||
el: '#out-modal',
|
||||
data: {
|
||||
outModal: outModal,
|
||||
get outbound() {
|
||||
return outModal.outbound;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
streamNetworkChange() {
|
||||
if (this.outModal.outbound.protocol == Protocols.VLESS && !outModal.outbound.canEnableTlsFlow()) {
|
||||
delete this.outModal.outbound.settings.flow;
|
||||
}
|
||||
},
|
||||
canEnableTls() {
|
||||
return this.outModal.outbound.canEnableTls();
|
||||
},
|
||||
convertLink(){
|
||||
newOutbound = Outbound.fromLink(outModal.link);
|
||||
if(newOutbound){
|
||||
this.outModal.outbound = newOutbound;
|
||||
this.outModal.toggleJson(true);
|
||||
this.outModal.check();
|
||||
this.$message.success('Link imported successfully...');
|
||||
outModal.link = '';
|
||||
} else {
|
||||
this.$message.error('Wrong Link!');
|
||||
outModal.link = '';
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
</script>
|
||||
{{end}}
|
||||
140
web/html/xui/xray_reverse_modal.html
Normal file
140
web/html/xui/xray_reverse_modal.html
Normal file
@@ -0,0 +1,140 @@
|
||||
{{define "reverseModal"}}
|
||||
<a-modal id="reverse-modal" v-model="reverseModal.visible" :title="reverseModal.title" @ok="reverseModal.ok"
|
||||
:confirm-loading="reverseModal.confirmLoading" :closable="true" :mask-closable="false"
|
||||
:ok-text="reverseModal.okText" cancel-text='{{ i18n "close" }}' :class="themeSwitcher.currentTheme">
|
||||
<a-form :colon="false" :label-col="{ md: {span:6} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label='{{ i18n "pages.xray.outbound.type" }}'>
|
||||
<a-select v-model="reverseModal.reverse.type" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="x,y in reverseTypes" :value="y">[[ x ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.xray.outbound.tag" }}'>
|
||||
<a-input v-model.trim="reverseModal.reverse.tag"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.xray.outbound.domain" }}'>
|
||||
<a-input v-model.trim="reverseModal.reverse.domain"></a-input>
|
||||
</a-form-item>
|
||||
<template v-if="reverseModal.reverse.type=='bridge'">
|
||||
<a-form-item label='{{ i18n "pages.xray.outbound.intercon" }}'>
|
||||
<a-select v-model="reverseModal.rules[0].outboundTag" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="x in reverseModal.outboundTags" :value="x">[[ x ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.xray.rules.outbound" }}'>
|
||||
<a-select v-model="reverseModal.rules[1].outboundTag" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="x in reverseModal.outboundTags" :value="x">[[ x ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-form-item label='{{ i18n "pages.xray.outbound.intercon" }}'>
|
||||
<a-checkbox-group
|
||||
v-model="reverseModal.rules[0].inboundTag"
|
||||
:options="reverseModal.inboundTags"></a-checkbox-group>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.xray.rules.inbound" }}'>
|
||||
<a-checkbox-group
|
||||
v-model="reverseModal.rules[1].inboundTag"
|
||||
:options="reverseModal.inboundTags"></a-checkbox-group>
|
||||
</a-form-item>
|
||||
</template>
|
||||
</table>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
<script>
|
||||
const reverseModal = {
|
||||
title: '',
|
||||
visible: false,
|
||||
confirmLoading: false,
|
||||
okText: '{{ i18n "sure" }}',
|
||||
isEdit: false,
|
||||
confirm: null,
|
||||
reverse: {
|
||||
tag: "",
|
||||
type: "",
|
||||
domain: ""
|
||||
},
|
||||
rules: [
|
||||
{ outboundTag: '', inboundTag: []},
|
||||
{ outboundTag: '', inboundTag: []}
|
||||
],
|
||||
inboundTags: [],
|
||||
outboundTags: [],
|
||||
ok() {
|
||||
reverseModal.rules[0].domain = ["full:" + reverseModal.reverse.domain];
|
||||
reverseModal.rules[0].type = 'field';
|
||||
reverseModal.rules[1].type = 'field';
|
||||
|
||||
if(reverseModal.reverse.type == 'bridge'){
|
||||
reverseModal.rules[0].inboundTag = [reverseModal.reverse.tag];
|
||||
reverseModal.rules[1].inboundTag = [reverseModal.reverse.tag];
|
||||
} else {
|
||||
reverseModal.rules[0].outboundTag = reverseModal.reverse.tag;
|
||||
reverseModal.rules[1].outboundTag = reverseModal.reverse.tag;
|
||||
}
|
||||
ObjectUtil.execute(reverseModal.confirm, reverseModal.reverse, reverseModal.rules);
|
||||
},
|
||||
show({ title='', okText='{{ i18n "sure" }}', reverse, rules, confirm=(reverse, rules)=>{}, isEdit=false }) {
|
||||
this.title = title;
|
||||
this.okText = okText;
|
||||
this.confirm = confirm;
|
||||
this.visible = true;
|
||||
if(isEdit) {
|
||||
this.reverse = {
|
||||
tag: reverse.tag,
|
||||
type: reverse.type,
|
||||
domain: reverse.domain,
|
||||
};
|
||||
reverse;
|
||||
rules0 = rules.filter(r => r.domain != null);
|
||||
if(rules0.length == 0) rules0 = [{ outboundTag: '', domain: ["full:" + this.reverse.domain], inboundTag: []}];
|
||||
rules1 = rules.filter(r => r.domain == null);
|
||||
if(rules1.length == 0) rules1 = [{ outboundTag: '', inboundTag: []}];
|
||||
this.rules = [];
|
||||
this.rules.push({
|
||||
domain: rules0[0].domain,
|
||||
outboundTag: rules0[0].outboundTag,
|
||||
inboundTag: rules0.map(r => r.inboundTag).flat()
|
||||
});
|
||||
this.rules.push({
|
||||
outboundTag: rules1[0].outboundTag,
|
||||
inboundTag: rules1.map(r => r.inboundTag).flat()
|
||||
});
|
||||
} else {
|
||||
this.reverse = {
|
||||
tag: "reverse-" + app.reverseData.length,
|
||||
type: "bridge",
|
||||
domain: "reverse.xui"
|
||||
}
|
||||
this.rules = [
|
||||
{ outboundTag: '', inboundTag: []},
|
||||
{ outboundTag: '', inboundTag: []}
|
||||
]
|
||||
}
|
||||
this.isEdit = isEdit;
|
||||
this.inboundTags = app.templateSettings.inbounds.filter((i) => !ObjectUtil.isEmpty(i.tag)).map(obj => obj.tag);
|
||||
this.inboundTags.push(...app.inboundTags);
|
||||
this.outboundTags = app.templateSettings.outbounds.filter((o) => !ObjectUtil.isEmpty(o.tag)).map(obj => obj.tag);
|
||||
},
|
||||
close() {
|
||||
reverseModal.visible = false;
|
||||
reverseModal.loading(false);
|
||||
},
|
||||
loading(loading) {
|
||||
reverseModal.confirmLoading = loading;
|
||||
},
|
||||
};
|
||||
|
||||
new Vue({
|
||||
delimiters: ['[[', ']]'],
|
||||
el: '#reverse-modal',
|
||||
data: {
|
||||
reverseModal: reverseModal,
|
||||
reverseTypes: { bridge: '{{ i18n "pages.xray.outbound.bridge" }}', portal:'{{ i18n "pages.xray.outbound.portal" }}'},
|
||||
},
|
||||
methods: {
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
{{end}}
|
||||
239
web/html/xui/xray_rule_modal.html
Normal file
239
web/html/xui/xray_rule_modal.html
Normal file
@@ -0,0 +1,239 @@
|
||||
{{define "ruleModal"}}
|
||||
<a-modal id="rule-modal" v-model="ruleModal.visible" :title="ruleModal.title" @ok="ruleModal.ok"
|
||||
:confirm-loading="ruleModal.confirmLoading" :closable="true" :mask-closable="false"
|
||||
:ok-text="ruleModal.okText" cancel-text='{{ i18n "close" }}' :class="themeSwitcher.currentTheme">
|
||||
<a-form :colon="false" :label-col="{ md: {span:6} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label='Domain Matcher'>
|
||||
<a-select v-model="ruleModal.rule.domainMatcher" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="dm in ['','hybrid','linear']" :value="dm">[[ dm ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.xray.rules.useComma" }}</span>
|
||||
</template>
|
||||
Source IPs <a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input v-model.trim="ruleModal.rule.source"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<template slot="label">Source Port
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.xray.rules.useComma" }}</span>
|
||||
</template>
|
||||
Source Port <a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input v-model.trim="ruleModal.rule.sourcePort"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='Network'>
|
||||
<a-select v-model="ruleModal.rule.network" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="x in ['','tcp','udp','tcp,udp']" :value="x">[[ x ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='Protocol'>
|
||||
<a-select v-model="ruleModal.rule.protocol" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="x in ['','http','tls','bittorrent']" :value="x">[[ x ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='Attributes'>
|
||||
<a-button size="small" style="margin-left: 10px" @click="ruleModal.rule.attrs.push(['', ''])">+</a-button>
|
||||
</a-form-item>
|
||||
<a-form-item :wrapper-col="{span: 24}">
|
||||
<a-input-group compact v-for="(attr,index) in ruleModal.rule.attrs">
|
||||
<a-input style="width: 50%" v-model="attr[0]" placeholder='{{ i18n "pages.inbounds.stream.general.name" }}'>
|
||||
<template slot="addonBefore" style="margin: 0;">[[ index+1 ]]</template>
|
||||
</a-input>
|
||||
<a-input style="width: 50%" v-model="attr[1]" placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'>
|
||||
<a-button slot="addonAfter" size="small" @click="ruleModal.rule.attrs.splice(index,1)">-</a-button>
|
||||
</a-input>
|
||||
</a-input-group>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.xray.rules.useComma" }}</span>
|
||||
</template>
|
||||
IP <a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input v-model.trim="ruleModal.rule.ip"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.xray.rules.useComma" }}</span>
|
||||
</template>
|
||||
Domain <a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input v-model.trim="ruleModal.rule.domain"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.xray.rules.useComma" }}</span>
|
||||
</template>
|
||||
User <a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input v-model.trim="ruleModal.rule.user"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.xray.rules.useComma" }}</span>
|
||||
</template>
|
||||
Port <a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input v-model.trim="ruleModal.rule.port"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='Inbound Tags'>
|
||||
<a-select v-model="ruleModal.rule.inboundTag" mode="multiple" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="tag in ruleModal.inboundTags" :value="tag">[[ tag ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='Outbound Tag'>
|
||||
<a-select v-model="ruleModal.rule.outboundTag" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="tag in ruleModal.outboundTags" :value="tag">[[ tag ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</table>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
<script>
|
||||
|
||||
const ruleModal = {
|
||||
title: '',
|
||||
visible: false,
|
||||
confirmLoading: false,
|
||||
okText: '{{ i18n "sure" }}',
|
||||
isEdit: false,
|
||||
confirm: null,
|
||||
rule: {
|
||||
type: "field",
|
||||
domainMatcher: "",
|
||||
domain: "",
|
||||
ip: "",
|
||||
port: "",
|
||||
sourcePort: "",
|
||||
network: "",
|
||||
source: "",
|
||||
user: "",
|
||||
inboundTag: [],
|
||||
protocol: [],
|
||||
attrs: [],
|
||||
outboundTag: "",
|
||||
},
|
||||
inboundTags: [],
|
||||
outboundTags: [],
|
||||
users: [],
|
||||
balancerTag: [],
|
||||
ok() {
|
||||
newRule = ruleModal.getResult();
|
||||
ObjectUtil.execute(ruleModal.confirm, newRule);
|
||||
},
|
||||
show({ title='', okText='{{ i18n "sure" }}', rule, confirm=(rule)=>{}, isEdit=false }) {
|
||||
this.title = title;
|
||||
this.okText = okText;
|
||||
this.confirm = confirm;
|
||||
this.visible = true;
|
||||
if(isEdit) {
|
||||
this.rule.domainMatcher = rule.domainMatcher;
|
||||
this.rule.domain = rule.domain ? rule.domain.join(',') : [];
|
||||
this.rule.ip = rule.ip ? rule.ip.join(',') : [];
|
||||
this.rule.port = rule.port;
|
||||
this.rule.sourcePort = rule.sourcePort;
|
||||
this.rule.network = rule.network;
|
||||
this.rule.source = rule.source ? rule.source.join(',') : [];
|
||||
this.rule.user = rule.user ? rule.user.join(',') : [];
|
||||
this.rule.inboundTag = rule.inboundTag;
|
||||
this.rule.protocol = rule.protocol;
|
||||
this.rule.attrs = rule.attrs ? Object.entries(rule.attrs) : [];
|
||||
this.rule.outboundTag = rule.outboundTag;
|
||||
} else {
|
||||
this.rule = {
|
||||
domainMatcher: "",
|
||||
domain: "",
|
||||
ip: "",
|
||||
port: "",
|
||||
sourcePort: "",
|
||||
network: "",
|
||||
source: "",
|
||||
user: "",
|
||||
inboundTag: [],
|
||||
protocol: [],
|
||||
attrs: [],
|
||||
outboundTag: "",
|
||||
}
|
||||
}
|
||||
this.isEdit = isEdit;
|
||||
this.inboundTags = app.templateSettings.inbounds.filter((i) => !ObjectUtil.isEmpty(i.tag)).map(obj => obj.tag);
|
||||
this.inboundTags.push(...app.inboundTags);
|
||||
this.outboundTags = app.templateSettings.outbounds.filter((o) => !ObjectUtil.isEmpty(o.tag)).map(obj => obj.tag);
|
||||
if(app.templateSettings.reverse){
|
||||
if(app.templateSettings.reverse.bridges) {
|
||||
this.inboundTags.push(...app.templateSettings.reverse.bridges.map(b => b.tag));
|
||||
}
|
||||
if(app.templateSettings.reverse.portals) this.outboundTags.push(...app.templateSettings.reverse.portals.map(b => b.tag));
|
||||
}
|
||||
},
|
||||
close() {
|
||||
ruleModal.visible = false;
|
||||
ruleModal.loading(false);
|
||||
},
|
||||
loading(loading) {
|
||||
ruleModal.confirmLoading = loading;
|
||||
},
|
||||
getResult() {
|
||||
value = ruleModal.rule;
|
||||
rule = {};
|
||||
newRule = {};
|
||||
rule.type = "field";
|
||||
rule.domainMatcher = value.domainMatcher;
|
||||
rule.domain = value.domain.length>0 ? value.domain.split(',') : [];
|
||||
rule.ip = value.ip.length>0 ? value.ip.split(',') : [];
|
||||
rule.port = value.port;
|
||||
rule.sourcePort = value.sourcePort;
|
||||
rule.network = value.network;
|
||||
rule.source = value.source.length>0 ? value.source.split(',') : [];
|
||||
rule.user = value.user.length>0 ? value.user.split(',') : [];
|
||||
rule.inboundTag = value.inboundTag;
|
||||
rule.protocol = value.protocol;
|
||||
rule.attrs = Object.fromEntries(value.attrs);
|
||||
rule.outboundTag = value.outboundTag;
|
||||
|
||||
for (const [key, value] of Object.entries(rule)) {
|
||||
if (
|
||||
value !== null &&
|
||||
value !== undefined &&
|
||||
!(Array.isArray(value) && value.length === 0) &&
|
||||
!(typeof value === 'object' && Object.keys(value).length === 0) &&
|
||||
value !== ''
|
||||
) {
|
||||
newRule[key] = value;
|
||||
}
|
||||
}
|
||||
return newRule;
|
||||
}
|
||||
};
|
||||
|
||||
new Vue({
|
||||
delimiters: ['[[', ']]'],
|
||||
el: '#rule-modal',
|
||||
data: {
|
||||
ruleModal: ruleModal,
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
{{end}}
|
||||
@@ -19,6 +19,7 @@
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"tag": "direct",
|
||||
"protocol": "freedom",
|
||||
"settings": {}
|
||||
},
|
||||
|
||||
@@ -168,9 +168,13 @@ func (s *InboundService) AddInbound(inbound *model.Inbound) (*model.Inbound, boo
|
||||
|
||||
err = tx.Save(inbound).Error
|
||||
if err == nil {
|
||||
for _, client := range clients {
|
||||
s.AddClientStat(tx, inbound.Id, &client)
|
||||
if len(inbound.ClientStats) == 0 {
|
||||
for _, client := range clients {
|
||||
s.AddClientStat(tx, inbound.Id, &client)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return inbound, false, err
|
||||
}
|
||||
|
||||
needRestart := false
|
||||
@@ -1194,27 +1198,16 @@ func (s *InboundService) DelDepletedClients(id int) (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *InboundService) GetClientTrafficTgBot(tguname string) ([]*xray.ClientTraffic, error) {
|
||||
func (s *InboundService) GetClientTrafficTgBot(tgid string, tguname string) ([]*xray.ClientTraffic, error) {
|
||||
db := database.GetDB()
|
||||
var inbounds []*model.Inbound
|
||||
err := db.Model(model.Inbound{}).Where("settings like ?", fmt.Sprintf(`%%"tgId": "%s"%%`, tguname)).Find(&inbounds).Error
|
||||
if err != nil && err != gorm.ErrRecordNotFound {
|
||||
return nil, err
|
||||
}
|
||||
var emails []string
|
||||
for _, inbound := range inbounds {
|
||||
clients, err := s.GetClients(inbound)
|
||||
if err != nil {
|
||||
logger.Error("Unable to get clients from inbound")
|
||||
}
|
||||
for _, client := range clients {
|
||||
if client.TgID == tguname {
|
||||
emails = append(emails, client.Email)
|
||||
}
|
||||
}
|
||||
}
|
||||
var traffics []*xray.ClientTraffic
|
||||
err = db.Model(xray.ClientTraffic{}).Where("email IN ?", emails).Find(&traffics).Error
|
||||
err := db.Model(xray.ClientTraffic{}).Where(`email IN(
|
||||
SELECT JSON_EXTRACT(client.value, '$.email') as email
|
||||
FROM inbounds,
|
||||
JSON_EACH(JSON_EXTRACT(inbounds.settings, '$.clients')) AS client
|
||||
WHERE
|
||||
JSON_EXTRACT(client.value, '$.tgId') in (?,?)
|
||||
)`, tgid, tguname).Find(&traffics).Error
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
logger.Warning(err)
|
||||
@@ -1289,6 +1282,17 @@ func (s *InboundService) SearchInbounds(query string) ([]*model.Inbound, error)
|
||||
return inbounds, nil
|
||||
}
|
||||
|
||||
func (s *InboundService) GetInboundTags() (string, error) {
|
||||
db := database.GetDB()
|
||||
var inboundTags []string
|
||||
err := db.Model(model.Inbound{}).Select("tag").Find(&inboundTags).Error
|
||||
if err != nil && err != gorm.ErrRecordNotFound {
|
||||
return "", err
|
||||
}
|
||||
tags, _ := json.Marshal(inboundTags)
|
||||
return string(tags), nil
|
||||
}
|
||||
|
||||
func (s *InboundService) MigrationRequirements() {
|
||||
db := database.GetDB()
|
||||
tx := db.Begin()
|
||||
@@ -1358,6 +1362,46 @@ func (s *InboundService) MigrationRequirements() {
|
||||
|
||||
// Remove orphaned traffics
|
||||
tx.Where("inbound_id = 0").Delete(xray.ClientTraffic{})
|
||||
|
||||
// Migrate old MultiDomain to External Proxy
|
||||
var externalProxy []struct {
|
||||
Id int
|
||||
Port int
|
||||
StreamSettings []byte
|
||||
}
|
||||
err = tx.Raw(`select id, port, stream_settings
|
||||
from inbounds
|
||||
WHERE protocol in ('vmess','vless','trojan')
|
||||
AND json_extract(stream_settings, '$.security') = 'tls'
|
||||
AND json_extract(stream_settings, '$.tlsSettings.settings.domains') IS NOT NULL`).Scan(&externalProxy).Error
|
||||
if err != nil || len(externalProxy) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
for _, ep := range externalProxy {
|
||||
var reverses interface{}
|
||||
var stream map[string]interface{}
|
||||
json.Unmarshal(ep.StreamSettings, &stream)
|
||||
if tlsSettings, ok := stream["tlsSettings"].(map[string]interface{}); ok {
|
||||
if settings, ok := tlsSettings["settings"].(map[string]interface{}); ok {
|
||||
if domains, ok := settings["domains"].([]interface{}); ok {
|
||||
for _, domain := range domains {
|
||||
if domainMap, ok := domain.(map[string]interface{}); ok {
|
||||
domainMap["forceTls"] = "same"
|
||||
domainMap["port"] = ep.Port
|
||||
domainMap["dest"] = domainMap["domain"].(string)
|
||||
delete(domainMap, "domain")
|
||||
}
|
||||
}
|
||||
}
|
||||
reverses = settings["domains"]
|
||||
delete(settings, "domains")
|
||||
}
|
||||
}
|
||||
stream["externalProxy"] = reverses
|
||||
newStream, _ := json.MarshalIndent(stream, " ", " ")
|
||||
tx.Model(model.Inbound{}).Where("id = ?", ep.Id).Update("stream_settings", newStream)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *InboundService) MigrateDB() {
|
||||
|
||||
@@ -363,14 +363,6 @@ func (s *ServerService) UpdateXray(version string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = copyZipFile("geosite.dat", xray.GetGeositePath())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = copyZipFile("geoip.dat", xray.GetGeoipPath())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -418,6 +410,11 @@ func (s *ServerService) GetConfigJson() (interface{}, error) {
|
||||
}
|
||||
|
||||
func (s *ServerService) GetDb() ([]byte, error) {
|
||||
// Update by manually trigger a checkpoint operation
|
||||
err := database.Checkpoint()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Open the file for reading
|
||||
file, err := os.Open(config.GetDBPath())
|
||||
if err != nil {
|
||||
|
||||
@@ -34,6 +34,7 @@ var defaultValueMap = map[string]string{
|
||||
"pageSize": "0",
|
||||
"expireDiff": "0",
|
||||
"trafficDiff": "0",
|
||||
"remarkModel": "-ieo",
|
||||
"timeLocation": "Asia/Tehran",
|
||||
"tgBotEnable": "false",
|
||||
"tgBotToken": "",
|
||||
@@ -53,6 +54,7 @@ var defaultValueMap = map[string]string{
|
||||
"subUpdates": "12",
|
||||
"subEncrypt": "true",
|
||||
"subShowInfo": "false",
|
||||
"subURI": "",
|
||||
}
|
||||
|
||||
type SettingService struct {
|
||||
@@ -294,6 +296,10 @@ func (s *SettingService) GetSessionMaxAge() (int, error) {
|
||||
return s.getInt("sessionMaxAge")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetRemarkModel() (string, error) {
|
||||
return s.getString("remarkModel")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetSecret() ([]byte, error) {
|
||||
secret, err := s.getString("secret")
|
||||
if secret == defaultValueMap["secret"] {
|
||||
@@ -387,6 +393,10 @@ func (s *SettingService) GetPageSize() (int, error) {
|
||||
return s.getInt("pageSize")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetSubURI() (string, error) {
|
||||
return s.getString("subURI")
|
||||
}
|
||||
|
||||
func (s *SettingService) UpdateAllSetting(allSetting *entity.AllSetting) error {
|
||||
if err := allSetting.CheckValid(); err != nil {
|
||||
return err
|
||||
@@ -416,3 +426,62 @@ func (s *SettingService) GetDefaultXrayConfig() (interface{}, error) {
|
||||
}
|
||||
return jsonData, nil
|
||||
}
|
||||
|
||||
func (s *SettingService) GetDefaultSettings(host string) (interface{}, error) {
|
||||
type settingFunc func() (interface{}, error)
|
||||
settings := map[string]settingFunc{
|
||||
"expireDiff": func() (interface{}, error) { return s.GetExpireDiff() },
|
||||
"trafficDiff": func() (interface{}, error) { return s.GetTrafficDiff() },
|
||||
"pageSize": func() (interface{}, error) { return s.GetPageSize() },
|
||||
"defaultCert": func() (interface{}, error) { return s.GetCertFile() },
|
||||
"defaultKey": func() (interface{}, error) { return s.GetKeyFile() },
|
||||
"tgBotEnable": func() (interface{}, error) { return s.GetTgbotenabled() },
|
||||
"subEnable": func() (interface{}, error) { return s.GetSubEnable() },
|
||||
"subURI": func() (interface{}, error) { return s.GetSubURI() },
|
||||
"remarkModel": func() (interface{}, error) { return s.GetRemarkModel() },
|
||||
}
|
||||
|
||||
result := make(map[string]interface{})
|
||||
|
||||
for key, fn := range settings {
|
||||
value, err := fn()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
result[key] = value
|
||||
}
|
||||
|
||||
if result["subEnable"].(bool) && result["subURI"].(string) == "" {
|
||||
subURI := ""
|
||||
subPort, _ := s.GetSubPort()
|
||||
subPath, _ := s.GetSubPath()
|
||||
subDomain, _ := s.GetSubDomain()
|
||||
subKeyFile, _ := s.GetSubKeyFile()
|
||||
subCertFile, _ := s.GetSubCertFile()
|
||||
subTLS := false
|
||||
if subKeyFile != "" && subCertFile != "" {
|
||||
subTLS = true
|
||||
}
|
||||
if subDomain == "" {
|
||||
subDomain = strings.Split(host, ":")[0]
|
||||
}
|
||||
if subTLS {
|
||||
subURI = "https://"
|
||||
} else {
|
||||
subURI = "http://"
|
||||
}
|
||||
if (subPort == 443 && subTLS) || (subPort == 80 && !subTLS) {
|
||||
subURI += subDomain
|
||||
} else {
|
||||
subURI += fmt.Sprintf("%s:%d", subDomain, subPort)
|
||||
}
|
||||
if subPath[0] == byte('/') {
|
||||
subURI += subPath
|
||||
} else {
|
||||
subURI += "/" + subPath
|
||||
}
|
||||
result["subURI"] = subURI
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
"x-ui/config"
|
||||
"x-ui/database"
|
||||
"x-ui/database/model"
|
||||
"x-ui/logger"
|
||||
"x-ui/util/common"
|
||||
@@ -466,11 +467,11 @@ func (t *Tgbot) clientInfoMsg(traffic *xray.ClientTraffic) string {
|
||||
active = t.I18nBot("tgbot.messages.no")
|
||||
}
|
||||
|
||||
status := t.I18nBot("offline")
|
||||
status := "🔴 " + t.I18nBot("offline")
|
||||
if p.IsRunning() {
|
||||
for _, online := range p.GetOnlineClients() {
|
||||
if online == traffic.Email {
|
||||
status = t.I18nBot("online")
|
||||
status = "🟢 " + t.I18nBot("online")
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -495,7 +496,7 @@ func (t *Tgbot) getClientUsage(chatId int64, tgUserName string) {
|
||||
return
|
||||
}
|
||||
|
||||
traffics, err := t.inboundService.GetClientTrafficTgBot(tgUserName)
|
||||
traffics, err := t.inboundService.GetClientTrafficTgBot(strconv.FormatInt(chatId, 10), tgUserName)
|
||||
if err != nil {
|
||||
logger.Warning(err)
|
||||
msg := t.I18nBot("tgbot.wentWrong")
|
||||
@@ -504,6 +505,7 @@ func (t *Tgbot) getClientUsage(chatId int64, tgUserName string) {
|
||||
}
|
||||
if len(traffics) == 0 {
|
||||
msg := t.I18nBot("tgbot.answers.askToAddUserName", "TgUserName=="+tgUserName)
|
||||
msg += "\r\n" + t.I18nBot("tgbot.commands.getID", "ID=="+strconv.FormatInt(chatId, 10))
|
||||
t.SendMsgToTgbot(chatId, msg)
|
||||
return
|
||||
}
|
||||
@@ -696,12 +698,18 @@ func (t *Tgbot) sendBackup(chatId int64) {
|
||||
return
|
||||
}
|
||||
|
||||
// Update by manually trigger a checkpoint operation
|
||||
err := database.Checkpoint()
|
||||
if err != nil {
|
||||
logger.Warning("Error in trigger a checkpoint operation: ", err)
|
||||
}
|
||||
|
||||
output := t.I18nBot("tgbot.messages.backupTime", "Time=="+time.Now().Format("2006-01-02 15:04:05"))
|
||||
t.SendMsgToTgbot(chatId, output)
|
||||
|
||||
file := tgbotapi.FilePath(config.GetDBPath())
|
||||
msg := tgbotapi.NewDocument(chatId, file)
|
||||
_, err := bot.Send(msg)
|
||||
_, err = bot.Send(msg)
|
||||
if err != nil {
|
||||
logger.Warning("Error in uploading backup: ", err)
|
||||
}
|
||||
|
||||
@@ -132,6 +132,32 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
|
||||
|
||||
inbound.Settings = string(modifiedSettings)
|
||||
}
|
||||
|
||||
if len(inbound.StreamSettings) > 0 {
|
||||
// Unmarshal stream JSON
|
||||
var stream map[string]interface{}
|
||||
json.Unmarshal([]byte(inbound.StreamSettings), &stream)
|
||||
|
||||
// Remove the "settings" field under "tlsSettings" and "realitySettings"
|
||||
tlsSettings, ok1 := stream["tlsSettings"].(map[string]interface{})
|
||||
realitySettings, ok2 := stream["realitySettings"].(map[string]interface{})
|
||||
if ok1 || ok2 {
|
||||
if ok1 {
|
||||
delete(tlsSettings, "settings")
|
||||
} else if ok2 {
|
||||
delete(realitySettings, "settings")
|
||||
}
|
||||
}
|
||||
|
||||
delete(stream, "externalProxy")
|
||||
|
||||
newStream, err := json.MarshalIndent(stream, "", " ")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
inbound.StreamSettings = string(newStream)
|
||||
}
|
||||
|
||||
inboundConfig := inbound.GenXrayInboundConfig()
|
||||
xrayConfig.InboundConfigs = append(xrayConfig.InboundConfigs, *inboundConfig)
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
"edit" = "Edit"
|
||||
"delete" = "Delete"
|
||||
"reset" = "Reset"
|
||||
"copySuccess" = "Copied successfully"
|
||||
"copySuccess" = "Copied Successfully"
|
||||
"sure" = "Sure"
|
||||
"encryption" = "Encryption"
|
||||
"transmission" = "Transmission"
|
||||
@@ -40,18 +40,19 @@
|
||||
"depletingSoon" = "Depleting"
|
||||
"offline" = "Offline"
|
||||
"online" = "Online"
|
||||
"domainName" = "Domain name"
|
||||
"domainName" = "Domain Name"
|
||||
"monitor" = "Listen IP"
|
||||
"certificate" = "Certificate"
|
||||
"fail" = " Fail"
|
||||
"success" = " Success"
|
||||
"getVersion" = "Get version"
|
||||
"getVersion" = "Get Version"
|
||||
"install" = "Install"
|
||||
"clients" = "Clients"
|
||||
"usage" = "Usage"
|
||||
"remained" = "Remained"
|
||||
"secAlertTitle" = "Security Alert"
|
||||
"secAlertSsl" = "This connection is not secure; Please refrain from entering sensitive information until TLS is activated for data protection"
|
||||
"secAlertSsl" = "This connection is not secure; Please refrain from entering sensitive information until TLS is activated for data protection."
|
||||
"security" = "Security"
|
||||
|
||||
[menu]
|
||||
"dashboard" = "System Status"
|
||||
@@ -59,11 +60,11 @@
|
||||
"settings" = "Panel Settings"
|
||||
"xray" = "Xray Settings"
|
||||
"logout" = "Logout"
|
||||
"link" = "Other"
|
||||
"link" = "Management"
|
||||
|
||||
[pages.login]
|
||||
"title" = "Login"
|
||||
"loginAgain" = "The login time limit has expired, please log in again"
|
||||
"loginAgain" = "Your session has expired, please log in again."
|
||||
|
||||
[pages.login.toasts]
|
||||
"invalidFormData" = "Input data format is invalid."
|
||||
@@ -74,23 +75,23 @@
|
||||
|
||||
[pages.index]
|
||||
"title" = "System Status"
|
||||
"memory" = "Memory"
|
||||
"hard" = "Hard Disk"
|
||||
"memory" = "RAM"
|
||||
"hard" = "Disk"
|
||||
"xrayStatus" = "Xray Status"
|
||||
"stopXray" = "Stop"
|
||||
"restartXray" = "Restart"
|
||||
"xraySwitch" = "Switch Version"
|
||||
"xraySwitchClick" = "Choose the version you want to switch to."
|
||||
"xraySwitchClickDesk" = "Choose wisely, as older versions may not be compatible with current configurations."
|
||||
"operationHours" = "Operation Hours"
|
||||
"operationHoursDesc" = "System uptime: time since startup."
|
||||
"xraySwitch" = "Switch Xray Version"
|
||||
"xraySwitchClick" = "Choose the version you want to switch."
|
||||
"xraySwitchClickDesk" = "Choose wisely, as older versions may not be compatible with current configs."
|
||||
"operationHours" = "System Uptime"
|
||||
"operationHoursDesc" = "Time since startup"
|
||||
"systemLoad" = "System Load"
|
||||
"connectionCount" = "Connection Count"
|
||||
"connectionCountDesc" = "Total connections across all network cards."
|
||||
"upSpeed" = "Total upload speed for all network cards."
|
||||
"downSpeed" = "Total download speed for all network cards."
|
||||
"totalSent" = "Total upload traffic of all network cards since system startup."
|
||||
"totalReceive" = "Total download data across all network cards since system startup."
|
||||
"connectionTcpCountDesc" = "Total TCP connections across all network cards"
|
||||
"connectionUdpCountDesc" = "Total UDP connections across all network cards"
|
||||
"upSpeed" = "Total upload speed for all network cards"
|
||||
"downSpeed" = "Total download speed for all network cards"
|
||||
"totalSent" = "Total upload data across all network cards since OS startup"
|
||||
"totalReceive" = "Total download data across all network cards since OS startup"
|
||||
"xraySwitchVersionDialog" = "Switch Xray Version"
|
||||
"xraySwitchVersionDialogDesc" = "Are you sure you want to switch the Xray version to"
|
||||
"dontRefresh" = "Installation is in progress, please do not refresh this page."
|
||||
@@ -98,7 +99,7 @@
|
||||
"config" = "Config"
|
||||
"backup" = "Backup & Restore"
|
||||
"backupTitle" = "Backup & Restore Database"
|
||||
"backupDescription" = "Remember to backup before importing a new database."
|
||||
"backupDescription" = "It is recommended to backup before importing a new database."
|
||||
"exportDatabase" = "Download Database"
|
||||
"importDatabase" = "Upload Database"
|
||||
|
||||
@@ -133,7 +134,7 @@
|
||||
"destinationPort" = "Destination Port"
|
||||
"targetAddress" = "Target Address"
|
||||
"monitorDesc" = "Leave blank by default"
|
||||
"meansNoLimit" = "Means No Limit"
|
||||
"meansNoLimit" = "Means no limit"
|
||||
"totalFlow" = "Total Flow"
|
||||
"leaveBlankToNeverExpire" = "Leave blank to never expire"
|
||||
"noRecommendKeepDefault" = "No special requirements to keep the default"
|
||||
@@ -151,29 +152,34 @@
|
||||
"cloneInboundContent" = "All settings of this inbound, except for Port, Listening IP, and Clients, will be applied to the clone."
|
||||
"cloneInboundOk" = "Clone"
|
||||
"resetAllTraffic" = "Reset All Inbounds Traffic"
|
||||
"resetAllTrafficTitle" = "Reset all inbounds traffic"
|
||||
"resetAllTrafficTitle" = "Reset All Inbounds Traffic"
|
||||
"resetAllTrafficContent" = "Are you sure you want to reset all inbounds traffic?"
|
||||
"resetInboundClientTraffics" = "Reset Clients Traffic"
|
||||
"resetInboundClientTrafficTitle" = "Reset all clients traffic"
|
||||
"resetInboundClientTrafficTitle" = "Reset Clients Traffic"
|
||||
"resetInboundClientTrafficContent" = "Are you sure you want to reset all traffic for this inbound's clients?"
|
||||
"resetAllClientTraffics" = "Reset All Clients Traffic"
|
||||
"resetAllClientTrafficTitle" = "Reset all clients traffic"
|
||||
"resetAllClientTrafficTitle" = "Reset All Clients Traffic"
|
||||
"resetAllClientTrafficContent" = "Are you sure you want to reset all traffics for all clients?"
|
||||
"delDepletedClients" = "Delete Depleted Clients"
|
||||
"delDepletedClientsTitle" = "Delete depleted clients"
|
||||
"delDepletedClientsTitle" = "Delete Depleted Clients"
|
||||
"delDepletedClientsContent" = "Are you sure you want to delete all depleted clients?"
|
||||
"email" = "Email"
|
||||
"emailDesc" = "Please provide a unique email address."
|
||||
"setDefaultCert" = "Set cert from panel"
|
||||
"telegramDesc" = "Use Telegram ID without @ or chat IDs ( you can get it here @userinfobot or use '/id' command in bot )"
|
||||
"setDefaultCert" = "Set Cert from Panel"
|
||||
"telegramDesc" = "Use Telegram ID without @ or chat IDs (you can get it here @userinfobot or use '/id' command in bot)"
|
||||
"subscriptionDesc" = "You can find your sub link on Details, also you can use the same name for several configurations"
|
||||
"info" = "Info"
|
||||
"same" = "Same"
|
||||
"inboundData" = "Inbound's Data"
|
||||
"copyToClipboard" = "Copy to Clipboard"
|
||||
"import" = "Import"
|
||||
"importInbound" = "Import an Inbound"
|
||||
|
||||
[pages.client]
|
||||
"add" = "Add Client"
|
||||
"edit" = "Edit Client"
|
||||
"submitAdd" = "Add Client"
|
||||
"submitEdit" = "Save changes"
|
||||
"submitEdit" = "Save Changes"
|
||||
"clientCount" = "Number of Clients"
|
||||
"bulk" = "Add Bulk"
|
||||
"method" = "Method"
|
||||
@@ -181,28 +187,28 @@
|
||||
"last" = "Last"
|
||||
"prefix" = "Prefix"
|
||||
"postfix" = "Postfix"
|
||||
"delayedStart" = "Start after first use"
|
||||
"expireDays" = "Expire days"
|
||||
"days" = "day(s)"
|
||||
"renew" = "Auto renew"
|
||||
"delayedStart" = "Start After First Use"
|
||||
"expireDays" = "Expire Days"
|
||||
"days" = "Day(s)"
|
||||
"renew" = "Auto Renew"
|
||||
"renewDesc" = "Auto renew days after expiration. 0 = disable"
|
||||
|
||||
[pages.inbounds.toasts]
|
||||
"obtain" = "Obtain"
|
||||
|
||||
[pages.inbounds.stream.general]
|
||||
"requestHeader" = "Request header"
|
||||
"requestHeader" = "Request Header"
|
||||
"name" = "Name"
|
||||
"value" = "Value"
|
||||
|
||||
[pages.inbounds.stream.tcp]
|
||||
"requestVersion" = "Request version"
|
||||
"requestMethod" = "Request method"
|
||||
"requestPath" = "Request path"
|
||||
"responseVersion" = "Response version"
|
||||
"responseStatus" = "Response status"
|
||||
"responseStatusDescription" = "Response status description"
|
||||
"responseHeader" = "Response header"
|
||||
"requestVersion" = "Request Version"
|
||||
"requestMethod" = "Request Method"
|
||||
"requestPath" = "Request Path"
|
||||
"responseVersion" = "Response Version"
|
||||
"responseStatus" = "Response Status"
|
||||
"responseStatusDescription" = "Response Status Description"
|
||||
"responseHeader" = "Response Header"
|
||||
|
||||
[pages.inbounds.stream.quic]
|
||||
"encryption" = "Encryption"
|
||||
@@ -211,79 +217,91 @@
|
||||
"title" = "Settings"
|
||||
"save" = "Save"
|
||||
"infoDesc" = "Every change made here needs to be saved. Please restart the panel for the changes to take effect."
|
||||
"restartPanel" = "Restart Panel "
|
||||
"restartPanelDesc" = "Are you sure you want to restart the panel? Click OK to restart after 3 seconds. If you cannot access the panel after restarting, please view the panel log information on the server."
|
||||
"resetDefaultConfig" = "Reset to default config"
|
||||
"restartPanel" = "Restart Panel"
|
||||
"restartPanelDesc" = "Are you sure you want to restart the panel? click OK to restart after 3 Secs. If you cannot access the panel after restarting, please view the panel log info on the server."
|
||||
"resetDefaultConfig" = "Reset to Default Config"
|
||||
"panelConfig" = "Panel Configurations"
|
||||
"userSettings" = "User Settings"
|
||||
"TGBotSettings" = "Telegram Bot Settings"
|
||||
"panelListeningIP" = "Panel Listening IP"
|
||||
"panelListeningIPDesc" = "Leave blank by default to monitor all IPs."
|
||||
"panelListeningDomain" = "Panel Listening Domain"
|
||||
"panelListeningDomainDesc" = "Leave blank by default to monitor all domains and IPs"
|
||||
"panelListeningDomainDesc" = "Leave blank by default to monitor all domains and IPs."
|
||||
"panelPort" = "Panel Port"
|
||||
"panelPortDesc" = "Port number for serving the panel."
|
||||
"publicKeyPath" = "Panel Certificate Public Key File Path"
|
||||
"publicKeyPath" = "Panel Certificate Public Key Path"
|
||||
"publicKeyPathDesc" = "Fill in an absolute path starting with '/'"
|
||||
"privateKeyPath" = "Panel Certificate Private Key File Path"
|
||||
"privateKeyPath" = "Panel Certificate Private Key Path"
|
||||
"privateKeyPathDesc" = "Fill in an absolute path starting with '/'"
|
||||
"panelUrlPath" = "Panel URL Root Path"
|
||||
"panelUrlPathDesc" = "Must start with '/' and end with '/'"
|
||||
"pageSize" = "Pagination size"
|
||||
"pageSizeDesc" = "Define page size for inbounds table. Set 0 to disable"
|
||||
"pageSize" = "Pagination Size"
|
||||
"pageSizeDesc" = "Define page size for inbounds table. Set 0 to disable."
|
||||
"remarkModel" = "Remark Model and Seperation Charachter"
|
||||
"sampleRemark" = "Sample Remark"
|
||||
"oldUsername" = "Current Username"
|
||||
"currentPassword" = "Current Password"
|
||||
"newUsername" = "New Username"
|
||||
"newPassword" = "New Password"
|
||||
"telegramBotEnable" = "Enable Telegram bot"
|
||||
"telegramBotEnableDesc" = "Your telegram bot will interact with the panel"
|
||||
"telegramBotEnable" = "Enable Telegram Bot"
|
||||
"telegramBotEnableDesc" = "Your telegram bot will interact with the panel."
|
||||
"telegramToken" = "Telegram Token"
|
||||
"telegramTokenDesc" = "The Token you have got from @BotFather"
|
||||
"telegramChatId" = "Telegram Admin ChatIDs"
|
||||
"telegramChatIdDesc" = "Multiple Chat IDs separated by comma. use @userinfobot or use '/id' command in bot to get your Chat IDs."
|
||||
"telegramNotifyTime" = "Telegram bot notification time"
|
||||
"telegramNotifyTimeDesc" = "Use Crontab timing format."
|
||||
"telegramTokenDesc" = "The token you have got from @BotFather"
|
||||
"telegramChatId" = "Telegram Admin Chat IDs"
|
||||
"telegramChatIdDesc" = "Multiple chat IDs separated by comma. use @userinfobot or use '/id' command in bot to get your chat IDs."
|
||||
"telegramNotifyTime" = "Telegram Bot Notification Time"
|
||||
"telegramNotifyTimeDesc" = "Use crontab timing format."
|
||||
"tgNotifyBackup" = "Database Backup"
|
||||
"tgNotifyBackupDesc" = "Send database backup file with report notification"
|
||||
"tgNotifyBackupDesc" = "Send database backup file with report notification."
|
||||
"tgNotifyLogin" = "Login Notification"
|
||||
"tgNotifyLoginDesc" = "Displays the username, IP address, and time when someone tries to log into your panel."
|
||||
"sessionMaxAge" = "Session maximum age"
|
||||
"sessionMaxAgeDesc" = "The time that you can stay login (unit: minute)"
|
||||
"expireTimeDiff" = "Expiration threshold for notification"
|
||||
"expireTimeDiffDesc" = "Get notified about account expiration before the threshold (unit: day)"
|
||||
"trafficDiff" = "Traffic threshold for notification"
|
||||
"trafficDiffDesc" = "Get notified about traffic exhaustion before reaching the threshold (unit: GB)"
|
||||
"tgNotifyCpu" = "CPU percentage alert threshold"
|
||||
"tgNotifyCpuDesc" = "Receive notification if CPU usage exceeds this threshold (unit: %)"
|
||||
"sessionMaxAge" = "Session Duration"
|
||||
"sessionMaxAgeDesc" = "The time that you can stay login. (unit: minute)"
|
||||
"expireTimeDiff" = "Client Expiration Threshold Notification"
|
||||
"expireTimeDiffDesc" = "Get notified about client expiration before the threshold. (unit: day)"
|
||||
"trafficDiff" = "Traffic Limit Threshold Notification"
|
||||
"trafficDiffDesc" = "Get notified about traffic exhaustion before reaching the threshold. (unit: GB)"
|
||||
"tgNotifyCpu" = "CPU Load Threshold Notification"
|
||||
"tgNotifyCpuDesc" = "Get notified if CPU usage exceeds this threshold. (unit: %)"
|
||||
"timeZone" = "Time Zone"
|
||||
"timeZoneDesc" = "Scheduled tasks run according to the time in this time zone."
|
||||
"subSettings" = "Subscription"
|
||||
"subEnable" = "Enable service"
|
||||
"subEnableDesc" = "Subscription feature with separate configuration"
|
||||
"subEnable" = "Enable Service"
|
||||
"subEnableDesc" = "Subscription feature with separate configuration."
|
||||
"subListen" = "Listening IP"
|
||||
"subListenDesc" = "Leave blank by default to monitor all IPs"
|
||||
"subListenDesc" = "Leave blank by default to monitor all IPs."
|
||||
"subPort" = "Subscription Port"
|
||||
"subPortDesc" = "Port number for serving the subscription service must be unused in server"
|
||||
"subCertPath" = "Subscription Certificate Public Key File Path"
|
||||
"subPortDesc" = "Port number for serving the subscription service. Must be unused in the server."
|
||||
"subCertPath" = "Subscription Certificate Public Key Path"
|
||||
"subCertPathDesc" = "Fill in an absolute path starting with '/'"
|
||||
"subKeyPath" = "Subscription Certificate Private Key File Path"
|
||||
"subKeyPath" = "Subscription Certificate Private Key Path"
|
||||
"subKeyPathDesc" = "Fill in an absolute path starting with '/'"
|
||||
"subPath" = "Subscription URL Root Path"
|
||||
"subPathDesc" = "Must start with '/' and end with '/'"
|
||||
"subDomain" = "Listening Domain"
|
||||
"subDomainDesc" = "Leave blank by default to monitor all domains and IPs"
|
||||
"subDomainDesc" = "Leave blank by default to monitor all domains and IPs."
|
||||
"subUpdates" = "Subscription update intervals"
|
||||
"subUpdatesDesc" = "Interval hours between updates in client application"
|
||||
"subEncrypt" = "Encrypt configs"
|
||||
"subEncryptDesc" = "Encrypt the returned configs in subscription"
|
||||
"subShowInfo" = "Show usage info"
|
||||
"subShowInfoDesc" = "Show remianed traffic and date after config name"
|
||||
"subUpdatesDesc" = "Interval hours between updates in client application."
|
||||
"subEncrypt" = "Encode Configs"
|
||||
"subEncryptDesc" = "Encode the returned configs in subscription."
|
||||
"subShowInfo" = "Show Usage Info"
|
||||
"subShowInfoDesc" = "Show remained traffic and date after config name."
|
||||
"subURI" = "Reverse Proxy URI"
|
||||
"subURIDesc" = "Change base URI of subscription URL for using on behind of proxies."
|
||||
|
||||
[pages.settings.toasts]
|
||||
"modifySettings" = "Modify Settings"
|
||||
"getSettings" = "Get Settings"
|
||||
"modifyUser" = "Modify User"
|
||||
"originalUserPassIncorrect" = "Incorrect original username or password"
|
||||
"userPassMustBeNotEmpty" = "New username and password cannot be empty"
|
||||
|
||||
[pages.xray]
|
||||
"title" = "Xray Settings"
|
||||
"save" = "Save Settings"
|
||||
"restart" = "Restart Xray"
|
||||
"basicTemplate" = "Basic Template"
|
||||
"advancedTemplate" = "Advanced Template"
|
||||
"completeTemplate" = "Complete Template"
|
||||
"generalConfigs" = "General Configs"
|
||||
"generalConfigsDesc" = "These options will provide general adjustments."
|
||||
"blockConfigs" = "Blocking Configs"
|
||||
@@ -300,62 +318,76 @@
|
||||
"FreedomStrategyDesc" = "Set the output strategy of the network in the Freedom Protocol."
|
||||
"RoutingStrategy" = "Configure Domains Routing Strategy"
|
||||
"RoutingStrategyDesc" = "Set the overall routing strategy for DNS resolving."
|
||||
"Torrent" = "Ban BitTorrent Usage"
|
||||
"TorrentDesc" = "Change the configuration template to avoid using BitTorrent by users."
|
||||
"PrivateIp" = "Ban Private IP Ranges to Connect"
|
||||
"Torrent" = "Ban BitTorrent Protocol"
|
||||
"TorrentDesc" = "Change the configuration template to avoid using BitTorrent protocol."
|
||||
"PrivateIp" = "Ban Private IPs Connection"
|
||||
"PrivateIpDesc" = "Change the configuration template to avoid connecting to private IP ranges."
|
||||
"Ads" = "Block Ads"
|
||||
"AdsDesc" = "Change the configuration template to block ads"
|
||||
"AdsDesc" = "Change the configuration template to block ads."
|
||||
"Family" = "Enable Family-Friendly Configuration"
|
||||
"FamilyDesc" = "Avoid connecting to unsafe websites for family protection."
|
||||
"IRIp" = "Disable connection to Iran IP ranges"
|
||||
"IRIp" = "Disable Connection to Iran IPs"
|
||||
"IRIpDesc" = "Change the configuration template to avoid connecting to Iran IP ranges."
|
||||
"IRDomain" = "Disable connection to Iran domains"
|
||||
"IRDomain" = "Disable Connection to Iran Domains"
|
||||
"IRDomainDesc" = "Change the configuration template to avoid connecting to Iran domains."
|
||||
"ChinaIp" = "Disable connection to China IP ranges"
|
||||
"ChinaIp" = "Disable Connection to China IPs"
|
||||
"ChinaIpDesc" = "Change the configuration template to avoid connecting to China IP ranges."
|
||||
"ChinaDomain" = "Disable connection to China domains"
|
||||
"ChinaDomain" = "Disable Connection to China Domains"
|
||||
"ChinaDomainDesc" = "Change the configuration template to avoid connecting to China domains."
|
||||
"RussiaIp" = "Disable connection to Russia IP ranges"
|
||||
"RussiaIp" = "Disable Connection to Russia IPs"
|
||||
"RussiaIpDesc" = "Change the configuration template to avoid connecting to Russia IP ranges."
|
||||
"RussiaDomain" = "Disable connection to Russia domains"
|
||||
"RussiaDomain" = "Disable Connection to Russia Domains"
|
||||
"RussiaDomainDesc" = "Change the configuration template to avoid connecting to Russia domains."
|
||||
"DirectIRIp" = "Direct connection to Iran IP ranges"
|
||||
"DirectIRIp" = "Direct Connection to Iran IPs"
|
||||
"DirectIRIpDesc" = "Change the configuration template for direct connecting to Iran IP ranges."
|
||||
"DirectIRDomain" = "Direct connection to Iran domains"
|
||||
"DirectIRDomain" = "Direct Connection to Iran Domains"
|
||||
"DirectIRDomainDesc" = "Change the configuration template for direct connecting to Iran domains."
|
||||
"DirectChinaIp" = "Direct connection to China IP ranges"
|
||||
"DirectChinaIp" = "Direct Connection to China IPs"
|
||||
"DirectChinaIpDesc" = "Change the configuration template for direct connecting to China IP ranges."
|
||||
"DirectChinaDomain" = "Direct connection to China domains"
|
||||
"DirectChinaDomain" = "Direct Connection to China Domains"
|
||||
"DirectChinaDomainDesc" = "Change the configuration template for direct connecting to China domains."
|
||||
"DirectRussiaIp" = "Direct connection to Russia IP ranges"
|
||||
"DirectRussiaIp" = "Direct Connection to Russia IPs"
|
||||
"DirectRussiaIpDesc" = "Change the configuration template for direct connecting to Russia IP ranges."
|
||||
"DirectRussiaDomain" = "Direct connection to Russia domains"
|
||||
"DirectRussiaDomain" = "Direct Connection to Russia Domains"
|
||||
"DirectRussiaDomainDesc" = "Change the configuration template for direct connecting to Russia domains."
|
||||
"GoogleIPv4" = "Use IPv4 for Google"
|
||||
"GoogleIPv4Desc" = "Add routing for Google to connect with IPv4."
|
||||
"NetflixIPv4" = "Use IPv4 for Netflix"
|
||||
"NetflixIPv4Desc" = "Add routing for Netflix to connect with IPv4."
|
||||
"Inbounds" = "Configuration of Inbounds"
|
||||
"InboundsDesc" = "Change the configuration template to accept specific clients."
|
||||
"Outbounds" = "Configuration of Outbounds"
|
||||
"OutboundsDesc" = "Change the configuration template to define outgoing ways for this server."
|
||||
"Routings" = "Configuration of routing rules"
|
||||
"RoutingsDesc" = "Change the configuration template to define routing rules for this server."
|
||||
"manualLists" = "Manual Lists"
|
||||
"manualListsDesc" = "Please use the JSON array format."
|
||||
"manualBlockedIPs" = "List of Blocked IPs"
|
||||
"manualBlockedDomains" = "List of Blocked Domains"
|
||||
"manualDirectIPs" = "List of Direct IPs"
|
||||
"manualDirectDomains" = "List of Direct Domains"
|
||||
"manualIPv4Domains" = "List of IPv4 Domains"
|
||||
"completeTemplate" = "All"
|
||||
"Inbounds" = "Inbounds"
|
||||
"Outbounds" = "Outbounds"
|
||||
"Routings" = "Routing Rules"
|
||||
"RoutingsDesc" = "The priority of each rule is important!"
|
||||
|
||||
[pages.settings.toasts]
|
||||
"modifySettings" = "Modify Settings "
|
||||
"getSettings" = "Get Settings "
|
||||
"modifyUser" = "Modify User "
|
||||
"originalUserPassIncorrect" = "Incorrect original username or password"
|
||||
"userPassMustBeNotEmpty" = "New username and new password cannot be empty"
|
||||
[pages.xray.rules]
|
||||
"first" = "First"
|
||||
"last" = "Last"
|
||||
"up" = "Up"
|
||||
"down" = "Down"
|
||||
"source" = "Source"
|
||||
"dest" = "Destination"
|
||||
"inbound" = "Inbound"
|
||||
"outbound" = "Outbound"
|
||||
"info" = "Info"
|
||||
"add" = "Add Rule"
|
||||
"edit" = "Edit Rule"
|
||||
"useComma" = "Comma separated items"
|
||||
|
||||
[pages.xray.outbound]
|
||||
"addOutbound" = "Add Outbound"
|
||||
"addReverse" = "Add Reverse"
|
||||
"editOutbound" = "Edit Outbound"
|
||||
"editReverse" = "Edit Reverse"
|
||||
"tag" = "Tag"
|
||||
"tagDesc" = "Unique Tag"
|
||||
"address" = "Address"
|
||||
"reverse" = "Reverse"
|
||||
"domain" = "Domain"
|
||||
"type" = "Type"
|
||||
"bridge" = "Bridge"
|
||||
"portal" = "Portal"
|
||||
"intercon" = "Interconnection"
|
||||
|
||||
[tgbot]
|
||||
"noResult" = "❗ No result!"
|
||||
@@ -374,36 +406,36 @@
|
||||
"help" = "🤖 Welcome to this bot! It's designed to offer you specific data from the server, and it allows you to make modifications as needed.\r\n\r\n"
|
||||
"start" = "👋 Hello <i>{{ .Firstname }}</i>.\r\n"
|
||||
"welcome" = "🤖 Welcome to <b>{{ .Hostname }}</b> management bot.\r\n"
|
||||
"status" = "✅ Bot is ok!"
|
||||
"status" = "✅ Bot is OK!"
|
||||
"usage" = "❗ Please provide a text to search!"
|
||||
"getID" = "🆔 Your ID: <code>{{ .ID }}</code>"
|
||||
"helpAdminCommands" = "Search for a client email:\r\n<code>/usage [Email]</code>\r\n \r\nSearch for inbounds (with client stats):\r\n<code>/inbound [Remark]</code>"
|
||||
"helpClientCommands" = "To search for statistics, just use folowing command:\r\n \r\n<code>/usage [UUID|Password]</code>\r\n \r\nUse UUID for vmess/vless and Password for Trojan."
|
||||
"helpClientCommands" = "To search for statistics, just use the following command:\r\n \r\n<code>/usage [UUID|Password]</code>\r\n \r\nUse UUID for vmess/vless and Password for Trojan."
|
||||
|
||||
[tgbot.messages]
|
||||
"cpuThreshold" = "🔴 The CPU usage {{ .Percent }}% is more than threshold {{ .Threshold }}%"
|
||||
"cpuThreshold" = "🔴 CPU load {{ .Percent }}% is more than threshold {{ .Threshold }}%"
|
||||
"loginSuccess" = "✅ Successfully logged-in to the panel.\r\n"
|
||||
"loginFailed" = "❗️ Login to the panel failed.\r\n"
|
||||
"report" = "🕰 Scheduled Reports: {{ .RunTime }}\r\n"
|
||||
"report" = "🕰 Scheduled reports: {{ .RunTime }}\r\n"
|
||||
"datetime" = "⏰ Date-Time: {{ .DateTime }}\r\n"
|
||||
"hostname" = "💻 Hostname: {{ .Hostname }}\r\n"
|
||||
"version" = "🚀 X-UI Version: {{ .Version }}\r\n"
|
||||
"version" = "🚀 X-UI version: {{ .Version }}\r\n"
|
||||
"ipv6" = "🌐 IPv6: {{ .IPv6 }}\r\n"
|
||||
"ipv4" = "🌐 IPv4: {{ .IPv4 }}\r\n"
|
||||
"ip" = "🌐 IP: {{ .IP }}\r\n"
|
||||
"serverUpTime" = "⏳ Server Uptime: {{ .UpTime }} {{ .Unit }}\r\n"
|
||||
"serverLoad" = "📈 Server Load: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n"
|
||||
"serverMemory" = "📋 Server Memory: {{ .Current }}/{{ .Total }}\r\n"
|
||||
"tcpCount" = "🔹 TcpCount: {{ .Count }}\r\n"
|
||||
"udpCount" = "🔸 UdpCount: {{ .Count }}\r\n"
|
||||
"serverUpTime" = "⏳ Server uptime: {{ .UpTime }} {{ .Unit }}\r\n"
|
||||
"serverLoad" = "📈 Server load: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n"
|
||||
"serverMemory" = "📋 Server RAM: {{ .Current }}/{{ .Total }}\r\n"
|
||||
"tcpCount" = "🔹 TCP: {{ .Count }}\r\n"
|
||||
"udpCount" = "🔸 UDP: {{ .Count }}\r\n"
|
||||
"traffic" = "🚦 Traffic: {{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n"
|
||||
"xrayStatus" = "ℹ️ Xray Status: {{ .State }}\r\n"
|
||||
"xrayStatus" = "ℹ️ Xray status: {{ .State }}\r\n"
|
||||
"username" = "👤 Username: {{ .Username }}\r\n"
|
||||
"time" = "⏰ Time: {{ .Time }}\r\n"
|
||||
"inbound" = "📍 Inbound: {{ .Remark }}\r\n"
|
||||
"port" = "🔌 Port: {{ .Port }}\r\n"
|
||||
"expire" = "📅 Expire Date: {{ .DateTime }}\r\n \r\n"
|
||||
"expireIn" = "📅 Expire In: {{ .Time }}\r\n \r\n"
|
||||
"expire" = "📅 Expire date: {{ .DateTime }}\r\n \r\n"
|
||||
"expireIn" = "📅 Expire in: {{ .Time }}\r\n \r\n"
|
||||
"active" = "💡 Active: {{ .Enable }}\r\n"
|
||||
"online" = "🌐 Connection status: {{ .Status }}\r\n"
|
||||
"email" = "📧 Email: {{ .Email }}\r\n"
|
||||
@@ -415,7 +447,7 @@
|
||||
"onlinesCount" = "🌐 Online clients count: {{ .Count }}\r\n"
|
||||
"disabled" = "🛑 Disabled: {{ .Disabled }}\r\n"
|
||||
"depleteSoon" = "🔜 Deplete soon: {{ .Deplete }}\r\n \r\n"
|
||||
"backupTime" = "🗄 Backup Time: {{ .Time }}\r\n"
|
||||
"backupTime" = "🗄 Backup time: {{ .Time }}\r\n"
|
||||
"yes" = "✅ Yes"
|
||||
"no" = "❌ No"
|
||||
|
||||
@@ -423,7 +455,7 @@
|
||||
"dbBackup" = "Get DB Backup"
|
||||
"serverUsage" = "Server Usage"
|
||||
"getInbounds" = "Get Inbounds"
|
||||
"depleteSoon" = "Deplete soon"
|
||||
"depleteSoon" = "Deplete Soon"
|
||||
"clientUsage" = "Get Usage"
|
||||
"onlines" = "Online Clients"
|
||||
"commands" = "Commands"
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
"remained" = "باقیمانده"
|
||||
"secAlertTitle" = "هشدار امنیتی"
|
||||
"secAlertSsl" = "این اتصال امن نیست؛ لطفا تا زمانی که تیالاس برای حفاظت از داده ها فعال نشده است از وارد کردن اطلاعات حساس خودداری کنید"
|
||||
"security" = "امنیت"
|
||||
|
||||
[menu]
|
||||
"dashboard" = "وضعیت سیستم"
|
||||
@@ -59,7 +60,7 @@
|
||||
"settings" = "تنظیمات پنل"
|
||||
"xray" = "الگوی ایکسری"
|
||||
"logout" = "خروج"
|
||||
"link" = "دیگر"
|
||||
"link" = "مدیریت"
|
||||
|
||||
[pages.login]
|
||||
"title" = "ورود به سیستم"
|
||||
@@ -85,8 +86,8 @@
|
||||
"operationHours" = "مدت فعالیت"
|
||||
"operationHoursDesc" = "مدت فعالیت سیستم بعد از روشن شدن"
|
||||
"systemLoad" = "بار روی سیستم"
|
||||
"connectionCount" = "تعداد کانکشن ها"
|
||||
"connectionCountDesc" = "تعداد کانکشن ها برای کل شبکه"
|
||||
"connectionTcpCountDesc" = "مجموع اتصالات TCP در تمام کارت های شبکه"
|
||||
"connectionUdpCountDesc" = "مجموع اتصالات UDP در تمام کارت های شبکه"
|
||||
"upSpeed" = "سرعت آپلود در حال حاضر سیستم"
|
||||
"downSpeed" = "سرعت دانلود در حال حاضر سیستم"
|
||||
"totalSent" = "جمع کل ترافیک آپلود مصرفی"
|
||||
@@ -147,7 +148,7 @@
|
||||
"client" = "کاربر"
|
||||
"export" = "استخراج لینکها"
|
||||
"clone" = "شبیه سازی"
|
||||
"cloneInbound" = "ایجاد"
|
||||
"cloneInbound" = "شبیهسازی سرویس"
|
||||
"cloneInboundContent" = "همه موارد این ورودی بجز پورت ، ای پی و کلاینت ها شبیه سازی خواهند شد"
|
||||
"resetAllTraffic" = "ریست ترافیک کل سرویس ها"
|
||||
"resetAllTrafficTitle" = "ریست ترافیک کل سرویس ها"
|
||||
@@ -167,6 +168,11 @@
|
||||
"telegramDesc" = "از آیدی تلگرام بدون @ یا آیدی چت استفاده کنید (می توانید آن را از اینجا دریافت کنید @userinfobot یا در ربات دستور '/id' را وارد کنید)"
|
||||
"subscriptionDesc" = "می توانید ساب لینک خود را در جزئیات پیدا کنید، همچنین می توانید از همین نام برای چندین کانفیگ استفاده کنید"
|
||||
"info" = "اطلاعات"
|
||||
"same" = "همسان"
|
||||
"inboundData" = "دادههای سرویس"
|
||||
"copyToClipboard" = "کپی در حافظه"
|
||||
"import" = "وارد کردن"
|
||||
"importInbound" = "وارد کردن یک سرویس"
|
||||
|
||||
[pages.client]
|
||||
"add" = "کاربر جدید"
|
||||
@@ -230,6 +236,8 @@
|
||||
"panelUrlPathDesc" = "باید با '/' شروع شود و با '/' تمام شود"
|
||||
"pageSize" = "اندازه صفحه بندی جدول"
|
||||
"pageSizeDesc" = "اندازه صفحه را برای جدول سرویس ها تعریف کنید. 0: غیرفعال"
|
||||
"remarkModel" = "نام کانفیگ و جداکننده"
|
||||
"sampleRemark" = "نمونه نام"
|
||||
"oldUsername" = "نام کاربری فعلی"
|
||||
"currentPassword" = "رمز عبور فعلی"
|
||||
"newUsername" = "نام کاربری جدید"
|
||||
@@ -277,12 +285,22 @@
|
||||
"subEncryptDesc" = "رمزگذاری کانفیگ های بازگشتی سابسکریپشن"
|
||||
"subShowInfo" = "نمایش اطلاعات مصرف"
|
||||
"subShowInfoDesc" = "ترافیک و زمان باقیمانده را در هر کانفیگ نمایش میدهد"
|
||||
"subURI" = "آدرس پایه پروکسی معکوس"
|
||||
"subURIDesc" = "آدرس پایه سابسکریپشن را برای استفاده در پشت پراکسی ها تغییر میدهد"
|
||||
|
||||
[pages.settings.toasts]
|
||||
"modifySettings" = "ویرایش تنظیمات"
|
||||
"getSettings" = "دریافت تنظیمات"
|
||||
"modifyUser" = "ویرایش کاربر"
|
||||
"originalUserPassIncorrect" = "نام کاربری و رمز عبور فعلی اشتباه می باشد "
|
||||
"userPassMustBeNotEmpty" = "نام کاربری و رمز عبور جدید نمیتواند خالی باشد "
|
||||
|
||||
[pages.xray]
|
||||
"title" = "تنظیمات Xray"
|
||||
"save" = "ذخیره تنظیمات"
|
||||
"restart" = "ریستارت ایکسری"
|
||||
"basicTemplate" = "بخش الگو پایه"
|
||||
"advancedTemplate" = "بخش الگو پیشرفته"
|
||||
"completeTemplate" = "بخش الگو کامل"
|
||||
"generalConfigs" = "تنظیمات عمومی"
|
||||
"generalConfigsDesc" = "این تنظیمات میتواند ترافیک کلی سرویس را متاثر کند"
|
||||
"blockConfigs" = "مسدود سازی"
|
||||
@@ -335,26 +353,40 @@
|
||||
"GoogleIPv4Desc" = "مسیردهی جدید برای اتصال به گوگل با آیپی ورژن 4 اضافه میکند"
|
||||
"NetflixIPv4" = "استفاده از آیپی ورژن 4 برای اتصال به نتفلیکس"
|
||||
"NetflixIPv4Desc" = "مسیردهی جدید برای اتصال به نتفلیکس با آیپی ورژن 4 اضافه میکند"
|
||||
"Inbounds" = "تنظیمات ورودی"
|
||||
"InboundsDesc" = "میتوانید الگوی تنظیمات را برای ورودی های خاص تنظیم نمایید"
|
||||
"Outbounds" = "تنظیمات خروجی"
|
||||
"OutboundsDesc" = "میتوانید الگوی تنظیمات را برای خروجی اینترنت تنظیم نمایید"
|
||||
"Routings" = "تنظیمات قوانین مسیریابی"
|
||||
"RoutingsDesc" = "میتوانید الگوی تنظیمات را برای مسیریابی تنظیم نمایید"
|
||||
"manualLists" = "لیست های دستی"
|
||||
"manualListsDesc" = "فرمت: JSON Array"
|
||||
"manualBlockedIPs" = "لیست آیپی های مسدود شده"
|
||||
"manualBlockedDomains" = "لیست دامنه های مسدود شده"
|
||||
"manualDirectIPs" = "لیست آیپی های مستقیم"
|
||||
"manualDirectDomains" = "لیست دامنه های مستقیم"
|
||||
"manualIPv4Domains" = "لیست دامنههای IPv4"
|
||||
"completeTemplate" = "کامل"
|
||||
"Inbounds" = "ورودیها"
|
||||
"Outbounds" = "خروجیها"
|
||||
"Routings" = "قوانین مسیریابی"
|
||||
"RoutingsDesc" = "اولویت هر قانون مهم است!"
|
||||
|
||||
[pages.settings.toasts]
|
||||
"modifySettings" = "ویرایش تنظیمات"
|
||||
"getSettings" = "دریافت تنظیمات"
|
||||
"modifyUser" = "ویرایش کاربر"
|
||||
"originalUserPassIncorrect" = "نام کاربری و رمز عبور فعلی اشتباه می باشد "
|
||||
"userPassMustBeNotEmpty" = "نام کاربری و رمز عبور جدید نمیتواند خالی باشد "
|
||||
[pages.xray.rules]
|
||||
"first" = "اولین"
|
||||
"last" = "آخرین"
|
||||
"up" = "بالا"
|
||||
"down" = "پایین"
|
||||
"source" = "مبدا"
|
||||
"dest" = "مقصد"
|
||||
"inbound" = "ورودی"
|
||||
"outbound" = "خروجی"
|
||||
"info" = "اطلاعات"
|
||||
"add" = "افزودن قانون"
|
||||
"edit" = "ویرایش قانون"
|
||||
"useComma" = "موارد جدا شده با کاما"
|
||||
|
||||
[pages.xray.outbound]
|
||||
"addOutbound" = "افزودن خروجی"
|
||||
"addReverse" = "افزودن معکوس"
|
||||
"editOutbound" = "ویرایش خروجی"
|
||||
"editReverse" = "ویرایش معکوس"
|
||||
"tag" = "برچسب"
|
||||
"tagDesc" = "برچسب یگانه"
|
||||
"address" = "آدرس"
|
||||
"reverse" = "معکوس"
|
||||
"domain" = "دامنه"
|
||||
"type" = "نوع"
|
||||
"bridge" = "پل"
|
||||
"portal" = "پرتال"
|
||||
"intercon" = "اتصال میانی"
|
||||
|
||||
[tgbot]
|
||||
"noResult" = "❗ نتیجهای یافت نشد!"
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
"remained" = "Осталось"
|
||||
"secAlertTitle" = "Предупреждение системы безопасности"
|
||||
"secAlertSsl" = "Это соединение не защищено. Пожалуйста, воздержитесь от ввода конфиденциальной информации до тех пор, пока не будет активирован TLS для защиты данных"
|
||||
"security" = "Безопасность"
|
||||
|
||||
[menu]
|
||||
"dashboard" = "Статус системы"
|
||||
@@ -59,7 +60,7 @@
|
||||
"settings" = "Настройки"
|
||||
"xray" = "Xray Настройки"
|
||||
"logout" = "Выйти"
|
||||
"link" = "Другое"
|
||||
"link" = "менеджмент"
|
||||
|
||||
[pages.login]
|
||||
"title" = "Войти"
|
||||
@@ -85,8 +86,8 @@
|
||||
"operationHours" = "Время работы"
|
||||
"operationHoursDesc" = "Время работы системы: время с момента запуска."
|
||||
"systemLoad" = "Системная нагрузка"
|
||||
"connectionCount" = "Количество соединений"
|
||||
"connectionCountDesc" = "Всего подключений по всем сетям»"
|
||||
"connectionTcpCountDesc" = "Всего подключений TCP по всем сетевым картам."
|
||||
"connectionUdpCountDesc" = "Общее количество подключений UDP по всем сетевым картам."
|
||||
"upSpeed" = "Общая скорость отдачи"
|
||||
"downSpeed" = "Общая скорость получения"
|
||||
"totalSent" = "Общий объем загруженных данных с момента запуска системы"
|
||||
@@ -168,6 +169,11 @@
|
||||
"telegramDesc" = "Используйте идентификатор Telegram без символа @ или идентификатора чата (можно получить его здесь @userinfobot или использовать команду '/id' в боте)"
|
||||
"subscriptionDesc" = "вы можете найти свою ссылку подписки в разделе «Подробнее», также вы можете использовать одно и то же имя для нескольких конфигов"
|
||||
"info" = "Информация"
|
||||
"same" = "Тот же"
|
||||
"inboundData" = "Входящие данные"
|
||||
"copyToClipboard" = "Копировать в буфер обмена"
|
||||
"import" = "Импортировать"
|
||||
"importInbound" = "Импортировать входящее сообщение"
|
||||
|
||||
[pages.client]
|
||||
"add" = "Добавить клиента"
|
||||
@@ -231,6 +237,8 @@
|
||||
"panelUrlPathDesc" = "Должен начинаться с «/» и заканчиваться на «/»."
|
||||
"pageSize" = "Размер нумерации страниц"
|
||||
"pageSizeDesc" = "Определить размер страницы для входящей таблицы. Установите 0, чтобы отключить"
|
||||
"remarkModel" = "Модель примечания и символ разделения"
|
||||
"sampleRemark" = "Пример замечания"
|
||||
"oldUsername" = "Текущее имя пользователя"
|
||||
"currentPassword" = "Текущий пароль"
|
||||
"newUsername" = "Новое имя пользователя"
|
||||
@@ -278,12 +286,22 @@
|
||||
"subEncryptDesc" = "Шифрование возвращаемых конфигураций в подписке"
|
||||
"subShowInfo" = "Показать информацию об использовании"
|
||||
"subShowInfoDesc" = "Показывать восстановленный трафик и дату после имени конфигурации"
|
||||
"subURI" = "URI обратного прокси"
|
||||
"subURIDesc" = "Изменить базовый URI URL-адреса подписки для использования за прокси-серверами"
|
||||
|
||||
[pages.settings.toasts]
|
||||
"modifySettings" = "Изменение настроек"
|
||||
"getSettings" = "Просмотр настроек"
|
||||
"modifyUser" = "Изменение пользователя "
|
||||
"originalUserPassIncorrect" = "Неверное имя пользователя или пароль"
|
||||
"userPassMustBeNotEmpty" = "Новое имя пользователя и новый пароль должны быть заполнены"
|
||||
|
||||
[pages.xray]
|
||||
"title" = "Xray Настройки"
|
||||
"save" = "Сохранить настройки"
|
||||
"restart" = "Перезапустить рентген"
|
||||
"basicTemplate" = "Базовые шаблоны"
|
||||
"advancedTemplate" = "Расширенные шаблоны"
|
||||
"completeTemplate" = "Итоговый шаблон"
|
||||
"generalConfigs" = "Основные настройки"
|
||||
"generalConfigsDesc" = "Общие настройки"
|
||||
"blockConfigs" = "Блокирующие конфигурации"
|
||||
@@ -336,26 +354,40 @@
|
||||
"GoogleIPv4Desc" = "Применить маршрутизацию Google для подключения к IPv4."
|
||||
"NetflixIPv4" = "Использовать IPv4 для Netflix"
|
||||
"NetflixIPv4Desc" = "Применить маршрутизацию Netflix для подключения к IPv4."
|
||||
"Inbounds" = "Конфигурация подключений"
|
||||
"InboundsDesc" = "Изменение шаблона конфигурации, для подключения определенных пользователей."
|
||||
"Outbounds" = "Конфигурация исходящих"
|
||||
"OutboundsDesc" = "Изменение шаблона конфигурации, чтобы определить исходящие пути для этого сервера."
|
||||
"Routings" = "Настройка правил маршрутизации"
|
||||
"RoutingsDesc" = "Изменение шаблона конфигурации, для определения правил маршрутизации для этого сервера."
|
||||
"manualLists" = "Пользовательские списки"
|
||||
"manualListsDesc" = "Пожалуйста, используйте формат массива JSON"
|
||||
"manualBlockedIPs" = "Список заблокированных IP-адресов"
|
||||
"manualBlockedDomains" = "Список заблокированных доменов"
|
||||
"manualDirectIPs" = "Список прямых IP-адресов"
|
||||
"manualDirectDomains" = "Список прямых доменов"
|
||||
"manualIPv4Domains" = "Список доменов IPv4"
|
||||
"completeTemplate" = "Все"
|
||||
"Inbounds" = "Входящие"
|
||||
"Outbounds" = "Исходящие"
|
||||
"Routings" = "Правила маршрутизации"
|
||||
"RoutingsDesc" = "Важен приоритет каждого правила!"
|
||||
|
||||
[pages.settings.toasts]
|
||||
"modifySettings" = "Изменение настроек"
|
||||
"getSettings" = "Просмотр настроек"
|
||||
"modifyUser" = "Изменение пользователя "
|
||||
"originalUserPassIncorrect" = "Неверное имя пользователя или пароль"
|
||||
"userPassMustBeNotEmpty" = "Новое имя пользователя и новый пароль должны быть заполнены"
|
||||
[pages.xray.rules]
|
||||
"first" = "Первый"
|
||||
"last" = "Последний"
|
||||
"up" = "Вверх"
|
||||
"down" = "Вниз"
|
||||
"source" = "Источник"
|
||||
"dest" = "Пункт назначения"
|
||||
"inbound" = "Входящий"
|
||||
"outbound" = "Исходящий"
|
||||
"info" = "Информация"
|
||||
"add" = "Добавить правило"
|
||||
"edit" = "Редактировать правило"
|
||||
"useComma" = "Элементы, разделенные запятыми"
|
||||
|
||||
[pages.xray.outbound]
|
||||
"addOutbound" = "Добавить исходящий"
|
||||
"addReverse" = "Добавить реверс"
|
||||
"editOutbound" = "Изменить исходящий"
|
||||
"editReverse" = "Редактировать реверс"
|
||||
"tag" = "Тег"
|
||||
"tagDesc" = "уникальный тег"
|
||||
"address" = "Адрес"
|
||||
"reverse" = "Обратный"
|
||||
"domain" = "Домен"
|
||||
"type" = "Тип"
|
||||
"bridge" = "Мост"
|
||||
"portal" = "Портал"
|
||||
"intercon" = "Соединение"
|
||||
|
||||
[tgbot]
|
||||
"noResult" = "❗ Нет результатов!"
|
||||
|
||||
466
web/translation/translate.vi_VN.toml
Normal file
466
web/translation/translate.vi_VN.toml
Normal file
@@ -0,0 +1,466 @@
|
||||
"username" = "Tên người dùng"
|
||||
"password" = "Mật khẩu"
|
||||
"login" = "Đăng nhập"
|
||||
"confirm" = "Xác nhận"
|
||||
"cancel" = "Hủy bỏ"
|
||||
"close" = "Đóng"
|
||||
"copy" = "Sao chép"
|
||||
"copied" = "Đã sao chép"
|
||||
"download" = "Tải xuống"
|
||||
"remark" = "Ghi chú"
|
||||
"enable" = "Kích hoạt"
|
||||
"protocol" = "Giao thức"
|
||||
"search" = "Tìm kiếm"
|
||||
"filter" = "Bộ lọc"
|
||||
"loading" = "Đang tải..."
|
||||
"second" = "Giây"
|
||||
"minute" = "Phút"
|
||||
"hour" = "Giờ"
|
||||
"day" = "Ngày"
|
||||
"check" = "Kiểm tra"
|
||||
"indefinite" = "Không xác định"
|
||||
"unlimited" = "Không giới hạn"
|
||||
"none" = "Không có"
|
||||
"qrCode" = "Mã QR"
|
||||
"info" = "Thông tin thêm"
|
||||
"edit" = "Chỉnh sửa"
|
||||
"delete" = "Xóa"
|
||||
"reset" = "Đặt lại"
|
||||
"copySuccess" = "Đã sao chép thành công"
|
||||
"sure" = "Chắc chắn"
|
||||
"encryption" = "Mã hóa"
|
||||
"transmission" = "Truyền tải"
|
||||
"host" = "Máy chủ"
|
||||
"path" = "Đường dẫn"
|
||||
"camouflage" = "Ngụy trang"
|
||||
"status" = "Trạng thái"
|
||||
"enabled" = "Đã kích hoạt"
|
||||
"disabled" = "Đã tắt"
|
||||
"depleted" = "Đã Dùng hết"
|
||||
"depletingSoon" = "Sắp dùng hết"
|
||||
"offline" = "Ngoại tuyến"
|
||||
"online" = "Trực tuyến"
|
||||
"domainName" = "Tên miền"
|
||||
"monitor" = "Nghe IP"
|
||||
"certificate" = "Chứng chỉ"
|
||||
"fail" = " Thất bại"
|
||||
"success" = " Thành công"
|
||||
"getVersion" = "Lấy phiên bản"
|
||||
"install" = "Cài đặt"
|
||||
"clients" = "Khách hàng"
|
||||
"usage" = "Sử dụng"
|
||||
"remained" = "Còn lại"
|
||||
"secAlertTitle" = "Cảnh báo an ninh-Tiếng Việt by Ohoang7"
|
||||
"secAlertSsl" = "Kết nối này không an toàn; Vui lòng không nhập thông tin nhạy cảm cho đến khi TLS được kích hoạt để bảo vệ dữ liệu của Bạn"
|
||||
"security" = "Bảo vệ"
|
||||
|
||||
[menu]
|
||||
"dashboard" = "Trạng thái hệ thống"
|
||||
"inbounds" = "Đầu Vào khách hàng"
|
||||
"settings" = "Cài đặt bảng điều khiển"
|
||||
"xray" = "Cài đặt Xray"
|
||||
"logout" = "Đăng xuất"
|
||||
"link" = "sự quản lý"
|
||||
|
||||
[pages.login]
|
||||
"title" = "Đăng nhập"
|
||||
"loginAgain" = "Thời hạn đăng nhập đã hết, Vui lòng đăng nhập lại."
|
||||
|
||||
[pages.login.toasts]
|
||||
"invalidFormData" = "Dạng dữ liệu nhập không hợp lệ."
|
||||
"emptyUsername" = "Vui lòng nhập tên người dùng."
|
||||
"emptyPassword" = "Vui lòng nhập mật khẩu."
|
||||
"wrongUsernameOrPassword" = "Tên người dùng hoặc mật khẩu không đúng."
|
||||
"successLogin" = "Đăng nhập thành công."
|
||||
|
||||
[pages.index]
|
||||
"title" = "Trạng thái hệ thống"
|
||||
"memory" = "Bộ nhớ"
|
||||
"hard" = "Ổ cứng"
|
||||
"xrayStatus" = "Trạng thái của Xray"
|
||||
"stopXray" = "Dừng Xray"
|
||||
"restartXray" = "Khởi động lại Xray"
|
||||
"xraySwitch" = "Chuyển đổi phiên bản"
|
||||
"xraySwitchClick" = "Chọn phiên bản mà bạn muốn chuyển đổi sang."
|
||||
"xraySwitchClickDesk" = "Hãy lựa chọn thận trọng, vì các phiên bản cũ có thể không tương thích với các cấu hình hiện tại, của Bạn"
|
||||
"operationHours" = "Thời gian hoạt động"
|
||||
"operationHoursDesc" = "Thời gian hoạt động của hệ thống: thời gian kể từ khi khởi động."
|
||||
"systemLoad" = "Tải hệ thống"
|
||||
"connectionTcpCountDesc" = "Tổng số kết nối TCP trên tất cả các card mạng."
|
||||
"connectionUdpCountDesc" = "Tổng số kết nối UDP trên tất cả các card mạng."
|
||||
"upSpeed" = "Tổng tốc độ tải lên cho tất cả các thẻ mạng."
|
||||
"downSpeed" = "Tổng tốc độ tải xuống cho tất cả các thẻ mạng."
|
||||
"totalSent" = "Tổng lưu lượng tải lên của tất cả các thẻ mạng kể từ khi khởi động hệ thống."
|
||||
"totalReceive" = "Tổng số dữ liệu tải xuống trên tất cả các thẻ mạng kể từ khi khởi động hệ thống."
|
||||
"xraySwitchVersionDialog" = "Chuyển đổi phiên bản Xray"
|
||||
"xraySwitchVersionDialogDesc" = "Bạn có chắc chắn muốn chuyển phiên bản Xray sang phiên bản khác"
|
||||
"dontRefresh" = "Quá trình cài đặt đang diễn ra, vui lòng không làm mới trang này."
|
||||
"logs" = "Nhật ký"
|
||||
"config" = "Cấu hình"
|
||||
"backup" = "Phục hồi dữ liệu đã lưu"
|
||||
"backupTitle" = "Sao lưu và Khôi phục cơ sở dữ liệu"
|
||||
"backupDescription" = "Bạn hãy nhớ sao lưu trước khi nhập cơ sở dữ liệu mới."
|
||||
"exportDatabase" = "Tải xuống cơ sở dữ liệu"
|
||||
"importDatabase" = "Tải lên cơ sở dữ liệu"
|
||||
|
||||
[pages.inbounds]
|
||||
"title" = "Điểm vào (Inbounds)"
|
||||
"totalDownUp" = "Tổng tải lên/tải xuống"
|
||||
"totalUsage" = "Tổng sử dụng"
|
||||
"inboundCount" = "Số lần vào"
|
||||
"operate" = "Bảng Chọn"
|
||||
"enable" = "Cho phép"
|
||||
"remark" = "Nhận xét"
|
||||
"protocol" = "Giao thức"
|
||||
"port" = "Cổng"
|
||||
"traffic" = "Lưu lượng"
|
||||
"details" = "Chi tiết"
|
||||
"transportConfig" = "Cấu hình vận chuyển"
|
||||
"expireDate" = "Ngày hết hạn"
|
||||
"resetTraffic" = "Đặt lại lưu lượng"
|
||||
"addInbound" = "Thêm điểm vào"
|
||||
"generalActions" = "Hành động chung"
|
||||
"create" = "Tạo mới"
|
||||
"update" = "Cập nhật"
|
||||
"modifyInbound" = "Chỉnh sửa điểm vào (Inbound)"
|
||||
"deleteInbound" = "Xóa điểm vào (Inbound)"
|
||||
"deleteInboundContent" = "Xác nhận xóa điểm vào? (Inbound)"
|
||||
"deleteClient" = "Xóa khách hàng"
|
||||
"deleteClientContent" = "Bạn có chắc chắn muốn xóa khách hàng không?"
|
||||
"resetTrafficContent" = "Bạn có chắc chắn muốn đặt lại lưu lượng truy cập không?"
|
||||
"copyLink" = "Sao chép liên kết"
|
||||
"address" = "Địa chỉ"
|
||||
"network" = "Mạng"
|
||||
"destinationPort" = "Cổng đích"
|
||||
"targetAddress" = "Địa chỉ mục tiêu"
|
||||
"monitorDesc" = "Mặc định để trống"
|
||||
"meansNoLimit" = "Nghĩa là không giới hạn"
|
||||
"totalFlow" = "Tổng lưu lượng"
|
||||
"leaveBlankToNeverExpire" = "Để trống để không bao giờ hết hạn"
|
||||
"noRecommendKeepDefault" = "Không yêu cầu đặc biệt để giữ nguyên cài đặt mặc định"
|
||||
"certificatePath" = "Đường dẫn tập tin chứng chỉ"
|
||||
"certificateContent" = "Nội dung tập tin chứng chỉ"
|
||||
"publicKeyPath" = "Đường dẫn khóa công khai"
|
||||
"publicKeyContent" = "Nội dung khóa công khai"
|
||||
"keyPath" = "Đường dẫn khóa riêng tư"
|
||||
"keyContent" = "Nội dung khóa riêng tư"
|
||||
"clickOnQRcode" = "Nhấn vào Mã QR để sao chép"
|
||||
"client" = "Khách hàng"
|
||||
"export" = "Xuất liên kết"
|
||||
"clone" = "Bản sao"
|
||||
"cloneInbound" = "Nhân bản"
|
||||
"cloneInboundContent" = "Tất cả cài đặt của điểm vào này, ngoại trừ Cổng, IP nghe và Máy khách, sẽ được áp dụng cho bản sao."
|
||||
"cloneInboundOk" = "Bản sao"
|
||||
"resetAllTraffic" = "Đặt lại tất cả lưu lượng truy cập"
|
||||
"resetAllTrafficTitle" = "Đặt lại tất cả lưu lượng truy cập"
|
||||
"resetAllTrafficContent" = "Bạn có chắc chắn muốn đặt lại lưu lượng cho tất cả điểm vào không?"
|
||||
"resetInboundClientTraffics" = "Đặt lại lưu lượng cho các khách hàng của điểm vào"
|
||||
"resetInboundClientTrafficTitle" = "Đặt lại tất cả lưu lượng truy cập của khách hàng"
|
||||
"resetInboundClientTrafficContent" = "Bạn có chắc chắn muốn đặt lại tất cả lưu lượng cho các khách hàng của điểm vào này không?"
|
||||
"resetAllClientTraffics" = "Đặt lại lưu lượng cho tất cả máy khách"
|
||||
"resetAllClientTrafficTitle" = "Đặt lại lưu lượng cho tất cả các khách hàng"
|
||||
"resetAllClientTrafficContent" = "Bạn có chắc chắn muốn đặt lại tất cả lưu lượng cho tất cả khách hàng không?"
|
||||
"delDepletedClients" = "Xóa các máy khách đã cạn kiệt"
|
||||
"delDepletedClientsTitle" = "Xóa các khách khách đã cạn kiệt"
|
||||
"delDepletedClientsContent" = "Bạn có chắc chắn muốn xóa tất cả các máy khách đã cạn kiệt không??"
|
||||
"email" = "Email"
|
||||
"emailDesc" = "vui lòng cung cấp một địa chỉ email duy nhất."
|
||||
"setDefaultCert" = "Đặt chứng chỉ từ bảng điều khiển"
|
||||
"telegramDesc" = "Sử dụng ID Telegram không có @ hoặc ID trò chuyện (bạn có thể lấy nó tại đây @userinfobot hoặc sử dụng lệnh '/id' trong bot)"
|
||||
"subscriptionDesc" = "Bạn có thể tìm thấy liên kết phụ của mình trên Chi tiết, bạn cũng có thể sử dụng cùng tên cho một số cấu hình"
|
||||
"info" = "Thông tin"
|
||||
"same" = "Như nhau"
|
||||
"inboundData" = "Dữ liệu gửi đến"
|
||||
"copyToClipboard" = "Sao chép vào bảng nhớ tạm"
|
||||
"import" = "Nhập"
|
||||
"importInbound" = "Nhập hàng gửi về"
|
||||
|
||||
[pages.client]
|
||||
"add" = "Thêm máy khách"
|
||||
"edit" = "Chỉnh sửa Máy khách"
|
||||
"submitAdd" = "Thêm máy khách"
|
||||
"submitEdit" = "Lưu thay đổi"
|
||||
"clientCount" = "Số lượng khách hàng"
|
||||
"bulk" = "Thêm số lượng lớn"
|
||||
"method" = "Các thức"
|
||||
"first" = "Ban đầu"
|
||||
"last" = "Đến cùng"
|
||||
"prefix" = "Tiền Tố (Được ưu đãi)"
|
||||
"postfix" = "Hậu tố"
|
||||
"delayedStart" = "Bắt đầu sau lần sử dụng đầu tiên"
|
||||
"expireDays" = "Số ngày hết hạn"
|
||||
"days" = "Ngày(s)"
|
||||
"renew" = "Tự động gia hạn"
|
||||
"renewDesc" = "Tự động gia hạn ngày sau khi hết hạn. 0 = tắt"
|
||||
|
||||
[pages.inbounds.toasts]
|
||||
"obtain" = "Nhận được"
|
||||
|
||||
[pages.inbounds.stream.general]
|
||||
"requestHeader" = "Tiêu đề yêu cầu"
|
||||
"name" = "Tên"
|
||||
"value" = "Giá trị"
|
||||
|
||||
[pages.inbounds.stream.tcp]
|
||||
"requestVersion" = "Phiên bản yêu cầu"
|
||||
"requestMethod" = "Phương thức yêu cầu"
|
||||
"requestPath" = "Đường dẫn yêu cầu"
|
||||
"responseVersion" = "Phiên bản phản hồi"
|
||||
"responseStatus" = "Trạng thái phản hồi"
|
||||
"responseStatusDescription" = "Mô tả trạng thái phản hồi"
|
||||
"responseHeader" = "Tiêu đề phản hồi"
|
||||
|
||||
[pages.inbounds.stream.quic]
|
||||
"encryption" = "Mã hóa"
|
||||
|
||||
[pages.settings]
|
||||
"title" = "Cài đặt"
|
||||
"save" = "Lưu"
|
||||
"infoDesc" = "Mọi thay đổi được thực hiện ở đây cần phải được lưu. Vui lòng khởi động lại bảng điều khiển để áp dụng các thay đổi. bản dich bởi Ohoang7"
|
||||
"restartPanel" = "Khởi động lại Bảng điều khiển"
|
||||
"restartPanelDesc" = "Bạn có chắc chắn muốn khởi động lại bảng điều khiển? Nhấn OK để khởi động lại sau 3 giây. Nếu bạn không thể truy cập bảng điều khiển sau khi khởi động lại, vui lòng xem thông tin nhật ký của bảng điều khiển trên máy chủ."
|
||||
"resetDefaultConfig" = "Đặt lại Cấu hình Mặc định"
|
||||
"panelConfig" = "Cấu hình bảng điều khiển"
|
||||
"userSettings" = "Thiết lập người dùng"
|
||||
"TGBotSettings" = "Cài đặt Bot Telegram"
|
||||
"panelListeningIP" = "IP Nghe của Bảng điều khiển"
|
||||
"panelListeningIPDesc" = "Mặc định để trống để nghe tất cả các IP."
|
||||
"panelListeningDomain" = "Tên miền của nghe Bảng điều khiển"
|
||||
"panelListeningDomainDesc" = "Mặc định để trống để nghe tất cả các tên miền và IP"
|
||||
"panelPort" = "Cổng Bảng điều khiển"
|
||||
"panelPortDesc" = "Cổng được sử dụng để hiển thị bảng điều khiển này"
|
||||
"publicKeyPath" = "Đường dẫn tập tin khóa công khai Chứng chỉ Bảng điều khiển"
|
||||
"publicKeyPathDesc" = "Điền vào đường dẫn tuyệt đối bắt đầu với."
|
||||
"privateKeyPath" = "Đường dẫn tập tin khóa riêng tư Chứng chỉ Bảng điều khiển"
|
||||
"privateKeyPathDesc" = "Điền vào đường dẫn tuyệt đối bắt đầu với."
|
||||
"panelUrlPath" = "Đường dẫn gốc URL Bảng điều khiển"
|
||||
"panelUrlPathDesc" = "Phải bắt đầu bằng '/' và kết thúc bằng."
|
||||
"pageSize" = "Kích thước phân trang"
|
||||
"pageSizeDesc" = "Xác định kích thước trang cho bảng gửi đến. Đặt 0 để tắt"
|
||||
"remarkModel" = "Ghi chú mô hình và ký tự phân tách"
|
||||
"sampleRemark" = "Nhận xét mẫu"
|
||||
"oldUsername" = "Tên người dùng hiện tại"
|
||||
"currentPassword" = "Mật khẩu hiện tại"
|
||||
"newUsername" = "Tên người dùng mới"
|
||||
"newPassword" = "Mật khẩu mới"
|
||||
"telegramBotEnable" = "Bật Bot Telegram"
|
||||
"telegramBotEnableDesc" = "Kết nối với các tính năng của bảng điều khiển này thông qua bot Telegram"
|
||||
"telegramToken" = "Token Telegram"
|
||||
"telegramTokenDesc" = "Bạn phải nhận token từ quản lý bot Telegram @botfather"
|
||||
"telegramChatId" = "Chat ID Telegram của quản trị viên"
|
||||
"telegramChatIdDesc" = "Nhiều Chat ID phân tách bằng dấu phẩy. Sử dụng @userinfobot hoặc sử dụng lệnh '/id' trong bot để lấy Chat ID của bạn."
|
||||
"telegramNotifyTime" = "Thời gian thông báo của bot Telegram"
|
||||
"telegramNotifyTimeDesc" = "Sử dụng định dạng thời gian Crontab."
|
||||
"tgNotifyBackup" = "Sao lưu Cơ sở dữ liệu"
|
||||
"tgNotifyBackupDesc" = "Bao gồm tệp sao lưu cơ sở dữ liệu với thông báo báo cáo."
|
||||
"tgNotifyLogin" = "Thông báo Đăng nhập"
|
||||
"tgNotifyLoginDesc" = "Hiển thị tên người dùng, địa chỉ IP và thời gian khi ai đó cố gắng đăng nhập vào bảng điều khiển của bạn."
|
||||
"sessionMaxAge" = "Tuổi tối đa của phiên"
|
||||
"sessionMaxAgeDesc" = "Thời gian của phiên đăng nhập (đơn vị: phút)"
|
||||
"expireTimeDiff" = "Ngưỡng hết hạn cho thông báo"
|
||||
"expireTimeDiffDesc" = "Nhận thông báo về việc hết hạn tài khoản trước ngưỡng này (đơn vị: ngày)"
|
||||
"trafficDiff" = "Ngưỡng lưu lượng cho thông báo"
|
||||
"trafficDiffDesc" = "Nhận thông báo về việc cạn kiệt lưu lượng trước khi đạt đến ngưỡng này (đơn vị: GB)"
|
||||
"tgNotifyCpu" = "Ngưỡng cảnh báo tỷ lệ CPU"
|
||||
"tgNotifyCpuDesc" = "Nhận thông báo nếu tỷ lệ sử dụng CPU vượt quá ngưỡng này (đơn vị: %)"
|
||||
"timeZone" = "Múi giờ"
|
||||
"timeZoneDesc" = "Các tác vụ được lên lịch chạy theo thời gian trong múi giờ này."
|
||||
"subSettings" = "Đăng ký"
|
||||
"subEnable" = "Bật dịch vụ"
|
||||
"subEnableDesc" = "Tính năng đăng ký với cấu hình riêng"
|
||||
"subListen" = "Listening IP"
|
||||
"subListenDesc" = "Mặc định để trống để nghe tất cả các IP"
|
||||
"subPort" = "Cổng Đăng ký"
|
||||
"subPortDesc" = "Số cổng dịch vụ đăng ký phải chưa được sử dụng trên máy chủ"
|
||||
"subCertPath" = "Đường dẫn tập tin khóa công khai Chứng chỉ Đăng ký"
|
||||
"subCertPathDesc" = "Điền vào đường dẫn tuyệt đối bắt đầu với '/'"
|
||||
"subKeyPath" = "Đường dẫn tập tin khóa riêng tư Chứng chỉ Đăng ký"
|
||||
"subKeyPathDesc" = "Điền vào đường dẫn tuyệt đối bắt đầu với '/'"
|
||||
"subPath" = "Đường dẫn gốc URL Đăng ký"
|
||||
"subPathDesc" = "Phải bắt đầu bằng '/' và kết thúc bằng '/'"
|
||||
"subDomain" = "Tên miền con"
|
||||
"subDomainDesc" = "Mặc định để trống để nghe tất cả các tên miền và IP"
|
||||
"subUpdates" = "Khoảng thời gian cập nhật đăng ký"
|
||||
"subUpdatesDesc" = "Số giờ giữa các cập nhật trong ứng dụng khách"
|
||||
"subEncrypt" = "Mã hóa cấu hình"
|
||||
"subEncryptDesc" = "Mã hóa các cấu hình được trả về trong đăng ký"
|
||||
"subShowInfo" = "Hiển thị thông tin sử dụng"
|
||||
"subShowInfoDesc" = "Hiển thị lưu lượng truy cập còn lại và ngày sau tên cấu hình"
|
||||
"subURI" = "URI proxy ngược"
|
||||
"subURIDesc" = "Thay đổi URI cơ sở của URL đăng ký để sử dụng ở phía sau proxy"
|
||||
|
||||
[pages.settings.toasts]
|
||||
"modifySettings" = "Sửa đổi cài đặt"
|
||||
"getSettings" = "Nhận cài đặt "
|
||||
"modifyUser" = "Sửa đổi người dùng"
|
||||
"originalUserPassIncorrect" = "Tên người dùng hoặc mật khẩu ban đầu không chính xác"
|
||||
"userPassMustBeNotEmpty" = "Tên người dùng mới và mật khẩu mới không được để trống"
|
||||
|
||||
[pages.xray]
|
||||
"title" = "Cài đặt Xray"
|
||||
"save" = "Lưu các thiết lập"
|
||||
"restart" = "Khởi động lại Xray"
|
||||
"basicTemplate" = "Mẫu cơ bản"
|
||||
"advancedTemplate" = "Mẫu nâng cao"
|
||||
"generalConfigs" = "Cấu hình Chung"
|
||||
"generalConfigsDesc" = "Những tùy chọn này sẽ cung cấp điều chỉnh tổng quát."
|
||||
"blockConfigs" = "Cấu hình Chặn"
|
||||
"blockConfigsDesc" = "Những tùy chọn này sẽ ngăn người dùng kết nối đến các giao thức và trang web cụ thể."
|
||||
"blockCountryConfigs" = "Cấu hình Chặn Quốc gia"
|
||||
"blockCountryConfigsDesc" = "Những tùy chọn này sẽ ngăn người dùng kết nối đến các tên miền quốc gia cụ thể."
|
||||
"directCountryConfigs" = "Cấu hình Kết nối Trực tiếp Quốc gia"
|
||||
"directCountryConfigsDesc" = "Những tùy chọn này sẽ kết nối người dùng trực tiếp đến các tên miền quốc gia cụ thể."
|
||||
"ipv4Configs" = "Cấu hình IPv4"
|
||||
"ipv4ConfigsDesc" = "Những tùy chọn này sẽ chỉ định kết nối đến các tên miền mục tiêu qua IPv4."
|
||||
"Template" = "Mẫu cấu hình Xray"
|
||||
"TemplateDesc" = "Tạo tệp cấu hình Xray cuối cùng dựa trên mẫu này."
|
||||
"FreedomStrategy" = "Cấu hình chiến lược cho giao thức tự do"
|
||||
"FreedomStrategyDesc" = "Đặt chiến lược đầu ra của mạng trong Giao thức Tự do."
|
||||
"RoutingStrategy" = "Định cấu hình chiến lược định tuyến tên miền"
|
||||
"RoutingStrategyDesc" = "Đặt chiến lược định tuyến tổng thể để phân giải DNS."
|
||||
"Torrent" = "Cấm sử dụng BitTorrent"
|
||||
"TorrentDesc" = "Thay đổi mẫu cấu hình để tránh việc người dùng sử dụng BitTorrent."
|
||||
"PrivateIp" = "Cấm dãy IP riêng để kết nối"
|
||||
"PrivateIpDesc" = "Thay đổi mẫu cấu hình để tránh kết nối với dải IP riêng."
|
||||
"Ads" = "Chặn quảng cáo"
|
||||
"AdsDesc" = "Thay đổi mẫu cấu hình để chặn quảng cáo"
|
||||
"Family" = "Kích hoạt cấu hình thân thiện với gia đình"
|
||||
"FamilyDesc" = "Tránh kết nối đến các trang web không an toàn để bảo vệ gia đình."
|
||||
"IRIp" = "Vô hiệu hóa kết nối với dải IP Iran"
|
||||
"IRIpDesc" = "Thay đổi mẫu cấu hình để tránh kết nối với dãy IP Iran."
|
||||
"IRDomain" = "Vô hiệu hóa kết nối với tên miền Iran"
|
||||
"IRDomainDesc" = "Thay đổi mẫu cấu hình để tránh kết nối với tên miền Iran."
|
||||
"ChinaIp" = "Vô hiệu hóa kết nối với dải IP Trung Quốc"
|
||||
"ChinaIpDesc" = "Thay đổi mẫu cấu hình để tránh kết nối tới dãy IP Trung Quốc."
|
||||
"ChinaDomain" = "Vô hiệu hóa kết nối với tên miền Trung Quốc"
|
||||
"ChinaDomainDesc" = "Thay đổi mẫu cấu hình để tránh kết nối với miền Trung Quốc."
|
||||
"RussiaIp" = "Vô hiệu hóa kết nối với dải IP của Nga"
|
||||
"RussiaIpDesc" = "Thay đổi mẫu cấu hình để tránh kết nối với dãy IP của Nga."
|
||||
"RussiaDomain" = "Vô hiệu hóa kết nối với tên miền của Nga"
|
||||
"RussiaDomainDesc" = "Thay đổi mẫu cấu hình để tránh kết nối với miền Nga."
|
||||
"DirectIRIp" = "Kết nối trực tiếp tới dãy IP Iran"
|
||||
"DirectIRIpDesc" = "Thay đổi mẫu cấu hình để kết nối trực tiếp với dải IP Iran."
|
||||
"DirectIRDomain" = "Kết nối trực tiếp tới các miền của Iran"
|
||||
"DirectIRDomainDesc" = "Thay đổi mẫu cấu hình để kết nối trực tiếp với miền Iran."
|
||||
"DirectChinaIp" = "Thay đổi mẫu cấu hình để kết nối trực tiếp với miền Iran."
|
||||
"DirectChinaIpDesc" = "Thay đổi mẫu cấu hình để kết nối trực tiếp với dải IP Trung Quốc."
|
||||
"DirectChinaDomain" = "Kết nối trực tiếp tới các miền Trung Quốc"
|
||||
"DirectChinaDomainDesc" = "Kết nối trực tiếp tới các miền Trung Quốc"
|
||||
"DirectRussiaIp" = "Kết nối trực tiếp tới dãy IP của Nga"
|
||||
"DirectRussiaIpDesc" = "Thay đổi mẫu cấu hình để kết nối trực tiếp với dải IP của Nga."
|
||||
"DirectRussiaDomain" = "Kết nối trực tiếp tới các miền của Nga"
|
||||
"DirectRussiaDomainDesc" = "Thay đổi mẫu cấu hình để kết nối trực tiếp với miền Nga."
|
||||
"GoogleIPv4" = "Sử dụng IPv4 cho Google"
|
||||
"GoogleIPv4Desc" = "Thêm định tuyến để Google kết nối với IPv4."
|
||||
"NetflixIPv4" = "Sử dụng IPv4 cho Netflix"
|
||||
"NetflixIPv4Desc" = "Thêm định tuyến cho Netflix để kết nối với IPv4."
|
||||
"completeTemplate" = "Tất cả"
|
||||
"Inbounds" = "Đầu vào"
|
||||
"Outbounds" = "Đầu ra"
|
||||
"Routings" = "Quy tắc định tuyến"
|
||||
"RoutingsDesc" = "Mức độ ưu tiên của mỗi quy tắc là quan trọng!"
|
||||
|
||||
[pages.xray.rules]
|
||||
"first" = "Đầu tiên"
|
||||
"last" = "Cuối cùng"
|
||||
"up" = "hướng lên"
|
||||
"down" = "Xuống"
|
||||
"source" = "Nguồn"
|
||||
"dest" = "Điểm đến"
|
||||
"inbound" = "Đầu vào"
|
||||
"outbound" = "Đầu ra"
|
||||
"info" = "Thông tin"
|
||||
"add" = "Thêm quy tắc"
|
||||
"edit" = "Chỉnh sửa quy tắc"
|
||||
"useComma" = "Các mục được phân tách bằng dấu phẩy"
|
||||
|
||||
[pages.xray.outbound]
|
||||
"addOutbound" = "Thêm Đầu vào"
|
||||
"addReverse" = "Thêm đảo ngược"
|
||||
"editOutbound" = "Chỉnh sửa đầu vào"
|
||||
"editReverse" = "Chỉnh sửa đảo ngược"
|
||||
"tag" = "Nhãn"
|
||||
"tagDesc" = "thẻ duy nhất"
|
||||
"address" = "Địa chỉ"
|
||||
"reverse" = "Đảo ngược"
|
||||
"domain" = "Tên Miền"
|
||||
"type" = "Kiểu"
|
||||
"bridge" = "Liên kết"
|
||||
"portal" = "Cổng thông tin"
|
||||
"intercon" = "Kết nối"
|
||||
|
||||
[tgbot]
|
||||
"noResult" = "❗ Không có kết quả!"
|
||||
"wentWrong" = "❌ Đã xảy ra lỗi!"
|
||||
"noInbounds" = "❗ Không tìm thấy inbound!"
|
||||
"unlimited" = "♾ Không giới hạn"
|
||||
"day" = "Ngày"
|
||||
"days" = "hôm nay"
|
||||
"unknown" = "không xác định"
|
||||
"inbounds" = "Đầu vào"
|
||||
"clients" = "Khách hàng"
|
||||
|
||||
[tgbot.commands]
|
||||
"unknown" = "❗ Lệnh không rõ"
|
||||
"pleaseChoose" = "👇 Vui lòng chọn:\r\n"
|
||||
"help" = "🤖 Chào mừng bạn đến với bot này! Bot được thiết kế để cung cấp cho bạn dữ liệu cụ thể từ máy chủ và cho phép bạn thực hiện các thay đổi cần thiết.\r\n\r\n"
|
||||
"start" = "👋 Xin chào <i>{{ .Firstname }}</i>.\r\n"
|
||||
"welcome" = "🤖 Chào mừng đến với bot quản lý của <b>{{ .Hostname }}</b>.\r\n"
|
||||
"status" = "✅ Bot hoạt động bình thường!"
|
||||
"usage" = "❗ Vui lòng cung cấp văn bản để tìm kiếm!"
|
||||
"getID" = "🆔 ID của bạn: <code>{{ .ID }}</code>"
|
||||
"helpAdminCommands" = "Tìm kiếm email của khách hàng:\r\n<code>/usage [Email]</code>\r\n \r\nTìm kiếm inbounds (với thống kê của khách hàng):\r\n<code>/inbound [Ghi chú]</code>"
|
||||
"helpClientCommands" = "Để tìm kiếm thống kê, hãy sử dụng lệnh sau:\r\n \r\n<code>/usage [UUID|Mật khẩu]</code>\r\n \r\nSử dụng UUID cho vmess/vless và Mật khẩu cho Trojan."
|
||||
|
||||
[tgbot.messages]
|
||||
"cpuThreshold" = "🔴 Sử dụng CPU {{ .Percent }}% vượt quá ngưỡng {{ .Threshold }}%"
|
||||
"loginSuccess" = "✅ Đăng nhập thành công vào bảng điều khiển.\r\n"
|
||||
"loginFailed" = "❗️ Đăng nhập vào bảng không thành công.\r\n"
|
||||
"report" = "🕰 Báo cáo theo lịch trình: {{ .RunTime }}\r\n"
|
||||
"datetime" = "⏰ Ngày-Giờ: {{ .DateTime }}\r\n"
|
||||
"hostname" = "💻 Tên máy chủ: {{ .Hostname }}\r\n"
|
||||
"version" = "🚀 Phiên bản X-UI: {{ .Version }}\r\n"
|
||||
"ipv6" = "🌐 IPv6: {{ .IPv6 }}\r\n"
|
||||
"ipv4" = "🌐 IPv4: {{ .IPv4 }}\r\n"
|
||||
"ip" = "🌐 IP: {{ .IP }}\r\n"
|
||||
"serverUpTime" = "⏳ Thời gian hoạt động của máy chủ: {{ .UpTime }} {{ .Unit }}\r\n"
|
||||
"serverLoad" = "📈 Tải máy chủ: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n"
|
||||
"serverMemory" = "📋 Bộ nhớ máy chủ: {{ .Current }}/{{ .Total }}\r\n"
|
||||
"tcpCount" = "🔹 TcpCount: {{ .Count }}\r\n"
|
||||
"udpCount" = "🔸 UdpCount: {{ .Count }}\r\n"
|
||||
"traffic" = "🚦 Giao thông: {{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n"
|
||||
"xrayStatus" = "ℹ️ Tình trạng Xray: {{ .State }}\r\n"
|
||||
"username" = "👤 Tên tài khoản: {{ .Username }}\r\n"
|
||||
"time" = "⏰ Thời gian: {{ .Time }}\r\n"
|
||||
"inbound" = "📍 Điểm vào: {{ .Remark }}\r\n"
|
||||
"port" = "🔌 Cổng: {{ .Port }}\r\n"
|
||||
"expire" = "📅 Hạn sử dụng: {{ .DateTime }}\r\n \r\n"
|
||||
"expireIn" = "📅 Hết hạn vào: {{ .Time }}\r\n \r\n"
|
||||
"active" = "💡 Có hiệu lực {{ .Enable }}\r\n"
|
||||
"online" = "🌐 Tình trạng kết nối: {{ .Status }}\r\n"
|
||||
"email" = "📧 Email: {{ .Email }}\r\n"
|
||||
"upload" = "🔼 Tải lên↑: {{ .Upload }}\r\n"
|
||||
"download" = "🔽 Tải xuống↓: {{ .Download }}\r\n"
|
||||
"total" = "🔄 Tổng cộng: {{ .UpDown }} / {{ .Total }}\r\n"
|
||||
"exhaustedMsg" = "🚨 Đã cạn kiệt {{ .Type }}:\r\n"
|
||||
"exhaustedCount" = "🚨 Đã cạn kiệt {{ .Type }} count:\r\n"
|
||||
"onlinesCount" = "🌐 Số lượng khách hàng trực tuyến: {{ .Count }}\r\n"
|
||||
"disabled" = "🛑 Cấm sử dụng {{ .Disabled }}\r\n"
|
||||
"depleteSoon" = "🔜 Sớm cạn kiệt: {{ .Deplete }}\r\n \r\n"
|
||||
"backupTime" = "🗄 Thời gian sao lưu: {{ .Time }}\r\n"
|
||||
"yes" = "✅ Yes"
|
||||
"no" = "❌ No"
|
||||
|
||||
[tgbot.buttons]
|
||||
"dbBackup" = "Tải Backup DB"
|
||||
"serverUsage" = "Sử Dụng Máy Chủ"
|
||||
"getInbounds" = "Lấy Inbounds"
|
||||
"depleteSoon" = "Sắp Cạn Kiệt"
|
||||
"clientUsage" = "Lấy Sử Dụng"
|
||||
"onlines" = "Khách hàng trực tuyến"
|
||||
"commands" = "Lệnh"
|
||||
|
||||
[tgbot.answers]
|
||||
"getInboundsFailed" = "❌ Không vào được"
|
||||
"askToAddUser" = "Không tìm thấy cấu hình của bạn!\r\nBạn nên định cấu hình tên người dùng telegram của mình và yêu cầu Quản trị viên thêm nó vào cấu hình của bạn."
|
||||
"askToAddUserName" = "Không tìm thấy cấu hình của bạn!\r\nVui lòng yêu cầu Quản trị viên của bạn sử dụng tên người dùng telegram trong cấu hình của bạn(s).\r\n\r\nTên người dùng của bạn: <b>@{{ .TgUserName }}</b>"
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user