mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2025-10-27 02:24:40 +00:00
Compare commits
7 commits
b2ad033bfc
...
56d1b75021
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
56d1b75021 | ||
|
|
b46a0b404b | ||
|
|
0ce58a095a | ||
|
|
59ea2645db | ||
|
|
8c8d280f14 | ||
|
|
c720008187 | ||
|
|
170d24499e |
31 changed files with 233 additions and 62 deletions
35
.vscode/launch.json
vendored
Normal file
35
.vscode/launch.json
vendored
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
{
|
||||||
|
"$schema": "vscode://schemas/launch",
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Run 3x-ui (Debug)",
|
||||||
|
"type": "go",
|
||||||
|
"request": "launch",
|
||||||
|
"mode": "auto",
|
||||||
|
"program": "${workspaceFolder}",
|
||||||
|
"cwd": "${workspaceFolder}",
|
||||||
|
"env": {
|
||||||
|
"XUI_DEBUG": "true"
|
||||||
|
},
|
||||||
|
"console": "integratedTerminal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Run 3x-ui (Debug, custom env)",
|
||||||
|
"type": "go",
|
||||||
|
"request": "launch",
|
||||||
|
"mode": "auto",
|
||||||
|
"program": "${workspaceFolder}",
|
||||||
|
"cwd": "${workspaceFolder}",
|
||||||
|
"env": {
|
||||||
|
// Set to true to serve assets/templates directly from disk for development
|
||||||
|
"XUI_DEBUG": "true",
|
||||||
|
// Uncomment to override DB folder location (by default uses working dir on Windows when debug)
|
||||||
|
// "XUI_DB_FOLDER": "${workspaceFolder}",
|
||||||
|
// Example: override log level (debug|info|notice|warn|error)
|
||||||
|
// "XUI_LOG_LEVEL": "debug"
|
||||||
|
},
|
||||||
|
"console": "integratedTerminal"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
40
.vscode/tasks.json
vendored
Normal file
40
.vscode/tasks.json
vendored
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
{
|
||||||
|
"version": "2.0.0",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"label": "go: build",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "go",
|
||||||
|
"args": ["build", "-o", "bin/3x-ui.exe", "./main.go"],
|
||||||
|
"options": {
|
||||||
|
"cwd": "${workspaceFolder}"
|
||||||
|
},
|
||||||
|
"problemMatcher": ["$go"],
|
||||||
|
"group": { "kind": "build", "isDefault": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "go: run",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "go",
|
||||||
|
"args": ["run", "./main.go"],
|
||||||
|
"options": {
|
||||||
|
"cwd": "${workspaceFolder}",
|
||||||
|
"env": {
|
||||||
|
"XUI_DEBUG": "true"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"problemMatcher": ["$go"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "go: test",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "go",
|
||||||
|
"args": ["test", "./..."],
|
||||||
|
"options": {
|
||||||
|
"cwd": "${workspaceFolder}"
|
||||||
|
},
|
||||||
|
"problemMatcher": ["$go"],
|
||||||
|
"group": "test"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
12
sub/sub.go
12
sub/sub.go
|
|
@ -30,7 +30,7 @@ func setEmbeddedTemplates(engine *gin.Engine) error {
|
||||||
webpkg.EmbeddedHTML(),
|
webpkg.EmbeddedHTML(),
|
||||||
"html/common/page.html",
|
"html/common/page.html",
|
||||||
"html/component/aThemeSwitch.html",
|
"html/component/aThemeSwitch.html",
|
||||||
"html/subscription.html",
|
"html/settings/panel/subscription/subpage.html",
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -85,6 +85,12 @@ func (s *Server) initRouter() (*gin.Engine, error) {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Determine if JSON subscription endpoint is enabled
|
||||||
|
subJsonEnable, err := s.settingService.GetSubJsonEnable()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
// Set base_path based on LinksPath for template rendering
|
// Set base_path based on LinksPath for template rendering
|
||||||
engine.Use(func(c *gin.Context) {
|
engine.Use(func(c *gin.Context) {
|
||||||
c.Set("base_path", LinksPath)
|
c.Set("base_path", LinksPath)
|
||||||
|
|
@ -186,7 +192,7 @@ func (s *Server) initRouter() (*gin.Engine, error) {
|
||||||
g := engine.Group("/")
|
g := engine.Group("/")
|
||||||
|
|
||||||
s.sub = NewSUBController(
|
s.sub = NewSUBController(
|
||||||
g, LinksPath, JsonPath, Encrypt, ShowInfo, RemarkModel, SubUpdates,
|
g, LinksPath, JsonPath, subJsonEnable, Encrypt, ShowInfo, RemarkModel, SubUpdates,
|
||||||
SubJsonFragment, SubJsonNoises, SubJsonMux, SubJsonRules, SubTitle)
|
SubJsonFragment, SubJsonNoises, SubJsonMux, SubJsonRules, SubTitle)
|
||||||
|
|
||||||
return engine, nil
|
return engine, nil
|
||||||
|
|
@ -207,7 +213,7 @@ func (s *Server) getHtmlFiles() ([]string, error) {
|
||||||
files = append(files, theme)
|
files = append(files, theme)
|
||||||
}
|
}
|
||||||
// page itself
|
// page itself
|
||||||
page := filepath.Join(dir, "web", "html", "subscription.html")
|
page := filepath.Join(dir, "web", "html", "subpage.html")
|
||||||
if _, err := os.Stat(page); err == nil {
|
if _, err := os.Stat(page); err == nil {
|
||||||
files = append(files, page)
|
files = append(files, page)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ type SUBController struct {
|
||||||
subTitle string
|
subTitle string
|
||||||
subPath string
|
subPath string
|
||||||
subJsonPath string
|
subJsonPath string
|
||||||
|
jsonEnabled bool
|
||||||
subEncrypt bool
|
subEncrypt bool
|
||||||
updateInterval string
|
updateInterval string
|
||||||
|
|
||||||
|
|
@ -24,6 +25,7 @@ func NewSUBController(
|
||||||
g *gin.RouterGroup,
|
g *gin.RouterGroup,
|
||||||
subPath string,
|
subPath string,
|
||||||
jsonPath string,
|
jsonPath string,
|
||||||
|
jsonEnabled bool,
|
||||||
encrypt bool,
|
encrypt bool,
|
||||||
showInfo bool,
|
showInfo bool,
|
||||||
rModel string,
|
rModel string,
|
||||||
|
|
@ -39,6 +41,7 @@ func NewSUBController(
|
||||||
subTitle: subTitle,
|
subTitle: subTitle,
|
||||||
subPath: subPath,
|
subPath: subPath,
|
||||||
subJsonPath: jsonPath,
|
subJsonPath: jsonPath,
|
||||||
|
jsonEnabled: jsonEnabled,
|
||||||
subEncrypt: encrypt,
|
subEncrypt: encrypt,
|
||||||
updateInterval: update,
|
updateInterval: update,
|
||||||
|
|
||||||
|
|
@ -51,10 +54,11 @@ func NewSUBController(
|
||||||
|
|
||||||
func (a *SUBController) initRouter(g *gin.RouterGroup) {
|
func (a *SUBController) initRouter(g *gin.RouterGroup) {
|
||||||
gLink := g.Group(a.subPath)
|
gLink := g.Group(a.subPath)
|
||||||
gJson := g.Group(a.subJsonPath)
|
|
||||||
|
|
||||||
gLink.GET(":subid", a.subs)
|
gLink.GET(":subid", a.subs)
|
||||||
gJson.GET(":subid", a.subJsons)
|
if a.jsonEnabled {
|
||||||
|
gJson := g.Group(a.subJsonPath)
|
||||||
|
gJson.GET(":subid", a.subJsons)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *SUBController) subs(c *gin.Context) {
|
func (a *SUBController) subs(c *gin.Context) {
|
||||||
|
|
@ -74,8 +78,11 @@ func (a *SUBController) subs(c *gin.Context) {
|
||||||
if strings.Contains(strings.ToLower(accept), "text/html") || c.Query("html") == "1" || strings.EqualFold(c.Query("view"), "html") {
|
if strings.Contains(strings.ToLower(accept), "text/html") || c.Query("html") == "1" || strings.EqualFold(c.Query("view"), "html") {
|
||||||
// Build page data in service
|
// Build page data in service
|
||||||
subURL, subJsonURL := a.subService.BuildURLs(scheme, hostWithPort, a.subPath, a.subJsonPath, subId)
|
subURL, subJsonURL := a.subService.BuildURLs(scheme, hostWithPort, a.subPath, a.subJsonPath, subId)
|
||||||
|
if !a.jsonEnabled {
|
||||||
|
subJsonURL = ""
|
||||||
|
}
|
||||||
page := a.subService.BuildPageData(subId, hostHeader, traffic, lastOnline, subs, subURL, subJsonURL)
|
page := a.subService.BuildPageData(subId, hostHeader, traffic, lastOnline, subs, subURL, subJsonURL)
|
||||||
c.HTML(200, "subscription.html", gin.H{
|
c.HTML(200, "subpage.html", gin.H{
|
||||||
"title": "subscription.title",
|
"title": "subscription.title",
|
||||||
"cur_ver": config.GetVersion(),
|
"cur_ver": config.GetVersion(),
|
||||||
"host": page.Host,
|
"host": page.Host,
|
||||||
|
|
|
||||||
|
|
@ -1028,7 +1028,7 @@ func searchHost(headers any) string {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// PageData is a view model for subscription.html
|
// PageData is a view model for subpage.html
|
||||||
type PageData struct {
|
type PageData struct {
|
||||||
Host string
|
Host string
|
||||||
BasePath string
|
BasePath string
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,8 @@ class AllSetting {
|
||||||
this.twoFactorEnable = false;
|
this.twoFactorEnable = false;
|
||||||
this.twoFactorToken = "";
|
this.twoFactorToken = "";
|
||||||
this.xrayTemplateConfig = "";
|
this.xrayTemplateConfig = "";
|
||||||
this.subEnable = false;
|
this.subEnable = true;
|
||||||
|
this.subJsonEnable = false;
|
||||||
this.subTitle = "";
|
this.subTitle = "";
|
||||||
this.subListen = "";
|
this.subListen = "";
|
||||||
this.subPort = 2096;
|
this.subPort = 2096;
|
||||||
|
|
|
||||||
|
|
@ -101,7 +101,10 @@
|
||||||
if (sj) this.app.subJsonUrl = sj;
|
if (sj) this.app.subJsonUrl = sj;
|
||||||
drawQR(this.app.subUrl);
|
drawQR(this.app.subUrl);
|
||||||
try {
|
try {
|
||||||
new QRious({ element: document.getElementById('qrcode-subjson'), value: this.app.subJsonUrl || '', size: 220 });
|
const elJson = document.getElementById('qrcode-subjson');
|
||||||
|
if (elJson && this.app.subJsonUrl) {
|
||||||
|
new QRious({ element: elJson, value: this.app.subJsonUrl, size: 220 });
|
||||||
|
}
|
||||||
} catch (e) { /* ignore */ }
|
} catch (e) { /* ignore */ }
|
||||||
this._onResize = () => { this.viewportWidth = window.innerWidth; };
|
this._onResize = () => { this.viewportWidth = window.innerWidth; };
|
||||||
window.addEventListener('resize', this._onResize);
|
window.addEventListener('resize', this._onResize);
|
||||||
|
|
@ -127,6 +130,18 @@
|
||||||
const base64Url = btoa(rawUrl);
|
const base64Url = btoa(rawUrl);
|
||||||
const remark = encodeURIComponent(this.app.sId || 'Subscription');
|
const remark = encodeURIComponent(this.app.sId || 'Subscription');
|
||||||
return `shadowrocket://add/sub/${base64Url}?remark=${remark}`;
|
return `shadowrocket://add/sub/${base64Url}?remark=${remark}`;
|
||||||
|
},
|
||||||
|
v2boxUrl() {
|
||||||
|
return `v2box://install-sub?url=${encodeURIComponent(this.app.subUrl)}&name=${encodeURIComponent(this.app.sId)}`;
|
||||||
|
},
|
||||||
|
streisandUrl() {
|
||||||
|
return `streisand://import/${encodeURIComponent(this.app.subUrl)}`;
|
||||||
|
},
|
||||||
|
v2raytunUrl() {
|
||||||
|
return this.app.subUrl;
|
||||||
|
},
|
||||||
|
npvtunUrl() {
|
||||||
|
return this.app.subUrl;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ type AllSetting struct {
|
||||||
TwoFactorEnable bool `json:"twoFactorEnable" form:"twoFactorEnable"`
|
TwoFactorEnable bool `json:"twoFactorEnable" form:"twoFactorEnable"`
|
||||||
TwoFactorToken string `json:"twoFactorToken" form:"twoFactorToken"`
|
TwoFactorToken string `json:"twoFactorToken" form:"twoFactorToken"`
|
||||||
SubEnable bool `json:"subEnable" form:"subEnable"`
|
SubEnable bool `json:"subEnable" form:"subEnable"`
|
||||||
|
SubJsonEnable bool `json:"subJsonEnable" form:"subJsonEnable"`
|
||||||
SubTitle string `json:"subTitle" form:"subTitle"`
|
SubTitle string `json:"subTitle" form:"subTitle"`
|
||||||
SubListen string `json:"subListen" form:"subListen"`
|
SubListen string `json:"subListen" form:"subListen"`
|
||||||
SubPort int `json:"subPort" form:"subPort"`
|
SubPort int `json:"subPort" form:"subPort"`
|
||||||
|
|
|
||||||
|
|
@ -49,9 +49,9 @@
|
||||||
<a-space direction="horizontal" :size="2">
|
<a-space direction="horizontal" :size="2">
|
||||||
<a-tooltip>
|
<a-tooltip>
|
||||||
<template slot="title">
|
<template slot="title">
|
||||||
<template v-if="isClientDepleted">{{ i18n "depleted" }}</template>
|
<template v-if="isClientDepleted(record, client.email)">{{ i18n "depleted" }}</template>
|
||||||
<template v-if="!isClientDepleted && !client.enable">{{ i18n "disabled" }}</template>
|
<template v-else-if="!client.enable">{{ i18n "disabled" }}</template>
|
||||||
<template v-if="client.enable && isClientOnline(client.email)">{{ i18n "online" }}</template>
|
<template v-else-if="client.enable && isClientOnline(client.email)">{{ i18n "online" }}</template>
|
||||||
</template>
|
</template>
|
||||||
<a-badge :class="isClientOnline(client.email)? 'online-animation' : ''" :color="client.enable ? statsExpColor(record, client.email) : themeSwitcher.isDarkTheme ? '#2c3950' : '#bcbcbc'"></a-badge>
|
<a-badge :class="isClientOnline(client.email)? 'online-animation' : ''" :color="client.enable ? statsExpColor(record, client.email) : themeSwitcher.isDarkTheme ? '#2c3950' : '#bcbcbc'"></a-badge>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
|
|
@ -90,7 +90,7 @@
|
||||||
<a-progress :stroke-color="themeSwitcher.isDarkTheme ? 'rgb(72 84 105)' : '#bcbcbc'" :show-info="false" :percent="statsProgress(record, client.email)" />
|
<a-progress :stroke-color="themeSwitcher.isDarkTheme ? 'rgb(72 84 105)' : '#bcbcbc'" :show-info="false" :percent="statsProgress(record, client.email)" />
|
||||||
</td>
|
</td>
|
||||||
<td class="tr-table-bar" v-else-if="client.totalGB > 0">
|
<td class="tr-table-bar" v-else-if="client.totalGB > 0">
|
||||||
<a-progress :stroke-color="clientStatsColor(record, client.email)" :show-info="false" :status="isClientEnabled(record, client.email)? 'exception' : ''" :percent="statsProgress(record, client.email)" />
|
<a-progress :stroke-color="clientStatsColor(record, client.email)" :show-info="false" :status="isClientDepleted(record, client.email)? 'exception' : ''" :percent="statsProgress(record, client.email)" />
|
||||||
</td>
|
</td>
|
||||||
<td v-else class="infinite-bar tr-table-bar">
|
<td v-else class="infinite-bar tr-table-bar">
|
||||||
<a-progress :show-info="false" :percent="100"></a-progress>
|
<a-progress :show-info="false" :percent="100"></a-progress>
|
||||||
|
|
@ -126,7 +126,7 @@
|
||||||
<tr class="tr-table-box">
|
<tr class="tr-table-box">
|
||||||
<td class="tr-table-rt"> [[ remainedDays(client.expiryTime) ]] </td>
|
<td class="tr-table-rt"> [[ remainedDays(client.expiryTime) ]] </td>
|
||||||
<td class="infinite-bar tr-table-bar">
|
<td class="infinite-bar tr-table-bar">
|
||||||
<a-progress :show-info="false" :status="isClientEnabled(record, client.email)? 'exception' : ''" :percent="expireProgress(client.expiryTime, client.reset)" />
|
<a-progress :show-info="false" :status="isClientDepleted(record, client.email)? 'exception' : ''" :percent="expireProgress(client.expiryTime, client.reset)" />
|
||||||
</td>
|
</td>
|
||||||
<td class="tr-table-lt">[[ client.reset + "d" ]]</td>
|
<td class="tr-table-lt">[[ client.reset + "d" ]]</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -213,7 +213,7 @@
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</template>
|
</template>
|
||||||
<a-progress :stroke-color="clientStatsColor(record, client.email)" :show-info="false" :status="isClientEnabled(record, client.email)? 'exception' : ''" :percent="statsProgress(record, client.email)" />
|
<a-progress :stroke-color="clientStatsColor(record, client.email)" :show-info="false" :status="isClientDepleted(record, client.email)? 'exception' : ''" :percent="statsProgress(record, client.email)" />
|
||||||
</a-popover>
|
</a-popover>
|
||||||
</td>
|
</td>
|
||||||
<td width="120px" v-else class="infinite-bar">
|
<td width="120px" v-else class="infinite-bar">
|
||||||
|
|
@ -247,7 +247,7 @@
|
||||||
</template>
|
</template>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
<a-progress :show-info="false" :status="isClientEnabled(record, client.email)? 'exception' : ''" :percent="expireProgress(client.expiryTime, client.reset)" />
|
<a-progress :show-info="false" :status="isClientDepleted(record, client.email)? 'exception' : ''" :percent="expireProgress(client.expiryTime, client.reset)" />
|
||||||
</a-popover>
|
</a-popover>
|
||||||
</td>
|
</td>
|
||||||
<td width="60px">[[ client.reset + "d" ]]</td>
|
<td width="60px">[[ client.reset + "d" ]]</td>
|
||||||
|
|
|
||||||
|
|
@ -736,10 +736,11 @@
|
||||||
refreshing: false,
|
refreshing: false,
|
||||||
refreshInterval: Number(localStorage.getItem("refreshInterval")) || 5000,
|
refreshInterval: Number(localStorage.getItem("refreshInterval")) || 5000,
|
||||||
subSettings: {
|
subSettings: {
|
||||||
enable: false,
|
enable: true,
|
||||||
subTitle: '',
|
subTitle: '',
|
||||||
subURI: '',
|
subURI: '',
|
||||||
subJsonURI: '',
|
subJsonURI: '',
|
||||||
|
subJsonEnable: false,
|
||||||
},
|
},
|
||||||
remarkModel: '-ieo',
|
remarkModel: '-ieo',
|
||||||
datepicker: 'gregorian',
|
datepicker: 'gregorian',
|
||||||
|
|
@ -795,7 +796,8 @@
|
||||||
enable: subEnable,
|
enable: subEnable,
|
||||||
subTitle: subTitle,
|
subTitle: subTitle,
|
||||||
subURI: subURI,
|
subURI: subURI,
|
||||||
subJsonURI: subJsonURI
|
subJsonURI: subJsonURI,
|
||||||
|
subJsonEnable: subJsonEnable,
|
||||||
};
|
};
|
||||||
this.pageSize = pageSize;
|
this.pageSize = pageSize;
|
||||||
this.remarkModel = remarkModel;
|
this.remarkModel = remarkModel;
|
||||||
|
|
@ -1432,15 +1434,19 @@
|
||||||
clientStats = dbInbound.clientStats ? dbInbound.clientStats.find(stats => stats.email === email) : null;
|
clientStats = dbInbound.clientStats ? dbInbound.clientStats.find(stats => stats.email === email) : null;
|
||||||
return clientStats ? clientStats['enable'] : true;
|
return clientStats ? clientStats['enable'] : true;
|
||||||
},
|
},
|
||||||
// Returns true when client's traffic is exhausted or expiry time is passed
|
|
||||||
isClientDepleted(dbInbound, email) {
|
isClientDepleted(dbInbound, email) {
|
||||||
if (!email || !dbInbound || !dbInbound.clientStats) return false;
|
if (!email || !dbInbound || !dbInbound.clientStats) return false;
|
||||||
const stats = dbInbound.clientStats.find(s => s.email === email);
|
const stats = dbInbound.clientStats.find(s => s.email === email);
|
||||||
if (!stats) return false;
|
if (!stats) return false;
|
||||||
const now = new Date().getTime();
|
const total = stats.total ?? 0;
|
||||||
const exhausted = stats.total > 0 && (stats.up + stats.down) >= stats.total;
|
const used = (stats.up ?? 0) + (stats.down ?? 0);
|
||||||
const expired = stats.expiryTime > 0 && now >= stats.expiryTime;
|
const hasTotal = total > 0;
|
||||||
return exhausted || expired;
|
const exhausted = hasTotal && used >= total;
|
||||||
|
const expiryTime = stats.expiryTime ?? 0;
|
||||||
|
const hasExpiry = expiryTime > 0;
|
||||||
|
const now = Date.now();
|
||||||
|
const expired = hasExpiry && expiryTime <= now;
|
||||||
|
return expired || exhausted;
|
||||||
},
|
},
|
||||||
isClientOnline(email) {
|
isClientOnline(email) {
|
||||||
return this.onlineClients.includes(email);
|
return this.onlineClients.includes(email);
|
||||||
|
|
|
||||||
|
|
@ -180,9 +180,9 @@
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ i18n "status" }}</td>
|
<td>{{ i18n "status" }}</td>
|
||||||
<td>
|
<td>
|
||||||
<a-tag v-if="isEnable && isActive && !isDepleted" color="green">{{ i18n "enabled" }}</a-tag>
|
|
||||||
<a-tag v-if="!isEnable && !isDepleted">{{ i18n "disabled" }}</a-tag>
|
|
||||||
<a-tag v-if="isDepleted" color="red">{{ i18n "depleted" }}</a-tag>
|
<a-tag v-if="isDepleted" color="red">{{ i18n "depleted" }}</a-tag>
|
||||||
|
<a-tag v-else-if="isEnable" color="green">{{ i18n "enabled" }}</a-tag>
|
||||||
|
<a-tag v-else>{{ i18n "disabled" }}</a-tag>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-if="infoModal.clientStats">
|
<tr v-if="infoModal.clientStats">
|
||||||
|
|
@ -308,7 +308,7 @@
|
||||||
</tr-info-title>
|
</tr-info-title>
|
||||||
<a :href="[[ infoModal.subLink ]]" target="_blank">[[ infoModal.subLink ]]</a>
|
<a :href="[[ infoModal.subLink ]]" target="_blank">[[ infoModal.subLink ]]</a>
|
||||||
</tr-info-row>
|
</tr-info-row>
|
||||||
<tr-info-row class="tr-info-row">
|
<tr-info-row class="tr-info-row" v-if="app.subSettings.subJsonEnable">
|
||||||
<tr-info-title class="tr-info-title">
|
<tr-info-title class="tr-info-title">
|
||||||
<a-tag color="purple">Json Link</a-tag>
|
<a-tag color="purple">Json Link</a-tag>
|
||||||
<a-tooltip title='{{ i18n "copy" }}'>
|
<a-tooltip title='{{ i18n "copy" }}'>
|
||||||
|
|
@ -524,7 +524,7 @@
|
||||||
this.dbInbound = new DBInbound(dbInbound);
|
this.dbInbound = new DBInbound(dbInbound);
|
||||||
this.clientSettings = this.inbound.clients ? this.inbound.clients[index] : null;
|
this.clientSettings = this.inbound.clients ? this.inbound.clients[index] : null;
|
||||||
this.isExpired = this.inbound.clients ? this.inbound.isExpiry(index) : this.dbInbound.isExpiry;
|
this.isExpired = this.inbound.clients ? this.inbound.isExpiry(index) : this.dbInbound.isExpiry;
|
||||||
this.clientStats = this.inbound.clients ? this.dbInbound.clientStats.find(row => row.email === this.clientSettings.email) : [];
|
this.clientStats = this.inbound.clients ? (this.dbInbound.clientStats.find(row => row.email === this.clientSettings.email) || null) : null;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
[
|
[
|
||||||
|
|
@ -548,7 +548,7 @@
|
||||||
if (this.clientSettings) {
|
if (this.clientSettings) {
|
||||||
if (this.clientSettings.subId) {
|
if (this.clientSettings.subId) {
|
||||||
this.subLink = this.genSubLink(this.clientSettings.subId);
|
this.subLink = this.genSubLink(this.clientSettings.subId);
|
||||||
this.subJsonLink = this.genSubJsonLink(this.clientSettings.subId);
|
this.subJsonLink = app.subSettings.subJsonEnable ? this.genSubJsonLink(this.clientSettings.subId) : '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.visible = true;
|
this.visible = true;
|
||||||
|
|
@ -588,11 +588,21 @@
|
||||||
return infoModal.dbInbound.isEnable;
|
return infoModal.dbInbound.isEnable;
|
||||||
},
|
},
|
||||||
get isDepleted() {
|
get isDepleted() {
|
||||||
const stats = this.infoModal.clientStats;
|
const stats = infoModal.clientStats;
|
||||||
if (!stats) return false;
|
const settings = infoModal.clientSettings;
|
||||||
const now = new Date().getTime();
|
if (!stats || !settings) {
|
||||||
const expired = stats.expiryTime > 0 && now >= stats.expiryTime;
|
return false;
|
||||||
const exhausted = stats.total > 0 && (stats.up + stats.down) >= stats.total;
|
}
|
||||||
|
const total = stats.total ?? 0;
|
||||||
|
const used = (stats.up ?? 0) + (stats.down ?? 0);
|
||||||
|
const hasTotal = total > 0;
|
||||||
|
const exhausted = hasTotal && used >= total;
|
||||||
|
|
||||||
|
const expiryTime = settings.expiryTime ?? 0;
|
||||||
|
const hasExpiry = expiryTime > 0;
|
||||||
|
const now = Date.now();
|
||||||
|
const expired = hasExpiry && now >= expiryTime;
|
||||||
|
|
||||||
return expired || exhausted;
|
return expired || exhausted;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@
|
||||||
</tr-qr-bg-inner>
|
</tr-qr-bg-inner>
|
||||||
</tr-qr-bg>
|
</tr-qr-bg>
|
||||||
</tr-qr-box>
|
</tr-qr-box>
|
||||||
<tr-qr-box class="qr-box">
|
<tr-qr-box class="qr-box" v-if="app.subSettings.subJsonEnable">
|
||||||
<a-tag color="purple" class="qr-tag"><span>{{ i18n "pages.settings.subSettings"}} Json</span></a-tag>
|
<a-tag color="purple" class="qr-tag"><span>{{ i18n "pages.settings.subSettings"}} Json</span></a-tag>
|
||||||
<tr-qr-bg class="qr-bg-sub">
|
<tr-qr-bg class="qr-bg-sub">
|
||||||
<tr-qr-bg-inner class="qr-bg-sub-inner">
|
<tr-qr-bg-inner class="qr-bg-sub-inner">
|
||||||
|
|
@ -262,7 +262,9 @@
|
||||||
if (qrModal.client && qrModal.client.subId) {
|
if (qrModal.client && qrModal.client.subId) {
|
||||||
qrModal.subId = qrModal.client.subId;
|
qrModal.subId = qrModal.client.subId;
|
||||||
this.setQrCode("qrCode-sub", this.genSubLink(qrModal.subId));
|
this.setQrCode("qrCode-sub", this.genSubLink(qrModal.subId));
|
||||||
this.setQrCode("qrCode-subJson", this.genSubJsonLink(qrModal.subId));
|
if (app.subSettings.subJsonEnable) {
|
||||||
|
this.setQrCode("qrCode-subJson", this.genSubJsonLink(qrModal.subId));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
qrModal.qrcodes.forEach((element, index) => {
|
qrModal.qrcodes.forEach((element, index) => {
|
||||||
this.setQrCode("qrCode-" + index, element.link);
|
this.setQrCode("qrCode-" + index, element.link);
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,7 @@
|
||||||
</template>
|
</template>
|
||||||
{{ template "settings/panel/subscription/general" . }}
|
{{ template "settings/panel/subscription/general" . }}
|
||||||
</a-tab-pane>
|
</a-tab-pane>
|
||||||
<a-tab-pane key="5" v-if="allSetting.subEnable" :style="{ paddingTop: '20px' }">
|
<a-tab-pane key="5" v-if="allSetting.subJsonEnable" :style="{ paddingTop: '20px' }">
|
||||||
<template #tab>
|
<template #tab>
|
||||||
<a-icon type="code"></a-icon>
|
<a-icon type="code"></a-icon>
|
||||||
<span>{{ i18n "pages.settings.subSettings" }} (JSON)</span>
|
<span>{{ i18n "pages.settings.subSettings" }} (JSON)</span>
|
||||||
|
|
@ -523,6 +523,8 @@
|
||||||
if (this.allSetting.subEnable) {
|
if (this.allSetting.subEnable) {
|
||||||
subPath = this.allSetting.subURI.length > 0 ? new URL(this.allSetting.subURI).pathname : this.allSetting.subPath;
|
subPath = this.allSetting.subURI.length > 0 ? new URL(this.allSetting.subURI).pathname : this.allSetting.subPath;
|
||||||
if (subPath == '/sub/') alerts.push('{{ i18n "secAlertSubURI" }}');
|
if (subPath == '/sub/') alerts.push('{{ i18n "secAlertSubURI" }}');
|
||||||
|
}
|
||||||
|
if (this.allSetting.subJsonEnable) {
|
||||||
subJsonPath = this.allSetting.subJsonURI.length > 0 ? new URL(this.allSetting.subJsonURI).pathname : this.allSetting.subJsonPath;
|
subJsonPath = this.allSetting.subJsonURI.length > 0 ? new URL(this.allSetting.subJsonURI).pathname : this.allSetting.subJsonPath;
|
||||||
if (subJsonPath == '/json/') alerts.push('{{ i18n "secAlertSubJsonURI" }}');
|
if (subJsonPath == '/json/') alerts.push('{{ i18n "secAlertSubJsonURI" }}');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,13 @@
|
||||||
<a-switch v-model="allSetting.subEnable"></a-switch>
|
<a-switch v-model="allSetting.subEnable"></a-switch>
|
||||||
</template>
|
</template>
|
||||||
</a-setting-list-item>
|
</a-setting-list-item>
|
||||||
|
<a-setting-list-item paddings="small">
|
||||||
|
<template #title>JSON Subscription</template>
|
||||||
|
<template #description>{{ i18n "pages.settings.subJsonEnable"}}</template>
|
||||||
|
<template #control>
|
||||||
|
<a-switch v-model="allSetting.subJsonEnable"></a-switch>
|
||||||
|
</template>
|
||||||
|
</a-setting-list-item>
|
||||||
<a-setting-list-item paddings="small">
|
<a-setting-list-item paddings="small">
|
||||||
<template #title>{{ i18n "pages.settings.subTitle"}}</template>
|
<template #title>{{ i18n "pages.settings.subTitle"}}</template>
|
||||||
<template #description>{{ i18n "pages.settings.subTitleDesc"}}</template>
|
<template #description>{{ i18n "pages.settings.subTitleDesc"}}</template>
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@
|
||||||
<a-space direction="vertical" align="center">
|
<a-space direction="vertical" align="center">
|
||||||
<a-row type="flex" :gutter="[8,8]"
|
<a-row type="flex" :gutter="[8,8]"
|
||||||
justify="center" style="width:100%">
|
justify="center" style="width:100%">
|
||||||
<a-col :xs="24" :sm="12"
|
<a-col :xs="24" :sm="app.subJsonUrl ? 12 : 24"
|
||||||
style="text-align:center;">
|
style="text-align:center;">
|
||||||
<tr-qr-box class="qr-box">
|
<tr-qr-box class="qr-box">
|
||||||
<a-tag color="purple"
|
<a-tag color="purple"
|
||||||
|
|
@ -75,7 +75,7 @@
|
||||||
</tr-qr-bg>
|
</tr-qr-bg>
|
||||||
</tr-qr-box>
|
</tr-qr-box>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :xs="24" :sm="12"
|
<a-col v-if="app.subJsonUrl" :xs="24" :sm="12"
|
||||||
style="text-align:center;">
|
style="text-align:center;">
|
||||||
<tr-qr-box class="qr-box">
|
<tr-qr-box class="qr-box">
|
||||||
<a-tag color="purple"
|
<a-tag color="purple"
|
||||||
|
|
@ -235,14 +235,15 @@
|
||||||
<a-menu-item key="ios-shadowrocket"
|
<a-menu-item key="ios-shadowrocket"
|
||||||
@click="open(shadowrocketUrl)">Shadowrocket</a-menu-item>
|
@click="open(shadowrocketUrl)">Shadowrocket</a-menu-item>
|
||||||
<a-menu-item key="ios-v2box"
|
<a-menu-item key="ios-v2box"
|
||||||
@click="open('v2box://install-sub?url=' + encodeURIComponent(app.subUrl) + '&name=' + encodeURIComponent(app.sId))">V2Box</a-menu-item>
|
@click="open(v2boxUrl)">V2Box</a-menu-item>
|
||||||
<a-menu-item key="ios-streisand"
|
<a-menu-item key="ios-streisand"
|
||||||
@click="open('streisand://import/' + encodeURIComponent(app.subUrl))">Streisand</a-menu-item>
|
@click="open(streisandUrl)">Streisand</a-menu-item>
|
||||||
<a-menu-item key="ios-v2raytun"
|
<a-menu-item key="ios-v2raytun"
|
||||||
@click="copy(app.subUrl)">V2RayTun</a-menu-item>
|
@click="copy(v2raytunUrl)">V2RayTun</a-menu-item>
|
||||||
<a-menu-item key="ios-npvtunnel"
|
<a-menu-item key="ios-npvtunnel"
|
||||||
@click="copy(app.subUrl)">NPV
|
@click="copy(npvtunUrl)">NPV
|
||||||
Tunnel</a-menu-item>
|
Tunnel
|
||||||
|
</a-menu-item>
|
||||||
</a-menu>
|
</a-menu>
|
||||||
</a-dropdown>
|
</a-dropdown>
|
||||||
</a-col>
|
</a-col>
|
||||||
|
|
@ -20,12 +20,16 @@ func NewPeriodicTrafficResetJob(period Period) *PeriodicTrafficResetJob {
|
||||||
|
|
||||||
func (j *PeriodicTrafficResetJob) Run() {
|
func (j *PeriodicTrafficResetJob) Run() {
|
||||||
inbounds, err := j.inboundService.GetInboundsByTrafficReset(string(j.period))
|
inbounds, err := j.inboundService.GetInboundsByTrafficReset(string(j.period))
|
||||||
logger.Infof("Running periodic traffic reset job for period: %s", j.period)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warning("Failed to get inbounds for traffic reset:", err)
|
logger.Warning("Failed to get inbounds for traffic reset:", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(inbounds) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logger.Infof("Running periodic traffic reset job for period: %s (%d matching inbounds)", j.period, len(inbounds))
|
||||||
|
|
||||||
resetCount := 0
|
resetCount := 0
|
||||||
|
|
||||||
for _, inbound := range inbounds {
|
for _, inbound := range inbounds {
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,8 @@ var defaultValueMap = map[string]string{
|
||||||
"tgLang": "en-US",
|
"tgLang": "en-US",
|
||||||
"twoFactorEnable": "false",
|
"twoFactorEnable": "false",
|
||||||
"twoFactorToken": "",
|
"twoFactorToken": "",
|
||||||
"subEnable": "false",
|
"subEnable": "true",
|
||||||
|
"subJsonEnable": "false",
|
||||||
"subTitle": "",
|
"subTitle": "",
|
||||||
"subListen": "",
|
"subListen": "",
|
||||||
"subPort": "2096",
|
"subPort": "2096",
|
||||||
|
|
@ -442,6 +443,10 @@ func (s *SettingService) GetSubEnable() (bool, error) {
|
||||||
return s.getBool("subEnable")
|
return s.getBool("subEnable")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) GetSubJsonEnable() (bool, error) {
|
||||||
|
return s.getBool("subJsonEnable")
|
||||||
|
}
|
||||||
|
|
||||||
func (s *SettingService) GetSubTitle() (string, error) {
|
func (s *SettingService) GetSubTitle() (string, error) {
|
||||||
return s.getString("subTitle")
|
return s.getString("subTitle")
|
||||||
}
|
}
|
||||||
|
|
@ -590,6 +595,7 @@ func (s *SettingService) GetDefaultSettings(host string) (any, error) {
|
||||||
"defaultKey": func() (any, error) { return s.GetKeyFile() },
|
"defaultKey": func() (any, error) { return s.GetKeyFile() },
|
||||||
"tgBotEnable": func() (any, error) { return s.GetTgbotEnabled() },
|
"tgBotEnable": func() (any, error) { return s.GetTgbotEnabled() },
|
||||||
"subEnable": func() (any, error) { return s.GetSubEnable() },
|
"subEnable": func() (any, error) { return s.GetSubEnable() },
|
||||||
|
"subJsonEnable": func() (any, error) { return s.GetSubJsonEnable() },
|
||||||
"subTitle": func() (any, error) { return s.GetSubTitle() },
|
"subTitle": func() (any, error) { return s.GetSubTitle() },
|
||||||
"subURI": func() (any, error) { return s.GetSubURI() },
|
"subURI": func() (any, error) { return s.GetSubURI() },
|
||||||
"subJsonURI": func() (any, error) { return s.GetSubJsonURI() },
|
"subJsonURI": func() (any, error) { return s.GetSubJsonURI() },
|
||||||
|
|
@ -608,7 +614,14 @@ func (s *SettingService) GetDefaultSettings(host string) (any, error) {
|
||||||
result[key] = value
|
result[key] = value
|
||||||
}
|
}
|
||||||
|
|
||||||
if result["subEnable"].(bool) && (result["subURI"].(string) == "" || result["subJsonURI"].(string) == "") {
|
subEnable := result["subEnable"].(bool)
|
||||||
|
subJsonEnable := false
|
||||||
|
if v, ok := result["subJsonEnable"]; ok {
|
||||||
|
if b, ok2 := v.(bool); ok2 {
|
||||||
|
subJsonEnable = b
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (subEnable && result["subURI"].(string) == "") || (subJsonEnable && result["subJsonURI"].(string) == "") {
|
||||||
subURI := ""
|
subURI := ""
|
||||||
subTitle, _ := s.GetSubTitle()
|
subTitle, _ := s.GetSubTitle()
|
||||||
subPort, _ := s.GetSubPort()
|
subPort, _ := s.GetSubPort()
|
||||||
|
|
@ -634,13 +647,13 @@ func (s *SettingService) GetDefaultSettings(host string) (any, error) {
|
||||||
} else {
|
} else {
|
||||||
subURI += fmt.Sprintf("%s:%d", subDomain, subPort)
|
subURI += fmt.Sprintf("%s:%d", subDomain, subPort)
|
||||||
}
|
}
|
||||||
if result["subURI"].(string) == "" {
|
if subEnable && result["subURI"].(string) == "" {
|
||||||
result["subURI"] = subURI + subPath
|
result["subURI"] = subURI + subPath
|
||||||
}
|
}
|
||||||
if result["subTitle"].(string) == "" {
|
if result["subTitle"].(string) == "" {
|
||||||
result["subTitle"] = subTitle
|
result["subTitle"] = subTitle
|
||||||
}
|
}
|
||||||
if result["subJsonURI"].(string) == "" {
|
if subJsonEnable && result["subJsonURI"].(string) == "" {
|
||||||
result["subJsonURI"] = subURI + subJsonPath
|
result["subJsonURI"] = subURI + subJsonPath
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2093,6 +2093,7 @@ func (t *Tgbot) buildSubscriptionURLs(email string) (string, string, error) {
|
||||||
subPort, _ := t.settingService.GetSubPort()
|
subPort, _ := t.settingService.GetSubPort()
|
||||||
subPath, _ := t.settingService.GetSubPath()
|
subPath, _ := t.settingService.GetSubPath()
|
||||||
subJsonPath, _ := t.settingService.GetSubJsonPath()
|
subJsonPath, _ := t.settingService.GetSubJsonPath()
|
||||||
|
subJsonEnable, _ := t.settingService.GetSubJsonEnable()
|
||||||
subKeyFile, _ := t.settingService.GetSubKeyFile()
|
subKeyFile, _ := t.settingService.GetSubKeyFile()
|
||||||
subCertFile, _ := t.settingService.GetSubCertFile()
|
subCertFile, _ := t.settingService.GetSubCertFile()
|
||||||
|
|
||||||
|
|
@ -2137,6 +2138,9 @@ func (t *Tgbot) buildSubscriptionURLs(email string) (string, string, error) {
|
||||||
|
|
||||||
subURL := fmt.Sprintf("%s://%s%s%s", scheme, host, subPath, client.SubID)
|
subURL := fmt.Sprintf("%s://%s%s%s", scheme, host, subPath, client.SubID)
|
||||||
subJsonURL := fmt.Sprintf("%s://%s%s%s", scheme, host, subJsonPath, client.SubID)
|
subJsonURL := fmt.Sprintf("%s://%s%s%s", scheme, host, subJsonPath, client.SubID)
|
||||||
|
if !subJsonEnable {
|
||||||
|
subJsonURL = ""
|
||||||
|
}
|
||||||
return subURL, subJsonURL, nil
|
return subURL, subJsonURL, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2146,8 +2150,10 @@ func (t *Tgbot) sendClientSubLinks(chatId int64, email string) {
|
||||||
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation")+"\r\n"+err.Error())
|
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation")+"\r\n"+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
msg := "Subscription URL:\r\n<code>" + subURL + "</code>\r\n\r\n" +
|
msg := "Subscription URL:\r\n<code>" + subURL + "</code>"
|
||||||
"JSON URL:\r\n<code>" + subJsonURL + "</code>"
|
if subJsonURL != "" {
|
||||||
|
msg += "\r\n\r\nJSON URL:\r\n<code>" + subJsonURL + "</code>"
|
||||||
|
}
|
||||||
inlineKeyboard := tu.InlineKeyboard(
|
inlineKeyboard := tu.InlineKeyboard(
|
||||||
tu.InlineKeyboardRow(
|
tu.InlineKeyboardRow(
|
||||||
tu.InlineKeyboardButton(t.I18nBot("subscription.individualLinks")).WithCallbackData(t.encodeQuery("client_individual_links "+email)),
|
tu.InlineKeyboardButton(t.I18nBot("subscription.individualLinks")).WithCallbackData(t.encodeQuery("client_individual_links "+email)),
|
||||||
|
|
@ -2271,15 +2277,17 @@ func (t *Tgbot) sendClientQRLinks(chatId int64, email string) {
|
||||||
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation")+"\r\n"+err.Error())
|
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation")+"\r\n"+err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send JSON URL QR (filename: subjson.png)
|
// Send JSON URL QR (filename: subjson.png) when available
|
||||||
if png, err := createQR(subJsonURL, 320); err == nil {
|
if subJsonURL != "" {
|
||||||
document := tu.Document(
|
if png, err := createQR(subJsonURL, 320); err == nil {
|
||||||
tu.ID(chatId),
|
document := tu.Document(
|
||||||
tu.FileFromBytes(png, "subjson.png"),
|
tu.ID(chatId),
|
||||||
)
|
tu.FileFromBytes(png, "subjson.png"),
|
||||||
_, _ = bot.SendDocument(context.Background(), document)
|
)
|
||||||
} else {
|
_, _ = bot.SendDocument(context.Background(), document)
|
||||||
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation")+"\r\n"+err.Error())
|
} else {
|
||||||
|
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation")+"\r\n"+err.Error())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also generate a few individual links' QRs (first up to 5)
|
// Also generate a few individual links' QRs (first up to 5)
|
||||||
|
|
|
||||||
|
|
@ -371,6 +371,7 @@
|
||||||
"subSettings" = "الاشتراك"
|
"subSettings" = "الاشتراك"
|
||||||
"subEnable" = "تفعيل خدمة الاشتراك"
|
"subEnable" = "تفعيل خدمة الاشتراك"
|
||||||
"subEnableDesc" = "يفعل خدمة الاشتراك."
|
"subEnableDesc" = "يفعل خدمة الاشتراك."
|
||||||
|
"subJsonEnable" = "تمكين/تعطيل نقطة نهاية اشتراك JSON بشكل مستقل."
|
||||||
"subTitle" = "عنوان الاشتراك"
|
"subTitle" = "عنوان الاشتراك"
|
||||||
"subTitleDesc" = "العنوان اللي هيظهر في عميل VPN"
|
"subTitleDesc" = "العنوان اللي هيظهر في عميل VPN"
|
||||||
"subListen" = "IP الاستماع"
|
"subListen" = "IP الاستماع"
|
||||||
|
|
|
||||||
|
|
@ -369,8 +369,9 @@
|
||||||
"timeZone" = "Time Zone"
|
"timeZone" = "Time Zone"
|
||||||
"timeZoneDesc" = "Scheduled tasks will run based on this time zone."
|
"timeZoneDesc" = "Scheduled tasks will run based on this time zone."
|
||||||
"subSettings" = "Subscription"
|
"subSettings" = "Subscription"
|
||||||
"subEnable" = "Enable Subscription Service"
|
"subEnable" = "Subscription Service"
|
||||||
"subEnableDesc" = "Enables the subscription service."
|
"subEnableDesc" = "Enable/Disable the subscription service."
|
||||||
|
"subJsonEnable" = "Enable/Disable the JSON subscription endpoint independently."
|
||||||
"subTitle" = "Subscription Title"
|
"subTitle" = "Subscription Title"
|
||||||
"subTitleDesc" = "Title shown in VPN client"
|
"subTitleDesc" = "Title shown in VPN client"
|
||||||
"subListen" = "Listen IP"
|
"subListen" = "Listen IP"
|
||||||
|
|
|
||||||
|
|
@ -371,6 +371,7 @@
|
||||||
"subSettings" = "Suscripción"
|
"subSettings" = "Suscripción"
|
||||||
"subEnable" = "Habilitar Servicio"
|
"subEnable" = "Habilitar Servicio"
|
||||||
"subEnableDesc" = "Función de suscripción con configuración separada."
|
"subEnableDesc" = "Función de suscripción con configuración separada."
|
||||||
|
"subJsonEnable" = "Habilitar/Deshabilitar el endpoint de suscripción JSON de forma independiente."
|
||||||
"subTitle" = "Título de la Suscripción"
|
"subTitle" = "Título de la Suscripción"
|
||||||
"subTitleDesc" = "Título mostrado en el cliente de VPN"
|
"subTitleDesc" = "Título mostrado en el cliente de VPN"
|
||||||
"subListen" = "Listening IP"
|
"subListen" = "Listening IP"
|
||||||
|
|
|
||||||
|
|
@ -371,6 +371,7 @@
|
||||||
"subSettings" = "سابسکریپشن"
|
"subSettings" = "سابسکریپشن"
|
||||||
"subEnable" = "فعالسازی سرویس سابسکریپشن"
|
"subEnable" = "فعالسازی سرویس سابسکریپشن"
|
||||||
"subEnableDesc" = "سرویس سابسکریپشن را فعالمیکند"
|
"subEnableDesc" = "سرویس سابسکریپشن را فعالمیکند"
|
||||||
|
"subJsonEnable" = "فعال/غیرفعالسازی مستقل نقطه دسترسی سابسکریپشن JSON."
|
||||||
"subTitle" = "عنوان اشتراک"
|
"subTitle" = "عنوان اشتراک"
|
||||||
"subTitleDesc" = "عنوان نمایش داده شده در کلاینت VPN"
|
"subTitleDesc" = "عنوان نمایش داده شده در کلاینت VPN"
|
||||||
"subListen" = "آدرس آیپی"
|
"subListen" = "آدرس آیپی"
|
||||||
|
|
|
||||||
|
|
@ -371,6 +371,7 @@
|
||||||
"subSettings" = "Langganan"
|
"subSettings" = "Langganan"
|
||||||
"subEnable" = "Aktifkan Layanan Langganan"
|
"subEnable" = "Aktifkan Layanan Langganan"
|
||||||
"subEnableDesc" = "Mengaktifkan layanan langganan."
|
"subEnableDesc" = "Mengaktifkan layanan langganan."
|
||||||
|
"subJsonEnable" = "Aktifkan/Nonaktifkan endpoint langganan JSON secara mandiri."
|
||||||
"subTitle" = "Judul Langganan"
|
"subTitle" = "Judul Langganan"
|
||||||
"subTitleDesc" = "Judul yang ditampilkan di klien VPN"
|
"subTitleDesc" = "Judul yang ditampilkan di klien VPN"
|
||||||
"subListen" = "IP Pendengar"
|
"subListen" = "IP Pendengar"
|
||||||
|
|
|
||||||
|
|
@ -371,6 +371,7 @@
|
||||||
"subSettings" = "サブスクリプション設定"
|
"subSettings" = "サブスクリプション設定"
|
||||||
"subEnable" = "サブスクリプションサービスを有効にする"
|
"subEnable" = "サブスクリプションサービスを有効にする"
|
||||||
"subEnableDesc" = "サブスクリプションサービス機能を有効にする"
|
"subEnableDesc" = "サブスクリプションサービス機能を有効にする"
|
||||||
|
"subJsonEnable" = "JSON サブスクリプションのエンドポイントを個別に有効/無効にする。"
|
||||||
"subTitle" = "サブスクリプションタイトル"
|
"subTitle" = "サブスクリプションタイトル"
|
||||||
"subTitleDesc" = "VPNクライアントに表示されるタイトル"
|
"subTitleDesc" = "VPNクライアントに表示されるタイトル"
|
||||||
"subListen" = "監視IP"
|
"subListen" = "監視IP"
|
||||||
|
|
|
||||||
|
|
@ -371,6 +371,7 @@
|
||||||
"subSettings" = "Assinatura"
|
"subSettings" = "Assinatura"
|
||||||
"subEnable" = "Ativar Serviço de Assinatura"
|
"subEnable" = "Ativar Serviço de Assinatura"
|
||||||
"subEnableDesc" = "Ativa o serviço de assinatura."
|
"subEnableDesc" = "Ativa o serviço de assinatura."
|
||||||
|
"subJsonEnable" = "Ativar/Desativar o endpoint de assinatura JSON de forma independente."
|
||||||
"subTitle" = "Título da Assinatura"
|
"subTitle" = "Título da Assinatura"
|
||||||
"subTitleDesc" = "Título exibido no cliente VPN"
|
"subTitleDesc" = "Título exibido no cliente VPN"
|
||||||
"subListen" = "IP de Escuta"
|
"subListen" = "IP de Escuta"
|
||||||
|
|
|
||||||
|
|
@ -371,6 +371,7 @@
|
||||||
"subSettings" = "Подписка"
|
"subSettings" = "Подписка"
|
||||||
"subEnable" = "Включить подписку"
|
"subEnable" = "Включить подписку"
|
||||||
"subEnableDesc" = "Функция подписки с отдельной конфигурацией"
|
"subEnableDesc" = "Функция подписки с отдельной конфигурацией"
|
||||||
|
"subJsonEnable" = "Включить/отключить JSON-эндпоинт подписки независимо."
|
||||||
"subTitle" = "Заголовок подписки"
|
"subTitle" = "Заголовок подписки"
|
||||||
"subTitleDesc" = "Название подписки, которое видит клиент в VPN клиенте"
|
"subTitleDesc" = "Название подписки, которое видит клиент в VPN клиенте"
|
||||||
"subListen" = "Прослушивание IP"
|
"subListen" = "Прослушивание IP"
|
||||||
|
|
|
||||||
|
|
@ -371,6 +371,7 @@
|
||||||
"subSettings" = "Abonelik"
|
"subSettings" = "Abonelik"
|
||||||
"subEnable" = "Abonelik Hizmetini Etkinleştir"
|
"subEnable" = "Abonelik Hizmetini Etkinleştir"
|
||||||
"subEnableDesc" = "Abonelik hizmetini etkinleştirir."
|
"subEnableDesc" = "Abonelik hizmetini etkinleştirir."
|
||||||
|
"subJsonEnable" = "JSON abonelik uç noktasını bağımsız olarak Etkinleştir/Devre Dışı bırak."
|
||||||
"subTitle" = "Abonelik Başlığı"
|
"subTitle" = "Abonelik Başlığı"
|
||||||
"subTitleDesc" = "VPN istemcisinde gösterilen başlık"
|
"subTitleDesc" = "VPN istemcisinde gösterilen başlık"
|
||||||
"subListen" = "Dinleme IP"
|
"subListen" = "Dinleme IP"
|
||||||
|
|
|
||||||
|
|
@ -371,6 +371,7 @@
|
||||||
"subSettings" = "Підписка"
|
"subSettings" = "Підписка"
|
||||||
"subEnable" = "Увімкнути службу підписки"
|
"subEnable" = "Увімкнути службу підписки"
|
||||||
"subEnableDesc" = "Вмикає службу підписки."
|
"subEnableDesc" = "Вмикає службу підписки."
|
||||||
|
"subJsonEnable" = "Увімкнути/вимкнути JSON-кінець підписки незалежно."
|
||||||
"subTitle" = "Назва Підписки"
|
"subTitle" = "Назва Підписки"
|
||||||
"subTitleDesc" = "Назва, яка відображається у VPN-клієнті"
|
"subTitleDesc" = "Назва, яка відображається у VPN-клієнті"
|
||||||
"subListen" = "Слухати IP"
|
"subListen" = "Слухати IP"
|
||||||
|
|
|
||||||
|
|
@ -371,6 +371,7 @@
|
||||||
"subSettings" = "Gói đăng ký"
|
"subSettings" = "Gói đăng ký"
|
||||||
"subEnable" = "Bật dịch vụ"
|
"subEnable" = "Bật dịch vụ"
|
||||||
"subEnableDesc" = "Tính năng gói đăng ký với cấu hình riêng"
|
"subEnableDesc" = "Tính năng gói đăng ký với cấu hình riêng"
|
||||||
|
"subJsonEnable" = "Bật/Tắt điểm cuối đăng ký JSON độc lập."
|
||||||
"subTitle" = "Tiêu đề Đăng ký"
|
"subTitle" = "Tiêu đề Đăng ký"
|
||||||
"subTitleDesc" = "Tiêu đề hiển thị trong ứng dụng VPN"
|
"subTitleDesc" = "Tiêu đề hiển thị trong ứng dụng VPN"
|
||||||
"subListen" = "Listening IP"
|
"subListen" = "Listening IP"
|
||||||
|
|
|
||||||
|
|
@ -371,6 +371,7 @@
|
||||||
"subSettings" = "订阅设置"
|
"subSettings" = "订阅设置"
|
||||||
"subEnable" = "启用订阅服务"
|
"subEnable" = "启用订阅服务"
|
||||||
"subEnableDesc" = "启用订阅服务功能"
|
"subEnableDesc" = "启用订阅服务功能"
|
||||||
|
"subJsonEnable" = "单独启用/禁用 JSON 订阅端点。"
|
||||||
"subTitle" = "订阅标题"
|
"subTitle" = "订阅标题"
|
||||||
"subTitleDesc" = "在VPN客户端中显示的标题"
|
"subTitleDesc" = "在VPN客户端中显示的标题"
|
||||||
"subListen" = "监听 IP"
|
"subListen" = "监听 IP"
|
||||||
|
|
|
||||||
|
|
@ -371,6 +371,7 @@
|
||||||
"subSettings" = "訂閱設定"
|
"subSettings" = "訂閱設定"
|
||||||
"subEnable" = "啟用訂閱服務"
|
"subEnable" = "啟用訂閱服務"
|
||||||
"subEnableDesc" = "啟用訂閱服務功能"
|
"subEnableDesc" = "啟用訂閱服務功能"
|
||||||
|
"subJsonEnable" = "獨立啟用/停用 JSON 訂閱端點。"
|
||||||
"subTitle" = "訂閱標題"
|
"subTitle" = "訂閱標題"
|
||||||
"subTitleDesc" = "在VPN客戶端中顯示的標題"
|
"subTitleDesc" = "在VPN客戶端中顯示的標題"
|
||||||
"subListen" = "監聽 IP"
|
"subListen" = "監聽 IP"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue