mirror of
https://github.com/koho/frpmgr.git
synced 2025-10-20 16:03:47 +08:00
Add properties dialog (#250)
* Add properties dialog * Resize window * Count TCP and UDP connections * Resize the first column * Fix lint error
This commit is contained in:
2
go.mod
2
go.mod
@ -66,4 +66,4 @@ require (
|
||||
sigs.k8s.io/yaml v1.3.0 // indirect
|
||||
)
|
||||
|
||||
replace github.com/lxn/walk => github.com/koho/frpmgr v0.0.0-20250614023804-912b463eb7d6
|
||||
replace github.com/lxn/walk => github.com/koho/frpmgr v0.0.0-20250619085234-ed99ab60add0
|
||||
|
4
go.sum
4
go.sum
@ -63,8 +63,8 @@ github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/4
|
||||
github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/klauspost/reedsolomon v1.12.0 h1:I5FEp3xSwVCcEh3F5A7dofEfhXdF/bWhQWPH+XwBFno=
|
||||
github.com/klauspost/reedsolomon v1.12.0/go.mod h1:EPLZJeh4l27pUGC3aXOjheaoh1I9yut7xTURiW3LQ9Y=
|
||||
github.com/koho/frpmgr v0.0.0-20250614023804-912b463eb7d6 h1:vFU+4JwxgVA1S8200tuSXNRWUzxOMVYppCOfYW3mGE4=
|
||||
github.com/koho/frpmgr v0.0.0-20250614023804-912b463eb7d6/go.mod h1:BEvTAgZsEET00wLNsOhKg8fX//k6l4b5INTzX2APBb8=
|
||||
github.com/koho/frpmgr v0.0.0-20250619085234-ed99ab60add0 h1:45SbnvjExN+92n5O4YVWPXfF/sQG0iLvijF247q0kjs=
|
||||
github.com/koho/frpmgr v0.0.0-20250619085234-ed99ab60add0/go.mod h1:BEvTAgZsEET00wLNsOhKg8fX//k6l4b5INTzX2APBb8=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
|
1918
i18n/catalog.go
1918
i18n/catalog.go
File diff suppressed because it is too large
Load Diff
@ -401,6 +401,13 @@
|
||||
"translatorComment": "Copied from source.",
|
||||
"fuzzy": true
|
||||
},
|
||||
{
|
||||
"id": "Properties",
|
||||
"message": "Properties",
|
||||
"translation": "Properties",
|
||||
"translatorComment": "Copied from source.",
|
||||
"fuzzy": true
|
||||
},
|
||||
{
|
||||
"id": "Select all",
|
||||
"message": "Select all",
|
||||
@ -1246,16 +1253,16 @@
|
||||
"fuzzy": true
|
||||
},
|
||||
{
|
||||
"id": "auto",
|
||||
"message": "auto",
|
||||
"translation": "auto",
|
||||
"id": "Auto",
|
||||
"message": "Auto",
|
||||
"translation": "Auto",
|
||||
"translatorComment": "Copied from source.",
|
||||
"fuzzy": true
|
||||
},
|
||||
{
|
||||
"id": "default",
|
||||
"message": "default",
|
||||
"translation": "default",
|
||||
"id": "Default",
|
||||
"message": "Default",
|
||||
"translation": "Default",
|
||||
"translatorComment": "Copied from source.",
|
||||
"fuzzy": true
|
||||
},
|
||||
@ -1901,6 +1908,132 @@
|
||||
"translatorComment": "Copied from source.",
|
||||
"fuzzy": true
|
||||
},
|
||||
{
|
||||
"id": "Manual",
|
||||
"message": "Manual",
|
||||
"translation": "Manual",
|
||||
"translatorComment": "Copied from source.",
|
||||
"fuzzy": true
|
||||
},
|
||||
{
|
||||
"id": "Identifier",
|
||||
"message": "Identifier",
|
||||
"translation": "Identifier",
|
||||
"translatorComment": "Copied from source.",
|
||||
"fuzzy": true
|
||||
},
|
||||
{
|
||||
"id": "Service Name",
|
||||
"message": "Service Name",
|
||||
"translation": "Service Name",
|
||||
"translatorComment": "Copied from source.",
|
||||
"fuzzy": true
|
||||
},
|
||||
{
|
||||
"id": "File Format",
|
||||
"message": "File Format",
|
||||
"translation": "File Format",
|
||||
"translatorComment": "Copied from source.",
|
||||
"fuzzy": true
|
||||
},
|
||||
{
|
||||
"id": "Number of Proxies",
|
||||
"message": "Number of Proxies",
|
||||
"translation": "Number of Proxies",
|
||||
"translatorComment": "Copied from source.",
|
||||
"fuzzy": true
|
||||
},
|
||||
{
|
||||
"id": "Start Type",
|
||||
"message": "Start Type",
|
||||
"translation": "Start Type",
|
||||
"translatorComment": "Copied from source.",
|
||||
"fuzzy": true
|
||||
},
|
||||
{
|
||||
"id": "{LogFileCount} Files, {LogSizeDesc}",
|
||||
"message": "{LogFileCount} Files, {LogSizeDesc}",
|
||||
"translation": "{LogFileCount} Files, {LogSizeDesc}",
|
||||
"translatorComment": "Copied from source.",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "LogFileCount",
|
||||
"string": "%[1]d",
|
||||
"type": "int",
|
||||
"underlyingType": "int",
|
||||
"argNum": 1,
|
||||
"expr": "logFileCount"
|
||||
},
|
||||
{
|
||||
"id": "LogSizeDesc",
|
||||
"string": "%[2]s",
|
||||
"type": "string",
|
||||
"underlyingType": "string",
|
||||
"argNum": 2,
|
||||
"expr": "logSizeDesc"
|
||||
}
|
||||
],
|
||||
"fuzzy": true
|
||||
},
|
||||
{
|
||||
"id": "Created",
|
||||
"message": "Created",
|
||||
"translation": "Created",
|
||||
"translatorComment": "Copied from source.",
|
||||
"fuzzy": true
|
||||
},
|
||||
{
|
||||
"id": "Modified",
|
||||
"message": "Modified",
|
||||
"translation": "Modified",
|
||||
"translatorComment": "Copied from source.",
|
||||
"fuzzy": true
|
||||
},
|
||||
{
|
||||
"id": "Started",
|
||||
"message": "Started",
|
||||
"translation": "Started",
|
||||
"translatorComment": "Copied from source.",
|
||||
"fuzzy": true
|
||||
},
|
||||
{
|
||||
"id": "Number of TCP Connections",
|
||||
"message": "Number of TCP Connections",
|
||||
"translation": "Number of TCP Connections",
|
||||
"translatorComment": "Copied from source.",
|
||||
"fuzzy": true
|
||||
},
|
||||
{
|
||||
"id": "Number of UDP Connections",
|
||||
"message": "Number of UDP Connections",
|
||||
"translation": "Number of UDP Connections",
|
||||
"translatorComment": "Copied from source.",
|
||||
"fuzzy": true
|
||||
},
|
||||
{
|
||||
"id": "{Name} Properties",
|
||||
"message": "{Name} Properties",
|
||||
"translation": "{Name} Properties",
|
||||
"translatorComment": "Copied from source.",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "Name",
|
||||
"string": "%[1]s",
|
||||
"type": "string",
|
||||
"underlyingType": "string",
|
||||
"argNum": 1,
|
||||
"expr": "pd.conf.Name()"
|
||||
}
|
||||
],
|
||||
"fuzzy": true
|
||||
},
|
||||
{
|
||||
"id": "Copy Value",
|
||||
"message": "Copy Value",
|
||||
"translation": "Copy Value",
|
||||
"translatorComment": "Copied from source.",
|
||||
"fuzzy": true
|
||||
},
|
||||
{
|
||||
"id": "Error",
|
||||
"message": "Error",
|
||||
|
@ -301,6 +301,11 @@
|
||||
"message": "Export All Configs to ZIP",
|
||||
"translation": "Exportar todas las configuraciones a ZIP"
|
||||
},
|
||||
{
|
||||
"id": "Properties",
|
||||
"message": "Properties",
|
||||
"translation": "Propiedades"
|
||||
},
|
||||
{
|
||||
"id": "Select all",
|
||||
"message": "Select all",
|
||||
@ -938,14 +943,14 @@
|
||||
"translation": "Protocolo proxy"
|
||||
},
|
||||
{
|
||||
"id": "auto",
|
||||
"message": "auto",
|
||||
"translation": "auto"
|
||||
"id": "Auto",
|
||||
"message": "Auto",
|
||||
"translation": "Auto"
|
||||
},
|
||||
{
|
||||
"id": "default",
|
||||
"message": "default",
|
||||
"translation": "por defecto"
|
||||
"id": "Default",
|
||||
"message": "Default",
|
||||
"translation": "Por defecto"
|
||||
},
|
||||
{
|
||||
"id": "Keep Tunnel",
|
||||
@ -1417,6 +1422,104 @@
|
||||
"message": "Log retention",
|
||||
"translation": "Retención de registros"
|
||||
},
|
||||
{
|
||||
"id": "Manual",
|
||||
"message": "Manual",
|
||||
"translation": "Manual"
|
||||
},
|
||||
{
|
||||
"id": "Identifier",
|
||||
"message": "Identifier",
|
||||
"translation": "Identificador"
|
||||
},
|
||||
{
|
||||
"id": "Service Name",
|
||||
"message": "Service Name",
|
||||
"translation": "Nombre del servicio"
|
||||
},
|
||||
{
|
||||
"id": "File Format",
|
||||
"message": "File Format",
|
||||
"translation": "Formato de archivo"
|
||||
},
|
||||
{
|
||||
"id": "Number of Proxies",
|
||||
"message": "Number of Proxies",
|
||||
"translation": "Número de proxies"
|
||||
},
|
||||
{
|
||||
"id": "Start Type",
|
||||
"message": "Start Type",
|
||||
"translation": "Tipo de inicio"
|
||||
},
|
||||
{
|
||||
"id": "{LogFileCount} Files, {LogSizeDesc}",
|
||||
"message": "{LogFileCount} Files, {LogSizeDesc}",
|
||||
"translation": "{LogFileCount} archivos, {LogSizeDesc}",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "LogFileCount",
|
||||
"string": "%[1]d",
|
||||
"type": "int",
|
||||
"underlyingType": "int",
|
||||
"argNum": 1,
|
||||
"expr": "logFileCount"
|
||||
},
|
||||
{
|
||||
"id": "LogSizeDesc",
|
||||
"string": "%[2]s",
|
||||
"type": "string",
|
||||
"underlyingType": "string",
|
||||
"argNum": 2,
|
||||
"expr": "logSizeDesc"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "Created",
|
||||
"message": "Created",
|
||||
"translation": "Creado"
|
||||
},
|
||||
{
|
||||
"id": "Modified",
|
||||
"message": "Modified",
|
||||
"translation": "Modificado"
|
||||
},
|
||||
{
|
||||
"id": "Started",
|
||||
"message": "Started",
|
||||
"translation": "Empezado"
|
||||
},
|
||||
{
|
||||
"id": "Number of TCP Connections",
|
||||
"message": "Number of TCP Connections",
|
||||
"translation": "Número de conexiones TCP"
|
||||
},
|
||||
{
|
||||
"id": "Number of UDP Connections",
|
||||
"message": "Number of UDP Connections",
|
||||
"translation": "Número de conexiones UDP"
|
||||
},
|
||||
{
|
||||
"id": "{Name} Properties",
|
||||
"message": "{Name} Properties",
|
||||
"translation": "Propiedades de {Name}",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "Name",
|
||||
"string": "%[1]s",
|
||||
"type": "string",
|
||||
"underlyingType": "string",
|
||||
"argNum": 1,
|
||||
"expr": "pd.conf.Name()"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "Copy Value",
|
||||
"message": "Copy Value",
|
||||
"translation": "Copiar valor"
|
||||
},
|
||||
{
|
||||
"id": "Error",
|
||||
"message": "Error",
|
||||
|
@ -109,7 +109,7 @@
|
||||
{
|
||||
"id": "For FRP configuration documentation, please visit the FRP project page:",
|
||||
"message": "For FRP configuration documentation, please visit the FRP project page:",
|
||||
"translation": "FRP 設定ドキュメントについては、FRP プロジェクトページにアクセスしてください:"
|
||||
"translation": "FRP のドキュメントについては、FRP プロジェクト ページをご覧ください:"
|
||||
},
|
||||
{
|
||||
"id": "An error occurred while checking for a software update.",
|
||||
@ -301,6 +301,11 @@
|
||||
"message": "Export All Configs to ZIP",
|
||||
"translation": "すべての設定をZIPにエクスポート"
|
||||
},
|
||||
{
|
||||
"id": "Properties",
|
||||
"message": "Properties",
|
||||
"translation": "プロパティ"
|
||||
},
|
||||
{
|
||||
"id": "Select all",
|
||||
"message": "Select all",
|
||||
@ -948,13 +953,13 @@
|
||||
"translation": "プロキシプロトコル"
|
||||
},
|
||||
{
|
||||
"id": "auto",
|
||||
"message": "auto",
|
||||
"id": "Auto",
|
||||
"message": "Auto",
|
||||
"translation": "自動"
|
||||
},
|
||||
{
|
||||
"id": "default",
|
||||
"message": "default",
|
||||
"id": "Default",
|
||||
"message": "Default",
|
||||
"translation": "既定値"
|
||||
},
|
||||
{
|
||||
@ -1427,6 +1432,104 @@
|
||||
"message": "Log retention",
|
||||
"translation": "ログ保持"
|
||||
},
|
||||
{
|
||||
"id": "Manual",
|
||||
"message": "Manual",
|
||||
"translation": "マニュアル"
|
||||
},
|
||||
{
|
||||
"id": "Identifier",
|
||||
"message": "Identifier",
|
||||
"translation": "識別子"
|
||||
},
|
||||
{
|
||||
"id": "Service Name",
|
||||
"message": "Service Name",
|
||||
"translation": "サービス名"
|
||||
},
|
||||
{
|
||||
"id": "File Format",
|
||||
"message": "File Format",
|
||||
"translation": "ファイル形式"
|
||||
},
|
||||
{
|
||||
"id": "Number of Proxies",
|
||||
"message": "Number of Proxies",
|
||||
"translation": "プロキシの数"
|
||||
},
|
||||
{
|
||||
"id": "Start Type",
|
||||
"message": "Start Type",
|
||||
"translation": "スタートアップの種類"
|
||||
},
|
||||
{
|
||||
"id": "{LogFileCount} Files, {LogSizeDesc}",
|
||||
"message": "{LogFileCount} Files, {LogSizeDesc}",
|
||||
"translation": "{LogFileCount} ファイル、{LogSizeDesc}",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "LogFileCount",
|
||||
"string": "%[1]d",
|
||||
"type": "int",
|
||||
"underlyingType": "int",
|
||||
"argNum": 1,
|
||||
"expr": "logFileCount"
|
||||
},
|
||||
{
|
||||
"id": "LogSizeDesc",
|
||||
"string": "%[2]s",
|
||||
"type": "string",
|
||||
"underlyingType": "string",
|
||||
"argNum": 2,
|
||||
"expr": "logSizeDesc"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "Created",
|
||||
"message": "Created",
|
||||
"translation": "作成時間"
|
||||
},
|
||||
{
|
||||
"id": "Modified",
|
||||
"message": "Modified",
|
||||
"translation": "修正時間"
|
||||
},
|
||||
{
|
||||
"id": "Started",
|
||||
"message": "Started",
|
||||
"translation": "起動時間"
|
||||
},
|
||||
{
|
||||
"id": "Number of TCP Connections",
|
||||
"message": "Number of TCP Connections",
|
||||
"translation": "TCP接続数"
|
||||
},
|
||||
{
|
||||
"id": "Number of UDP Connections",
|
||||
"message": "Number of UDP Connections",
|
||||
"translation": "UDP接続数"
|
||||
},
|
||||
{
|
||||
"id": "{Name} Properties",
|
||||
"message": "{Name} Properties",
|
||||
"translation": "{Name}のプロパティ",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "Name",
|
||||
"string": "%[1]s",
|
||||
"type": "string",
|
||||
"underlyingType": "string",
|
||||
"argNum": 1,
|
||||
"expr": "pd.conf.Name()"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "Copy Value",
|
||||
"message": "Copy Value",
|
||||
"translation": "コピー値"
|
||||
},
|
||||
{
|
||||
"id": "Error",
|
||||
"message": "Error",
|
||||
|
@ -301,6 +301,11 @@
|
||||
"message": "Export All Configs to ZIP",
|
||||
"translation": "모든 구성을 ZIP 으로 내보내기"
|
||||
},
|
||||
{
|
||||
"id": "Properties",
|
||||
"message": "Properties",
|
||||
"translation": "속성"
|
||||
},
|
||||
{
|
||||
"id": "Select all",
|
||||
"message": "Select all",
|
||||
@ -938,13 +943,13 @@
|
||||
"translation": "프록시 프로토콜"
|
||||
},
|
||||
{
|
||||
"id": "auto",
|
||||
"message": "auto",
|
||||
"id": "Auto",
|
||||
"message": "Auto",
|
||||
"translation": "자동"
|
||||
},
|
||||
{
|
||||
"id": "default",
|
||||
"message": "default",
|
||||
"id": "Default",
|
||||
"message": "Default",
|
||||
"translation": "기본값"
|
||||
},
|
||||
{
|
||||
@ -1417,6 +1422,104 @@
|
||||
"message": "Log retention",
|
||||
"translation": "로그 보존"
|
||||
},
|
||||
{
|
||||
"id": "Manual",
|
||||
"message": "Manual",
|
||||
"translation": "매뉴얼"
|
||||
},
|
||||
{
|
||||
"id": "Identifier",
|
||||
"message": "Identifier",
|
||||
"translation": "식별자"
|
||||
},
|
||||
{
|
||||
"id": "Service Name",
|
||||
"message": "Service Name",
|
||||
"translation": "서비스 이름"
|
||||
},
|
||||
{
|
||||
"id": "File Format",
|
||||
"message": "File Format",
|
||||
"translation": "파일 형식"
|
||||
},
|
||||
{
|
||||
"id": "Number of Proxies",
|
||||
"message": "Number of Proxies",
|
||||
"translation": "프록시 수"
|
||||
},
|
||||
{
|
||||
"id": "Start Type",
|
||||
"message": "Start Type",
|
||||
"translation": "시작 유형"
|
||||
},
|
||||
{
|
||||
"id": "{LogFileCount} Files, {LogSizeDesc}",
|
||||
"message": "{LogFileCount} Files, {LogSizeDesc}",
|
||||
"translation": "{LogFileCount}개 파일, {LogSizeDesc}",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "LogFileCount",
|
||||
"string": "%[1]d",
|
||||
"type": "int",
|
||||
"underlyingType": "int",
|
||||
"argNum": 1,
|
||||
"expr": "logFileCount"
|
||||
},
|
||||
{
|
||||
"id": "LogSizeDesc",
|
||||
"string": "%[2]s",
|
||||
"type": "string",
|
||||
"underlyingType": "string",
|
||||
"argNum": 2,
|
||||
"expr": "logSizeDesc"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "Created",
|
||||
"message": "Created",
|
||||
"translation": "창조 시간"
|
||||
},
|
||||
{
|
||||
"id": "Modified",
|
||||
"message": "Modified",
|
||||
"translation": "수정 시간"
|
||||
},
|
||||
{
|
||||
"id": "Started",
|
||||
"message": "Started",
|
||||
"translation": "시작 시간"
|
||||
},
|
||||
{
|
||||
"id": "Number of TCP Connections",
|
||||
"message": "Number of TCP Connections",
|
||||
"translation": "TCP 연결 수"
|
||||
},
|
||||
{
|
||||
"id": "Number of UDP Connections",
|
||||
"message": "Number of UDP Connections",
|
||||
"translation": "UDP 연결 수"
|
||||
},
|
||||
{
|
||||
"id": "{Name} Properties",
|
||||
"message": "{Name} Properties",
|
||||
"translation": "{Name} 속성",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "Name",
|
||||
"string": "%[1]s",
|
||||
"type": "string",
|
||||
"underlyingType": "string",
|
||||
"argNum": 1,
|
||||
"expr": "pd.conf.Name()"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "Copy Value",
|
||||
"message": "Copy Value",
|
||||
"translation": "복사 값"
|
||||
},
|
||||
{
|
||||
"id": "Error",
|
||||
"message": "Error",
|
||||
|
@ -301,6 +301,11 @@
|
||||
"message": "Export All Configs to ZIP",
|
||||
"translation": "导出所有配置 (ZIP 压缩包)"
|
||||
},
|
||||
{
|
||||
"id": "Properties",
|
||||
"message": "Properties",
|
||||
"translation": "属性"
|
||||
},
|
||||
{
|
||||
"id": "Select all",
|
||||
"message": "Select all",
|
||||
@ -938,13 +943,13 @@
|
||||
"translation": "代理协议"
|
||||
},
|
||||
{
|
||||
"id": "auto",
|
||||
"message": "auto",
|
||||
"id": "Auto",
|
||||
"message": "Auto",
|
||||
"translation": "自动"
|
||||
},
|
||||
{
|
||||
"id": "default",
|
||||
"message": "default",
|
||||
"id": "Default",
|
||||
"message": "Default",
|
||||
"translation": "默认"
|
||||
},
|
||||
{
|
||||
@ -1417,6 +1422,104 @@
|
||||
"message": "Log retention",
|
||||
"translation": "日志保留"
|
||||
},
|
||||
{
|
||||
"id": "Manual",
|
||||
"message": "Manual",
|
||||
"translation": "手动"
|
||||
},
|
||||
{
|
||||
"id": "Identifier",
|
||||
"message": "Identifier",
|
||||
"translation": "标识符"
|
||||
},
|
||||
{
|
||||
"id": "Service Name",
|
||||
"message": "Service Name",
|
||||
"translation": "服务名称"
|
||||
},
|
||||
{
|
||||
"id": "File Format",
|
||||
"message": "File Format",
|
||||
"translation": "文件格式"
|
||||
},
|
||||
{
|
||||
"id": "Number of Proxies",
|
||||
"message": "Number of Proxies",
|
||||
"translation": "代理数量"
|
||||
},
|
||||
{
|
||||
"id": "Start Type",
|
||||
"message": "Start Type",
|
||||
"translation": "启动类型"
|
||||
},
|
||||
{
|
||||
"id": "{LogFileCount} Files, {LogSizeDesc}",
|
||||
"message": "{LogFileCount} Files, {LogSizeDesc}",
|
||||
"translation": "{LogFileCount} 个文件,{LogSizeDesc}",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "LogFileCount",
|
||||
"string": "%[1]d",
|
||||
"type": "int",
|
||||
"underlyingType": "int",
|
||||
"argNum": 1,
|
||||
"expr": "logFileCount"
|
||||
},
|
||||
{
|
||||
"id": "LogSizeDesc",
|
||||
"string": "%[2]s",
|
||||
"type": "string",
|
||||
"underlyingType": "string",
|
||||
"argNum": 2,
|
||||
"expr": "logSizeDesc"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "Created",
|
||||
"message": "Created",
|
||||
"translation": "创建时间"
|
||||
},
|
||||
{
|
||||
"id": "Modified",
|
||||
"message": "Modified",
|
||||
"translation": "修改时间"
|
||||
},
|
||||
{
|
||||
"id": "Started",
|
||||
"message": "Started",
|
||||
"translation": "启动时间"
|
||||
},
|
||||
{
|
||||
"id": "Number of TCP Connections",
|
||||
"message": "Number of TCP Connections",
|
||||
"translation": "TCP 连接数"
|
||||
},
|
||||
{
|
||||
"id": "Number of UDP Connections",
|
||||
"message": "Number of UDP Connections",
|
||||
"translation": "UDP 连接数"
|
||||
},
|
||||
{
|
||||
"id": "{Name} Properties",
|
||||
"message": "{Name} Properties",
|
||||
"translation": "{Name} 属性",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "Name",
|
||||
"string": "%[1]s",
|
||||
"type": "string",
|
||||
"underlyingType": "string",
|
||||
"argNum": 1,
|
||||
"expr": "pd.conf.Name()"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "Copy Value",
|
||||
"message": "Copy Value",
|
||||
"translation": "复制值"
|
||||
},
|
||||
{
|
||||
"id": "Error",
|
||||
"message": "Error",
|
||||
|
@ -301,6 +301,11 @@
|
||||
"message": "Export All Configs to ZIP",
|
||||
"translation": "導出所有配置 (ZIP 壓縮檔)"
|
||||
},
|
||||
{
|
||||
"id": "Properties",
|
||||
"message": "Properties",
|
||||
"translation": "內容"
|
||||
},
|
||||
{
|
||||
"id": "Select all",
|
||||
"message": "Select all",
|
||||
@ -938,13 +943,13 @@
|
||||
"translation": "代理協定"
|
||||
},
|
||||
{
|
||||
"id": "auto",
|
||||
"message": "auto",
|
||||
"id": "Auto",
|
||||
"message": "Auto",
|
||||
"translation": "自動"
|
||||
},
|
||||
{
|
||||
"id": "default",
|
||||
"message": "default",
|
||||
"id": "Default",
|
||||
"message": "Default",
|
||||
"translation": "預設"
|
||||
},
|
||||
{
|
||||
@ -1417,6 +1422,104 @@
|
||||
"message": "Log retention",
|
||||
"translation": "日誌保留"
|
||||
},
|
||||
{
|
||||
"id": "Manual",
|
||||
"message": "Manual",
|
||||
"translation": "手動"
|
||||
},
|
||||
{
|
||||
"id": "Identifier",
|
||||
"message": "Identifier",
|
||||
"translation": "識別符"
|
||||
},
|
||||
{
|
||||
"id": "Service Name",
|
||||
"message": "Service Name",
|
||||
"translation": "服務名稱"
|
||||
},
|
||||
{
|
||||
"id": "File Format",
|
||||
"message": "File Format",
|
||||
"translation": "檔案格式"
|
||||
},
|
||||
{
|
||||
"id": "Number of Proxies",
|
||||
"message": "Number of Proxies",
|
||||
"translation": "代理數量"
|
||||
},
|
||||
{
|
||||
"id": "Start Type",
|
||||
"message": "Start Type",
|
||||
"translation": "啟動類型"
|
||||
},
|
||||
{
|
||||
"id": "{LogFileCount} Files, {LogSizeDesc}",
|
||||
"message": "{LogFileCount} Files, {LogSizeDesc}",
|
||||
"translation": "{LogFileCount} 個文件,{LogSizeDesc}",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "LogFileCount",
|
||||
"string": "%[1]d",
|
||||
"type": "int",
|
||||
"underlyingType": "int",
|
||||
"argNum": 1,
|
||||
"expr": "logFileCount"
|
||||
},
|
||||
{
|
||||
"id": "LogSizeDesc",
|
||||
"string": "%[2]s",
|
||||
"type": "string",
|
||||
"underlyingType": "string",
|
||||
"argNum": 2,
|
||||
"expr": "logSizeDesc"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "Created",
|
||||
"message": "Created",
|
||||
"translation": "建立日期"
|
||||
},
|
||||
{
|
||||
"id": "Modified",
|
||||
"message": "Modified",
|
||||
"translation": "修改日期"
|
||||
},
|
||||
{
|
||||
"id": "Started",
|
||||
"message": "Started",
|
||||
"translation": "啟動日期"
|
||||
},
|
||||
{
|
||||
"id": "Number of TCP Connections",
|
||||
"message": "Number of TCP Connections",
|
||||
"translation": "TCP 連線數"
|
||||
},
|
||||
{
|
||||
"id": "Number of UDP Connections",
|
||||
"message": "Number of UDP Connections",
|
||||
"translation": "UDP 連線數"
|
||||
},
|
||||
{
|
||||
"id": "{Name} Properties",
|
||||
"message": "{Name} Properties",
|
||||
"translation": "{Name} - 內容",
|
||||
"placeholders": [
|
||||
{
|
||||
"id": "Name",
|
||||
"string": "%[1]s",
|
||||
"type": "string",
|
||||
"underlyingType": "string",
|
||||
"argNum": 1,
|
||||
"expr": "pd.conf.Name()"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "Copy Value",
|
||||
"message": "Copy Value",
|
||||
"translation": "複製值"
|
||||
},
|
||||
{
|
||||
"id": "Error",
|
||||
"message": "Error",
|
||||
|
@ -1,6 +1,7 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
@ -72,3 +73,18 @@ func MoveSlice[S ~[]E, E any](s S, i, j int) {
|
||||
}
|
||||
s[j] = x
|
||||
}
|
||||
|
||||
// ByteCountIEC converts a size in bytes to a human-readable string in IEC (binary) format.
|
||||
func ByteCountIEC(b int64) string {
|
||||
const unit = 1024
|
||||
if b < unit {
|
||||
return fmt.Sprintf("%d B", b)
|
||||
}
|
||||
div, exp := int64(unit), 0
|
||||
for n := b / unit; n >= unit; n /= unit {
|
||||
div *= unit
|
||||
exp++
|
||||
}
|
||||
return fmt.Sprintf("%.1f %ciB",
|
||||
float64(b)/float64(div), "KMGTPE"[exp])
|
||||
}
|
||||
|
113
pkg/util/net.go
113
pkg/util/net.go
@ -2,12 +2,23 @@ package util
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime"
|
||||
"net/http"
|
||||
"path"
|
||||
"syscall"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
var (
|
||||
modIPHelp = syscall.NewLazyDLL("iphlpapi.dll")
|
||||
procGetExtendedTcpTable = modIPHelp.NewProc("GetExtendedTcpTable")
|
||||
procGetExtendedUdpTable = modIPHelp.NewProc("GetExtendedUdpTable")
|
||||
)
|
||||
|
||||
// DownloadFile downloads a file from the given url
|
||||
@ -45,3 +56,105 @@ func DownloadFile(ctx context.Context, url string) (filename, mediaType string,
|
||||
return "", "", nil, err
|
||||
}
|
||||
}
|
||||
|
||||
//nolint:unused
|
||||
type mibTCPRowOwnerPid struct {
|
||||
dwState uint32
|
||||
dwLocalAddr uint32
|
||||
dwLocalPort uint32
|
||||
dwRemoteAddr uint32
|
||||
dwRemotePort uint32
|
||||
dwOwningPid uint32
|
||||
}
|
||||
|
||||
//nolint:unused
|
||||
type mibTCP6RowOwnerPid struct {
|
||||
ucLocalAddr [16]byte
|
||||
dwLocalScopeId uint32
|
||||
dwLocalPort uint32
|
||||
ucRemoteAddr [16]byte
|
||||
dwRemoteScopeId uint32
|
||||
dwRemotePort uint32
|
||||
dwState uint32
|
||||
dwOwningPid uint32
|
||||
}
|
||||
|
||||
//nolint:unused
|
||||
type mibUDPRowOwnerPid struct {
|
||||
dwLocalAddr uint32
|
||||
dwLocalPort uint32
|
||||
dwOwningPid uint32
|
||||
}
|
||||
|
||||
//nolint:unused
|
||||
type mibUDP6RowOwnerPid struct {
|
||||
ucLocalAddr [16]byte
|
||||
dwLocalScopeId uint32
|
||||
dwLocalPort uint32
|
||||
dwOwningPid uint32
|
||||
}
|
||||
|
||||
type mibTableOwnerPid[T any] struct {
|
||||
dwNumEntries uint32
|
||||
table [1]T
|
||||
}
|
||||
|
||||
// countConnections returns the number of IPv4 and IPv6 connections that match the given filter.
|
||||
// - https://learn.microsoft.com/en-us/windows/win32/api/iphlpapi/nf-iphlpapi-getextendedtcptable
|
||||
// - https://learn.microsoft.com/en-us/windows/win32/api/iphlpapi/nf-iphlpapi-getextendedudptable
|
||||
func countConnections[R4, R6 any](proc *syscall.LazyProc, tableClass uintptr, filter4 func(R4) bool, filter6 func(R6) bool) (count int) {
|
||||
var size uint32
|
||||
var buf []byte
|
||||
getTable := func(af uintptr) bool {
|
||||
for {
|
||||
var pTable *byte
|
||||
if len(buf) > 0 {
|
||||
pTable = &buf[0]
|
||||
}
|
||||
ret, _, _ := proc.Call(uintptr(unsafe.Pointer(pTable)), uintptr(unsafe.Pointer(&size)), 0, af, tableClass, 0)
|
||||
if ret != 0 {
|
||||
if errors.Is(syscall.Errno(ret), syscall.ERROR_INSUFFICIENT_BUFFER) {
|
||||
buf = make([]byte, int(size))
|
||||
continue
|
||||
}
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
if getTable(windows.AF_INET) {
|
||||
table := (*mibTableOwnerPid[R4])(unsafe.Pointer(&buf[0]))
|
||||
for _, conn := range unsafe.Slice(&table.table[0], table.dwNumEntries) {
|
||||
if filter4(conn) {
|
||||
count++
|
||||
}
|
||||
}
|
||||
}
|
||||
if getTable(windows.AF_INET6) {
|
||||
table := (*mibTableOwnerPid[R6])(unsafe.Pointer(&buf[0]))
|
||||
for _, conn := range unsafe.Slice(&table.table[0], table.dwNumEntries) {
|
||||
if filter6(conn) {
|
||||
count++
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// CountTCPConnections returns the number of connected TCP endpoints for a given process.
|
||||
func CountTCPConnections(pid uint32) int {
|
||||
return countConnections(procGetExtendedTcpTable, 4, func(r4 mibTCPRowOwnerPid) bool {
|
||||
return r4.dwOwningPid == pid
|
||||
}, func(r6 mibTCP6RowOwnerPid) bool {
|
||||
return r6.dwOwningPid == pid
|
||||
})
|
||||
}
|
||||
|
||||
// CountUDPConnections returns the number of UDP endpoints for a given process.
|
||||
func CountUDPConnections(pid uint32) int {
|
||||
return countConnections(procGetExtendedUdpTable, 1, func(r4 mibUDPRowOwnerPid) bool {
|
||||
return r4.dwOwningPid == pid
|
||||
}, func(r6 mibUDP6RowOwnerPid) bool {
|
||||
return r6.dwOwningPid == pid
|
||||
})
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ type PasswordValidator struct {
|
||||
func (p *PasswordValidator) Validate(v interface{}) error {
|
||||
text := v.(string)
|
||||
if text == "" {
|
||||
return silentErr
|
||||
return errSilent
|
||||
}
|
||||
if (*p.Password).Text() == text {
|
||||
return nil
|
||||
|
@ -6,7 +6,7 @@ import (
|
||||
"github.com/lxn/walk"
|
||||
)
|
||||
|
||||
var silentErr = errors.New("")
|
||||
var errSilent = errors.New("")
|
||||
|
||||
type ToolTipErrorPresenter struct {
|
||||
*walk.ToolTipErrorPresenter
|
||||
@ -21,7 +21,7 @@ func NewToolTipErrorPresenter() (*ToolTipErrorPresenter, error) {
|
||||
}
|
||||
|
||||
func (ttep *ToolTipErrorPresenter) PresentError(err error, widget walk.Widget) {
|
||||
if errors.Is(err, silentErr) {
|
||||
if errors.Is(err, errSilent) {
|
||||
ttep.ToolTipErrorPresenter.PresentError(nil, widget)
|
||||
} else {
|
||||
ttep.ToolTipErrorPresenter.PresentError(err, widget)
|
||||
|
@ -20,7 +20,7 @@ func NewRegexpValidator(pattern string) (*RegexpValidator, error) {
|
||||
func (rv *RegexpValidator) Validate(v interface{}) error {
|
||||
err := rv.RegexpValidator.Validate(v)
|
||||
if str, ok := v.(string); ok && str == "" && err != nil {
|
||||
return silentErr
|
||||
return errSilent
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
@ -10,8 +10,8 @@ func TestRegexp(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = r.Validate(""); !errors.Is(err, silentErr) {
|
||||
t.Errorf("Expected: %v, got: %v", silentErr, err)
|
||||
if err = r.Validate(""); !errors.Is(err, errSilent) {
|
||||
t.Errorf("Expected: %v, got: %v", errSilent, err)
|
||||
}
|
||||
tests := []struct {
|
||||
input string
|
||||
|
@ -116,3 +116,26 @@ func UninstallService(configPath string, wait bool) error {
|
||||
}
|
||||
return err2
|
||||
}
|
||||
|
||||
// QueryStartInfo returns the start type and process id of the given service.
|
||||
func QueryStartInfo(configPath string) (uint32, uint32, error) {
|
||||
m, err := serviceManager()
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
serviceName := ServiceNameOfClient(configPath)
|
||||
service, err := m.OpenService(serviceName)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
defer service.Close()
|
||||
cfg, err := service.Config()
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
var pid uint32
|
||||
if status, err := service.Query(); err == nil {
|
||||
pid = status.ProcessId
|
||||
}
|
||||
return cfg.StartType, pid, nil
|
||||
}
|
||||
|
@ -144,6 +144,15 @@ func (cv *ConfView) View() Widget {
|
||||
Enabled: Bind("confView.ItemCount > 0"),
|
||||
OnTriggered: cv.onExport,
|
||||
},
|
||||
Action{
|
||||
Text: i18n.Sprintf("Properties"),
|
||||
Enabled: Bind("confView.SelectedCount == 1"),
|
||||
OnTriggered: func() {
|
||||
if conf := getCurrentConf(); conf != nil {
|
||||
NewPropertiesDialog(conf).Run(cv.Form())
|
||||
}
|
||||
},
|
||||
},
|
||||
Separator{},
|
||||
Action{
|
||||
Enabled: Bind("confView.SelectedCount < confView.ItemCount"),
|
||||
|
@ -295,7 +295,7 @@ func (pd *EditProxyDialog) advancedProxyPage() TabPage {
|
||||
Label{Visible: Bind("vm.PluginEnable"), Text: i18n.SprintfColon("Proxy Protocol")},
|
||||
ComboBox{
|
||||
Visible: Bind("vm.PluginEnable"),
|
||||
Model: NewListModel([]string{"", "v1", "v2"}, i18n.Sprintf("auto")),
|
||||
Model: NewListModel([]string{"", "v1", "v2"}, i18n.Sprintf("Auto")),
|
||||
BindingMember: "Value",
|
||||
DisplayMember: "Title",
|
||||
Value: Bind("ProxyProtocolVersion"),
|
||||
@ -303,7 +303,7 @@ func (pd *EditProxyDialog) advancedProxyPage() TabPage {
|
||||
Label{Visible: xtcpVisitor, Text: i18n.SprintfColon("Protocol")},
|
||||
ComboBox{
|
||||
Visible: xtcpVisitor,
|
||||
Model: NewListModel([]string{"", consts.ProtoQUIC, consts.ProtoKCP}, i18n.Sprintf("default")),
|
||||
Model: NewListModel([]string{"", consts.ProtoQUIC, consts.ProtoKCP}, i18n.Sprintf("Default")),
|
||||
BindingMember: "Value",
|
||||
DisplayMember: "Title",
|
||||
Value: Bind("Protocol"),
|
||||
|
@ -79,7 +79,7 @@ func (nd *NATDiscoveryDialog) discover() (err error) {
|
||||
return err
|
||||
}
|
||||
if len(addrs) < 2 {
|
||||
return fmt.Errorf("can not get enough addresses, need 2, got: %v\n", addrs)
|
||||
return fmt.Errorf("can not get enough addresses")
|
||||
}
|
||||
|
||||
localIPs, _ := nathole.ListLocalIPsForNatHole(10)
|
||||
|
131
ui/properties.go
Normal file
131
ui/properties.go
Normal file
@ -0,0 +1,131 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"os"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/lxn/walk"
|
||||
. "github.com/lxn/walk/declarative"
|
||||
"golang.org/x/sys/windows"
|
||||
|
||||
"github.com/koho/frpmgr/i18n"
|
||||
"github.com/koho/frpmgr/pkg/res"
|
||||
"github.com/koho/frpmgr/pkg/util"
|
||||
"github.com/koho/frpmgr/services"
|
||||
)
|
||||
|
||||
type PropertiesDialog struct {
|
||||
*walk.Dialog
|
||||
|
||||
table *walk.TableView
|
||||
conf *Conf
|
||||
}
|
||||
|
||||
func NewPropertiesDialog(conf *Conf) *PropertiesDialog {
|
||||
return &PropertiesDialog{conf: conf}
|
||||
}
|
||||
|
||||
func (pd *PropertiesDialog) logFileStat() (count int, size int64) {
|
||||
if logs, _, err := util.FindLogFiles(pd.conf.Data.GetLogFile()); err == nil {
|
||||
for _, logFile := range logs {
|
||||
if fileInfo, err := os.Stat(logFile); err == nil {
|
||||
count++
|
||||
size += fileInfo.Size()
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (pd *PropertiesDialog) Run(owner walk.Form) (int, error) {
|
||||
logFileCount, logFileSize := pd.logFileStat()
|
||||
logSizeDesc := util.ByteCountIEC(logFileSize)
|
||||
|
||||
var startTypeDesc string
|
||||
startType, pid, _ := services.QueryStartInfo(pd.conf.Path)
|
||||
switch startType {
|
||||
case windows.SERVICE_AUTO_START:
|
||||
startTypeDesc = i18n.Sprintf("Auto")
|
||||
case windows.SERVICE_DEMAND_START:
|
||||
startTypeDesc = i18n.Sprintf("Manual")
|
||||
default:
|
||||
startTypeDesc = i18n.Sprintf("None")
|
||||
}
|
||||
items := []*ListItem{
|
||||
{Title: i18n.Sprintf("Name"), Value: pd.conf.Name()},
|
||||
{Title: i18n.Sprintf("Identifier"), Value: util.FileNameWithoutExt(pd.conf.Path)},
|
||||
{Title: i18n.Sprintf("Service Name"), Value: services.ServiceNameOfClient(pd.conf.Path)},
|
||||
{Title: i18n.Sprintf("File Format"), Value: strings.ToUpper(pd.conf.Data.Ext()[1:])},
|
||||
{Title: i18n.Sprintf("Number of Proxies"), Value: strconv.Itoa(reflect.ValueOf(pd.conf.Data.Items()).Len())},
|
||||
{Title: i18n.Sprintf("Start Type"), Value: startTypeDesc},
|
||||
{Title: i18n.Sprintf("Log"), Value: i18n.Sprintf("%d Files, %s", logFileCount, logSizeDesc)},
|
||||
}
|
||||
if pid > 0 {
|
||||
items = append(items, &ListItem{
|
||||
Title: i18n.Sprintf("Number of TCP Connections"),
|
||||
Value: strconv.Itoa(util.CountTCPConnections(pid)),
|
||||
}, &ListItem{
|
||||
Title: i18n.Sprintf("Number of UDP Connections"),
|
||||
Value: strconv.Itoa(util.CountUDPConnections(pid)),
|
||||
})
|
||||
if process, err := syscall.OpenProcess(windows.PROCESS_QUERY_LIMITED_INFORMATION, false, pid); err == nil {
|
||||
var creationTime, unusedTime syscall.Filetime
|
||||
if err = syscall.GetProcessTimes(process, &creationTime, &unusedTime, &unusedTime, &unusedTime); err == nil {
|
||||
items = append(items, &ListItem{
|
||||
Title: i18n.Sprintf("Started"),
|
||||
Value: time.Unix(0, creationTime.Nanoseconds()).Format(time.DateTime),
|
||||
})
|
||||
}
|
||||
syscall.CloseHandle(process)
|
||||
}
|
||||
}
|
||||
if info, err := os.Stat(pd.conf.Path); err == nil {
|
||||
created := time.Unix(0, info.Sys().(*syscall.Win32FileAttributeData).CreationTime.Nanoseconds())
|
||||
modified := info.ModTime()
|
||||
items = append(items, &ListItem{
|
||||
Title: i18n.Sprintf("Created"),
|
||||
Value: created.Format(time.DateTime),
|
||||
}, &ListItem{
|
||||
Title: i18n.Sprintf("Modified"),
|
||||
Value: modified.Format(time.DateTime),
|
||||
})
|
||||
}
|
||||
dlg := NewBasicDialog(&pd.Dialog, i18n.Sprintf("%s Properties", pd.conf.Name()),
|
||||
loadIcon(res.IconFile, 32),
|
||||
DataBinder{}, nil,
|
||||
TableView{
|
||||
AssignTo: &pd.table,
|
||||
Name: "properties",
|
||||
Columns: []TableViewColumn{
|
||||
{Title: i18n.Sprintf("Item"), DataMember: "Title"},
|
||||
{Title: i18n.Sprintf("Value"), DataMember: "Value", Width: 180},
|
||||
},
|
||||
ColumnsOrderable: false,
|
||||
Model: NewNonSortedModel(items),
|
||||
ContextMenuItems: []MenuItem{
|
||||
Action{
|
||||
Text: i18n.Sprintf("Copy Value"),
|
||||
Enabled: Bind("properties.CurrentIndex >= 0"),
|
||||
Visible: Bind("properties.CurrentIndex >= 0"),
|
||||
OnTriggered: func() {
|
||||
if idx := pd.table.CurrentIndex(); idx >= 0 && idx < len(items) {
|
||||
walk.Clipboard().SetText(items[idx].Value)
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
dlg.MinSize = Size{Width: 420, Height: 350}
|
||||
if err := dlg.Create(owner); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
pd.table.BoundsChanged().Once(func() {
|
||||
pd.table.FitColumn(0, 140)
|
||||
})
|
||||
return pd.Dialog.Run(), nil
|
||||
}
|
Reference in New Issue
Block a user