Pārlūkot izejas kodu

new frontend and mobile view #1286

Alireza Ahmadi 1 gadu atpakaļ
vecāks
revīzija
729d8549e2

+ 13 - 0
web/assets/js/model/models.js

@@ -141,6 +141,19 @@ class DBInbound {
         return Inbound.fromJson(config);
     }
 
+    isMultiUser() {
+        switch (this.protocol) {
+            case Protocols.VMESS:
+            case Protocols.VLESS:
+            case Protocols.TROJAN:
+                return true;
+            case Protocols.SHADOWSOCKS:
+                return this.toInbound().isSSMultiUser;
+            default:
+                return false;
+        }
+    }
+
     hasLink() {
         switch (this.protocol) {
             case Protocols.VMESS:

+ 37 - 11
web/assets/js/util/common.js

@@ -52,13 +52,15 @@ function safeBase64(str) {
 
 function formatSecond(second) {
     if (second < 60) {
-        return second.toFixed(0) + ' s';
+        return second.toFixed(0) + 's';
     } else if (second < 3600) {
-        return (second / 60).toFixed(0) + ' m';
+        return (second / 60).toFixed(0) + 'm';
     } else if (second < 3600 * 24) {
-        return (second / 3600).toFixed(0) + ' h';
+        return (second / 3600).toFixed(0) + 'h';
     } else {
-        return (second / 3600 / 24).toFixed(0) + ' d';
+        day = Math.floor(second / 3600 / 24);
+        remain = ((second/3600) - (day*24)).toFixed(0);
+        return day + 'd' + (remain > 0 ? ' ' + remain + 'h' : '');
     }
 }
 
@@ -72,7 +74,7 @@ function addZero(num) {
 
 function toFixed(num, n) {
     n = Math.pow(10, n);
-    return Math.round(num * n) / n;
+    return Math.floor(num * n) / n;
 }
 
 function debounce(fn, delay) {
@@ -115,15 +117,39 @@ function setCookie(cname, cvalue, exdays) {
 function usageColor(data, threshold, total) {
     switch (true) {
         case data === null:
-            return 'blue';
-        case total <= 0:
-            return 'blue';
+            return "green";
+        case total < 0:
+            return "blue";
+        case total == 0:
+            return "purple";
         case data < total - threshold:
-            return 'cyan';
+            return "blue";
         case data < total:
-            return 'orange';
+            return "orange";
         default:
-            return 'red';
+            return "red";
+    }
+}
+
+function userExpiryColor(threshold, client, isDark = false) {
+    if (!client.enable) {
+        return isDark ? '#2c3950' : '#bcbcbc';
+    }
+    now = new Date().getTime(),
+    expiry = client.expiryTime;
+    switch (true) {
+        case expiry === null:
+            return "#389e0d";
+        case expiry < 0:
+            return "#0e49b5";
+        case expiry == 0:
+            return "#7a316f";
+        case now < expiry - threshold:
+            return "#0e49b5";
+        case now < expiry:
+            return "#ffa031";
+        default:
+            return "#e04141";
     }
 }
 

+ 1 - 1
web/html/common/prompt_modal.html

@@ -2,7 +2,7 @@
 <a-modal id="prompt-modal" v-model="promptModal.visible" :title="promptModal.title"
          :closable="true" @ok="promptModal.ok" :mask-closable="false"
          :class="themeSwitcher.darkCardClass"
-         :ok-text="promptModal.okText" cancel-text='{{ i18n "cancel" }}'>
+         :ok-text="promptModal.okText" cancel-text='{{ i18n "cancel" }}' :class="themeSwitcher.currentTheme">
     <a-input id="prompt-modal-input" :type="promptModal.type"
              v-model="promptModal.value"
              :autosize="{minRows: 10, maxRows: 20}"

+ 2 - 2
web/html/common/qrcode_modal.html

@@ -1,7 +1,7 @@
 {{define "qrcodeModal"}}
 <a-modal id="qrcode-modal" v-model="qrModal.visible" :title="qrModal.title"
          :closable="true"
-         :class="themeSwitcher.darkCardClass"
+         :class="themeSwitcher.currentTheme"
          :footer="null"
          width="300px">
     <a-tag color="green" style="margin-bottom: 10px;display: block;text-align: center;">
@@ -13,7 +13,7 @@
     </template>
     <a-divider>{{ i18n "pages.inbounds.client" }}</a-divider>
     <template v-for="(row, index) in qrModal.qrcodes">
-        <a-tag color="orange" style="margin-top: 10px;display: block;text-align: center;">[[ row.remark ]]</a-tag>
+        <a-tag color="green" style="margin: 10px 0; display: block; text-align: center;">[[ row.remark ]]</a-tag>
         <canvas @click="copyToClipboard('qrCode-'+index, row.link)" :id="'qrCode-'+index" style="width: 100%; height: 100%;"></canvas>
     </template>
 </a-modal>

+ 1 - 1
web/html/common/text_modal.html

@@ -1,7 +1,7 @@
 {{define "textModal"}}
 <a-modal id="text-modal" v-model="txtModal.visible" :title="txtModal.title"
          :closable="true" ok-text='{{ i18n "copy" }}' cancel-text='{{ i18n "close" }}'
-         :class="themeSwitcher.darkCardClass"
+         :class="themeSwitcher.currentTheme"
          :ok-button-props="{attrs:{id:'txt-modal-ok-btn'}}">
     <a-button v-if="!ObjectUtil.isEmpty(txtModal.fileName)" type="primary" style="margin-bottom: 10px;"
               :href="'data:application/text;charset=utf-8,' + encodeURIComponent(txtModal.content)"

+ 4 - 4
web/html/xui/client_bulk_modal.html

@@ -1,11 +1,11 @@
 {{define "clientsBulkModal"}}
 <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"
-         :class="themeSwitcher.darkCardClass"
+         :class="themeSwitcher.currentTheme"
          :ok-text="clientsBulkModal.okText" cancel-text='{{ i18n "close" }}'>
     <a-form layout="inline">
         <a-form-item label='{{ i18n "pages.client.method" }}'>
-            <a-select v-model="clientsBulkModal.emailMethod" buttonStyle="solid" style="width: 350px" :dropdown-class-name="themeSwitcher.darkCardClass">
+            <a-select v-model="clientsBulkModal.emailMethod" buttonStyle="solid" style="width: 350px" :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>
@@ -72,13 +72,13 @@
         </a-form-item>
         <br>
         <a-form-item v-if="clientsBulkModal.inbound.xtls" label="Flow">
-            <a-select v-model="clientsBulkModal.flow" style="width: 200px" :dropdown-class-name="themeSwitcher.darkCardClass">
+            <a-select v-model="clientsBulkModal.flow" style="width: 200px" :dropdown-class-name="themeSwitcher.currentTheme">
                 <a-select-option value="">{{ i18n "none" }}</a-select-option>
                 <a-select-option v-for="key in XTLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
             </a-select>
         </a-form-item>
         <a-form-item v-if="clientsBulkModal.inbound.canEnableTlsFlow()" label="Flow" layout="inline">
-            <a-select v-model="clientsBulkModal.flow" style="width: 200px" :dropdown-class-name="themeSwitcher.darkCardClass">
+            <a-select v-model="clientsBulkModal.flow" style="width: 200px" :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>

+ 5 - 2
web/html/xui/client_modal.html

@@ -1,8 +1,11 @@
 {{define "clientsModal"}}
 <a-modal id="client-modal" v-model="clientModal.visible" :title="clientModal.title" @ok="clientModal.ok"
          :confirm-loading="clientModal.confirmLoading" :closable="true" :mask-closable="false"
-         :class="themeSwitcher.darkCardClass"
+         :class="themeSwitcher.currentTheme"
          :ok-text="clientModal.okText" cancel-text='{{ i18n "close" }}'>
+    <template v-if="isEdit">
+        <a-tag v-if="isExpiry || isTrafficExhausted" color="red" style="margin-bottom: 10px;display: block;text-align: center;">Account is (Expired|Traffic Ended) And Disabled</a-tag>
+    </template>
     {{template "form/client"}}
 </a-modal>
 <script>
@@ -151,7 +154,7 @@
                 this.$confirm({
                     title: '{{ i18n "pages.inbounds.resetTraffic"}}',
                     content: '{{ i18n "pages.inbounds.resetTrafficContent"}}',
-                    class: themeSwitcher.darkCardClass,
+                    class: themeSwitcher.currentTheme,
                     okText: '{{ i18n "reset"}}',
                     cancelText: '{{ i18n "cancel"}}',
                     onOk: async () => {

+ 7 - 3
web/html/xui/common_sider.html

@@ -11,6 +11,10 @@
     <a-icon type="setting"></a-icon>
     <span>{{ i18n "menu.settings"}}</span>
 </a-menu-item>
+<a-menu-item key="{{ .base_path }}xui/xray">
+    <a-icon type="tool"></a-icon>
+    <span>{{ i18n "menu.xray"}}</span>
+</a-menu-item>
 <!--<a-menu-item key="{{ .base_path }}panel/clients">-->
 <!--    <a-icon type="laptop"></a-icon>-->
 <!--    <span>Client</span>-->
@@ -26,7 +30,7 @@
 <a-layout-sider :theme="themeSwitcher.currentTheme" id="sider" collapsible breakpoint="md" collapsed-width="0">
     <a-menu :theme="themeSwitcher.currentTheme" mode="inline" selected-keys="">
         <a-menu-item mode="inline">
-            <a-icon type="bg-colors"></a-icon>
+            <a-icon type="bulb" :theme="themeSwitcher.isDarkTheme ? 'filled' : 'outlined'"></a-icon>
             <theme-switch />
         </a-menu-item>
     </a-menu>
@@ -38,14 +42,14 @@
 <a-drawer id="sider-drawer" placement="left" :closable="false"
           @close="siderDrawer.close()"
           :visible="siderDrawer.visible"
-          :wrap-class-name="themeSwitcher.darkDrawerClass"
+          :wrap-class-name="themeSwitcher.currentTheme"
           :wrap-style="{ padding: 0 }">
     <div class="drawer-handle" @click="siderDrawer.change()" slot="handle">
         <a-icon :type="siderDrawer.visible ? 'close' : 'menu-fold'"></a-icon>
     </div>
     <a-menu :theme="themeSwitcher.currentTheme" mode="inline" selected-keys="">
         <a-menu-item mode="inline">
-            <a-icon type="bg-colors"></a-icon>
+            <a-icon type="bulb" :theme="themeSwitcher.isDarkTheme ? 'filled' : 'outlined'"></a-icon>
             <theme-switch />
         </a-menu-item>
     </a-menu>

+ 2 - 2
web/html/xui/component/password.html

@@ -4,12 +4,12 @@
             :placeholder="placeholder"
             @input="$emit('input', $event.target.value)">
         <template v-if="icon" #prefix>
-            <a-icon :type="icon" :style="'font-size: 16px;' + themeSwitcher.textStyle" />
+            <a-icon :type="icon" style="font-size: 16px;" />
         </template>
         <template #addonAfter>
             <a-icon :type="showPassword ? 'eye-invisible' : 'eye'"
                     @click="toggleShowPassword"
-                    :style="'font-size: 16px;' + themeSwitcher.textStyle" />
+                    style="font-size: 16px;" />
         </template>
     </a-input>
 </template>

+ 1 - 25
web/html/xui/component/themeSwitch.html

@@ -1,8 +1,6 @@
 {{define "component/themeSwitchTemplate"}}
 <template>
-  <a-switch :default-checked="themeSwitcher.isDarkTheme"
-            checked-children="☀"
-            un-checked-children="🌙"
+  <a-switch size="small" :default-checked="themeSwitcher.isDarkTheme"
             @change="themeSwitcher.toggleTheme()">
   </a-switch>
 </template>
@@ -10,39 +8,17 @@
 
 {{define "component/themeSwitcher"}}
 <script>
-  const colors = {
-    dark: {
-      bg: "#242c3a",
-      text: "hsla(0,0%,100%,.65)"
-    },
-    light: {
-      bg: '#f0f2f5',
-      text: "rgba(0, 0, 0, 0.7)",
-    }
-  }
-
   function createThemeSwitcher() {
     const isDarkTheme = localStorage.getItem('dark-mode') === 'true';
     const theme = isDarkTheme ? 'dark' : 'light';
     return {
       isDarkTheme,
-      bgStyle: `background: ${colors[theme].bg};`,
-      textStyle: `color: ${colors[theme].text};`,
-      darkClass: isDarkTheme ? 'ant-dark' : '',
-      darkCardClass: isDarkTheme ? 'ant-card-dark' : '',
-      darkDrawerClass: isDarkTheme ? 'ant-drawer-dark' : '',
       get currentTheme() {
         return this.isDarkTheme ? 'dark' : 'light';
       },
       toggleTheme() {
         this.isDarkTheme = !this.isDarkTheme;
-        this.theme = this.isDarkTheme ? 'dark' : 'light';
         localStorage.setItem('dark-mode', this.isDarkTheme);
-        this.bgStyle = `background: ${colors[this.theme].bg};`;
-        this.textStyle = `color: ${colors[this.theme].text};`;
-        this.darkClass = this.isDarkTheme ? 'ant-dark' : '';
-        this.darkCardClass = this.isDarkTheme ? 'ant-card-dark' : '';
-        this.darkDrawerClass = this.isDarkTheme ? 'ant-drawer-dark' : '';
       },
     };
   }

+ 18 - 11
web/html/xui/form/client.html

@@ -1,10 +1,5 @@
 {{define "form/client"}}
 <a-form layout="inline" v-if="client">
-    <template v-if="isEdit">
-        <a-tag v-if="isExpiry || isTrafficExhausted" color="red" style="margin-bottom: 10px;display: block;text-align: center;">
-            Account is (Expired|Traffic Ended) And Disabled
-        </a-tag>
-    </template>
     <a-form-item label='{{ i18n "pages.inbounds.enable" }}'>
         <a-switch v-model="client.enable"></a-switch>
     </a-form-item>
@@ -97,13 +92,13 @@
     </a-form-item>
     <br>
     <a-form-item v-if="inbound.xtls" label="Flow">
-        <a-select v-model="client.flow" style="width: 200px" :dropdown-class-name="themeSwitcher.darkCardClass">
+        <a-select v-model="client.flow" style="width: 200px" :dropdown-class-name="themeSwitcher.currentTheme">
             <a-select-option value="">{{ i18n "none" }}</a-select-option>
             <a-select-option v-for="key in XTLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
         </a-select>
     </a-form-item>
     <a-form-item v-else-if="inbound.canEnableTlsFlow()" label="Flow">
-        <a-select v-model="client.flow" style="width: 200px" :dropdown-class-name="themeSwitcher.darkCardClass">
+        <a-select v-model="client.flow" style="width: 200px" :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>
@@ -136,10 +131,10 @@
     </a-form-item>
     <br>
     <a-form-item label='{{ i18n "pages.client.delayedStart" }}'>
-        <a-switch v-model="clientModal.delayedStart" @click="client._expiryTime=0"></a-switch>
+        <a-switch v-model="delayedStart" @click="client._expiryTime=0"></a-switch>
     </a-form-item>
     <br>
-    <a-form-item label='{{ i18n "pages.client.expireDays" }}' v-if="clientModal.delayedStart">
+    <a-form-item label='{{ i18n "pages.client.expireDays" }}' v-if="delayedStart">
         <a-input-number v-model="delayedExpireDays" :min="0"></a-input-number>
     </a-form-item>
     <a-form-item v-else>
@@ -153,9 +148,21 @@
             </a-tooltip>
         </span>
         <a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
-                       :dropdown-class-name="themeSwitcher.darkCardClass"
+                       :dropdown-class-name="themeSwitcher.currentTheme"
                        v-model="client._expiryTime" style="width: 170px;"></a-date-picker>
-        <a-tag color="red" v-if="isExpiry">Expired</a-tag>
+        <a-tag color="red" v-if="isEdit && isExpiry">Expired</a-tag>
+    </a-form-item>
+    <a-form-item v-if="client.expiryTime != 0">
+        <span 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" theme="filled"></a-icon>
+            </a-tooltip>
+        </span>
+        <a-input-number v-model.number="client.reset" :min="0"></a-input-number>
     </a-form-item>
 </a-form>
 {{end}}

+ 2 - 2
web/html/xui/form/inbound.html

@@ -9,7 +9,7 @@
         <a-input v-model.trim="dbInbound.remark"></a-input>
     </a-form-item>
     <a-form-item label='{{ i18n "protocol" }}'>
-        <a-select v-model="inbound.protocol" style="width: 160px;" :disabled="isEdit" :dropdown-class-name="themeSwitcher.darkCardClass">
+        <a-select v-model="inbound.protocol" style="width: 160px;" :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>
@@ -53,7 +53,7 @@
             </a-tooltip>
         </span>
         <a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
-                       :dropdown-class-name="themeSwitcher.darkCardClass"
+                       :dropdown-class-name="themeSwitcher.currentTheme"
                        v-model="dbInbound._expiryTime" style="width: 250px;"></a-date-picker>
     </a-form-item>
 </a-form>

+ 1 - 1
web/html/xui/form/protocol/dokodemo.html

@@ -8,7 +8,7 @@
     </a-form-item>
     <br>
     <a-form-item label='{{ i18n "pages.inbounds.network"}}'>
-        <a-select v-model="inbound.settings.network" style="width: 100px;" :dropdown-class-name="themeSwitcher.darkCardClass">
+        <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>

+ 3 - 90
web/html/xui/form/protocol/shadowsocks.html

@@ -3,94 +3,7 @@
     <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" }}'>
-            <a-form-item>
-                <span slot="label">
-                    <span>{{ i18n "pages.inbounds.email" }}</span>
-                    <a-tooltip>
-                        <template slot="title">
-                            <span>{{ i18n "pages.inbounds.emailDesc" }}</span>
-                        </template>
-                    </a-tooltip>
-                </span>
-                <a-icon @click="client.email = RandomUtil.randomLowerAndNum(8)" type="sync"> </a-icon>
-                <a-input v-model.trim="client.email" style="width: 200px;"></a-input>
-            </a-form-item>
-            <a-form-item label="Password">
-                <a-icon @click="client.password = RandomUtil.randomShadowsocksPassword()" type="sync"> </a-icon>
-                <a-input v-model.trim="client.password" style="width: 250px;"></a-input>
-            </a-form-item>
-            <a-form-item v-if="client.email && app.subSettings.enable">
-                <span slot="label">
-                    Subscription
-                    <a-tooltip>
-                        <template slot="title">
-                            <span>{{ i18n "pages.inbounds.subscriptionDesc" }}</span>
-                        </template>
-                        <a-icon type="question-circle" theme="filled"></a-icon>
-                    </a-tooltip>
-                </span>
-                <a-icon @click="client.subId = RandomUtil.randomLowerAndNum(16)" type="sync"> </a-icon>
-                <a-input v-model.trim="client.subId" style="width: 150px;"></a-input>
-            </a-form-item>
-            <a-form-item v-if="client.email && app.tgBotEnable">
-                <span slot="label">
-                    Telegram ID
-                    <a-tooltip>
-                        <template slot="title">
-                            <span>{{ i18n "pages.inbounds.telegramDesc" }}</span>
-                        </template>
-                        <a-icon type="question-circle" theme="filled"></a-icon>
-                    </a-tooltip>
-                </span>
-                <a-input v-model.trim="client.tgId"></a-input>
-            </a-form-item>
-            <a-form-item>
-                <span slot="label">
-                    <span>{{ i18n "pages.inbounds.IPLimit" }}</span>
-                    <a-tooltip>
-                        <template slot="title">
-                            <span>{{ i18n "pages.inbounds.IPLimitDesc" }}</span>
-                        </template>
-                        <a-icon type="question-circle" theme="filled"></a-icon>
-                    </a-tooltip>
-                </span>
-                <a-input-number v-model="client.limitIp" min="0"></a-input-number>
-            </a-form-item>
-            <br>
-            <a-form-item>
-                <span slot="label">
-                    <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>
-                </span>
-                <a-input-number v-model="client._totalGB" :min="0"></a-input-number>
-            </a-form-item>
-            <br>
-            <a-form-item label='{{ i18n "pages.client.delayedStart" }}'>
-                <a-switch v-model="delayedStart" @click="client._expiryTime=0"></a-switch>
-            </a-form-item>
-            <br>
-            <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>
-                <span slot="label">
-                    <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>
-                </span>
-                <a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
-                                :dropdown-class-name="themeSwitcher.darkCardClass"
-                                v-model="client._expiryTime" style="width: 170px;"></a-date-picker>
-            </a-form-item>
+            {{template "form/client"}}
         </a-collapse-panel>
     </a-collapse>
     <a-collapse v-else>
@@ -111,7 +24,7 @@
 </a-form>
 <a-form layout="inline">
     <a-form-item label='{{ i18n "encryption" }}'>
-        <a-select v-model="inbound.settings.method" style="width: 250px;" :dropdown-class-name="themeSwitcher.darkCardClass" @change="SSMethodChange">
+        <a-select v-model="inbound.settings.method" style="width: 250px;" :dropdown-class-name="themeSwitcher.currentTheme" @change="SSMethodChange">
             <a-select-option v-for="method in SSMethods" :value="method">[[ method ]]</a-select-option>
         </a-select>
     </a-form-item>
@@ -120,7 +33,7 @@
         <a-input v-model.trim="inbound.settings.password" style="width: 250px;"></a-input>
     </a-form-item>
     <a-form-item label='{{ i18n "pages.inbounds.network" }}'>
-        <a-select v-model="inbound.settings.network" style="width: 100px;" :dropdown-class-name="themeSwitcher.darkCardClass">
+        <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>

+ 3 - 96
web/html/xui/form/protocol/trojan.html

@@ -2,100 +2,7 @@
 <a-form layout="inline" style="padding: 10px 0px;">
     <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" }}'>
-            <a-form-item>
-                <span slot="label">
-                    <span>{{ i18n "pages.inbounds.email" }}</span>
-                    <a-tooltip>
-                        <template slot="title">
-                            <span>{{ i18n "pages.inbounds.emailDesc" }}</span>
-                        </template>
-                    </a-tooltip>
-                </span>
-                <a-icon @click="client.email = RandomUtil.randomLowerAndNum(8)" type="sync"> </a-icon>
-                <a-input v-model.trim="client.email" style="width: 200px;"></a-input>
-            </a-form-item>
-            <a-form-item label="Password">
-                <a-icon @click="client.password = RandomUtil.randomSeq(10)" type="sync"> </a-icon>
-                <a-input v-model.trim="client.password" style="width: 150px;"></a-input>
-            </a-form-item>
-            <a-form-item v-if="client.email && app.subSettings.enable">
-                <span slot="label">
-                    Subscription
-                    <a-tooltip>
-                        <template slot="title">
-                            <span>{{ i18n "pages.inbounds.subscriptionDesc" }}</span>
-                        </template>
-                        <a-icon type="question-circle" theme="filled"></a-icon>
-                    </a-tooltip>
-                </span>
-                <a-icon @click="client.subId = RandomUtil.randomLowerAndNum(16)" type="sync"> </a-icon>
-                <a-input v-model.trim="client.subId" style="width: 150px;"></a-input>
-            </a-form-item>
-            <a-form-item v-if="client.email && app.tgBotEnable">
-                <span slot="label">
-                    Telegram ID
-                    <a-tooltip>
-                        <template slot="title">
-                            <span>{{ i18n "pages.inbounds.telegramDesc" }}</span>
-                        </template>
-                        <a-icon type="question-circle" theme="filled"></a-icon>
-                    </a-tooltip>
-                </span>
-                <a-input v-model.trim="client.tgId"></a-input>
-            </a-form-item>
-            <a-form-item>
-                <span slot="label">
-                    <span>{{ i18n "pages.inbounds.IPLimit" }}</span>
-                    <a-tooltip>
-                        <template slot="title">
-                            <span>{{ i18n "pages.inbounds.IPLimitDesc" }}</span>
-                        </template>
-                        <a-icon type="question-circle" theme="filled"></a-icon>
-                    </a-tooltip>
-                </span>
-                <a-input-number v-model="client.limitIp" min="0"></a-input-number>
-            </a-form-item>
-            <br>
-            <a-form-item v-if="inbound.xtls" label="Flow">
-                <a-select v-model="client.flow" style="width: 150px" :dropdown-class-name="themeSwitcher.darkCardClass">
-                    <a-select-option value="">{{ i18n "none" }}</a-select-option>
-                    <a-select-option v-for="key in XTLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
-                </a-select>
-            </a-form-item>
-            <a-form-item>
-                <span slot="label">
-                    <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>
-                </span>
-                <a-input-number v-model="client._totalGB" :min="0"></a-input-number>
-            </a-form-item>
-            <br>
-            <a-form-item label='{{ i18n "pages.client.delayedStart" }}'>
-                <a-switch v-model="delayedStart" @click="client._expiryTime=0"></a-switch>
-            </a-form-item>
-            <br>
-            <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>
-                <span slot="label">
-                    <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>
-                </span>
-                <a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
-                                :dropdown-class-name="themeSwitcher.darkCardClass"
-                                v-model="client._expiryTime" style="width: 170px;"></a-date-picker>
-            </a-form-item>
+            {{template "form/client"}}
         </a-collapse-panel>
     </a-collapse>
     <a-collapse v-else>
@@ -126,7 +33,7 @@
 
     <!-- trojan fallbacks -->
     <a-form v-for="(fallback, index) in inbound.settings.fallbacks" layout="inline">
-        <a-divider>
+        <a-divider style="margin:0;">
             fallback[[ index + 1 ]]
             <a-icon type="delete" @click="() => inbound.settings.delTrojanFallback(index)"
                     style="color: rgb(255, 77, 79);cursor: pointer;"/>
@@ -146,7 +53,7 @@
         <a-form-item label="xVer">
             <a-input-number v-model="fallback.xver"></a-input-number>
         </a-form-item>
-        <a-divider v-if="inbound.settings.fallbacks.length - 1 === index"/>
     </a-form>
+    <a-divider style="margin:0;"></a-divider>
 </template>
 {{end}}

+ 2 - 102
web/html/xui/form/protocol/vless.html

@@ -2,106 +2,7 @@
 <a-form layout="inline" style="padding: 10px 0px;">
     <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" }}'>
-            <a-form-item>
-                <span slot="label">
-                    <span>{{ i18n "pages.inbounds.email" }}</span>
-                    <a-tooltip>
-                        <template slot="title">
-                            <span>{{ i18n "pages.inbounds.emailDesc" }}</span>
-                        </template>
-                    </a-tooltip>
-                </span>
-                <a-icon @click="client.email = RandomUtil.randomLowerAndNum(8)" type="sync"> </a-icon>
-                <a-input v-model.trim="client.email" style="width: 200px;"></a-input>
-            </a-form-item>
-            <a-form-item label="ID">
-                <a-icon @click="client.id = RandomUtil.randomUUID()" type="sync"> </a-icon>
-                <a-input v-model.trim="client.id" style="width: 300px;"></a-input>
-            </a-form-item>
-            <a-form-item v-if="client.email && app.subSettings.enable">
-                <span slot="label">
-                    Subscription
-                    <a-tooltip>
-                        <template slot="title">
-                            <span>{{ i18n "pages.inbounds.subscriptionDesc" }}</span>
-                        </template>
-                        <a-icon type="question-circle" theme="filled"></a-icon>
-                    </a-tooltip>
-                </span>
-                <a-icon @click="client.subId = RandomUtil.randomLowerAndNum(16)" type="sync"> </a-icon>
-                <a-input v-model.trim="client.subId" style="width: 150px;"></a-input>
-            </a-form-item>
-            <a-form-item v-if="client.email && app.tgBotEnable">
-                <span slot="label">
-                    Telegram ID
-                    <a-tooltip>
-                        <template slot="title">
-                            <span>{{ i18n "pages.inbounds.telegramDesc" }}</span>
-                        </template>
-                        <a-icon type="question-circle" theme="filled"></a-icon>
-                    </a-tooltip>
-                </span>
-                <a-input v-model.trim="client.tgId"></a-input>
-            </a-form-item>
-            <a-form-item>
-                <span slot="label">
-                    <span>{{ i18n "pages.inbounds.IPLimit" }}</span>
-                    <a-tooltip>
-                        <template slot="title">
-                            <span>{{ i18n "pages.inbounds.IPLimitDesc" }}</span>
-                        </template>
-                        <a-icon type="question-circle" theme="filled"></a-icon>
-                    </a-tooltip>
-                </span>
-                <a-input-number v-model="client.limitIp" min="0"></a-input-number>
-            </a-form-item>
-            <br>
-            <a-form-item v-if="inbound.xtls" label="Flow">
-                <a-select v-model="inbound.settings.vlesses[index].flow" style="width: 200px" :dropdown-class-name="themeSwitcher.darkCardClass">
-                    <a-select-option value="" selected>{{ i18n "none" }}</a-select-option>
-                    <a-select-option v-for="key in XTLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
-                </a-select>
-            </a-form-item>
-            <a-form-item v-else-if="inbound.canEnableTlsFlow()" label="Flow">
-                <a-select v-model="inbound.settings.vlesses[index].flow" style="width: 200px" :dropdown-class-name="themeSwitcher.darkCardClass">
-                    <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>
-                <span slot="label">
-                    <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>
-                </span>
-                <a-input-number v-model="client._totalGB" :min="0"></a-input-number>
-            </a-form-item>
-            <br>
-            <a-form-item label='{{ i18n "pages.client.delayedStart" }}'>
-                <a-switch v-model="delayedStart" @click="client._expiryTime=0"></a-switch>
-            </a-form-item>
-            <br>
-            <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>
-                <span slot="label">
-                    <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>
-                </span>
-                <a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
-                               :dropdown-class-name="themeSwitcher.darkCardClass"
-                               v-model="client._expiryTime" style="width: 170px;"></a-date-picker>
-            </a-form-item>
+            {{template "form/client"}}
         </a-collapse-panel>     
     </a-collapse>
     <a-collapse v-else>
@@ -134,7 +35,7 @@
     
     <!-- vless fallbacks -->
     <a-form v-for="(fallback, index) in inbound.settings.fallbacks" layout="inline">
-        <a-divider>
+        <a-divider style="margin:0;">
             fallback[[ index + 1 ]]
             <a-icon type="delete" @click="() => inbound.settings.delFallback(index)"
                     style="color: rgb(255, 77, 79);cursor: pointer;"/>
@@ -154,7 +55,6 @@
         <a-form-item label="xVer">
             <a-input-number v-model="fallback.xver"></a-input-number>
         </a-form-item>
-        <a-divider v-if="inbound.settings.fallbacks.length - 1 === index"/>
     </a-form>
 </template>
 {{end}}

+ 1 - 89
web/html/xui/form/protocol/vmess.html

@@ -2,95 +2,7 @@
 <a-form layout="inline" style="padding: 10px 0px;">
     <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" }}'>
-            <a-form-item>
-                <span slot="label">
-                    <span>{{ i18n "pages.inbounds.email" }}</span>
-                    <a-tooltip>
-                        <template slot="title">
-                            <span>{{ i18n "pages.inbounds.emailDesc" }}</span>
-                        </template>
-                    </a-tooltip>
-                </span>
-                <a-icon @click="client.email = RandomUtil.randomLowerAndNum(8)" type="sync"> </a-icon>
-                <a-input v-model.trim="client.email" style="width: 200px;"></a-input>
-            </a-form-item>
-            <br>
-            <a-form-item label="ID">
-                <a-icon @click="client.id = RandomUtil.randomUUID()" type="sync"> </a-icon>
-                <a-input v-model.trim="client.id" style="width: 300px;"></a-input>
-            </a-form-item>
-            <a-form-item v-if="client.email && app.subSettings.enable">
-                <span slot="label">
-                    Subscription
-                    <a-tooltip>
-                        <template slot="title">
-                            <span>{{ i18n "pages.inbounds.subscriptionDesc" }}</span>
-                        </template>
-                        <a-icon type="question-circle" theme="filled"></a-icon>
-                    </a-tooltip>
-                </span>
-                <a-icon @click="client.subId = RandomUtil.randomLowerAndNum(16)" type="sync"> </a-icon>
-                <a-input v-model.trim="client.subId" style="width: 150px;"></a-input>
-            </a-form-item>
-            <a-form-item v-if="client.email && app.tgBotEnable">
-                <span slot="label">
-                    Telegram ID
-                    <a-tooltip>
-                        <template slot="title">
-                            <span>{{ i18n "pages.inbounds.telegramDesc" }}</span>
-                        </template>
-                        <a-icon type="question-circle" theme="filled"></a-icon>
-                    </a-tooltip>
-                </span>
-                <a-input v-model.trim="client.tgId"></a-input>
-            </a-form-item>
-            <a-form-item>
-                <span slot="label">
-                    <span>{{ i18n "pages.inbounds.IPLimit" }}</span>
-                    <a-tooltip>
-                        <template slot="title">
-                            <span>{{ i18n "pages.inbounds.IPLimitDesc" }}</span>
-                        </template>
-                        <a-icon type="question-circle" theme="filled"></a-icon>
-                    </a-tooltip>
-                </span>
-                <a-input-number v-model="client.limitIp" min="0"></a-input-number>
-            </a-form-item>
-            <br>
-            <a-form-item>
-                <span slot="label">
-                    <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>
-                </span>
-                <a-input-number v-model="client._totalGB" :min="0"></a-input-number>
-            </a-form-item>
-            <br>
-            <a-form-item label='{{ i18n "pages.client.delayedStart" }}'>
-                <a-switch v-model="delayedStart" @click="client._expiryTime=0"></a-switch>
-            </a-form-item>
-            <br>
-            <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>
-                <span slot="label">
-                    <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>
-                </span>
-                <a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
-                                :dropdown-class-name="themeSwitcher.darkCardClass"
-                                v-model="client._expiryTime" style="width: 170px;"></a-date-picker>
-            </a-form-item>
+            {{template "form/client"}}
         </a-collapse-panel>     
     </a-collapse>
     <a-collapse v-else>

+ 1 - 1
web/html/xui/form/stream/stream_kcp.html

@@ -1,7 +1,7 @@
 {{define "form/streamKCP"}}
 <a-form layout="inline">
     <a-form-item label='{{ i18n "camouflage" }}'>
-        <a-select v-model="inbound.stream.kcp.type" style="width: 280px;" :dropdown-class-name="themeSwitcher.darkCardClass">
+        <a-select v-model="inbound.stream.kcp.type" style="width: 280px;" :dropdown-class-name="themeSwitcher.currentTheme">
             <a-select-option value="none">None (Not Camouflage)</a-select-option>
             <a-select-option value="srtp">SRTP (Camouflage Video Call)</a-select-option>
             <a-select-option value="utp">UTP (Camouflage BT Download)</a-select-option>

+ 2 - 2
web/html/xui/form/stream/stream_quic.html

@@ -1,7 +1,7 @@
 {{define "form/streamQUIC"}}
 <a-form layout="inline">
     <a-form-item label='{{ i18n "pages.inbounds.stream.quic.encryption" }}'>
-        <a-select v-model="inbound.stream.quic.security" style="width: 165px;" :dropdown-class-name="themeSwitcher.darkCardClass">
+        <a-select v-model="inbound.stream.quic.security" style="width: 165px;" :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>
@@ -12,7 +12,7 @@
         <a-input v-model.trim="inbound.stream.quic.key" style="width: 150px;"></a-input>
     </a-form-item>
     <a-form-item label='{{ i18n "camouflage" }}'>
-        <a-select v-model="inbound.stream.quic.type" style="width: 280px;" :dropdown-class-name="themeSwitcher.darkCardClass">
+        <a-select v-model="inbound.stream.quic.type" style="width: 280px;" :dropdown-class-name="themeSwitcher.currentTheme">
             <a-select-option value="none">none (not camouflage)</a-select-option>
             <a-select-option value="srtp">srtp (camouflage video call)</a-select-option>
             <a-select-option value="utp">utp (camouflage BT download)</a-select-option>

+ 1 - 1
web/html/xui/form/stream/stream_settings.html

@@ -2,7 +2,7 @@
 <!-- select stream network -->
 <a-form layout="inline">
     <a-form-item label='{{ i18n "transmission" }}'>
-        <a-select v-model="inbound.stream.network" @change="streamNetworkChange" :dropdown-class-name="themeSwitcher.darkCardClass">
+        <a-select v-model="inbound.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">WS</a-select-option>

+ 1 - 1
web/html/xui/form/stream/stream_sockopt.html

@@ -33,7 +33,7 @@
             <td>
                 <a-form-item>
                     <a-select v-model="inbound.stream.sockopt.tproxy" style="width: 250px;"
-                        :dropdown-class-name="themeSwitcher.darkCardClass">
+                        :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>

+ 15 - 14
web/html/xui/form/tls_settings.html

@@ -1,6 +1,7 @@
 {{define "form/tlsSettings"}}
 <!-- tls enable -->
 <a-form layout="inline" v-if="inbound.canSetTls()">
+    <a-divider style="margin:0;"></a-divider>
     <a-form-item v-if="inbound.canEnableTls()" label="TLS">
         <a-switch v-model="inbound.tls">
         </a-switch>
@@ -54,27 +55,27 @@
         <a-input v-model.trim="inbound.stream.tls.server" style="width: 250px"></a-input>
     </a-form-item>
     <a-form-item label="CipherSuites">
-        <a-select v-model="inbound.stream.tls.cipherSuites" style="width: 300px" :dropdown-class-name="themeSwitcher.darkCardClass">
+        <a-select v-model="inbound.stream.tls.cipherSuites" style="width: 300px" :dropdown-class-name="themeSwitcher.currentTheme">
             <a-select-option value="">auto</a-select-option>
-            <a-select-option v-for="key in TLS_CIPHER_OPTION" :value="key">[[ key ]]</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="MinVersion">
-        <a-select v-model="inbound.stream.tls.minVersion" style="width: 60px" :dropdown-class-name="themeSwitcher.darkCardClass">
-            <a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option>
-        </a-select>
-    </a-form-item>
-    <a-form-item label="MaxVersion">
-        <a-select v-model="inbound.stream.tls.maxVersion" style="width: 60px" :dropdown-class-name="themeSwitcher.darkCardClass">
-            <a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option>
-        </a-select>
+    <a-form-item label="Min/Max Version">
+        <a-input-group compact>
+            <a-select style="width: 60px" 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: 60px" 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="SNI" placeholder="Server Name Indication">
         <a-input v-model.trim="inbound.stream.tls.settings.serverName" style="width: 250px"></a-input>
     </a-form-item>
     <a-form-item label="uTLS">
         <a-select v-model="inbound.stream.tls.settings.fingerprint"
-                  style="width: 170px" :dropdown-class-name="themeSwitcher.darkCardClass">
+                  style="width: 170px" :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>
@@ -83,7 +84,7 @@
         <a-select
             mode="multiple"
             style="width: 250px"
-            :dropdown-class-name="themeSwitcher.darkCardClass"
+            :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>
@@ -185,7 +186,7 @@
     </a-form-item>
     <a-form-item label="uTLS">
         <a-select v-model="inbound.stream.reality.settings.fingerprint" 
-                    style="width: 135px" :dropdown-class-name="themeSwitcher.darkCardClass">
+                    style="width: 135px" :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>

+ 225 - 21
web/html/xui/inbound_client_table.html

@@ -2,34 +2,66 @@
 <template slot="actions" slot-scope="text, client, index">
     <a-tooltip>
         <template slot="title">{{ i18n "qrCode" }}</template>
-        <a-icon style="font-size: 24px;" type="qrcode" v-if="record.hasLink()" @click="showQrcode(record,index);"></a-icon>
+        <a-icon style="font-size: 24px;" class="normal-icon" type="qrcode" v-if="record.hasLink()" @click="showQrcode(record,index);"></a-icon>
     </a-tooltip>
     <a-tooltip>
         <template slot="title">{{ i18n "pages.client.edit" }}</template>
-        <a-icon style="font-size: 24px;" type="edit" @click="openEditClient(record.id,client);"></a-icon>
+        <a-icon style="font-size: 24px;" class="normal-icon" type="edit" @click="openEditClient(record.id,client);"></a-icon>
     </a-tooltip>
     <a-tooltip>
         <template slot="title">{{ i18n "info" }}</template>
-        <a-icon style="font-size: 24px;" type="info-circle" @click="showInfo(record,index);"></a-icon>
+        <a-icon style="font-size: 24px;" class="normal-icon" type="info-circle" @click="showInfo(record,index);"></a-icon>
     </a-tooltip>
     <a-tooltip>
         <template slot="title">{{ i18n "pages.inbounds.resetTraffic" }}</template>
-        <a-icon style="font-size: 24px;" type="retweet" @click="resetClientTraffic(client,record.id)" v-if="client.email.length > 0"></a-icon>
+        <a-popconfirm @confirm="resetClientTraffic(client,record.id,false)"
+            title='{{ i18n "pages.inbounds.resetTrafficContent"}}'
+            :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-popconfirm>
     </a-tooltip>
     <a-tooltip>
         <template slot="title"><span style="color: #FF4D4F"> {{ i18n "delete"}}</span></template>
-        <a-icon style="font-size: 24px;" type="delete" v-if="isRemovable(record.id)" @click="delClient(record.id,client)"></a-icon>
+        <a-popconfirm @confirm="delClient(record.id,client,false)"
+            title='{{ i18n "pages.inbounds.deleteClientContent"}}'
+            :overlay-class-name="themeSwitcher.currentTheme"
+            ok-text='{{ i18n "delete"}}'
+            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-popconfirm>
     </a-tooltip>
 </template>
 <template slot="enable" slot-scope="text, client, index">
     <a-switch v-model="client.enable" @change="switchEnableClient(record.id,client)"></a-switch>
 </template>   
+<template slot="online" slot-scope="text, client, index">
+    <template v-if="isClientOnline(client.email)">
+        <a-tag color="green">{{ i18n "online" }}</a-tag>
+    </template>
+    <template v-else>
+        <a-tag>{{ i18n "offline" }}</a-tag>
+    </template>
+</template>
 <template slot="client" slot-scope="text, client">
+    <a-tooltip>
+        <template slot="title">
+            <template v-if="!isClientEnabled(record, client.email)">{{ i18n "depleted" }}</template>
+            <template v-else-if="!client.enable">{{ i18n "disabled" }}</template>
+            <template v-else-if="isClientOnline(client.email)">{{ i18n "online" }}</template>
+        </template>
+    <a-badge :class="isClientOnline(client.email)? 'online-animation' : ''" :color="client.enable ? statsExpColor(record, client.email) : themeSwitcher.isDarkTheme ? '#2c3950' : '#bcbcbc'">
+    </a-badge>
+    </a-tooltip>
     [[ client.email ]]
     <a-tag v-if="!isClientEnabled(record, client.email)" color="red">{{ i18n "depleted" }}</a-tag>
 </template>                                    
 <template slot="traffic" slot-scope="text, client">
-    <a-popover :overlay-class-name="themeSwitcher.darkClass">
+    <a-popover :overlay-class-name="themeSwitcher.currentTheme">
         <template slot="content" v-if="client.email">
             <table cellpadding="2" width="100%">
                 <tr>
@@ -38,28 +70,200 @@
                 </tr>
                 <tr v-if="client.totalGB > 0">
                     <td>{{ i18n "remained" }}</td>
-                    <td>[[ sizeFormat(client.totalGB - getUpStats(record, client.email) - getDownStats(record, client.email)) ]]</td>
+                    <td>[[ sizeFormat(getRemStats(record, client.email)) ]]</td>
                 </tr>
             </table>
         </template>
-        <a-tag :color="statsColor(record, client.email)">
-            [[ sizeFormat(getUpStats(record, client.email) + getDownStats(record, client.email)) ]] /
-            <template v-if="client.totalGB > 0">[[client._totalGB]]GB</template>
-            <template v-else>
-                <svg style="fill: currentColor; height: 10px;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><path d="M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z"/></svg>
-            </template>
-        </a-tag>
+        <table>
+            <tr>
+                <td width="80px" style="margin:0; text-align: right;font-size: 1em;">
+                    [[ sizeFormat(getSumStats(record, client.email)) ]]
+                </td>
+                <td width="120px" v-if="!client.enable">
+                    <a-progress :stroke-color="themeSwitcher.isDarkTheme ? 'rgb(72 84 105)' : '#bcbcbc'"
+                                :show-info="false"
+                                :percent="statsProgress(record, client.email)"/>
+                </td>
+                <td width="120px" v-else-if="client.totalGB > 0">
+                    <a-progress :stroke-color="statsColor(record, client.email)"
+                                :show-info="false"
+                                :status="isClientOnline(client.email)? 'active' : isClientEnabled(record, client.email)? 'exception' : ''"
+                                :percent="statsProgress(record, client.email)"/>
+                </td>
+                <td width="120px" v-else class="infinite-bar">
+                    <a-progress
+                    :show-info="false"
+                    :status="isClientOnline(client.email)? 'active' : ''"
+                    :percent="100"></a-progress>
+                </td>
+                <td width="60px">
+                    <template v-if="client.totalGB > 0">[[ client._totalGB + "GB" ]]</template>
+                    <span v-else style="font-weight: 100;font-size: 14pt;">&infin;</span>
+                </td>
+            </tr>
+        </table>
     </a-popover>
 </template>                                    
 <template slot="expiryTime" slot-scope="text, client, index">
-    <template v-if="client.expiryTime > 0">
-        <a-tag :color="usageColor(new Date().getTime(), app.expireDiff, client.expiryTime)">
-            [[ DateUtil.formatMillis(client._expiryTime) ]]
+    <template v-if="client.expiryTime !=0 && client.reset >0">
+        <a-popover :overlay-class-name="themeSwitcher.currentTheme">
+            <template slot="content">
+                <span v-if="client.expiryTime < 0">{{ i18n "pages.client.delayedStart" }}</span>
+                <span v-else>[[ DateUtil.formatMillis(client._expiryTime) ]]</span>
+            </template>
+            <table>
+                <tr>
+                    <td width="80px" style="margin:0; text-align: right;font-size: 1em;">
+                        [[ remainedDays(client.expiryTime) ]]
+                    </td>
+                    <td width="120px" class="infinite-bar">
+                        <a-progress :show-info="false"
+                        :status="isClientOnline(client.email)? 'active' : isClientEnabled(record, client.email)? 'exception' : ''"
+                        :percent="expireProgress(client.expiryTime, client.reset)"/>
+                    </td>
+                    <td width="60px">[[ client.reset + "d" ]]</td>
+                </tr>
+            </table>
+        </a-popover>
+    </template>
+    <template v-else>
+        <a-popover v-if="client.expiryTime != 0" :overlay-class-name="themeSwitcher.currentTheme">
+            <template slot="content">
+                <span v-if="client.expiryTime < 0">{{ i18n "pages.client.delayedStart" }}</span>
+                <span v-else>[[ DateUtil.formatMillis(client._expiryTime) ]]</span>
+            </template>
+        <a-tag style="min-width: 50px; border: none;" :color="userExpiryColor(app.expireDiff, client, themeSwitcher.isDarkTheme)">
+            [[ remainedDays(client.expiryTime) ]]
         </a-tag>
+        </a-popover>
+        <a-tag v-else :color="userExpiryColor(app.expireDiff, client, themeSwitcher.isDarkTheme)" style="border: 0;" class="infinite-tag">&infin;</a-tag>
     </template>
-    <a-tag v-else-if="client.expiryTime < 0" color="cyan">
-        [[ client._expiryTime ]] {{ i18n "pages.client.days" }}
-    </a-tag>
-    <a-tag v-else color="green">{{ i18n "indefinite" }}</a-tag>
+</template>
+<template slot="actionMenu" slot-scope="text, client, index">
+    <a-dropdown :trigger="['click']">
+        <a-icon @click="e => e.preventDefault()" type="ellipsis" style="font-size: 20px;"></a-icon>
+        <a-menu slot="overlay" :theme="themeSwitcher.currentTheme">
+            <a-menu-item v-if="record.hasLink()" @click="showQrcode(record.id,client);">
+                <a-icon style="font-size: 14px;" type="qrcode"></a-icon>
+                {{ i18n "qrCode" }}
+            </a-menu-item>
+            <a-menu-item @click="openEditClient(record.id,client);">
+                <a-icon style="font-size: 14px;" type="edit"></a-icon>
+                {{ i18n "pages.client.edit" }}
+            </a-menu-item>
+            <a-menu-item @click="showInfo(record.id,client);">
+                <a-icon style="font-size: 14px;" type="info-circle"></a-icon>
+                {{ i18n "info" }}
+            </a-menu-item>
+            <a-menu-item @click="resetClientTraffic(client,record.id)" v-if="client.email.length > 0">
+                <a-icon style="font-size: 14px;" type="retweet"></a-icon>
+                {{ i18n "pages.inbounds.resetTraffic" }}
+            </a-menu-item>
+            <a-menu-item v-if="isRemovable(record.id)" @click="delClient(record.id,client)">
+                <a-icon style="font-size: 14px;" type="delete"></a-icon>
+                <span style="color: #FF4D4F"> {{ i18n "delete"}}</span>
+            </a-menu-item>
+            <a-menu-item>
+                <a-switch v-model="client.enable" size="small" @change="switchEnableClient(record.id,client)">
+                </a-switch>
+                    {{ i18n "enable"}}
+            </a-menu-item>
+        </a-menu>
+    </a-dropdown>
+</template>
+<template slot="info" slot-scope="text, client, index">
+    <a-popover placement="bottomRight" :overlay-class-name="themeSwitcher.currentTheme" trigger="click">
+        <template slot="content">
+            <table>
+                <tr>
+                    <td colspan="3" style="text-align: center;">{{ i18n "pages.inbounds.traffic" }}</td>
+                </tr>
+                <tr>
+                    <td width="80px" style="margin:0; text-align: right;font-size: 1em;">
+                        [[ sizeFormat(getUpStats(record, client.email) + getDownStats(record, client.email)) ]]
+                    </td>
+                    <td width="120px" v-if="!client.enable">
+                        <a-progress :stroke-color="themeSwitcher.isDarkTheme ? 'rgb(72 84 105)' : '#bcbcbc'"
+                                    :show-info="false"
+                                    :percent="statsProgress(record, client.email)"/>
+                    </td>
+                    <td width="120px" v-else-if="client.totalGB > 0">
+                        <a-popover :overlay-class-name="themeSwitcher.currentTheme">
+                            <template slot="content" v-if="client.email">
+                                <table cellpadding="2" width="100%">
+                                    <tr>
+                                        <td>↑[[ sizeFormat(getUpStats(record, client.email)) ]]</td>
+                                        <td>↓[[ sizeFormat(getDownStats(record, client.email)) ]]</td>
+                                    </tr>
+                                    <tr>
+                                        <td>{{ i18n "remained" }}</td>
+                                        <td>[[ sizeFormat(getRemStats(record, client.email)) ]]</td>
+                                    </tr>
+                                </table>
+                            </template>
+                            <a-progress :stroke-color="statsColor(record, client.email)"
+                                        :show-info="false"
+                                        :status="isClientOnline(client.email)? 'active' : isClientEnabled(record, client.email)? 'exception' : ''"
+                                        :percent="statsProgress(record, client.email)"/>
+                        </a-popover>
+                    </td>
+                    <td width="120px" v-else class="infinite-bar">
+                        <a-progress :stroke-color="themeSwitcher.isDarkTheme ? '#2c1e32':'#F2EAF1'"
+                        :show-info="false"
+                        :status="isClientOnline(client.email)? 'active' : ''"
+                        :percent="100"></a-progress>
+                    </td>
+                    <td width="80px">
+                        <template v-if="client.totalGB > 0">[[ client._totalGB + "GB" ]]</template>
+                        <span v-else style="font-weight: 100;font-size: 14pt;">&infin;</span>
+                    </td>
+                </tr>
+                <tr>
+                    <td colspan="3" style="text-align: center;">
+                        <a-divider style="margin: 0; border-collapse: separate;"></a-divider>
+                        {{ i18n "pages.inbounds.expireDate" }}
+                    </td>
+                </tr>
+                <tr>
+                <template v-if="client.expiryTime !=0 && client.reset >0">
+                    <td width="80px" style="margin:0; text-align: right;font-size: 1em;">
+                        [[ remainedDays(client.expiryTime) ]]
+                    </td>
+                    <td width="120px" class="infinite-bar">
+                        <a-popover :overlay-class-name="themeSwitcher.currentTheme">
+                            <template slot="content">
+                                <span v-if="client.expiryTime < 0">{{ i18n "pages.client.delayedStart" }}</span>
+                                <span v-else>[[ DateUtil.formatMillis(client._expiryTime) ]]</span>
+                            </template>
+                            <a-progress :show-info="false"
+                            :status="isClientOnline(client.email)? 'active' : isClientEnabled(record, client.email)? 'exception' : ''"
+                            :percent="expireProgress(client.expiryTime, client.reset)"/>
+                        </a-popover>
+                    </td>
+                    <td width="60px">[[ client.reset + "d" ]]</td>
+                </template>
+                <template v-else>
+                    <td colspan="3" style="text-align: center;">
+                            <a-popover v-if="client.expiryTime != 0" :overlay-class-name="themeSwitcher.currentTheme">
+                                <template slot="content">
+                                    <span v-if="client.expiryTime < 0">{{ i18n "pages.client.delayedStart" }}</span>
+                                    <span v-else>[[ DateUtil.formatMillis(client._expiryTime) ]]</span>
+                                </template>
+                                <a-tag style="min-width: 50px; border: none;" 
+                                    :color="userExpiryColor(app.expireDiff, client, themeSwitcher.isDarkTheme)">
+                                    [[ remainedDays(client.expiryTime) ]]
+                                </a-tag>
+                            </a-popover>
+                            <a-tag v-else :color="client.enable ? 'purple' : themeSwitcher.isDarkTheme ? '#2c3950' : '#bcbcbc'" class="infinite-tag">&infin;</a-tag>
+                        </template>
+                    </td>
+                </tr>
+            </table>
+        </template>
+        <a-badge>
+            <a-icon v-if="!client.enable" slot="count" type="pause-circle" :style="'color: ' + themeSwitcher.isDarkTheme ? '#2c3950' : '#bcbcbc'"></a-icon>
+            <a-button shape="round" size="small" style="font-size: 14px; padding: 0 10px;"><a-icon type="solution"></a-icon></a-button>
+        </a-badge>
+    </a-popover>
 </template>
 {{end}}

+ 2 - 2
web/html/xui/inbound_info_modal.html

@@ -3,7 +3,7 @@
     v-model="infoModal.visible" title='{{ i18n "pages.inbounds.details"}}'
     :closable="true"
     :mask-closable="true"
-    :class="themeSwitcher.darkCardClass"
+    :class="themeSwitcher.currentTheme"
     :footer="null"
     width="600px"
     >
@@ -315,7 +315,7 @@
                 if (infoModal.clientStats) {
                     return infoModal.clientStats.enable;
                 }
-                return infoModal.dbInbound.isEnable;
+                return true;
             },
             get isEnable() {
                 if (infoModal.clientSettings) {

+ 1 - 1
web/html/xui/inbound_modal.html

@@ -1,7 +1,7 @@
 {{define "inboundModal"}}
 <a-modal id="inbound-modal" v-model="inModal.visible" :title="inModal.title" @ok="inModal.ok"
          :confirm-loading="inModal.confirmLoading" :closable="true" :mask-closable="false"
-         :class="themeSwitcher.darkCardClass"
+         :class="themeSwitcher.currentTheme"
          :ok-text="inModal.okText" cancel-text='{{ i18n "close" }}'>
     {{template "form/inbound"}}
 </a-modal>

+ 360 - 114
web/html/xui/inbounds.html

@@ -8,27 +8,56 @@
         }
     }
 
+    @media (max-width: 768px) {
+        .ant-card-body {
+            padding: .5rem;
+        }
+    }
+
     .ant-col-sm-24 {
-        margin-top: 10px;
+        margin: 0.5rem -2rem 0.5rem 2rem;
     }
     tr.hideExpandIcon .ant-table-row-expand-icon {
         display: none;
     }
+    .infinite-tag {
+        padding: 0 5px;
+        border-radius: 2rem;
+        min-width: 50px;
+    }
+    .infinite-bar .ant-progress-inner .ant-progress-bg {
+        background-color: #F2EAF1;
+        border: #D5BED2 solid 1px;
+    }
+    .dark .infinite-bar .ant-progress-inner .ant-progress-bg {
+        background-color: #3c1536;
+        border: #7a316f solid 1px;
+    }
+    .ant-collapse {
+        margin: 5px 0;
+    }
+    .online-animation .ant-badge-status-dot {
+        animation: 1.2s ease infinite normal none running onlineAnimation;
+    }
+    @keyframes onlineAnimation {
+        0%, 50%, 100%   { transform: scale(1);  opacity:  1; }
+        10%             { transform: scale(1.5); opacity: .2; }
+    }
 </style>
 
 <body>
-<a-layout id="app" v-cloak>
+<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme">
     {{ template "commonSider" . }}
-    <a-layout id="content-layout" :style="themeSwitcher.bgStyle">
+    <a-layout id="content-layout">
         <a-layout-content>
-            <a-spin :spinning="spinning" :delay="500" tip="loading">
+            <a-spin :spinning="spinning" :delay="500" tip='{{ i18n "loading"}}'>
                 <transition name="list" appear>
                     <a-tag v-if="false" color="red" style="margin-bottom: 10px">
                         Please go to the panel settings as soon as possible to modify the username and password, otherwise there may be a risk of leaking account information
                     </a-tag>
                 </transition>
                 <transition name="list" appear>
-                    <a-card hoverable style="margin-bottom: 20px;" :class="themeSwitcher.darkCardClass">
+                    <a-card hoverable>
                         <a-row>
                             <a-col :xs="24" :sm="24" :lg="12">
                                 {{ i18n "pages.inbounds.totalDownUp" }}:
@@ -45,36 +74,46 @@
                             <a-col :xs="24" :sm="24" :lg="12">
                                 {{ i18n "clients" }}:
                                 <a-tag color="green">[[ total.clients ]]</a-tag>
-                                <a-popover title='{{ i18n "disabled" }}' :overlay-class-name="themeSwitcher.darkClass">
+                                <a-popover title='{{ i18n "disabled" }}' :overlay-class-name="themeSwitcher.currentTheme">
                                     <template slot="content">
                                         <p v-for="clientEmail in total.deactive">[[ clientEmail ]]</p>
                                     </template>
                                     <a-tag v-if="total.deactive.length">[[ total.deactive.length ]]</a-tag>
                                 </a-popover>
-                                <a-popover title='{{ i18n "depleted" }}' :overlay-class-name="themeSwitcher.darkClass">
+                                <a-popover title='{{ i18n "depleted" }}' :overlay-class-name="themeSwitcher.currentTheme">
                                     <template slot="content">
                                         <p v-for="clientEmail in total.depleted">[[ clientEmail ]]</p>
                                     </template>
                                     <a-tag color="red" v-if="total.depleted.length">[[ total.depleted.length ]]</a-tag>
                                 </a-popover>
-                                <a-popover title='{{ i18n "depletingSoon" }}' :overlay-class-name="themeSwitcher.darkClass">
+                                <a-popover title='{{ i18n "depletingSoon" }}' :overlay-class-name="themeSwitcher.currentTheme">
                                     <template slot="content">
                                         <p v-for="clientEmail in total.expiring">[[ clientEmail ]]</p>
                                     </template>
                                     <a-tag color="orange" v-if="total.expiring.length">[[ total.expiring.length ]]</a-tag>
                                 </a-popover>
+                                <a-popover title='{{ i18n "online" }}' :overlay-class-name="themeSwitcher.currentTheme">
+                                    <template slot="content">
+                                        <p v-for="clientEmail in onlineClients">[[ clientEmail ]]</p>
+                                    </template>
+                                    <a-tag color="blue" v-if="onlineClients.length">[[ onlineClients.length ]]</a-tag>
+                                </a-popover>
                             </a-col>
                         </a-row>
                     </a-card>
                 </transition>
                 <transition name="list" appear>
-                    <a-card hoverable :class="themeSwitcher.darkCardClass">
+                    <a-card hoverable>
                         <div slot="title">
                             <a-row>
-                                <a-col :xs="24" :sm="24" :lg="12">
-                                    <a-button type="primary" icon="plus" @click="openAddInbound">{{ i18n "pages.inbounds.addInbound" }}</a-button>
+                                <a-col :xs="12" :sm="12" :lg="12">
+                                    <a-button type="primary" icon="plus" @click="openAddInbound">
+                                        <template v-if="!isMobile">{{ i18n "pages.inbounds.addInbound" }}</template>
+                                    </a-button>
                                     <a-dropdown :trigger="['click']">
-                                        <a-button type="primary" icon="menu">{{ i18n "pages.inbounds.generalActions" }}</a-button>
+                                        <a-button type="primary" icon="menu">
+                                            <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="export">
                                                 <a-icon type="export"></a-icon>
@@ -95,12 +134,12 @@
                                         </a-menu>
                                     </a-dropdown>
                                 </a-col>
-                                <a-col :xs="24" :sm="24" :lg="12" style="text-align: right;">
+                                <a-col :xs="12" :sm="12" :lg="12" style="text-align: right;">
                                     <a-select v-model="refreshInterval"
                                               style="width: 65px;"
                                               v-if="isRefreshEnabled"
                                               @change="changeRefreshInterval"
-                                              :dropdown-class-name="themeSwitcher.darkCardClass">
+                                              :dropdown-class-name="themeSwitcher.currentTheme">
                                         <a-select-option v-for="key in [5,10,30,60]" :value="key*1000">[[ key ]]s</a-select-option>
                                     </a-select>
                                     <a-icon type="sync" :spin="refreshing" @click="manualRefresh" style="margin: 0 5px;"></a-icon>
@@ -108,26 +147,32 @@
                                 </a-col>
                             </a-row>
                         </div>
-                        <a-switch v-model="enableFilter"
-                            checked-children='{{ i18n "search" }}' un-checked-children='{{ i18n "filter" }}'
-                            @change="toggleFilter" style="margin-right: 10px;">
-                        </a-switch>
-                        <a-input v-if="!enableFilter" v-model.lazy="searchKey" placeholder='{{ i18n "search" }}' autofocus style="max-width: 300px"></a-input>
-                        <a-radio-group v-if="enableFilter" v-model="filterBy" @change="filterInbounds" button-style="solid">
-                            <a-radio-button value="">{{ i18n "none" }}</a-radio-button>
-                            <a-radio-button value="deactive">{{ i18n "disabled" }}</a-radio-button>
-                            <a-radio-button value="depleted">{{ i18n "depleted" }}</a-radio-button>
-                            <a-radio-button value="expiring">{{ i18n "depletingSoon" }}</a-radio-button>
-                        </a-radio-group>
-                        <a-table :columns="columns" :row-key="dbInbound => dbInbound.id"
+                        <div style="display: flex; align-items: center; justify-content: flex-start;">
+                            <a-switch v-model="enableFilter"
+                                style="margin-right: .5rem;"
+                                @change="toggleFilter">
+                                <a-icon slot="checkedChildren" type="search"></a-icon>
+                                <a-icon slot="unCheckedChildren" type="filter"></a-icon>
+                            </a-switch>
+                            <a-input v-if="!enableFilter" v-model.lazy="searchKey" placeholder='{{ i18n "search" }}' autofocus style="max-width: 300px" :size="isMobile ? 'small' : ''"></a-input>
+                            <a-radio-group v-if="enableFilter" v-model="filterBy" @change="filterInbounds" button-style="solid" :size="isMobile ? 'small' : ''">
+                                <a-radio-button value="">{{ i18n "none" }}</a-radio-button>
+                                <a-radio-button value="deactive">{{ i18n "disabled" }}</a-radio-button>
+                                <a-radio-button value="depleted">{{ i18n "depleted" }}</a-radio-button>
+                                <a-radio-button value="expiring">{{ i18n "depletingSoon" }}</a-radio-button>
+                                <a-radio-button value="online">{{ i18n "online" }}</a-radio-button>
+                            </a-radio-group>
+                        </div>
+                        <a-table :columns="isMobile ? mobileColums : columns" :row-key="dbInbound => dbInbound.id"
                                  :data-source="searchedInbounds"
-                                 :loading="spinning" :scroll="{ x: 1200 }"
+                                 :scroll="isMobile ? {} : { x: 1000 }"
                                  :pagination="false"
                                  :expand-icon-as-cell="false"
                                  :expand-row-by-click="false"
                                  :expand-icon-column-index="0"
-                                 :row-class-name="dbInbound => (dbInbound.isTrojan || dbInbound.isVLess || dbInbound.isVMess || (dbInbound.isSS && dbInbound.toInbound().isSSMultiUser) ? '' : 'hideExpandIcon')"
-                                 style="margin-top: 20px"
+                                 :indent-size="0"
+                                 :row-class-name="dbInbound => (dbInbound.isMultiUser() ? '' : 'hideExpandIcon')"
+                                 style="margin-top: 10px"
                                  @change="() => getDBInbounds()">
                             <template slot="action" slot-scope="text, dbInbound">
                                 <a-icon type="edit" style="font-size: 22px" @click="openEditInbound(dbInbound.id);"></a-icon>
@@ -142,7 +187,7 @@
                                             <a-icon type="qrcode"></a-icon>
                                             {{ i18n "qrCode" }}
                                         </a-menu-item>
-                                        <template v-if="dbInbound.isTrojan || dbInbound.isVLess || dbInbound.isVMess || (dbInbound.isSS && dbInbound.toInbound().isSSMultiUser)">
+                                        <template v-if="dbInbound.isMultiUser()">
                                             <a-menu-item key="addClient">
                                                 <a-icon type="user-add"></a-icon>
                                                 {{ i18n "pages.client.add"}}
@@ -181,43 +226,53 @@
                                                 <a-icon type="delete"></a-icon> {{ i18n "delete"}}
                                             </span>
                                         </a-menu-item>
+                                        <a-menu-item v-if="isMobile">
+                                            <a-switch size="small" v-model="dbInbound.enable" @change="switchEnable(dbInbound.id)"></a-switch>
+                                            {{ i18n "pages.inbounds.enable" }}
+                                        </a-menu-item>
                                     </a-menu>
                                 </a-dropdown>
                             </template>
                             <template slot="protocol" slot-scope="text, dbInbound">
-                                <a-tag style="margin:0;" color="blue">[[ dbInbound.protocol ]]</a-tag>
+                                <a-tag style="margin:0;" color="purple">[[ dbInbound.protocol ]]</a-tag>
                                 <template v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS">
                                     <a-tag style="margin:0;" color="green">[[ dbInbound.toInbound().stream.network ]]</a-tag>
-                                    <a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isTls" color="cyan">TLS</a-tag>
-                                    <a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isXtls" color="cyan">XTLS</a-tag>
-                                    <a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isReality" color="cyan">Reality</a-tag>
+                                    <a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isTls" color="blue">TLS</a-tag>
+                                    <a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isXtls" color="blue">XTLS</a-tag>
+                                    <a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isReality" color="blue">Reality</a-tag>
                                 </template>
                             </template>
                             <template slot="clients" slot-scope="text, dbInbound">
                                 <template v-if="clientCount[dbInbound.id]">
                                     <a-tag style="margin:0;" color="green">[[ clientCount[dbInbound.id].clients ]]</a-tag>
-                                    <a-popover title='{{ i18n "disabled" }}' :overlay-class-name="themeSwitcher.darkClass">
+                                    <a-popover title='{{ i18n "disabled" }}' :overlay-class-name="themeSwitcher.currentTheme">
                                         <template slot="content">
                                             <p v-for="clientEmail in clientCount[dbInbound.id].deactive">[[ clientEmail ]]</p>
                                         </template>
                                         <a-tag style="margin:0; padding: 0 2px;" v-if="clientCount[dbInbound.id].deactive.length">[[ clientCount[dbInbound.id].deactive.length ]]</a-tag>
                                     </a-popover>
-                                    <a-popover title='{{ i18n "depleted" }}' :overlay-class-name="themeSwitcher.darkClass">
+                                    <a-popover title='{{ i18n "depleted" }}' :overlay-class-name="themeSwitcher.currentTheme">
                                         <template slot="content">
                                             <p v-for="clientEmail in clientCount[dbInbound.id].depleted">[[ clientEmail ]]</p>
                                         </template>
                                         <a-tag style="margin:0; padding: 0 2px;" color="red" v-if="clientCount[dbInbound.id].depleted.length">[[ clientCount[dbInbound.id].depleted.length ]]</a-tag>
                                     </a-popover>
-                                    <a-popover title='{{ i18n "depletingSoon" }}' :overlay-class-name="themeSwitcher.darkClass">
+                                    <a-popover title='{{ i18n "depletingSoon" }}' :overlay-class-name="themeSwitcher.currentTheme">
                                         <template slot="content">
                                             <p v-for="clientEmail in clientCount[dbInbound.id].expiring">[[ clientEmail ]]</p>
                                         </template>
                                         <a-tag style="margin:0; padding: 0 2px;" color="orange" v-if="clientCount[dbInbound.id].expiring.length">[[ clientCount[dbInbound.id].expiring.length ]]</a-tag>
                                     </a-popover>
+                                    <a-popover title='{{ i18n "online" }}' :overlay-class-name="themeSwitcher.currentTheme">
+                                        <template slot="content">
+                                            <p v-for="clientEmail in clientCount[dbInbound.id].online">[[ clientEmail ]]</p>
+                                        </template>
+                                        <a-tag style="margin:0; padding: 0 2px;" color="blue" v-if="clientCount[dbInbound.id].online.length">[[ clientCount[dbInbound.id].online.length ]]</a-tag>
+                                    </a-popover>  
                                 </template>
                             </template>
                             <template slot="traffic" slot-scope="text, dbInbound">
-                                <a-popover :overlay-class-name="themeSwitcher.darkClass">
+                                <a-popover :overlay-class-name="themeSwitcher.currentTheme">
                                     <template slot="content">
                                         <table cellpadding="2" width="100%">
                                             <tr>
@@ -230,7 +285,7 @@
                                             </tr>
                                         </table>
                                     </template>
-                                    <a-tag :color="dbInbound.total == 0 ? 'green' : dbInbound.up + dbInbound.down < dbInbound.total ? 'cyan' : 'red'">
+                                    <a-tag :color="usageColor(dbInbound.up + dbInbound.down, app.trafficDiff, dbInbound.total)">
                                         [[ sizeFormat(dbInbound.up + dbInbound.down) ]] /
                                         <template v-if="dbInbound.total > 0">
                                             [[ sizeFormat(dbInbound.total) ]]
@@ -245,35 +300,117 @@
                                 <a-switch v-model="dbInbound.enable" @change="switchEnable(dbInbound.id)"></a-switch>
                             </template>
                             <template slot="expiryTime" slot-scope="text, dbInbound">
-                                <template v-if="dbInbound.expiryTime > 0">
-                                    <a-tag v-if="dbInbound.isExpiry" color="red">
-                                        [[ DateUtil.formatMillis(dbInbound.expiryTime) ]]
-                                    </a-tag>
-                                    <a-tag v-else color="blue">
+                                <a-popover v-if="dbInbound.expiryTime > 0" :overlay-class-name="themeSwitcher.currentTheme">
+                                    <template slot="content">
                                         [[ DateUtil.formatMillis(dbInbound.expiryTime) ]]
+                                    </template>
+                                    <a-tag style="min-width: 50px;" :color="usageColor(new Date().getTime(), app.expireDiff, dbInbound._expiryTime)">
+                                        [[ remainedDays(dbInbound._expiryTime) ]]
                                     </a-tag>
-                                </template>
-                                <a-tag v-else color="green">{{ i18n "indefinite" }}</a-tag>
+                                </a-popover>
+                                <a-tag v-else color="purple" class="infinite-tag">&infin;</a-tag>
+                            </template>
+                            <template slot="info" slot-scope="text, dbInbound">
+                                <a-popover placement="bottomRight" :overlay-class-name="themeSwitcher.currentTheme" trigger="click">
+                                    <template slot="content">
+                                        <table cellpadding="2">
+                                            <tr>
+                                                <td>{{ i18n "pages.inbounds.protocol" }}</td>
+                                                <td>
+                                                    <a-tag style="margin:0;" color="purple">[[ dbInbound.protocol ]]</a-tag>
+                                                    <template v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS">
+                                                        <a-tag style="margin:0;" color="blue">[[ dbInbound.toInbound().stream.network ]]</a-tag>
+                                                        <a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isTls" color="green">tls</a-tag>
+                                                        <a-tag style="margin:0;" v-if="dbInbound.toInbound().stream.isReality" color="green">reality</a-tag>
+                                                    </template>                    
+                                                </td>
+                                            </tr>
+                                            <tr>
+                                                <td>{{ i18n "pages.inbounds.port" }}</td>
+                                                <td><a-tag>[[ dbInbound.port ]]</a-tag></td>
+                                            </tr>
+                                            <tr v-if="clientCount[dbInbound.id]">
+                                                <td>{{ i18n "clients" }}</td>
+                                                <td>
+                                                    <a-tag style="margin:0;" color="blue">[[ clientCount[dbInbound.id].clients ]]</a-tag>
+                                                    <a-popover title='{{ i18n "disabled" }}' :overlay-class-name="themeSwitcher.currentTheme">
+                                                        <template slot="content">
+                                                            <p v-for="clientEmail in clientCount[dbInbound.id].deactive">[[ clientEmail ]]</p>
+                                                        </template>
+                                                        <a-tag style="margin:0; padding: 0 2px;" v-if="clientCount[dbInbound.id].deactive.length">[[ clientCount[dbInbound.id].deactive.length ]]</a-tag>
+                                                    </a-popover>
+                                                    <a-popover title='{{ i18n "depleted" }}' :overlay-class-name="themeSwitcher.currentTheme">
+                                                        <template slot="content">
+                                                            <p v-for="clientEmail in clientCount[dbInbound.id].depleted">[[ clientEmail ]]</p>
+                                                        </template>
+                                                        <a-tag style="margin:0; padding: 0 2px;" color="red" v-if="clientCount[dbInbound.id].depleted.length">[[ clientCount[dbInbound.id].depleted.length ]]</a-tag>
+                                                    </a-popover>
+                                                    <a-popover title='{{ i18n "depletingSoon" }}' :overlay-class-name="themeSwitcher.currentTheme">
+                                                        <template slot="content">
+                                                            <p v-for="clientEmail in clientCount[dbInbound.id].expiring">[[ clientEmail ]]</p>
+                                                        </template>
+                                                        <a-tag style="margin:0; padding: 0 2px;" color="orange" v-if="clientCount[dbInbound.id].expiring.length">[[ clientCount[dbInbound.id].expiring.length ]]</a-tag>
+                                                    </a-popover>
+                                                    <a-popover title='{{ i18n "online" }}' :overlay-class-name="themeSwitcher.currentTheme">
+                                                        <template slot="content">
+                                                            <p v-for="clientEmail in clientCount[dbInbound.id].online">[[ clientEmail ]]</p>
+                                                        </template>
+                                                        <a-tag style="margin:0; padding: 0 2px;" color="green" v-if="clientCount[dbInbound.id].online.length">[[ clientCount[dbInbound.id].online.length ]]</a-tag>
+                                                    </a-popover>                
+                                                </td>
+                                            </tr>
+                                            <tr>
+                                                <td>{{ i18n "pages.inbounds.traffic" }}</td>
+                                                <td>
+                                                    <a-popover :overlay-class-name="themeSwitcher.currentTheme">
+                                                        <template slot="content">
+                                                            <table cellpadding="2" width="100%">
+                                                                <tr>
+                                                                    <td>↑[[ sizeFormat(dbInbound.up) ]]</td>
+                                                                    <td>↓[[ sizeFormat(dbInbound.down) ]]</td>
+                                                                </tr>
+                                                                <tr v-if="dbInbound.total > 0 &&  dbInbound.up + dbInbound.down < dbInbound.total">
+                                                                    <td>{{ i18n "remained" }}</td>
+                                                                    <td>[[ sizeFormat(dbInbound.total - dbInbound.up - dbInbound.down) ]]</td>
+                                                                </tr>
+                                                            </table>
+                                                        </template>
+                                                        <a-tag :color="usageColor(dbInbound.up + dbInbound.down, app.trafficDiff, dbInbound.total)">
+                                                            [[ sizeFormat(dbInbound.up + dbInbound.down) ]] /
+                                                            <template v-if="dbInbound.total > 0">
+                                                                [[ sizeFormat(dbInbound.total) ]]
+                                                            </template>
+                                                            <template v-else>&infin;</template>
+                                                        </a-tag>
+                                                    </a-popover>                    
+                                                </td>
+                                            </tr>
+                                            <tr>
+                                                <td>{{ i18n "pages.inbounds.expireDate" }}</td>
+                                                <td>
+                                                    <a-tag style="min-width: 50px; text-align: center;" v-if="dbInbound.expiryTime > 0" :color="dbInbound.isExpiry? 'red': 'blue'">
+                                                        [[ DateUtil.formatMillis(dbInbound.expiryTime) ]]
+                                                    </a-tag>
+                                                    <a-tag v-else style="text-align: center;" color="purple" class="infinite-tag">&infin;</a-tag>                    
+                                                </td>
+                                            </tr>
+                                        </table>
+                                    </template>
+                                    <a-badge>
+                                        <a-icon v-if="!dbInbound.enable" slot="count" type="pause-circle" :style="'color: ' + themeSwitcher.isDarkTheme ? '#2c3950' : '#bcbcbc'"></a-icon>
+                                        <a-button shape="round" size="small" style="font-size: 14px; padding: 0 10px;">
+                                            <a-icon type="info"></a-icon>
+                                        </a-button>
+                                    </a-badge>
+                                </a-popover>
                             </template>
                             <template slot="expandedRowRender" slot-scope="record">
                                 <a-table
-                                v-if="(record.protocol === Protocols.VLESS) || (record.protocol === Protocols.VMESS)"
-                                :row-key="client => client.id"
-                                :columns="innerColumns"
-                                :data-source="getInboundClients(record)"
-                                :pagination="false"
-                                style="margin-left: 20px;"
-                                >
-                                    {{template "client_table"}}
-                                </a-table>
-                                <a-table
-                                v-else-if="record.protocol === Protocols.TROJAN || record.toInbound().isSSMultiUser"
                                 :row-key="client => client.id"
-                                :columns="innerTrojanColumns"
+                                :columns="isMobile ? innerMobileColumns : innerColumns"
                                 :data-source="getInboundClients(record)"
                                 :pagination="false"
-                                style="margin-left: 20px;"
-                                >
+                                :style="isMobile ? 'margin: -16px -5px -17px;' : 'margin-left: 10px;'">
                                     {{template "client_table"}}
                                 </a-table>
                             </template>
@@ -292,6 +429,7 @@
         align: 'right',
         dataIndex: "id",
         width: 30,
+        responsive: ["xs"],
     }, {
         title: '{{ i18n "pages.inbounds.operate" }}',
         align: 'center',
@@ -320,7 +458,7 @@
     }, {
         title: '{{ i18n "clients" }}',
         align: 'left',
-        width: 40,
+        width: 50,
         scopedSlots: { customRender: 'clients' },
     }, {
         title: '{{ i18n "pages.inbounds.traffic" }}',
@@ -330,26 +468,46 @@
     }, {
         title: '{{ i18n "pages.inbounds.expireDate" }}',
         align: 'center',
-        width: 80,
+        width: 40,
         scopedSlots: { customRender: 'expiryTime' },
     }];
 
+    const mobileColums = [{
+        title: "ID",
+        align: 'right',
+        dataIndex: "id",
+        width: 10,
+        responsive: ["s"],
+    }, {
+        title: '{{ i18n "pages.inbounds.operate" }}',
+        align: 'center',
+        width: 25,
+        scopedSlots: { customRender: 'action' },
+    }, {
+        title: '{{ i18n "pages.inbounds.remark" }}',
+        align: 'left',
+        width: 70,
+        dataIndex: "remark",
+    }, {
+        title: '{{ i18n "pages.inbounds.info" }}',
+        align: 'center',
+        width: 10,
+        scopedSlots: { customRender: 'info' },
+    }];
+
     const innerColumns = [
-        { title: '{{ i18n "pages.inbounds.operate" }}', width: 70, scopedSlots: { customRender: 'actions' } },
-        { title: '{{ i18n "pages.inbounds.enable" }}', width: 40, scopedSlots: { customRender: 'enable' } },
+        { 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.traffic" }}', width: 50, scopedSlots: { customRender: 'traffic' } },
-        { title: '{{ i18n "pages.inbounds.expireDate" }}', width: 50, scopedSlots: { customRender: 'expiryTime' } },
-        { title: 'UUID', width: 120, dataIndex: "id" },
+        { title: '{{ i18n "pages.inbounds.traffic" }}', width: 80, align: 'center', scopedSlots: { customRender: 'traffic' } },
+        { title: '{{ i18n "pages.inbounds.expireDate" }}', width: 80, align: 'center', scopedSlots: { customRender: 'expiryTime' } },
     ];
 
-    const innerTrojanColumns = [
-        { title: '{{ i18n "pages.inbounds.operate" }}', width: 70, scopedSlots: { customRender: 'actions' } },
-        { title: '{{ i18n "pages.inbounds.enable" }}', width: 40, scopedSlots: { customRender: 'enable' } },
-        { title: '{{ i18n "pages.inbounds.client" }}', width: 80, scopedSlots: { customRender: 'client' } },
-        { title: '{{ i18n "pages.inbounds.traffic" }}', width: 50, scopedSlots: { customRender: 'traffic' } },
-        { title: '{{ i18n "pages.inbounds.expireDate" }}', width: 50, scopedSlots: { customRender: 'expiryTime' } },
-        { title: '{{ i18n "password" }}', width: 170, dataIndex: "password" },
+    const innerMobileColumns = [
+        { title: '{{ i18n "pages.inbounds.operate" }}', width: 10, align: 'center', scopedSlots: { customRender: 'actionMenu' } },
+        { title: '{{ i18n "pages.inbounds.client" }}', width: 90, align: 'left', scopedSlots: { customRender: 'client' } },
+        { title: '{{ i18n "pages.inbounds.info" }}', width: 10, align: 'center', scopedSlots: { customRender: 'info' } },
     ];
 
     const app = new Vue({
@@ -370,6 +528,7 @@
             defaultCert: '',
             defaultKey: '',
             clientCount: [],
+            onlineClients: [],
             isRefreshEnabled: localStorage.getItem("isRefreshEnabled") === "true" ? true : false,
             refreshing: false,
             refreshInterval: Number(localStorage.getItem("refreshInterval")) || 5000,
@@ -380,7 +539,8 @@
                 domain: '',
                 tls: false
             },
-            tgBotEnable: false
+            tgBotEnable: false,
+            isMobile: window.innerWidth <= 768,
         },
         methods: {
             loading(spinning = true) {
@@ -393,11 +553,19 @@
                     this.refreshing = false;
                     return;
                 }
+                await this.getOnlineUsers();
                 this.setInbounds(msg.obj);
                 setTimeout(() => {
                     this.refreshing = false;
                 }, 500);
             },
+            async getOnlineUsers() {
+                const msg = await HttpUtil.post('/panel/inbound/onlines');
+                if (!msg.success) {
+                    return;
+                }
+                this.onlineClients = msg.obj != null ? msg.obj : [];
+            },
             async getDefaultSettings() {
                 const msg = await HttpUtil.post('/panel/setting/defaultSettings');
                 if (!msg.success) {
@@ -441,7 +609,7 @@
                 }
             },
             getClientCounts(dbInbound, inbound) {
-                let clientCount = 0, active = [], deactive = [], depleted = [], expiring = [];
+                let clientCount = 0, active = [], deactive = [], depleted = [], expiring = [], online = [];
                 clients = this.getClients(dbInbound.protocol, inbound.settings);
                 clientStats = dbInbound.clientStats
                 now = new Date().getTime()
@@ -450,6 +618,7 @@
                     if (dbInbound.enable) {
                         clients.forEach(client => {
                             client.enable ? active.push(client.email) : deactive.push(client.email);
+                            if(this.isClientOnline(client.email)) online.push(client.email);
                         });
                         clientStats.forEach(client => {
                             if (!client.enable) {
@@ -471,6 +640,7 @@
                     deactive: deactive,
                     depleted: depleted,
                     expiring: expiring,
+                    online: online,
                 };
             },
             searchInbounds(key) {
@@ -547,10 +717,10 @@
             clickAction(action, dbInbound) {
                 switch (action.key) {
                     case "qrcode":
-                        this.showQrcode(dbInbound);
+                        this.showQrcode(dbInbound.id);
                         break;
                     case "showInfo":
-                        this.showInfo(dbInbound);
+                        this.showInfo(dbInbound.id);
                         break;
                     case "edit":
                         this.openEditInbound(dbInbound.id);
@@ -586,6 +756,7 @@
                     title: '{{ i18n "pages.inbounds.cloneInbound"}} \"' + dbInbound.remark + '\"',
                     content: '{{ i18n "pages.inbounds.cloneInboundContent"}}',
                     okText: '{{ i18n "pages.inbounds.cloneInboundOk"}}',
+                    class: themeSwitcher.currentTheme,
                     cancelText: '{{ i18n "cancel" }}',
                     onOk: () => {
                         const baseInbound = dbInbound.toInbound();
@@ -752,7 +923,7 @@
                 this.$confirm({
                     title: '{{ i18n "pages.inbounds.resetTraffic"}}',
                     content: '{{ i18n "pages.inbounds.resetTrafficContent"}}',
-                    class: themeSwitcher.darkCardClass,
+                    class: themeSwitcher.currentTheme,
                     okText: '{{ i18n "reset"}}',
                     cancelText: '{{ i18n "cancel"}}',
                     onOk: () => {
@@ -767,23 +938,27 @@
                 this.$confirm({
                     title: '{{ i18n "pages.inbounds.deleteInbound"}}',
                     content: '{{ i18n "pages.inbounds.deleteInboundContent"}}',
-                    class: themeSwitcher.darkCardClass,
+                    class: themeSwitcher.currentTheme,
                     okText: '{{ i18n "delete"}}',
                     cancelText: '{{ i18n "cancel"}}',
                     onOk: () => this.submit('/panel/inbound/del/' + dbInboundId),
                 });
             },
-            delClient(dbInboundId, client) {
+            delClient(dbInboundId, client,confirmation = true) {
                 dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
                 clientId = this.getClientId(dbInbound.protocol, client);
-                this.$confirm({
-                    title: '{{ i18n "pages.inbounds.deleteInbound"}}',
-                    content: '{{ i18n "pages.inbounds.deleteInboundContent"}}',
-                    class: themeSwitcher.darkCardClass,
-                    okText: '{{ i18n "delete"}}',
-                    cancelText: '{{ i18n "cancel"}}',
-                    onOk: () => this.submit(`/panel/inbound/${dbInboundId}/delClient/${clientId}`),
-                });
+                if (confirmation){
+                    this.$confirm({
+                        title: '{{ i18n "pages.inbounds.deleteClient"}}',
+                        content: '{{ i18n "pages.inbounds.deleteClientContent"}}',
+                        class: themeSwitcher.currentTheme,
+                        okText: '{{ i18n "delete"}}',
+                        cancelText: '{{ i18n "cancel"}}',
+                        onOk: () => this.submit(`/xui/inbound/${dbInboundId}/delClient/${clientId}`),
+                    });
+                } else {
+                    this.submit(`/xui/inbound/${dbInboundId}/delClient/${clientId}`);
+                }
             },
             getClients(protocol, clientSettings) {
                 switch (protocol) {
@@ -860,21 +1035,25 @@
                     return dbInbound.toInbound().settings.shadowsockses;
                 }
             },
-            resetClientTraffic(client, dbInboundId) {
-                this.$confirm({
-                    title: '{{ i18n "pages.inbounds.resetTraffic"}}',
-                    content: '{{ i18n "pages.inbounds.resetTrafficContent"}}',
-                    class: themeSwitcher.darkCardClass,
-                    okText: '{{ i18n "reset"}}',
-                    cancelText: '{{ i18n "cancel"}}',
-                    onOk: () => this.submit('/panel/inbound/' + dbInboundId + '/resetClientTraffic/' + client.email),
-                })
+            resetClientTraffic(client, dbInboundId, confirmation = true) {
+                if (confirmation){
+                    this.$confirm({
+                        title: '{{ i18n "pages.inbounds.resetTraffic"}}',
+                        content: '{{ i18n "pages.inbounds.resetTrafficContent"}}',
+                        class: themeSwitcher.currentTheme,
+                        okText: '{{ i18n "reset"}}',
+                        cancelText: '{{ i18n "cancel"}}',
+                        onOk: () => this.submit('/xui/inbound/' + dbInboundId + '/resetClientTraffic/' + client.email),
+                    })
+                } else {
+                    this.submit('/xui/inbound/' + dbInboundId + '/resetClientTraffic/' + client.email);
+                }
             },
             resetAllTraffic() {
                 this.$confirm({
                     title: '{{ i18n "pages.inbounds.resetAllTrafficTitle"}}',
                     content: '{{ i18n "pages.inbounds.resetAllTrafficContent"}}',
-                    class: themeSwitcher.darkCardClass,
+                    class: themeSwitcher.currentTheme,
                     okText: '{{ i18n "reset"}}',
                     cancelText: '{{ i18n "cancel"}}',
                     onOk: () => this.submit('/panel/inbound/resetAllTraffics'),
@@ -884,7 +1063,7 @@
                 this.$confirm({
                     title: dbInboundId > 0 ? '{{ i18n "pages.inbounds.resetInboundClientTrafficTitle"}}' : '{{ i18n "pages.inbounds.resetAllClientTrafficTitle"}}',
                     content: dbInboundId > 0 ? '{{ i18n "pages.inbounds.resetInboundClientTrafficContent"}}' : '{{ i18n "pages.inbounds.resetAllClientTrafficContent"}}',
-                    class: themeSwitcher.darkCardClass,
+                    class: themeSwitcher.currentTheme,
                     okText: '{{ i18n "reset"}}',
                     cancelText: '{{ i18n "cancel"}}',
                     onOk: () => this.submit('/panel/inbound/resetAllClientTraffics/' + dbInboundId),
@@ -894,36 +1073,98 @@
                 this.$confirm({
                     title: '{{ i18n "pages.inbounds.delDepletedClientsTitle"}}',
                     content: '{{ i18n "pages.inbounds.delDepletedClientsContent"}}',
-                    class: themeSwitcher.darkCardClass,
+                    class: themeSwitcher.currentTheme,
                     okText: '{{ i18n "reset"}}',
                     cancelText: '{{ i18n "cancel"}}',
                     onOk: () => this.submit('/panel/inbound/delDepletedClients/' + dbInboundId),
                 })
             },
             isExpiry(dbInbound, index) {
-                return dbInbound.toInbound().isExpiry(index)
+                return dbInbound.toInbound().isExpiry(index);
             },
             getUpStats(dbInbound, email) {
-                if (email.length == 0) return 0
-                clientStats = dbInbound.clientStats.find(stats => stats.email === email)
-                return clientStats ? clientStats.up : 0
+                if (email.length == 0) return 0;
+                clientStats = dbInbound.clientStats.find(stats => stats.email === email);
+                return clientStats ? clientStats.up : 0;
             },
             getDownStats(dbInbound, email) {
-                if (email.length == 0) return 0
-                clientStats = dbInbound.clientStats.find(stats => stats.email === email)
-                return clientStats ? clientStats.down : 0
+                if (email.length == 0) return 0;
+                clientStats = dbInbound.clientStats.find(stats => stats.email === email);
+                return clientStats ? clientStats.down : 0;
+            },
+            getSumStats(dbInbound, email) {
+                if (email.length == 0) return 0;
+                clientStats = dbInbound.clientStats.find(stats => stats.email === email);
+                return clientStats ? clientStats.up + clientStats.down : 0;
+            },
+            getRemStats(dbInbound, email) {
+                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);
+                return remained>0 ? remained : 0;
             },
             statsColor(dbInbound, email) {
-                if(email.length == 0) return 'blue';
+                if (email.length == 0) return '#0e49b5';
+                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";
+                }
+            },
+            statsProgress(dbInbound, email) {
+                if (email.length == 0) return 100;
+                clientStats = dbInbound.clientStats.find(stats => stats.email === email);
+                if (!clientStats) return 0;
+                if (clientStats.total == 0) return 100;
+                return 100*(clientStats.down + clientStats.up)/clientStats.total;
+            },
+            expireProgress(expTime, reset) {
+                now = new Date().getTime();
+                remainedSeconds = expTime < 0 ? -expTime/1000 : (expTime-now)/1000;
+                resetSeconds = reset * 86400;
+                if (remainedSeconds >= resetSeconds) return 0;
+                return 100*(1-(remainedSeconds/resetSeconds));
+            },
+            remainedDays(expTime){
+                if (expTime == 0) return null;
+                if (expTime < 0) return formatSecond(expTime/-1000);
+                now = new Date().getTime();
+                if (expTime < now) return '{{ i18n "depleted" }}';
+                return formatSecond((expTime-now)/1000);
+            },
+            statsExpColor(dbInbound, email){
+                if (email.length == 0) return '#7a316f';
                 clientStats = dbInbound.clientStats.find(stats => stats.email === email);
-                return usageColor(clientStats.down + clientStats.up, this.trafficDiff, clientStats.total);
+                if (!clientStats) return '#7a316f';
+                statsColor = usageColor(clientStats.down + clientStats.up, this.trafficDiff, clientStats.total);
+                expColor = usageColor(new Date().getTime(), this.expireDiff, clientStats.expiryTime);
+                switch (true) {
+                    case statsColor == "red" || expColor == "red":
+                        return "#E04141";
+                    case statsColor == "orange" || expColor == "orange":
+                        return "#FFA031";
+                    case statsColor == "blue" || expColor == "blue":
+                        return "#0e49b5";
+                    default:
+                        return "#7a316f";
+                }
             },
             isClientEnabled(dbInbound, email) {
-                clientStats = dbInbound.clientStats ? dbInbound.clientStats.find(stats => stats.email === email) : null
-                return clientStats ? clientStats['enable'] : true
+                clientStats = dbInbound.clientStats ? dbInbound.clientStats.find(stats => stats.email === email) : null;
+                return clientStats ? clientStats['enable'] : true;
+            },
+            isClientOnline(email) {
+                return this.onlineClients.includes(email);
             },
             isRemovable(dbInbound_id) {
-                return this.getInboundClients(this.dbInbounds.find(row => row.id === dbInbound_id)).length > 1
+                return this.getInboundClients(this.dbInbounds.find(row => row.id === dbInbound_id)).length > 1;
             },
             inboundLinks(dbInboundId) {
                 dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
@@ -933,7 +1174,7 @@
             exportAllLinks() {
                 let copyText = '';
                 for (const dbInbound of this.dbInbounds) {
-                    copyText += dbInbound.genInboundLinks
+                    copyText += dbInbound.genInboundLinks;
                 }
                 txtModal.show('{{ i18n "pages.inbounds.export"}}', copyText, 'All-Inbounds');
             },
@@ -963,6 +1204,9 @@
                     this.spinning = false;
                 }
             },
+            onResize() {
+                this.isMobile = window.innerWidth <= 768;
+            }
         },
         watch: {
             searchKey: debounce(function (newVal) {
@@ -970,6 +1214,8 @@
             }, 500)
         },
         mounted() {
+            window.addEventListener('resize', this.onResize);
+            this.onResize();
             this.loading();
             this.getDefaultSettings();
             if (this.isRefreshEnabled) {

+ 36 - 37
web/html/xui/index.html

@@ -6,6 +6,9 @@
         .ant-layout-content {
             margin: 24px 16px;
         }
+        .ant-card-hoverable {
+            margin-inline: 0.3rem;
+        }
     }
 
     .ant-col-sm-24 {
@@ -18,21 +21,20 @@
 </style>
 
 <body>
-<a-layout id="app" v-cloak>
+<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme">
     {{ template "commonSider" . }}
-    <a-layout id="content-layout" :style="themeSwitcher.bgStyle">
+    <a-layout id="content-layout">
         <a-layout-content>
             <a-spin :spinning="spinning" :delay="200" :tip="loadingTip"/>
             <transition name="list" appear>
                 <a-row>
-                    <a-card hoverable :class="themeSwitcher.darkCardClass">
+                    <a-card hoverable>
                         <a-row>
                             <a-col :sm="24" :md="12">
                                 <a-row>
                                     <a-col :span="12" style="text-align: center">
                                         <a-progress type="dashboard" status="normal"
                                                     :stroke-color="status.cpu.color"
-                                                    :class="themeSwitcher.darkCardClass"
                                                     :percent="status.cpu.percent"></a-progress>
                                         <div>CPU:  [[ cpuCoreFormat(status.cpuCores) ]]</div>
                                         <div>Speed:  [[ cpuSpeedFormat(status.cpuSpeedMhz) ]]</div>
@@ -40,7 +42,6 @@
                                     <a-col :span="12" style="text-align: center">
                                         <a-progress type="dashboard" status="normal"
                                                     :stroke-color="status.mem.color"
-                                                    :class="themeSwitcher.darkCardClass"
                                                     :percent="status.mem.percent"></a-progress>
                                         <div>
                                             {{ i18n "pages.index.memory"}}: [[ sizeFormat(status.mem.current) ]] / [[ sizeFormat(status.mem.total) ]]
@@ -53,7 +54,6 @@
                                     <a-col :span="12" style="text-align: center">
                                         <a-progress type="dashboard" status="normal"
                                                     :stroke-color="status.swap.color"
-                                                    :class="themeSwitcher.darkCardClass"
                                                     :percent="status.swap.percent"></a-progress>
                                         <div>
                                             Swap: [[ sizeFormat(status.swap.current) ]] / [[ sizeFormat(status.swap.total) ]]
@@ -62,7 +62,6 @@
                                     <a-col :span="12" style="text-align: center">
                                         <a-progress type="dashboard" status="normal"
                                                     :stroke-color="status.disk.color"
-                                                    :class="themeSwitcher.darkCardClass"
                                                     :percent="status.disk.percent"></a-progress>
                                         <div>
                                             {{ i18n "pages.index.hard"}}: [[ sizeFormat(status.disk.current) ]] / [[ sizeFormat(status.disk.total) ]]
@@ -77,22 +76,22 @@
             <transition name="list" appear>
                 <a-row>
                     <a-col :sm="24" :md="12">
-                        <a-card hoverable :class="themeSwitcher.darkCardClass">
+                        <a-card hoverable>
                             3X: <a href="https://github.com/MHSanaei/3x-ui/releases" target="_blank"><a-tag color="green">v{{ .cur_ver }}</a-tag></a>
                             Xray: <a-tag color="green" style="cursor: pointer;" @click="openSelectV2rayVersion">v[[ status.xray.version ]]</a-tag>
                             <a href="https://t.me/panel3xui" target="_blank"><a-tag color="green">@panel3xui</a-tag></a>
                         </a-card>
                     </a-col>
                     <a-col :sm="24" :md="12">
-                        <a-card hoverable :class="themeSwitcher.darkCardClass">
+                        <a-card hoverable>
                             {{ i18n "menu.link" }}:
-                            <a-tag color="blue" style="cursor: pointer;" @click="openLogs()">{{ i18n "pages.index.logs" }}</a-tag>
-                            <a-tag color="blue" style="cursor: pointer;" @click="openConfig">{{ i18n "pages.index.config" }}</a-tag>
-                            <a-tag color="blue" style="cursor: pointer;" @click="openBackup">{{ i18n "pages.index.backup" }}</a-tag>
+                            <a-tag color="purple" style="cursor: pointer;" @click="openLogs()">{{ i18n "pages.index.logs" }}</a-tag>
+                            <a-tag color="purple" style="cursor: pointer;" @click="openConfig">{{ i18n "pages.index.config" }}</a-tag>
+                            <a-tag color="purple" style="cursor: pointer;" @click="openBackup">{{ i18n "pages.index.backup" }}</a-tag>
                         </a-card>
                     </a-col>
                     <a-col :sm="24" :md="12">
-                        <a-card hoverable :class="themeSwitcher.darkCardClass">
+                        <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">
@@ -101,13 +100,13 @@
                                 </template>
                                 <a-icon type="question-circle" theme="filled"></a-icon>
                             </a-tooltip>
-                            <a-tag color="blue" style="cursor: pointer;" @click="stopXrayService">{{ i18n "pages.index.stopXray" }}</a-tag>
-                            <a-tag color="blue" style="cursor: pointer;" @click="restartXrayService">{{ i18n "pages.index.restartXray" }}</a-tag>                    
-                            <a-tag color="blue" style="cursor: pointer;" @click="openSelectV2rayVersion">{{ i18n "pages.index.xraySwitch" }}</a-tag>
+                            <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">
-                        <a-card hoverable :class="themeSwitcher.darkCardClass">
+                        <a-card hoverable>
                             {{ i18n "pages.index.operationHours" }}:
                             Xray:
                             <a-tag color="green">[[ formatSecond(status.appStats.uptime) ]]</a-tag>
@@ -116,7 +115,7 @@
                         </a-card>
                     </a-col>
                     <a-col :sm="24" :md="12">
-                        <a-card hoverable :class="themeSwitcher.darkCardClass">
+                        <a-card hoverable>
                             {{ i18n "pages.index.systemLoad" }}: [[ status.loads[0] ]] | [[ status.loads[1] ]] | [[ status.loads[2] ]]
                             <a-tooltip>
                                 <template slot="title">
@@ -127,7 +126,7 @@
                         </a-card>
                     </a-col>
                     <a-col :sm="24" :md="12">
-                        <a-card hoverable :class="themeSwitcher.darkCardClass">
+                        <a-card hoverable>
                             {{ i18n "usage"}}:
                             Memory [[ sizeFormat(status.appStats.mem) ]] -
                             Threads [[ status.appStats.threads ]]
@@ -135,7 +134,7 @@
                         </a-card>
                     </a-col>
                     <a-col :sm="24" :md="12">
-                        <a-card hoverable :class="themeSwitcher.darkCardClass">
+                        <a-card hoverable>
                             <a-row>
                                 <a-col :span="12">
                                     IPv4:
@@ -159,7 +158,7 @@
                         </a-card>
                     </a-col>                    
                     <a-col :sm="24" :md="12">
-                        <a-card hoverable :class="themeSwitcher.darkCardClass">
+                        <a-card hoverable>
                             <a-row>
                                 <a-col :span="12">
                                     TCP:  [[ status.tcpCount ]]
@@ -183,7 +182,7 @@
                         </a-card>
                     </a-col>
                     <a-col :sm="24" :md="12">
-                        <a-card hoverable :class="themeSwitcher.darkCardClass">
+                        <a-card hoverable>
                             <a-row>
                                 <a-col :span="12">
                                     <a-icon type="arrow-up"></a-icon>
@@ -209,7 +208,7 @@
                         </a-card>
                     </a-col>
                     <a-col :sm="24" :md="12">
-                        <a-card hoverable :class="themeSwitcher.darkCardClass">
+                        <a-card hoverable>
                             <a-row>
                                 <a-col :span="12">
                                     <a-icon type="cloud-upload"></a-icon>
@@ -241,12 +240,12 @@
 
     <a-modal id="version-modal" v-model="versionModal.visible" title='{{ i18n "pages.index.xraySwitch" }}'
              :closable="true" @ok="() => versionModal.visible = false"
-             :class="themeSwitcher.darkCardClass"
+             :class="themeSwitcher.currentTheme"
              footer="">
         <h2>{{ i18n "pages.index.xraySwitchClick"}}</h2>
         <h2>{{ i18n "pages.index.xraySwitchClickDesk"}}</h2>
         <template v-for="version, index in versionModal.versions">
-            <a-tag :color="index % 2 == 0 ? 'blue' : 'green'"
+            <a-tag :color="index % 2 == 0 ? 'purple' : 'green'"
                    style="margin: 10px" @click="switchV2rayVersion(version)">
                 [[ version ]]
             </a-tag>
@@ -255,7 +254,7 @@
 
     <a-modal id="log-modal" v-model="logModal.visible" title="X-UI logs"
              :closable="true" @ok="() => logModal.visible = false" @cancel="() => logModal.visible = false"
-             :class="themeSwitcher.darkCardClass"
+             :class="themeSwitcher.currentTheme"
              width="800px"
              footer="">
         <a-form layout="inline">
@@ -263,7 +262,7 @@
                 <a-select v-model="logModal.rows"
                 style="width: 80px"
                 @change="openLogs()"
-                :dropdown-class-name="themeSwitcher.darkCardClass">
+                :dropdown-class-name="themeSwitcher.currentTheme">
                     <a-select-option value="10">10</a-select-option>
                     <a-select-option value="20">20</a-select-option>
                     <a-select-option value="50">50</a-select-option>
@@ -274,7 +273,7 @@
                 <a-select v-model="logModal.level"
                 style="width: 120px"
                 @change="openLogs()"
-                :dropdown-class-name="themeSwitcher.darkCardClass">
+                :dropdown-class-name="themeSwitcher.currentTheme">
                     <a-select-option value="debug">Debug</a-select-option>
                     <a-select-option value="info">Info</a-select-option>
                     <a-select-option value="notice">Notice</a-select-option>
@@ -300,12 +299,12 @@
     </a-modal>
 
     <a-modal id="backup-modal" v-model="backupModal.visible" :title="backupModal.title"
-            :closable="true" :class="themeSwitcher.darkCardClass"
+            :closable="true" :class="themeSwitcher.currentTheme"
             @ok="() => backupModal.hide()" @cancel="() => backupModal.hide()">
-        <p style="color: inherit; font-size: 16px; padding: 4px 2px;">
-            <a-icon type="warning" style="color: inherit; font-size: 20px;"></a-icon>
-            [[ backupModal.description ]]
-        </p>
+            <a-alert type="warning" style="margin-bottom: 10px; width: fit-content"
+            :message="backupModal.description"
+            show-icon
+            ></a-alert>
         <a-space direction="horizontal" style="text-align: center" style="margin-bottom: 10px;">
             <a-button type="primary" @click="exportDatabase()">
                 [[ backupModal.exportText ]]
@@ -346,11 +345,11 @@
         get color() {
             const percent = this.percent;
             if (percent < 80) {
-                return '#67C23A';
+                return '#0a7557';
             } else if (percent < 90) {
-                return '#E6A23C';
+                return '#ffa031';
             } else {
-                return '#F56C6C';
+                return '#e04141';
             }
         }
     }
@@ -504,7 +503,7 @@
                     title: '{{ i18n "pages.index.xraySwitchVersionDialog"}}',
                     content: '{{ i18n "pages.index.xraySwitchVersionDialogDesc"}}' + ` ${version}?`,
                     okText: '{{ i18n "confirm"}}',
-                    class: themeSwitcher.darkCardClass,
+                    class: themeSwitcher.currentTheme,
                     cancelText: '{{ i18n "cancel"}}',
                     onOk: async () => {
                         versionModal.hide();

+ 39 - 786
web/html/xui/settings.html

@@ -8,8 +8,11 @@
         }
     }
 
-    .ant-col-sm-24 {
-        margin-top: 10px;
+    @media (max-width: 768px) {
+        .ant-tabs-nav .ant-tabs-tab {
+            margin: 0;
+            padding: 12px .5rem;
+        }
     }
 
     .ant-tabs-bar {
@@ -20,10 +23,6 @@
         display: block;
     }
 
-    :not(.ant-card-dark)>.ant-tabs-top-bar {
-        background: white;
-    }
-
     .alert-msg {
         color: rgb(194, 117, 18);
         font-weight: normal;
@@ -71,25 +70,31 @@
     }
 </style>
 <body>
-<a-layout id="app" v-cloak>
+<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme">
     {{ template "commonSider" . }}
-    <a-layout id="content-layout" :style="themeSwitcher.bgStyle">
+    <a-layout id="content-layout">
         <a-layout-content>
-            <a-spin :spinning="spinning" :delay="500" tip="loading">
+            <a-spin :spinning="spinning" :delay="500" tip='{{ i18n "loading"}}'>
                 <a-space direction="vertical">
-                    <a-space direction="horizontal">
-                        <a-button type="primary" :disabled="saveBtnDisable" @click="updateAllSetting">{{ i18n "pages.settings.save" }}</a-button>
-                        <a-button type="danger" :disabled="!saveBtnDisable" @click="restartPanel">{{ i18n "pages.settings.restartPanel" }}</a-button>
-                    </a-space>
-                    <a-tabs style="margin:1rem 0.5rem;" default-active-key="1" :class="themeSwitcher.darkCardClass">
+                    <a-card hoverable style="margin-bottom: .5rem;">
+                        <a-row>
+                            <a-col :xs="24" :sm="8" style="padding: 4px;">
+                                <a-space direction="horizontal">
+                                    <a-button type="primary" :disabled="saveBtnDisable" @click="updateAllSetting">{{ i18n "pages.settings.save" }}</a-button>
+                                    <a-button type="danger" :disabled="!saveBtnDisable" @click="restartPanel">{{ i18n "pages.settings.restartPanel" }}</a-button>
+                                </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
+                                >
+                            </a-col>
+                        </a-row>
+                    </a-card>
+                    <a-tabs default-active-key="1">
                         <a-tab-pane key="1" tab='{{ i18n "pages.settings.panelSettings"}}'>
-                            <a-row :xs="24" :sm="24" :lg="12">
-                                <h2 class="alert-msg">
-                                    <a-icon type="warning"></a-icon>
-                                    {{ i18n "pages.settings.infoDesc" }}
-                                </h2>
-                            </a-row>
-                            <a-list item-layout="horizontal" :style="themeSwitcher.textStyle">
+                            <a-list item-layout="horizontal">
                                 <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="allSetting.webPort" :min="0"></setting-list-item>
@@ -112,7 +117,7 @@
                                                     ref="selectLang"
                                                     v-model="lang"
                                                     @change="setLang(lang)"
-                                                    :dropdown-class-name="themeSwitcher.darkCardClass"
+                                                    :dropdown-class-name="themeSwitcher.currentTheme"
                                                     style="width: 100%"
                                                 >
                                                     <a-select-option :value="l.value" :label="l.value" v-for="l in supportLangs">
@@ -127,9 +132,9 @@
                             </a-list>
                         </a-tab-pane>
                         <a-tab-pane key="2" tab='{{ i18n "pages.settings.securitySettings"}}' style="padding: 20px;">
-                            <a-tabs class="ant-card-dark-securitybox-nohover" default-active-key="sec-1" :class="themeSwitcher.darkCardClass">
+                            <a-tabs class="ant-card-dark-securitybox-nohover" default-active-key="sec-1" :class="themeSwitcher.currentTheme">
                                 <a-tab-pane key="sec-1" tab='{{ i18n "pages.settings.security.admin"}}'>
-                                    <a-form :style="'padding: 20px;' + themeSwitcher.textStyle">
+                                    <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>
@@ -148,7 +153,7 @@
                                     </a-form>
                                 </a-tab-pane>
                                 <a-tab-pane key="sec-2" tab='{{ i18n "pages.settings.security.secret"}}'>
-                                    <a-form :style="'padding: 20px;' + themeSwitcher.textStyle">
+                                    <a-form style="padding: 20px;">
                                         <a-list-item style="padding: 20px">
                                             <a-row>
                                                 <a-col :lg="24" :xl="12">
@@ -183,188 +188,8 @@
                             </a-tabs>
                         </a-tab-pane>
 
-                        <a-tab-pane key="3" tab='{{ i18n "pages.settings.xrayConfiguration"}}'>
-                            <a-list item-layout="horizontal" :style="themeSwitcher.textStyle">
-                                <a-divider style="padding: 20px;">{{ i18n "pages.settings.actions"}}</a-divider>
-                                <a-space direction="horizontal" style="padding: 0px 20px">
-                                    <a-button type="primary" @click="resetXrayConfigToDefault">{{ i18n "pages.settings.resetDefaultConfig" }}</a-button>
-                                </a-space>
-                                <a-divider style="padding: 20px;">{{ i18n "pages.settings.templates.title"}} </a-divider>
-                                <a-row :xs="24" :sm="24" :lg="12">
-                                    <h2 class="alert-msg">
-                                        <a-icon type="warning"></a-icon>
-                                        {{ i18n "pages.settings.infoDesc" }}
-                                    </h2>
-                                </a-row>
-                                <a-tabs class="ant-card-dark-box-nohover" default-active-key="tpl-1" :class="themeSwitcher.darkCardClass" style="padding: 20px 20px;">
-                                    <a-tab-pane key="tpl-1" tab='{{ i18n "pages.settings.templates.basicTemplate"}}' style="padding-top: 20px;">
-                                        <a-collapse>
-                                            <a-collapse-panel header='{{ i18n "pages.settings.templates.generalConfigs"}}'>
-                                                <a-row :xs="24" :sm="24" :lg="12">
-                                                    <h2 class="collapse-title">
-                                                        <a-icon type="warning"></a-icon>
-                                                        {{ i18n "pages.settings.templates.generalConfigsDesc" }}
-                                                    </h2>
-                                                </a-row>
-                                                <a-list-item>
-                                                    <a-row style="padding: 20px">
-                                                        <a-col :lg="24" :xl="12">
-                                                            <a-list-item-meta 
-                                                                title='{{ i18n "pages.settings.templates.xrayConfigFreedomStrategy" }}'
-                                                                description='{{ i18n "pages.settings.templates.xrayConfigFreedomStrategyDesc" }}'/>
-                                                        </a-col>
-                                                        <a-col :lg="24" :xl="12">
-                                                            <template>
-                                                                <a-select
-                                                                    v-model="freedomStrategy"
-                                                                    :dropdown-class-name="themeSwitcher.darkCardClass"
-                                                                    style="width: 100%">
-                                                                    <a-select-option v-for="s in outboundDomainStrategies" :value="s">[[ s ]]</a-select-option>
-                                                                </a-select>
-                                                            </template>
-                                                        </a-col>
-                                                    </a-row>
-                                                </a-list-item>
-                                                <a-row style="padding: 20px">
-                                                    <a-col :lg="24" :xl="12">
-                                                        <a-list-item-meta 
-                                                            title='{{ i18n "pages.settings.templates.xrayConfigRoutingStrategy" }}'
-                                                            description='{{ i18n "pages.settings.templates.xrayConfigRoutingStrategyDesc" }}'/>
-                                                    </a-col>
-                                                    <a-col :lg="24" :xl="12">
-                                                        <template>
-                                                            <a-select
-                                                                v-model="routingStrategy"
-                                                                :dropdown-class-name="themeSwitcher.darkCardClass"
-                                                                style="width: 100%">
-                                                                <a-select-option v-for="s in routingDomainStrategies" :value="s">[[ s ]]</a-select-option>
-                                                            </a-select>
-                                                        </template>
-                                                    </a-col>
-                                                </a-row>
-                                            </a-list-item>
-                                            </a-collapse-panel>
-                                            <a-collapse-panel header='{{ i18n "pages.settings.templates.blockConfigs"}}'>
-                                                <a-row :xs="24" :sm="24" :lg="12">
-                                                    <h2 class="collapse-title">
-                                                        <a-icon type="warning"></a-icon>
-                                                        {{ i18n "pages.settings.templates.blockConfigsDesc" }}
-                                                    </h2>
-                                                </a-row>
-                                                <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigTorrent"}}' desc='{{ i18n "pages.settings.templates.xrayConfigTorrentDesc"}}' v-model="torrentSettings"></setting-list-item>
-                                                <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigPrivateIp"}}' desc='{{ i18n "pages.settings.templates.xrayConfigPrivateIpDesc"}}' v-model="privateIpSettings"></setting-list-item>
-                                                <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigAds"}}' desc='{{ i18n "pages.settings.templates.xrayConfigAdsDesc"}}' v-model="AdsSettings"></setting-list-item>
-                                                <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigFamily"}}' desc='{{ i18n "pages.settings.templates.xrayConfigFamilyDesc"}}' v-model="familyProtectSettings"></setting-list-item>
-                                                <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigSpeedtest"}}' desc='{{ i18n "pages.settings.templates.xrayConfigSpeedtestDesc"}}' v-model="SpeedTestSettings"></setting-list-item>
-                                            </a-collapse-panel>
-                                            <a-collapse-panel header='{{ i18n "pages.settings.templates.blockCountryConfigs"}}'>
-                                                <a-row :xs="24" :sm="24" :lg="12">
-                                                    <h2 class="collapse-title">
-                                                        <a-icon type="warning"></a-icon>
-                                                        {{ i18n "pages.settings.templates.blockCountryConfigsDesc" }}
-                                                    </h2>
-                                                </a-row>
-                                                <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigIRIp"}}' desc='{{ i18n "pages.settings.templates.xrayConfigIRIpDesc"}}' v-model="IRIpSettings"></setting-list-item>
-                                                <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigIRDomain"}}' desc='{{ i18n "pages.settings.templates.xrayConfigIRDomainDesc"}}' v-model="IRDomainSettings"></setting-list-item>
-                                                <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigChinaIp"}}' desc='{{ i18n "pages.settings.templates.xrayConfigChinaIpDesc"}}' v-model="ChinaIpSettings"></setting-list-item>
-                                                <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigChinaDomain"}}' desc='{{ i18n "pages.settings.templates.xrayConfigChinaDomainDesc"}}' v-model="ChinaDomainSettings"></setting-list-item>
-                                                <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigRussiaIp"}}' desc='{{ i18n "pages.settings.templates.xrayConfigRussiaIpDesc"}}' v-model="RussiaIpSettings"></setting-list-item>
-                                                <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigRussiaDomain"}}' desc='{{ i18n "pages.settings.templates.xrayConfigRussiaDomainDesc"}}' v-model="RussiaDomainSettings"></setting-list-item>
-                                            </a-collapse-panel>
-                                            <a-collapse-panel header='{{ i18n "pages.settings.templates.directCountryConfigs"}}'>
-                                                <a-row :xs="24" :sm="24" :lg="12">
-                                                    <h2 class="collapse-title">
-                                                        <a-icon type="warning"></a-icon>
-                                                        {{ i18n "pages.settings.templates.directCountryConfigsDesc" }}
-                                                    </h2>
-                                                </a-row>
-                                                <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigDirectIRIp"}}' desc='{{ i18n "pages.settings.templates.xrayConfigDirectIRIpDesc"}}' v-model="IRIpDirectSettings"></setting-list-item>
-                                                <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigDirectIRDomain"}}' desc='{{ i18n "pages.settings.templates.xrayConfigDirectIRDomainDesc"}}' v-model="IRDomainDirectSettings"></setting-list-item>
-                                                <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigDirectChinaIp"}}' desc='{{ i18n "pages.settings.templates.xrayConfigDirectChinaIpDesc"}}' v-model="ChinaIpDirectSettings"></setting-list-item>
-                                                <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigDirectChinaDomain"}}' desc='{{ i18n "pages.settings.templates.xrayConfigDirectChinaDomainDesc"}}' v-model="ChinaDomainDirectSettings"></setting-list-item>
-                                                <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigDirectRussiaIp"}}' desc='{{ i18n "pages.settings.templates.xrayConfigDirectRussiaIpDesc"}}' v-model="RussiaIpDirectSettings"></setting-list-item>
-                                                <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigDirectRussiaDomain"}}' desc='{{ i18n "pages.settings.templates.xrayConfigDirectRussiaDomainDesc"}}' v-model="RussiaDomainDirectSettings"></setting-list-item>
-                                            </a-collapse-panel>
-                                            <a-collapse-panel header='{{ i18n "pages.settings.templates.ipv4Configs"}}'>
-                                                <a-row :xs="24" :sm="24" :lg="12">
-                                                    <h2 class="collapse-title">
-                                                        <a-icon type="warning"></a-icon>
-                                                        {{ i18n "pages.settings.templates.ipv4ConfigsDesc" }}
-                                                    </h2>
-                                                </a-row>
-                                                <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigGoogleIPv4"}}' desc='{{ i18n "pages.settings.templates.xrayConfigGoogleIPv4Desc"}}' v-model="GoogleIPv4Settings"></setting-list-item>
-                                                <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigNetflixIPv4"}}' desc='{{ i18n "pages.settings.templates.xrayConfigNetflixIPv4Desc"}}' v-model="NetflixIPv4Settings"></setting-list-item>
-                                            </a-collapse-panel>
-                                            <a-collapse-panel header='{{ i18n "pages.settings.templates.warpConfigs"}}'>
-                                                <a-row :xs="24" :sm="24" :lg="12">
-                                                    <h2 class="collapse-title">
-                                                        <a-icon type="warning"></a-icon>
-                                                        {{ i18n "pages.settings.templates.warpConfigsDesc" }}
-                                                    </h2>
-                                                </a-row>
-                                                <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigGoogleWARP"}}' desc='{{ i18n "pages.settings.templates.xrayConfigGoogleWARPDesc"}}' v-model="GoogleWARPSettings"></setting-list-item>
-                                                <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigOpenAIWARP"}}' desc='{{ i18n "pages.settings.templates.xrayConfigOpenAIWARPDesc"}}' v-model="OpenAIWARPSettings"></setting-list-item>
-                                                <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigNetflixWARP"}}' desc='{{ i18n "pages.settings.templates.xrayConfigNetflixWARPDesc"}}' v-model="NetflixWARPSettings"></setting-list-item>
-                                                <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigSpotifyWARP"}}' desc='{{ i18n "pages.settings.templates.xrayConfigSpotifyWARPDesc"}}' v-model="SpotifyWARPSettings"></setting-list-item>
-                                            </a-collapse-panel>
-                                        </a-collapse>
-                                    </a-tab-pane>
-                                    <a-tab-pane key="tpl-2" tab='{{ i18n "pages.settings.templates.manualLists"}}' style="padding-top: 20px;">
-                                        <a-row :xs="24" :sm="24" :lg="12">
-                                            <h2 class="collapse-title">
-                                                <a-icon type="warning"></a-icon>
-                                                {{ i18n "pages.settings.templates.manualListsDesc" }}
-                                            </h2>
-                                        </a-row>
-                                        <a-collapse>
-                                            <a-collapse-panel header='{{ i18n "pages.settings.templates.manualBlockedIPs"}}'>
-                                                <setting-list-item type="textarea" v-model="manualBlockedIPs"></setting-list-item>
-                                            </a-collapse-panel>
-                                            <a-collapse-panel header='{{ i18n "pages.settings.templates.manualBlockedDomains"}}'>
-                                                <setting-list-item type="textarea" v-model="manualBlockedDomains"></setting-list-item>
-                                            </a-collapse-panel>
-                                            <a-collapse-panel header='{{ i18n "pages.settings.templates.manualDirectIPs"}}'>
-                                                <setting-list-item type="textarea" v-model="manualDirectIPs"></setting-list-item>
-                                            </a-collapse-panel>
-                                            <a-collapse-panel header='{{ i18n "pages.settings.templates.manualDirectDomains"}}'>
-                                                <setting-list-item type="textarea" v-model="manualDirectDomains"></setting-list-item>
-                                            </a-collapse-panel>
-                                            <a-collapse-panel header='{{ i18n "pages.settings.templates.manualIPv4Domains"}}'>
-                                                <setting-list-item type="textarea" v-model="manualIPv4Domains"></setting-list-item>
-                                            </a-collapse-panel>
-                                            <a-collapse-panel header='{{ i18n "pages.settings.templates.manualWARPDomains"}}'>
-                                                <setting-list-item type="textarea" v-model="manualWARPDomains"></setting-list-item>
-                                            </a-collapse-panel>
-                                        </a-collapse>
-                                    </a-tab-pane>
-                                    <a-tab-pane key="tpl-3" tab='{{ i18n "pages.settings.templates.advancedTemplate"}}' style="padding-top: 20px;">
-                                        <a-collapse>
-                                            <a-collapse-panel header='{{ i18n "pages.settings.templates.xrayConfigInbounds"}}'>
-                                                <setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.xrayConfigInbounds"}}' desc='{{ i18n "pages.settings.templates.xrayConfigInboundsDesc"}}' v-model="inboundSettings"></setting-list-item>
-                                            </a-collapse-panel>
-                                            <a-collapse-panel header='{{ i18n "pages.settings.templates.xrayConfigOutbounds"}}'>
-                                                <setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.xrayConfigOutbounds"}}' desc='{{ i18n "pages.settings.templates.xrayConfigOutboundsDesc"}}' v-model="outboundSettings"></setting-list-item>
-                                            </a-collapse-panel>
-                                            <a-collapse-panel header='{{ i18n "pages.settings.templates.xrayConfigRoutings"}}'>
-                                                <setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.xrayConfigRoutings"}}' desc='{{ i18n "pages.settings.templates.xrayConfigRoutingsDesc"}}' v-model="routingRuleSettings"></setting-list-item>
-                                            </a-collapse-panel>
-                                        </a-collapse>
-                                    </a-tab-pane>
-                                    <a-tab-pane key="tpl-4" tab='{{ i18n "pages.settings.templates.completeTemplate"}}' style="padding-top: 20px;">
-                                        <setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.xrayConfigTemplate"}}' desc='{{ i18n "pages.settings.templates.xrayConfigTemplateDesc"}}' v-model="allSetting.xrayTemplateConfig"></setting-list-item>
-                                    </a-tab-pane>
-                                </a-tabs>
-                            </a-list>
-                        </a-tab-pane>
-
-                        <a-tab-pane key="4" tab='{{ i18n "pages.settings.TGBotSettings"}}'>
-                            <a-row :xs="24" :sm="24" :lg="12">
-                                <h2 class="alert-msg">
-                                    <a-icon type="warning"></a-icon>
-                                    {{ i18n "pages.settings.infoDesc" }}
-                                </h2>
-                            </a-row>
-                            <a-list item-layout="horizontal" :style="themeSwitcher.textStyle">
+                        <a-tab-pane key="3" tab='{{ i18n "pages.settings.TGBotSettings"}}'>
+                            <a-list item-layout="horizontal">
                                 <setting-list-item type="switch" title='{{ i18n "pages.settings.telegramBotEnable" }}' desc='{{ i18n "pages.settings.telegramBotEnableDesc" }}' v-model="allSetting.tgBotEnable"></setting-list-item>
                                 <setting-list-item type="text" title='{{ i18n "pages.settings.telegramToken"}}' desc='{{ i18n "pages.settings.telegramTokenDesc"}}' v-model="allSetting.tgBotToken"></setting-list-item>
                                 <setting-list-item type="text" title='{{ i18n "pages.settings.telegramChatId"}}' desc='{{ i18n "pages.settings.telegramChatIdDesc"}}' v-model="allSetting.tgBotChatId"></setting-list-item>
@@ -383,7 +208,7 @@
                                                 <a-select
                                                     ref="selectBotLang"
                                                     v-model="allSetting.tgLang"
-                                                    :dropdown-class-name="themeSwitcher.darkCardClass"
+                                                    :dropdown-class-name="themeSwitcher.currentTheme"
                                                     style="width: 100%"
                                                 >
                                                     <a-select-option :value="l.value" :label="l.value" v-for="l in supportLangs">
@@ -397,14 +222,8 @@
                                 </a-list-item>
                             </a-list>
                         </a-tab-pane>
-                        <a-tab-pane key="5" tab='{{ i18n "pages.settings.subSettings" }}'>
-                            <a-row :xs="24" :sm="24" :lg="12">
-                                <h2 class="alert-msg">
-                                    <a-icon type="warning"></a-icon>
-                                    {{ i18n "pages.settings.infoDesc" }}
-                                </h2>
-                            </a-row>
-                            <a-list item-layout="horizontal" :style="themeSwitcher.textStyle">
+                        <a-tab-pane key="4" tab='{{ i18n "pages.settings.subSettings" }}'>
+                            <a-list item-layout="horizontal">
                                 <setting-list-item type="switch" title='{{ i18n "pages.settings.subEnable"}}' desc='{{ i18n "pages.settings.subEnableDesc"}}' v-model="allSetting.subEnable"></setting-list-item>
                                 <setting-list-item type="switch" title='{{ i18n "pages.settings.subEncrypt"}}' desc='{{ i18n "pages.settings.subEncryptDesc"}}' v-model="allSetting.subEncrypt"></setting-list-item>
                                 <setting-list-item type="switch" title='{{ i18n "pages.settings.subShowInfo"}}' desc='{{ i18n "pages.settings.subShowInfoDesc"}}' v-model="allSetting.subShowInfo"></setting-list-item>
@@ -440,75 +259,6 @@
             saveBtnDisable: true,
             user: new User(),
             lang: getLang(),
-            ipv4Settings: {
-                tag: "IPv4",
-                protocol: "freedom",
-                settings: {
-                    domainStrategy: "UseIPv4"
-                }
-            },
-            warpSettings: {
-                tag: "WARP",
-                protocol: "socks",
-                settings: {
-                    servers: [
-                        {
-                            address: "127.0.0.1",
-                            port: 40000
-                        }
-                    ]
-                }
-            },
-            directSettings: {
-                tag: "direct",
-                protocol: "freedom"
-            },
-            outboundDomainStrategies: ["AsIs", "UseIP", "UseIPv4", "UseIPv6"],
-            routingDomainStrategies: ["AsIs", "IPIfNonMatch", "IPOnDemand"],
-            settingsData: {
-                protocols: {
-                    bittorrent: ["bittorrent"],
-                },
-                ips: {
-                    local: ["geoip:private"],
-                    cn: ["geoip:cn"],
-                    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:geosite_IR.dat:category-ads-all"
-                    ],
-                    speedtest: ["geosite:speedtest"],
-                    openai: ["geosite:openai"],
-                    google: ["geosite:google"],
-                    spotify: ["geosite:spotify"],
-                    netflix: ["geosite:netflix"],
-                    cn: [
-                        "geosite:cn",
-                        "regexp:.*\\.cn$"
-                    ],
-                    ru: [
-                        "geosite:category-gov-ru",
-                        "regexp:.*\\.ru$"
-                    ],
-                    ir: [
-                        "regexp:.*\\.ir$",
-                        "regexp:.*\\.xn--mgba3a4f16a$",  // .ایران
-                        "ext:geosite_IR.dat:ir"  // have rules to bypass all .ir domains.
-                    ]
-                },
-                familyProtectDNS: {
-                    "servers": [
-                        "1.1.1.3",  // https://developers.cloudflare.com/1.1.1.1/setup/
-                        "1.0.0.3",
-                        "94.140.14.15",  // https://adguard-dns.io/kb/general/dns-providers/
-                        "94.140.15.16"
-                    ],
-                    "queryStrategy": "UseIPv4"
-                },
-            }
         },
         methods: {
             loading(spinning = true) {
@@ -547,6 +297,7 @@
                     this.$confirm({
                         title: '{{ i18n "pages.settings.restartPanel" }}',
                         content: '{{ i18n "pages.settings.restartPanelDesc" }}',
+                        class: themeSwitcher.currentTheme,
                         okText: '{{ i18n "sure" }}',
                         cancelText: '{{ i18n "cancel" }}',
                         onOk: () => resolve(),
@@ -558,7 +309,9 @@
                 if (msg.success) {
                     this.loading(true);
                     await PromiseUtil.sleep(5000);
-                    const { webCertFile, webKeyFile, webDomain: host, webPort: port, webBasePath: base } = this.allSetting;
+                    var { webCertFile, webKeyFile, webDomain: host, webPort: port, webBasePath: base } = this.allSetting;
+                    if (host == this.oldAllSetting.webDomain) host = null;
+                    if (port == this.oldAllSetting.webPort) port = null;
                     const isTLS = webCertFile !== "" || webKeyFile !== "";
                     const url = buildURL({ host, port, isTLS, base, path: "panel/settings" });
                     window.location.replace(url);
@@ -605,83 +358,6 @@
                 this.user.loginSecret = "";
                 }
             },
-            async resetXrayConfigToDefault() {
-                this.loading(true);
-                const msg = await HttpUtil.get("/panel/setting/getDefaultJsonConfig");
-                this.loading(false);
-                if (msg.success) {
-                    this.templateSettings = JSON.parse(JSON.stringify(msg.obj, null, 2));
-                    this.saveBtnDisable = true;
-                }
-            },
-            syncRulesWithOutbound(tag, setting) {
-                const newTemplateSettings = {...this.templateSettings};
-                const haveRules = newTemplateSettings.routing.rules.some((r) => r?.outboundTag === tag);
-                const outboundIndex = newTemplateSettings.outbounds.findIndex((o) => o.tag === tag);
-                if (!haveRules && outboundIndex >= 0) {
-                    newTemplateSettings.outbounds.splice(outboundIndex, 1);
-                }
-                if (haveRules && outboundIndex === -1) {
-                    newTemplateSettings.outbounds.push(setting);
-                }
-                this.templateSettings = newTemplateSettings;
-            },
-            templateRuleGetter(routeSettings) {
-                const { property, outboundTag } = routeSettings;
-                let result = [];
-                if (this.templateSettings != null) {
-                    this.templateSettings.routing.rules.forEach(
-                        (routingRule) => {
-                            if (
-                                routingRule.hasOwnProperty(property) &&
-                                routingRule.hasOwnProperty("outboundTag") &&
-                                routingRule.outboundTag === outboundTag
-                            ) {
-                                result.push(...routingRule[property]);
-                            }
-                        }
-                    );
-                }
-                return result;
-            },
-            templateRuleSetter(routeSettings) {
-                const { data, property, outboundTag } = routeSettings;
-                const oldTemplateSettings = this.templateSettings;
-                const newTemplateSettings = oldTemplateSettings;
-                currentProperty = this.templateRuleGetter({ outboundTag, property })
-                if (currentProperty.length == 0) {
-                    const propertyRule = {
-                        type: "field",
-                        outboundTag,
-                        [property]: data
-                    };
-                    newTemplateSettings.routing.rules.push(propertyRule);
-                }
-                else {
-                    const newRules = [];
-                    insertedOnce = false;
-                    newTemplateSettings.routing.rules.forEach(
-                        (routingRule) => {
-                            if (
-                                routingRule.hasOwnProperty(property) &&
-                                routingRule.hasOwnProperty("outboundTag") &&
-                                routingRule.outboundTag === outboundTag
-                            ) {
-                                if (!insertedOnce && data.length > 0) {
-                                    insertedOnce = true;
-                                    routingRule[property] = data;
-                                    newRules.push(routingRule);
-                                }
-                            }
-                            else {
-                                newRules.push(routingRule);
-                            }
-                        }
-                    );
-                    newTemplateSettings.routing.rules = newRules;
-                }
-                this.templateSettings = newTemplateSettings;
-            }
         },
         async mounted() {
             await this.getAllSetting();
@@ -690,429 +366,6 @@
                 this.saveBtnDisable = this.oldAllSetting.equals(this.allSetting);
             }
         },
-        computed: {
-            templateSettings: {
-                get: function () { return this.allSetting.xrayTemplateConfig ? JSON.parse(this.allSetting.xrayTemplateConfig) : null; },
-                set: function (newValue) { this.allSetting.xrayTemplateConfig = JSON.stringify(newValue, null, 2); },
-            },
-            inboundSettings: {
-                get: function () { return this.templateSettings ? JSON.stringify(this.templateSettings.inbounds, null, 2) : null; },
-                set: function (newValue) {
-                    newTemplateSettings = this.templateSettings;
-                    newTemplateSettings.inbounds = JSON.parse(newValue);
-                    this.templateSettings = newTemplateSettings;
-                },
-            },
-            outboundSettings: {
-                get: function () { return this.templateSettings ? JSON.stringify(this.templateSettings.outbounds, null, 2) : null; },
-                set: function (newValue) {
-                    newTemplateSettings = this.templateSettings;
-                    newTemplateSettings.outbounds = JSON.parse(newValue);
-                    this.templateSettings = newTemplateSettings;
-                },
-            },
-            routingRuleSettings: {
-                get: function () { return this.templateSettings ? JSON.stringify(this.templateSettings.routing.rules, null, 2) : null; },
-                set: function (newValue) {
-                    newTemplateSettings = this.templateSettings;
-                    newTemplateSettings.routing.rules = JSON.parse(newValue);
-                    this.templateSettings = newTemplateSettings;
-                },
-            },
-            freedomStrategy: {
-                get: function () {
-                    if (!this.templateSettings) return "AsIs";
-                    freedomOutbound = this.templateSettings.outbounds.find((o) => o.protocol === "freedom" && !o.tag);
-                    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) {
-                        newTemplateSettings.outbounds[freedomOutboundIndex].settings = {"domainStrategy": newValue};
-                    } else {
-                        newTemplateSettings.outbounds[freedomOutboundIndex].settings.domainStrategy = newValue;
-                    }
-                    this.templateSettings = newTemplateSettings;
-                }
-            },
-            routingStrategy: {
-                get: function () {
-                    if (!this.templateSettings || !this.templateSettings.routing || !this.templateSettings.routing.domainStrategy) return "AsIs";
-                    return this.templateSettings.routing.domainStrategy;
-                },
-                set: function (newValue) {
-                    newTemplateSettings = this.templateSettings;
-                    newTemplateSettings.routing.domainStrategy = newValue;
-                    this.templateSettings = newTemplateSettings;
-                }
-            },
-            blockedIPs: {
-                get: function () {
-                    return this.templateRuleGetter({ outboundTag: "blocked", property: "ip" });
-                },
-                set: function (newValue) {
-                    this.templateRuleSetter({ outboundTag: "blocked", property: "ip", data: newValue });
-                }
-            },
-            blockedDomains: {
-                get: function () {
-                    return this.templateRuleGetter({ outboundTag: "blocked", property: "domain" });
-                },
-                set: function (newValue) {
-                    this.templateRuleSetter({ outboundTag: "blocked", property: "domain", data: newValue });
-                }
-            },
-            blockedProtocols: {
-                get: function () {
-                    return this.templateRuleGetter({ outboundTag: "blocked", property: "protocol" });
-                },
-                set: function (newValue) {
-                    this.templateRuleSetter({ outboundTag: "blocked", property: "protocol", data: newValue });
-                }
-            },
-            directIPs: {
-                get: function () {
-                    return this.templateRuleGetter({ outboundTag: "direct", property: "ip" });
-                },
-                set: function (newValue) {
-                    this.templateRuleSetter({ outboundTag: "direct", property: "ip", data: newValue });
-                    this.syncRulesWithOutbound("direct", this.directSettings);
-                }
-            },
-            directDomains: {
-                get: function () {
-                    return this.templateRuleGetter({ outboundTag: "direct", property: "domain" });
-                },
-                set: function (newValue) {
-                    this.templateRuleSetter({ outboundTag: "direct", property: "domain", data: newValue });
-                    this.syncRulesWithOutbound("direct", this.directSettings);
-                }
-            },
-            ipv4Domains: {
-                get: function () {
-                    return this.templateRuleGetter({ outboundTag: "IPv4", property: "domain" });
-                },
-                set: function (newValue) {
-                    this.templateRuleSetter({ outboundTag: "IPv4", property: "domain", data: newValue });
-                    this.syncRulesWithOutbound("IPv4", this.ipv4Settings);
-                }
-            },
-            warpDomains: {
-                get: function () {
-                    return this.templateRuleGetter({ outboundTag: "WARP", property: "domain" });
-                },
-                set: function (newValue) {
-                    this.templateRuleSetter({ outboundTag: "WARP", property: "domain", data: newValue });
-                    this.syncRulesWithOutbound("WARP", this.warpSettings);
-                }
-            },
-            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)
-            },
-            manualWARPDomains: {
-                get: function () { return JSON.stringify(this.warpDomains, null, 2); },
-                set: debounce(function (value) { this.warpDomains = JSON.parse(value); }, 1000)
-            },
-            torrentSettings: {
-                get: function () {
-                    return doAllItemsExist(this.settingsData.protocols.bittorrent, this.blockedProtocols);
-                },
-                set: function (newValue) {
-                    if (newValue) {
-                        this.blockedProtocols = [...this.blockedProtocols, ...this.settingsData.protocols.bittorrent];
-                    } else {
-                        this.blockedProtocols = this.blockedProtocols.filter(data => !this.settingsData.protocols.bittorrent.includes(data));
-                    }
-                },
-            },
-            privateIpSettings: {
-                get: function () {
-                    return doAllItemsExist(this.settingsData.ips.local, this.blockedIPs);
-                },
-                set: function (newValue) {
-                    if (newValue) {
-                        this.blockedIPs = [...this.blockedIPs, ...this.settingsData.ips.local];
-                    } else {
-                        this.blockedIPs = this.blockedIPs.filter(data => !this.settingsData.ips.local.includes(data));
-                    }
-                },
-            },
-            AdsSettings: {
-                get: function () {
-                    return doAllItemsExist(this.settingsData.domains.ads, this.blockedDomains);
-                },
-                set: function (newValue) {
-                    if (newValue) {
-                        this.blockedDomains = [...this.blockedDomains, ...this.settingsData.domains.ads];
-                    } else {
-                        this.blockedDomains = this.blockedDomains.filter(data => !this.settingsData.domains.ads.includes(data));
-                    }
-                },
-            },
-            SpeedTestSettings: {
-                get: function () {
-                    return doAllItemsExist(this.settingsData.domains.speedtest, this.blockedDomains);
-                },
-                set: function (newValue) {
-                    if (newValue) {
-                        this.blockedDomains = [...this.blockedDomains, ...this.settingsData.domains.speedtest];
-                    } else {
-                        this.blockedDomains = this.blockedDomains.filter(data => !this.settingsData.domains.speedtest.includes(data));
-                    }
-                },
-            },
-            familyProtectSettings: {
-                get: function () {
-                    if (!this.templateSettings || !this.templateSettings.dns || !this.templateSettings.dns.servers) return false;
-                    return doAllItemsExist(this.templateSettings.dns.servers, this.settingsData.familyProtectDNS.servers);
-                },
-                set: function (newValue) {
-                    newTemplateSettings = this.templateSettings;
-                    if (newValue) {
-                        newTemplateSettings.dns = this.settingsData.familyProtectDNS;
-                    } else {
-                        delete newTemplateSettings.dns;
-                    }
-                    this.templateSettings = newTemplateSettings;
-                },
-            },
-            GoogleIPv4Settings: {
-                get: function () {
-                    return doAllItemsExist(this.settingsData.domains.google, this.ipv4Domains);
-                },
-                set: function (newValue) {
-                    if (newValue) {
-                        this.ipv4Domains = [...this.ipv4Domains, ...this.settingsData.domains.google];
-                    } else {
-                        this.ipv4Domains = this.ipv4Domains.filter(data => !this.settingsData.domains.google.includes(data));
-                    }
-                },
-            },
-            NetflixIPv4Settings: {
-                get: function () {
-                    return doAllItemsExist(this.settingsData.domains.netflix, this.ipv4Domains);
-                },
-                set: function (newValue) {
-                    if (newValue) {
-                        this.ipv4Domains = [...this.ipv4Domains, ...this.settingsData.domains.netflix];
-                    } else {
-                        this.ipv4Domains = this.ipv4Domains.filter(data => !this.settingsData.domains.netflix.includes(data));
-                    }
-                },
-            },
-            IRIpSettings: {
-                get: function () {
-                    return doAllItemsExist(this.settingsData.ips.ir, this.blockedIPs);
-                },
-                set: function (newValue) {
-                    if (newValue) {
-                        this.blockedIPs = [...this.blockedIPs, ...this.settingsData.ips.ir];
-                    } else {
-                        this.blockedIPs = this.blockedIPs.filter(data => !this.settingsData.ips.ir.includes(data));
-                    }
-                }
-            },
-            IRDomainSettings: {
-                get: function () {
-                    return doAllItemsExist(this.settingsData.domains.ir, this.blockedDomains);
-                },
-                set: function (newValue) {
-                    if (newValue) {
-                        this.blockedDomains = [...this.blockedDomains, ...this.settingsData.domains.ir];
-                    } else {
-                        this.blockedDomains = this.blockedDomains.filter(data => !this.settingsData.domains.ir.includes(data));
-                    }
-                }
-            },
-            ChinaIpSettings: {
-                get: function () {
-                    return doAllItemsExist(this.settingsData.ips.cn, this.blockedIPs);
-                },
-                set: function (newValue) {
-                    if (newValue) {
-                        this.blockedIPs = [...this.blockedIPs, ...this.settingsData.ips.cn];
-                    } else {
-                        this.blockedIPs = this.blockedIPs.filter(data => !this.settingsData.ips.cn.includes(data));
-                    }
-                }
-            },
-            ChinaDomainSettings: {
-                get: function () {
-                    return doAllItemsExist(this.settingsData.domains.cn, this.blockedDomains);
-                },
-                set: function (newValue) {
-                    if (newValue) {
-                        this.blockedDomains = [...this.blockedDomains, ...this.settingsData.domains.cn];
-                    } else {
-                        this.blockedDomains = this.blockedDomains.filter(data => !this.settingsData.domains.cn.includes(data));
-                    }
-                }
-            },
-            RussiaIpSettings: {
-                get: function () {
-                    return doAllItemsExist(this.settingsData.ips.ru, this.blockedIPs);
-                },
-                set: function (newValue) {
-                    if (newValue) {
-                        this.blockedIPs = [...this.blockedIPs, ...this.settingsData.ips.ru];
-                    } else {
-                        this.blockedIPs = this.blockedIPs.filter(data => !this.settingsData.ips.ru.includes(data));
-                    }
-                }
-            },
-            RussiaDomainSettings: {
-                get: function () {
-                    return doAllItemsExist(this.settingsData.domains.ru, this.blockedDomains);
-                },
-                set: function (newValue) {
-                    if (newValue) {
-                        this.blockedDomains = [...this.blockedDomains, ...this.settingsData.domains.ru];
-                    } else {
-                        this.blockedDomains = this.blockedDomains.filter(data => !this.settingsData.domains.ru.includes(data));
-                    }
-                }
-            },
-            IRIpDirectSettings: {
-                get: function () {
-                    return doAllItemsExist(this.settingsData.ips.ir, this.directIPs);
-                },
-                set: function (newValue) {
-                    if (newValue) {
-                        this.directIPs = [...this.directIPs, ...this.settingsData.ips.ir];
-                    } else {
-                        this.directIPs = this.directIPs.filter(data => !this.settingsData.ips.ir.includes(data));
-                    }
-                }
-            },
-            IRDomainDirectSettings: {
-                get: function () {
-                    return doAllItemsExist(this.settingsData.domains.ir, this.directDomains);
-                },
-                set: function (newValue) {
-                    if (newValue) {
-                        this.directDomains = [...this.directDomains, ...this.settingsData.domains.ir];
-                    } else {
-                        this.directDomains = this.directDomains.filter(data => !this.settingsData.domains.ir.includes(data));
-                    }
-                }
-            },
-            ChinaIpDirectSettings: {
-                get: function () {
-                    return doAllItemsExist(this.settingsData.ips.cn, this.directIPs);
-                },
-                set: function (newValue) {
-                    if (newValue) {
-                        this.directIPs = [...this.directIPs, ...this.settingsData.ips.cn];
-                    } else {
-                        this.directIPs = this.directIPs.filter(data => !this.settingsData.ips.cn.includes(data));
-                    }
-                }
-            },
-            ChinaDomainDirectSettings: {
-                get: function () {
-                    return doAllItemsExist(this.settingsData.domains.cn, this.directDomains);
-                },
-                set: function (newValue) {
-                    if (newValue) {
-                        this.directDomains = [...this.directDomains, ...this.settingsData.domains.cn];
-                    } else {
-                        this.directDomains = this.directDomains.filter(data => !this.settingsData.domains.cn.includes(data));
-                    }
-                }
-            },
-            RussiaIpDirectSettings: {
-                get: function () {
-                    return doAllItemsExist(this.settingsData.ips.ru, this.directIPs);
-                },
-                set: function (newValue) {
-                    if (newValue) {
-                        this.directIPs = [...this.directIPs, ...this.settingsData.ips.ru];
-                    } else {
-                        this.directIPs = this.directIPs.filter(data => !this.settingsData.ips.ru.includes(data));
-                    }
-                }
-            },
-            RussiaDomainDirectSettings: {
-                get: function () {
-                    return doAllItemsExist(this.settingsData.domains.ru, this.directDomains);
-                },
-                set: function (newValue) {
-                    if (newValue) {
-                        this.directDomains = [...this.directDomains, ...this.settingsData.domains.ru];
-                    } else {
-                        this.directDomains = this.directDomains.filter(data => !this.settingsData.domains.ru.includes(data));
-                    }
-                }
-            },
-            GoogleWARPSettings: {
-                get: function () {
-                    return doAllItemsExist(this.settingsData.domains.google, this.warpDomains);
-                },
-                set: function (newValue) {
-                    if (newValue) {
-                        this.warpDomains = [...this.warpDomains, ...this.settingsData.domains.google];
-                    } else {
-                        this.warpDomains = this.warpDomains.filter(data => !this.settingsData.domains.google.includes(data));
-                    }
-                },
-            },
-            OpenAIWARPSettings: {
-                get: function () {
-                    return doAllItemsExist(this.settingsData.domains.openai, this.warpDomains);
-                },
-                set: function (newValue) {
-                    if (newValue) {
-                        this.warpDomains = [...this.warpDomains, ...this.settingsData.domains.openai];
-                    } else {
-                        this.warpDomains = this.warpDomains.filter(data => !this.settingsData.domains.openai.includes(data));
-                    }
-                },
-            },
-            NetflixWARPSettings: {
-                get: function () {
-                    return doAllItemsExist(this.settingsData.domains.netflix, this.warpDomains);
-                },
-                set: function (newValue) {
-                    if (newValue) {
-                        this.warpDomains = [...this.warpDomains, ...this.settingsData.domains.netflix];
-                    } else {
-                        this.warpDomains = this.warpDomains.filter(data => !this.settingsData.domains.netflix.includes(data));
-                    }
-                },
-            },
-            SpotifyWARPSettings: {
-                get: function () {
-                    return doAllItemsExist(this.settingsData.domains.spotify, this.warpDomains);
-                },
-                set: function (newValue) {
-                    if (newValue) {
-                        this.warpDomains = [...this.warpDomains, ...this.settingsData.domains.spotify];
-                    } else {
-                        this.warpDomains = this.warpDomains.filter(data => !this.settingsData.domains.spotify.includes(data));
-                    }
-                },
-            },
-        },
     });
 </script>
 </body>

+ 911 - 0
web/html/xui/xray.html

@@ -0,0 +1,911 @@
+<!DOCTYPE html>
+<html lang="en">
+{{template "head" .}}
+<style>
+    @media (min-width: 769px) {
+        .ant-layout-content {
+            margin: 24px 16px;
+        }
+    }
+
+    @media (max-width: 768px) {
+        .ant-tabs-nav .ant-tabs-tab {
+            margin: 0;
+            padding: 12px .5rem;
+        }
+    }
+
+    .ant-tabs-bar {
+        margin: 0;
+    }
+
+    .ant-list-item {
+        display: block;
+    }
+
+    .collapse-title {
+        color: inherit;
+        font-weight: bold;
+        font-size: 18px;
+        padding: 10px 20px;
+        border-bottom: 2px solid;
+    }
+
+    .collapse-title > i {
+        color: inherit;
+        font-size: 24px;
+    }
+</style>
+<body>
+<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme">
+    {{ template "commonSider" . }}
+    <a-layout id="content-layout">
+        <a-layout-content>
+            <a-spin :spinning="spinning" :delay="500" tip='{{ i18n "loading"}}'>
+                <a-space direction="vertical">
+                    <a-card hoverable style="margin-bottom: .5rem;">
+                        <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-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
+                                >
+                            </a-col>
+                        </a-row>
+                    </a-card>
+                    <a-tabs class="ant-card-dark-box-nohover" default-active-key="tpl-1" :class="themeSwitcher.currentTheme" style="padding: 20px 20px;">
+                        <a-tab-pane key="tpl-1" tab='{{ i18n "pages.settings.templates.basicTemplate"}}' style="padding-top: 20px;">
+                            <a-space direction="horizontal" style="padding: 20px 20px">
+                                <a-button type="primary" @click="resetXrayConfigToDefault">{{ i18n "pages.settings.resetDefaultConfig" }}</a-button>
+                            </a-space>
+                            <a-collapse>
+                                <a-collapse-panel header='{{ i18n "pages.settings.templates.generalConfigs"}}'>
+                                    <a-row :xs="24" :sm="24" :lg="12">
+                                        <h2 class="collapse-title">
+                                            <a-icon type="warning"></a-icon>
+                                            {{ i18n "pages.settings.templates.generalConfigsDesc" }}
+                                        </h2>
+                                    </a-row>
+                                    <a-list-item>
+                                        <a-row style="padding: 20px">
+                                            <a-col :lg="24" :xl="12">
+                                                <a-list-item-meta 
+                                                    title='{{ i18n "pages.settings.templates.xrayConfigFreedomStrategy" }}'
+                                                    description='{{ i18n "pages.settings.templates.xrayConfigFreedomStrategyDesc" }}'/>
+                                            </a-col>
+                                            <a-col :lg="24" :xl="12">
+                                                <template>
+                                                    <a-select
+                                                        v-model="freedomStrategy"
+                                                        :dropdown-class-name="themeSwitcher.currentTheme"
+                                                        style="width: 100%">
+                                                        <a-select-option v-for="s in outboundDomainStrategies" :value="s">[[ s ]]</a-select-option>
+                                                    </a-select>
+                                                </template>
+                                            </a-col>
+                                        </a-row>
+                                    </a-list-item>
+                                    <a-row style="padding: 20px">
+                                        <a-col :lg="24" :xl="12">
+                                            <a-list-item-meta 
+                                                title='{{ i18n "pages.settings.templates.xrayConfigRoutingStrategy" }}'
+                                                description='{{ i18n "pages.settings.templates.xrayConfigRoutingStrategyDesc" }}'/>
+                                        </a-col>
+                                        <a-col :lg="24" :xl="12">
+                                            <template>
+                                                <a-select
+                                                    v-model="routingStrategy"
+                                                    :dropdown-class-name="themeSwitcher.currentTheme"
+                                                    style="width: 100%">
+                                                    <a-select-option v-for="s in routingDomainStrategies" :value="s">[[ s ]]</a-select-option>
+                                                </a-select>
+                                            </template>
+                                        </a-col>
+                                    </a-row>
+                                </a-list-item>
+                                </a-collapse-panel>
+                                <a-collapse-panel header='{{ i18n "pages.settings.templates.blockConfigs"}}'>
+                                    <a-row :xs="24" :sm="24" :lg="12">
+                                        <h2 class="collapse-title">
+                                            <a-icon type="warning"></a-icon>
+                                            {{ i18n "pages.settings.templates.blockConfigsDesc" }}
+                                        </h2>
+                                    </a-row>
+                                    <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigTorrent"}}' desc='{{ i18n "pages.settings.templates.xrayConfigTorrentDesc"}}' v-model="torrentSettings"></setting-list-item>
+                                    <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigPrivateIp"}}' desc='{{ i18n "pages.settings.templates.xrayConfigPrivateIpDesc"}}' v-model="privateIpSettings"></setting-list-item>
+                                    <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigAds"}}' desc='{{ i18n "pages.settings.templates.xrayConfigAdsDesc"}}' v-model="AdsSettings"></setting-list-item>
+                                    <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigFamily"}}' desc='{{ i18n "pages.settings.templates.xrayConfigFamilyDesc"}}' v-model="familyProtectSettings"></setting-list-item>
+                                    <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigSpeedtest"}}' desc='{{ i18n "pages.settings.templates.xrayConfigSpeedtestDesc"}}' v-model="SpeedTestSettings"></setting-list-item>
+                                </a-collapse-panel>
+                                <a-collapse-panel header='{{ i18n "pages.settings.templates.blockCountryConfigs"}}'>
+                                    <a-row :xs="24" :sm="24" :lg="12">
+                                        <h2 class="collapse-title">
+                                            <a-icon type="warning"></a-icon>
+                                            {{ i18n "pages.settings.templates.blockCountryConfigsDesc" }}
+                                        </h2>
+                                    </a-row>
+                                    <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigIRIp"}}' desc='{{ i18n "pages.settings.templates.xrayConfigIRIpDesc"}}' v-model="IRIpSettings"></setting-list-item>
+                                    <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigIRDomain"}}' desc='{{ i18n "pages.settings.templates.xrayConfigIRDomainDesc"}}' v-model="IRDomainSettings"></setting-list-item>
+                                    <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigChinaIp"}}' desc='{{ i18n "pages.settings.templates.xrayConfigChinaIpDesc"}}' v-model="ChinaIpSettings"></setting-list-item>
+                                    <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigChinaDomain"}}' desc='{{ i18n "pages.settings.templates.xrayConfigChinaDomainDesc"}}' v-model="ChinaDomainSettings"></setting-list-item>
+                                    <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigRussiaIp"}}' desc='{{ i18n "pages.settings.templates.xrayConfigRussiaIpDesc"}}' v-model="RussiaIpSettings"></setting-list-item>
+                                    <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigRussiaDomain"}}' desc='{{ i18n "pages.settings.templates.xrayConfigRussiaDomainDesc"}}' v-model="RussiaDomainSettings"></setting-list-item>
+                                </a-collapse-panel>
+                                <a-collapse-panel header='{{ i18n "pages.settings.templates.directCountryConfigs"}}'>
+                                    <a-row :xs="24" :sm="24" :lg="12">
+                                        <h2 class="collapse-title">
+                                            <a-icon type="warning"></a-icon>
+                                            {{ i18n "pages.settings.templates.directCountryConfigsDesc" }}
+                                        </h2>
+                                    </a-row>
+                                    <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigDirectIRIp"}}' desc='{{ i18n "pages.settings.templates.xrayConfigDirectIRIpDesc"}}' v-model="IRIpDirectSettings"></setting-list-item>
+                                    <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigDirectIRDomain"}}' desc='{{ i18n "pages.settings.templates.xrayConfigDirectIRDomainDesc"}}' v-model="IRDomainDirectSettings"></setting-list-item>
+                                    <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigDirectChinaIp"}}' desc='{{ i18n "pages.settings.templates.xrayConfigDirectChinaIpDesc"}}' v-model="ChinaIpDirectSettings"></setting-list-item>
+                                    <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigDirectChinaDomain"}}' desc='{{ i18n "pages.settings.templates.xrayConfigDirectChinaDomainDesc"}}' v-model="ChinaDomainDirectSettings"></setting-list-item>
+                                    <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigDirectRussiaIp"}}' desc='{{ i18n "pages.settings.templates.xrayConfigDirectRussiaIpDesc"}}' v-model="RussiaIpDirectSettings"></setting-list-item>
+                                    <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigDirectRussiaDomain"}}' desc='{{ i18n "pages.settings.templates.xrayConfigDirectRussiaDomainDesc"}}' v-model="RussiaDomainDirectSettings"></setting-list-item>
+                                </a-collapse-panel>
+                                <a-collapse-panel header='{{ i18n "pages.settings.templates.ipv4Configs"}}'>
+                                    <a-row :xs="24" :sm="24" :lg="12">
+                                        <h2 class="collapse-title">
+                                            <a-icon type="warning"></a-icon>
+                                            {{ i18n "pages.settings.templates.ipv4ConfigsDesc" }}
+                                        </h2>
+                                    </a-row>
+                                    <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigGoogleIPv4"}}' desc='{{ i18n "pages.settings.templates.xrayConfigGoogleIPv4Desc"}}' v-model="GoogleIPv4Settings"></setting-list-item>
+                                    <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigNetflixIPv4"}}' desc='{{ i18n "pages.settings.templates.xrayConfigNetflixIPv4Desc"}}' v-model="NetflixIPv4Settings"></setting-list-item>
+                                </a-collapse-panel>
+                                <a-collapse-panel header='{{ i18n "pages.settings.templates.warpConfigs"}}'>
+                                    <a-row :xs="24" :sm="24" :lg="12">
+                                        <h2 class="collapse-title">
+                                            <a-icon type="warning"></a-icon>
+                                            {{ i18n "pages.settings.templates.warpConfigsDesc" }}
+                                        </h2>
+                                    </a-row>
+                                    <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigGoogleWARP"}}' desc='{{ i18n "pages.settings.templates.xrayConfigGoogleWARPDesc"}}' v-model="GoogleWARPSettings"></setting-list-item>
+                                    <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigOpenAIWARP"}}' desc='{{ i18n "pages.settings.templates.xrayConfigOpenAIWARPDesc"}}' v-model="OpenAIWARPSettings"></setting-list-item>
+                                    <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigNetflixWARP"}}' desc='{{ i18n "pages.settings.templates.xrayConfigNetflixWARPDesc"}}' v-model="NetflixWARPSettings"></setting-list-item>
+                                    <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigSpotifyWARP"}}' desc='{{ i18n "pages.settings.templates.xrayConfigSpotifyWARPDesc"}}' v-model="SpotifyWARPSettings"></setting-list-item>
+                                </a-collapse-panel>
+                            </a-collapse>
+                        </a-tab-pane>
+                        <a-tab-pane key="tpl-2" tab='{{ i18n "pages.settings.templates.manualLists"}}' style="padding-top: 20px;">
+                            <a-row :xs="24" :sm="24" :lg="12">
+                                <h2 class="collapse-title">
+                                    <a-icon type="warning"></a-icon>
+                                    {{ i18n "pages.settings.templates.manualListsDesc" }}
+                                </h2>
+                            </a-row>
+                            <a-collapse>
+                                <a-collapse-panel header='{{ i18n "pages.settings.templates.manualBlockedIPs"}}'>
+                                    <setting-list-item type="textarea" v-model="manualBlockedIPs"></setting-list-item>
+                                </a-collapse-panel>
+                                <a-collapse-panel header='{{ i18n "pages.settings.templates.manualBlockedDomains"}}'>
+                                    <setting-list-item type="textarea" v-model="manualBlockedDomains"></setting-list-item>
+                                </a-collapse-panel>
+                                <a-collapse-panel header='{{ i18n "pages.settings.templates.manualDirectIPs"}}'>
+                                    <setting-list-item type="textarea" v-model="manualDirectIPs"></setting-list-item>
+                                </a-collapse-panel>
+                                <a-collapse-panel header='{{ i18n "pages.settings.templates.manualDirectDomains"}}'>
+                                    <setting-list-item type="textarea" v-model="manualDirectDomains"></setting-list-item>
+                                </a-collapse-panel>
+                                <a-collapse-panel header='{{ i18n "pages.settings.templates.manualIPv4Domains"}}'>
+                                    <setting-list-item type="textarea" v-model="manualIPv4Domains"></setting-list-item>
+                                </a-collapse-panel>
+                                <a-collapse-panel header='{{ i18n "pages.settings.templates.manualWARPDomains"}}'>
+                                    <setting-list-item type="textarea" v-model="manualWARPDomains"></setting-list-item>
+                                </a-collapse-panel>
+                            </a-collapse>
+                        </a-tab-pane>
+                        <a-tab-pane key="tpl-3" tab='{{ i18n "pages.settings.templates.advancedTemplate"}}' style="padding-top: 20px;">
+                            <a-collapse>
+                                <a-collapse-panel header='{{ i18n "pages.settings.templates.xrayConfigInbounds"}}'>
+                                    <setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.xrayConfigInbounds"}}' desc='{{ i18n "pages.settings.templates.xrayConfigInboundsDesc"}}' v-model="inboundSettings"></setting-list-item>
+                                </a-collapse-panel>
+                                <a-collapse-panel header='{{ i18n "pages.settings.templates.xrayConfigOutbounds"}}'>
+                                    <setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.xrayConfigOutbounds"}}' desc='{{ i18n "pages.settings.templates.xrayConfigOutboundsDesc"}}' v-model="outboundSettings"></setting-list-item>
+                                </a-collapse-panel>
+                                <a-collapse-panel header='{{ i18n "pages.settings.templates.xrayConfigRoutings"}}'>
+                                    <setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.xrayConfigRoutings"}}' desc='{{ i18n "pages.settings.templates.xrayConfigRoutingsDesc"}}' v-model="routingRuleSettings"></setting-list-item>
+                                </a-collapse-panel>
+                            </a-collapse>
+                        </a-tab-pane>
+                        <a-tab-pane key="tpl-4" tab='{{ i18n "pages.settings.templates.completeTemplate"}}' style="padding-top: 20px;">
+                            <setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.xrayConfigTemplate"}}' desc='{{ i18n "pages.settings.templates.xrayConfigTemplateDesc"}}' v-model="this.xraySetting"></setting-list-item>
+                        </a-tab-pane>
+                    </a-tabs>
+                </a-space>
+            </a-spin>
+        </a-layout-content>
+    </a-layout>
+</a-layout>
+{{template "js" .}}
+{{template "component/themeSwitcher" .}}
+{{template "component/setting"}}
+<script>
+    const app = new Vue({
+        delimiters: ['[[', ']]'],
+        el: '#app',
+        data: {
+            siderDrawer,
+            themeSwitcher,
+            spinning: false,
+            oldXraySetting: '',
+            xraySetting: '',
+            saveBtnDisable: true,
+            ipv4Settings: {
+                tag: "IPv4",
+                protocol: "freedom",
+                settings: {
+                    domainStrategy: "UseIPv4"
+                }
+            },
+            warpSettings: {
+                tag: "WARP",
+                protocol: "socks",
+                settings: {
+                    servers: [
+                        {
+                            address: "127.0.0.1",
+                            port: 40000
+                        }
+                    ]
+                }
+            },
+            directSettings: {
+                tag: "direct",
+                protocol: "freedom"
+            },
+            outboundDomainStrategies: ["AsIs", "UseIP", "UseIPv4", "UseIPv6"],
+            routingDomainStrategies: ["AsIs", "IPIfNonMatch", "IPOnDemand"],
+            settingsData: {
+                protocols: {
+                    bittorrent: ["bittorrent"],
+                },
+                ips: {
+                    local: ["geoip:private"],
+                    cn: ["geoip:cn"],
+                    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:geosite_IR.dat:category-ads-all"
+                    ],
+                    speedtest: ["geosite:speedtest"],
+                    openai: ["geosite:openai"],
+                    google: ["geosite:google"],
+                    spotify: ["geosite:spotify"],
+                    netflix: ["geosite:netflix"],
+                    cn: [
+                        "geosite:cn",
+                        "regexp:.*\\.cn$"
+                    ],
+                    ru: [
+                        "geosite:category-gov-ru",
+                        "regexp:.*\\.ru$"
+                    ],
+                    ir: [
+                        "regexp:.*\\.ir$",
+                        "regexp:.*\\.xn--mgba3a4f16a$",  // .ایران
+                        "ext:geosite_IR.dat:ir"  // have rules to bypass all .ir domains.
+                    ]
+                },
+                familyProtectDNS: {
+                    "servers": [
+                        "1.1.1.3",  // https://developers.cloudflare.com/1.1.1.1/setup/
+                        "1.0.0.3",
+                        "94.140.14.15",  // https://adguard-dns.io/kb/general/dns-providers/
+                        "94.140.15.16"
+                    ],
+                    "queryStrategy": "UseIPv4"
+                },
+            }
+        },
+        methods: {
+            loading(spinning = true) {
+                this.spinning = spinning;
+            },
+            async getXraySetting() {
+                this.loading(true);
+                const msg = await HttpUtil.post("/panel/xray/");
+                this.loading(false);
+                if (msg.success) {
+                    this.oldXraySetting = msg.obj;
+                    this.xraySetting = msg.obj;
+                    this.saveBtnDisable = true;
+                }
+            },
+            async updateXraySetting() {
+                this.loading(true);
+                const msg = await HttpUtil.post("/panel/xray/update", {xraySetting : this.xraySetting});
+                this.loading(false);
+                if (msg.success) {
+                    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(),
+                    });
+                });
+                this.loading(true);
+                const msg = await HttpUtil.post("/panel/setting/restartPanel");
+                this.loading(false);
+                if (msg.success) {
+                    this.loading(true);
+                    await PromiseUtil.sleep(5000);
+                    var { webCertFile, webKeyFile, webDomain: host, webPort: port, webBasePath: base } = this.xraySetting;
+                    if (host == this.oldXraySetting.webDomain) host = null;
+                    if (port == this.oldXraySetting.webPort) port = null;
+                    const isTLS = webCertFile !== "" || webKeyFile !== "";
+                    const url = buildURL({ host, port, isTLS, base, path: "panel/settings" });
+                    window.location.replace(url);
+                }
+            },
+            async fetchUserSecret() {
+                this.loading(true);
+                const userMessage = await HttpUtil.post("/panel/setting/getUserSecret", this.user);
+                if (userMessage.success) {
+                    this.user = userMessage.obj;
+                }
+                this.loading(false);
+            },
+            async updateSecret() {
+                this.loading(true);
+                const msg = await HttpUtil.post("/panel/setting/updateUserSecret", this.user);
+                if (msg.success) {
+                    this.user = msg.obj;
+                    window.location.replace(basePath + "logout");
+                }
+                this.loading(false);
+                await this.updateXraySetting();
+            },
+            generateRandomString(length) {
+                var chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";
+                let randomString = "";
+                for (let i = 0; i < length; i++) {
+                    randomString += chars[Math.floor(Math.random() * chars.length)];
+                }
+                return randomString;
+            },
+            async getNewSecret() {
+                this.loading(true);
+                await PromiseUtil.sleep(600);
+                const newSecret = this.generateRandomString(64);
+                this.user.loginSecret = newSecret;
+                document.getElementById("token").textContent = newSecret;
+                this.loading(false);
+            },
+            async toggleToken(value) {
+                if (value) {
+                await this.getNewSecret();
+                } else {
+                this.user.loginSecret = "";
+                }
+            },
+            async resetXrayConfigToDefault() {
+                this.loading(true);
+                const msg = await HttpUtil.get("/panel/setting/getDefaultJsonConfig");
+                this.loading(false);
+                if (msg.success) {
+                    this.templateSettings = JSON.parse(JSON.stringify(msg.obj, null, 2));
+                    this.saveBtnDisable = true;
+                }
+            },
+            syncRulesWithOutbound(tag, setting) {
+                const newTemplateSettings = {...this.templateSettings};
+                const haveRules = newTemplateSettings.routing.rules.some((r) => r?.outboundTag === tag);
+                const outboundIndex = newTemplateSettings.outbounds.findIndex((o) => o.tag === tag);
+                if (!haveRules && outboundIndex >= 0) {
+                    newTemplateSettings.outbounds.splice(outboundIndex, 1);
+                }
+                if (haveRules && outboundIndex === -1) {
+                    newTemplateSettings.outbounds.push(setting);
+                }
+                this.templateSettings = newTemplateSettings;
+            },
+            templateRuleGetter(routeSettings) {
+                const { property, outboundTag } = routeSettings;
+                let result = [];
+                if (this.templateSettings != null) {
+                    this.templateSettings.routing.rules.forEach(
+                        (routingRule) => {
+                            if (
+                                routingRule.hasOwnProperty(property) &&
+                                routingRule.hasOwnProperty("outboundTag") &&
+                                routingRule.outboundTag === outboundTag
+                            ) {
+                                result.push(...routingRule[property]);
+                            }
+                        }
+                    );
+                }
+                return result;
+            },
+            templateRuleSetter(routeSettings) {
+                const { data, property, outboundTag } = routeSettings;
+                const oldTemplateSettings = this.templateSettings;
+                const newTemplateSettings = oldTemplateSettings;
+                currentProperty = this.templateRuleGetter({ outboundTag, property })
+                if (currentProperty.length == 0) {
+                    const propertyRule = {
+                        type: "field",
+                        outboundTag,
+                        [property]: data
+                    };
+                    newTemplateSettings.routing.rules.push(propertyRule);
+                }
+                else {
+                    const newRules = [];
+                    insertedOnce = false;
+                    newTemplateSettings.routing.rules.forEach(
+                        (routingRule) => {
+                            if (
+                                routingRule.hasOwnProperty(property) &&
+                                routingRule.hasOwnProperty("outboundTag") &&
+                                routingRule.outboundTag === outboundTag
+                            ) {
+                                if (!insertedOnce && data.length > 0) {
+                                    insertedOnce = true;
+                                    routingRule[property] = data;
+                                    newRules.push(routingRule);
+                                }
+                            }
+                            else {
+                                newRules.push(routingRule);
+                            }
+                        }
+                    );
+                    newTemplateSettings.routing.rules = newRules;
+                }
+                this.templateSettings = newTemplateSettings;
+            }
+        },
+        async mounted() {
+            await this.getXraySetting();
+            while (true) {
+                await PromiseUtil.sleep(600);
+                this.saveBtnDisable = this.oldXraySetting === this.xraySetting;
+            }
+        },
+        computed: {
+            templateSettings: {
+                get: function () { return this.xraySetting ? JSON.parse(this.xraySetting) : null; },
+                set: function (newValue) { this.xraySetting = JSON.stringify(newValue, null, 2); },
+            },
+            inboundSettings: {
+                get: function () { return this.templateSettings ? JSON.stringify(this.templateSettings.inbounds, null, 2) : null; },
+                set: function (newValue) {
+                    newTemplateSettings = this.templateSettings;
+                    newTemplateSettings.inbounds = JSON.parse(newValue);
+                    this.templateSettings = newTemplateSettings;
+                },
+            },
+            outboundSettings: {
+                get: function () { return this.templateSettings ? JSON.stringify(this.templateSettings.outbounds, null, 2) : null; },
+                set: function (newValue) {
+                    newTemplateSettings = this.templateSettings;
+                    newTemplateSettings.outbounds = JSON.parse(newValue);
+                    this.templateSettings = newTemplateSettings;
+                },
+            },
+            routingRuleSettings: {
+                get: function () { return this.templateSettings ? JSON.stringify(this.templateSettings.routing.rules, null, 2) : null; },
+                set: function (newValue) {
+                    newTemplateSettings = this.templateSettings;
+                    newTemplateSettings.routing.rules = JSON.parse(newValue);
+                    this.templateSettings = newTemplateSettings;
+                },
+            },
+            freedomStrategy: {
+                get: function () {
+                    if (!this.templateSettings) return "AsIs";
+                    freedomOutbound = this.templateSettings.outbounds.find((o) => o.protocol === "freedom" && !o.tag);
+                    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) {
+                        newTemplateSettings.outbounds[freedomOutboundIndex].settings = {"domainStrategy": newValue};
+                    } else {
+                        newTemplateSettings.outbounds[freedomOutboundIndex].settings.domainStrategy = newValue;
+                    }
+                    this.templateSettings = newTemplateSettings;
+                }
+            },
+            routingStrategy: {
+                get: function () {
+                    if (!this.templateSettings || !this.templateSettings.routing || !this.templateSettings.routing.domainStrategy) return "AsIs";
+                    return this.templateSettings.routing.domainStrategy;
+                },
+                set: function (newValue) {
+                    newTemplateSettings = this.templateSettings;
+                    newTemplateSettings.routing.domainStrategy = newValue;
+                    this.templateSettings = newTemplateSettings;
+                }
+            },
+            blockedIPs: {
+                get: function () {
+                    return this.templateRuleGetter({ outboundTag: "blocked", property: "ip" });
+                },
+                set: function (newValue) {
+                    this.templateRuleSetter({ outboundTag: "blocked", property: "ip", data: newValue });
+                }
+            },
+            blockedDomains: {
+                get: function () {
+                    return this.templateRuleGetter({ outboundTag: "blocked", property: "domain" });
+                },
+                set: function (newValue) {
+                    this.templateRuleSetter({ outboundTag: "blocked", property: "domain", data: newValue });
+                }
+            },
+            blockedProtocols: {
+                get: function () {
+                    return this.templateRuleGetter({ outboundTag: "blocked", property: "protocol" });
+                },
+                set: function (newValue) {
+                    this.templateRuleSetter({ outboundTag: "blocked", property: "protocol", data: newValue });
+                }
+            },
+            directIPs: {
+                get: function () {
+                    return this.templateRuleGetter({ outboundTag: "direct", property: "ip" });
+                },
+                set: function (newValue) {
+                    this.templateRuleSetter({ outboundTag: "direct", property: "ip", data: newValue });
+                    this.syncRulesWithOutbound("direct", this.directSettings);
+                }
+            },
+            directDomains: {
+                get: function () {
+                    return this.templateRuleGetter({ outboundTag: "direct", property: "domain" });
+                },
+                set: function (newValue) {
+                    this.templateRuleSetter({ outboundTag: "direct", property: "domain", data: newValue });
+                    this.syncRulesWithOutbound("direct", this.directSettings);
+                }
+            },
+            ipv4Domains: {
+                get: function () {
+                    return this.templateRuleGetter({ outboundTag: "IPv4", property: "domain" });
+                },
+                set: function (newValue) {
+                    this.templateRuleSetter({ outboundTag: "IPv4", property: "domain", data: newValue });
+                    this.syncRulesWithOutbound("IPv4", this.ipv4Settings);
+                }
+            },
+            warpDomains: {
+                get: function () {
+                    return this.templateRuleGetter({ outboundTag: "WARP", property: "domain" });
+                },
+                set: function (newValue) {
+                    this.templateRuleSetter({ outboundTag: "WARP", property: "domain", data: newValue });
+                    this.syncRulesWithOutbound("WARP", this.warpSettings);
+                }
+            },
+            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)
+            },
+            manualWARPDomains: {
+                get: function () { return JSON.stringify(this.warpDomains, null, 2); },
+                set: debounce(function (value) { this.warpDomains = JSON.parse(value); }, 1000)
+            },
+            torrentSettings: {
+                get: function () {
+                    return doAllItemsExist(this.settingsData.protocols.bittorrent, this.blockedProtocols);
+                },
+                set: function (newValue) {
+                    if (newValue) {
+                        this.blockedProtocols = [...this.blockedProtocols, ...this.settingsData.protocols.bittorrent];
+                    } else {
+                        this.blockedProtocols = this.blockedProtocols.filter(data => !this.settingsData.protocols.bittorrent.includes(data));
+                    }
+                },
+            },
+            privateIpSettings: {
+                get: function () {
+                    return doAllItemsExist(this.settingsData.ips.local, this.blockedIPs);
+                },
+                set: function (newValue) {
+                    if (newValue) {
+                        this.blockedIPs = [...this.blockedIPs, ...this.settingsData.ips.local];
+                    } else {
+                        this.blockedIPs = this.blockedIPs.filter(data => !this.settingsData.ips.local.includes(data));
+                    }
+                },
+            },
+            AdsSettings: {
+                get: function () {
+                    return doAllItemsExist(this.settingsData.domains.ads, this.blockedDomains);
+                },
+                set: function (newValue) {
+                    if (newValue) {
+                        this.blockedDomains = [...this.blockedDomains, ...this.settingsData.domains.ads];
+                    } else {
+                        this.blockedDomains = this.blockedDomains.filter(data => !this.settingsData.domains.ads.includes(data));
+                    }
+                },
+            },
+            SpeedTestSettings: {
+                get: function () {
+                    return doAllItemsExist(this.settingsData.domains.speedtest, this.blockedDomains);
+                },
+                set: function (newValue) {
+                    if (newValue) {
+                        this.blockedDomains = [...this.blockedDomains, ...this.settingsData.domains.speedtest];
+                    } else {
+                        this.blockedDomains = this.blockedDomains.filter(data => !this.settingsData.domains.speedtest.includes(data));
+                    }
+                },
+            },
+            familyProtectSettings: {
+                get: function () {
+                    if (!this.templateSettings || !this.templateSettings.dns || !this.templateSettings.dns.servers) return false;
+                    return doAllItemsExist(this.templateSettings.dns.servers, this.settingsData.familyProtectDNS.servers);
+                },
+                set: function (newValue) {
+                    newTemplateSettings = this.templateSettings;
+                    if (newValue) {
+                        newTemplateSettings.dns = this.settingsData.familyProtectDNS;
+                    } else {
+                        delete newTemplateSettings.dns;
+                    }
+                    this.templateSettings = newTemplateSettings;
+                },
+            },
+            GoogleIPv4Settings: {
+                get: function () {
+                    return doAllItemsExist(this.settingsData.domains.google, this.ipv4Domains);
+                },
+                set: function (newValue) {
+                    if (newValue) {
+                        this.ipv4Domains = [...this.ipv4Domains, ...this.settingsData.domains.google];
+                    } else {
+                        this.ipv4Domains = this.ipv4Domains.filter(data => !this.settingsData.domains.google.includes(data));
+                    }
+                },
+            },
+            NetflixIPv4Settings: {
+                get: function () {
+                    return doAllItemsExist(this.settingsData.domains.netflix, this.ipv4Domains);
+                },
+                set: function (newValue) {
+                    if (newValue) {
+                        this.ipv4Domains = [...this.ipv4Domains, ...this.settingsData.domains.netflix];
+                    } else {
+                        this.ipv4Domains = this.ipv4Domains.filter(data => !this.settingsData.domains.netflix.includes(data));
+                    }
+                },
+            },
+            IRIpSettings: {
+                get: function () {
+                    return doAllItemsExist(this.settingsData.ips.ir, this.blockedIPs);
+                },
+                set: function (newValue) {
+                    if (newValue) {
+                        this.blockedIPs = [...this.blockedIPs, ...this.settingsData.ips.ir];
+                    } else {
+                        this.blockedIPs = this.blockedIPs.filter(data => !this.settingsData.ips.ir.includes(data));
+                    }
+                }
+            },
+            IRDomainSettings: {
+                get: function () {
+                    return doAllItemsExist(this.settingsData.domains.ir, this.blockedDomains);
+                },
+                set: function (newValue) {
+                    if (newValue) {
+                        this.blockedDomains = [...this.blockedDomains, ...this.settingsData.domains.ir];
+                    } else {
+                        this.blockedDomains = this.blockedDomains.filter(data => !this.settingsData.domains.ir.includes(data));
+                    }
+                }
+            },
+            ChinaIpSettings: {
+                get: function () {
+                    return doAllItemsExist(this.settingsData.ips.cn, this.blockedIPs);
+                },
+                set: function (newValue) {
+                    if (newValue) {
+                        this.blockedIPs = [...this.blockedIPs, ...this.settingsData.ips.cn];
+                    } else {
+                        this.blockedIPs = this.blockedIPs.filter(data => !this.settingsData.ips.cn.includes(data));
+                    }
+                }
+            },
+            ChinaDomainSettings: {
+                get: function () {
+                    return doAllItemsExist(this.settingsData.domains.cn, this.blockedDomains);
+                },
+                set: function (newValue) {
+                    if (newValue) {
+                        this.blockedDomains = [...this.blockedDomains, ...this.settingsData.domains.cn];
+                    } else {
+                        this.blockedDomains = this.blockedDomains.filter(data => !this.settingsData.domains.cn.includes(data));
+                    }
+                }
+            },
+            RussiaIpSettings: {
+                get: function () {
+                    return doAllItemsExist(this.settingsData.ips.ru, this.blockedIPs);
+                },
+                set: function (newValue) {
+                    if (newValue) {
+                        this.blockedIPs = [...this.blockedIPs, ...this.settingsData.ips.ru];
+                    } else {
+                        this.blockedIPs = this.blockedIPs.filter(data => !this.settingsData.ips.ru.includes(data));
+                    }
+                }
+            },
+            RussiaDomainSettings: {
+                get: function () {
+                    return doAllItemsExist(this.settingsData.domains.ru, this.blockedDomains);
+                },
+                set: function (newValue) {
+                    if (newValue) {
+                        this.blockedDomains = [...this.blockedDomains, ...this.settingsData.domains.ru];
+                    } else {
+                        this.blockedDomains = this.blockedDomains.filter(data => !this.settingsData.domains.ru.includes(data));
+                    }
+                }
+            },
+            IRIpDirectSettings: {
+                get: function () {
+                    return doAllItemsExist(this.settingsData.ips.ir, this.directIPs);
+                },
+                set: function (newValue) {
+                    if (newValue) {
+                        this.directIPs = [...this.directIPs, ...this.settingsData.ips.ir];
+                    } else {
+                        this.directIPs = this.directIPs.filter(data => !this.settingsData.ips.ir.includes(data));
+                    }
+                }
+            },
+            IRDomainDirectSettings: {
+                get: function () {
+                    return doAllItemsExist(this.settingsData.domains.ir, this.directDomains);
+                },
+                set: function (newValue) {
+                    if (newValue) {
+                        this.directDomains = [...this.directDomains, ...this.settingsData.domains.ir];
+                    } else {
+                        this.directDomains = this.directDomains.filter(data => !this.settingsData.domains.ir.includes(data));
+                    }
+                }
+            },
+            ChinaIpDirectSettings: {
+                get: function () {
+                    return doAllItemsExist(this.settingsData.ips.cn, this.directIPs);
+                },
+                set: function (newValue) {
+                    if (newValue) {
+                        this.directIPs = [...this.directIPs, ...this.settingsData.ips.cn];
+                    } else {
+                        this.directIPs = this.directIPs.filter(data => !this.settingsData.ips.cn.includes(data));
+                    }
+                }
+            },
+            ChinaDomainDirectSettings: {
+                get: function () {
+                    return doAllItemsExist(this.settingsData.domains.cn, this.directDomains);
+                },
+                set: function (newValue) {
+                    if (newValue) {
+                        this.directDomains = [...this.directDomains, ...this.settingsData.domains.cn];
+                    } else {
+                        this.directDomains = this.directDomains.filter(data => !this.settingsData.domains.cn.includes(data));
+                    }
+                }
+            },
+            RussiaIpDirectSettings: {
+                get: function () {
+                    return doAllItemsExist(this.settingsData.ips.ru, this.directIPs);
+                },
+                set: function (newValue) {
+                    if (newValue) {
+                        this.directIPs = [...this.directIPs, ...this.settingsData.ips.ru];
+                    } else {
+                        this.directIPs = this.directIPs.filter(data => !this.settingsData.ips.ru.includes(data));
+                    }
+                }
+            },
+            RussiaDomainDirectSettings: {
+                get: function () {
+                    return doAllItemsExist(this.settingsData.domains.ru, this.directDomains);
+                },
+                set: function (newValue) {
+                    if (newValue) {
+                        this.directDomains = [...this.directDomains, ...this.settingsData.domains.ru];
+                    } else {
+                        this.directDomains = this.directDomains.filter(data => !this.settingsData.domains.ru.includes(data));
+                    }
+                }
+            },
+            GoogleWARPSettings: {
+                get: function () {
+                    return doAllItemsExist(this.settingsData.domains.google, this.warpDomains);
+                },
+                set: function (newValue) {
+                    if (newValue) {
+                        this.warpDomains = [...this.warpDomains, ...this.settingsData.domains.google];
+                    } else {
+                        this.warpDomains = this.warpDomains.filter(data => !this.settingsData.domains.google.includes(data));
+                    }
+                },
+            },
+            OpenAIWARPSettings: {
+                get: function () {
+                    return doAllItemsExist(this.settingsData.domains.openai, this.warpDomains);
+                },
+                set: function (newValue) {
+                    if (newValue) {
+                        this.warpDomains = [...this.warpDomains, ...this.settingsData.domains.openai];
+                    } else {
+                        this.warpDomains = this.warpDomains.filter(data => !this.settingsData.domains.openai.includes(data));
+                    }
+                },
+            },
+            NetflixWARPSettings: {
+                get: function () {
+                    return doAllItemsExist(this.settingsData.domains.netflix, this.warpDomains);
+                },
+                set: function (newValue) {
+                    if (newValue) {
+                        this.warpDomains = [...this.warpDomains, ...this.settingsData.domains.netflix];
+                    } else {
+                        this.warpDomains = this.warpDomains.filter(data => !this.settingsData.domains.netflix.includes(data));
+                    }
+                },
+            },
+            SpotifyWARPSettings: {
+                get: function () {
+                    return doAllItemsExist(this.settingsData.domains.spotify, this.warpDomains);
+                },
+                set: function (newValue) {
+                    if (newValue) {
+                        this.warpDomains = [...this.warpDomains, ...this.settingsData.domains.spotify];
+                    } else {
+                        this.warpDomains = this.warpDomains.filter(data => !this.settingsData.domains.spotify.includes(data));
+                    }
+                },
+            },
+        },
+    });
+</script>
+</body>
+</html>