Add an open port action in quick add menu (#37)

* Add an open port action in quick add menu

* Add tooltip to show error

* Update passwd.go
This commit is contained in:
Gerhard Tan
2023-03-08 18:20:39 +08:00
committed by GitHub
parent a5ef9a7a16
commit 55d56d3c11
21 changed files with 1974 additions and 938 deletions

File diff suppressed because it is too large Load Diff

View File

@ -87,6 +87,58 @@
"translatorComment": "Copied from source.",
"fuzzy": true
},
{
"id": "Number out of allowed range",
"message": "Number out of allowed range",
"translation": "Number out of allowed range",
"translatorComment": "Copied from source.",
"fuzzy": true
},
{
"id": "Please enter a number greater than {Intt}.",
"message": "Please enter a number greater than {Intt}.",
"translation": "Please enter a number greater than {Intt}.",
"translatorComment": "Copied from source.",
"placeholders": [
{
"id": "Intt",
"string": "%[1]d",
"type": "int",
"underlyingType": "int",
"argNum": 1,
"expr": "int(t)"
}
],
"fuzzy": true
},
{
"id": "Password mismatch",
"message": "Password mismatch",
"translation": "Password mismatch",
"translatorComment": "Copied from source.",
"fuzzy": true
},
{
"id": "Please check and try again.",
"message": "Please check and try again.",
"translation": "Please check and try again.",
"translatorComment": "Copied from source.",
"fuzzy": true
},
{
"id": "Not a number",
"message": "Not a number",
"translation": "Not a number",
"translatorComment": "Copied from source.",
"fuzzy": true
},
{
"id": "Please enter a valid number.",
"message": "Please enter a valid number.",
"translation": "Please enter a valid number.",
"translatorComment": "Copied from source.",
"fuzzy": true
},
{
"id": "New Version!",
"message": "New Version!",
@ -1336,6 +1388,20 @@
"translatorComment": "Copied from source.",
"fuzzy": true
},
{
"id": "Port",
"message": "Port",
"translation": "Port",
"translatorComment": "Copied from source.",
"fuzzy": true
},
{
"id": "Open Port",
"message": "Open Port",
"translation": "Open Port",
"translatorComment": "Copied from source.",
"fuzzy": true
},
{
"id": "Preferences",
"message": "Preferences",
@ -1483,13 +1549,6 @@
"translatorComment": "Copied from source.",
"fuzzy": true
},
{
"id": "SOCKS5 Proxy",
"message": "SOCKS5 Proxy",
"translation": "SOCKS5 Proxy",
"translatorComment": "Copied from source.",
"fuzzy": true
},
{
"id": "Enable",
"message": "Enable",
@ -1600,13 +1659,6 @@
"translatorComment": "Copied from source.",
"fuzzy": true
},
{
"id": "Port",
"message": "Port",
"translation": "Port",
"translatorComment": "Copied from source.",
"fuzzy": true
},
{
"id": "Passive Port Range",
"message": "Passive Port Range",
@ -1696,6 +1748,87 @@
"translation": "The password is incorrect. Re-enter password.",
"translatorComment": "Copied from source.",
"fuzzy": true
},
{
"id": "Invalid Input",
"message": "Invalid Input",
"translation": "Invalid Input",
"translatorComment": "Copied from source.",
"fuzzy": true
},
{
"id": "Please enter a number from {Number} to {Number_1}.",
"message": "Please enter a number from {Number} to {Number_1}.",
"translation": "Please enter a number from {Number} to {Number_1}.",
"translatorComment": "Copied from source.",
"placeholders": [
{
"id": "Number",
"string": "%.[1]f",
"type": "",
"underlyingType": "float64",
"argNum": 1
},
{
"id": "Number_1",
"string": "%.[2]f",
"type": "",
"underlyingType": "float64",
"argNum": 2
}
],
"fuzzy": true
},
{
"id": "Please enter a number from {Arg_1} to {Arg_2}.",
"message": "Please enter a number from {Arg_1} to {Arg_2}.",
"translation": "Please enter a number from {Arg_1} to {Arg_2}.",
"translatorComment": "Copied from source.",
"placeholders": [
{
"id": "Arg_1",
"string": "%[1]s",
"type": "",
"underlyingType": "string",
"argNum": 1
},
{
"id": "Arg_2",
"string": "%[2]s",
"type": "",
"underlyingType": "string",
"argNum": 2
}
],
"fuzzy": true
},
{
"id": "The text does not match the required pattern.",
"message": "The text does not match the required pattern.",
"translation": "The text does not match the required pattern.",
"translatorComment": "Copied from source.",
"fuzzy": true
},
{
"id": "Selection Required",
"message": "Selection Required",
"translation": "Selection Required",
"translatorComment": "Copied from source.",
"fuzzy": true
},
{
"id": "Please select one of the provided options.",
"message": "Please select one of the provided options.",
"translation": "Please select one of the provided options.",
"translatorComment": "Copied from source.",
"fuzzy": true
},
{
"id": "A selection is required.",
"message": "A selection is required.",
"translation": "A selection is required.",
"translatorComment": "Copied from source.",
"fuzzy": true
}
]
}

View File

@ -71,6 +71,46 @@
"message": "Log Files",
"translation": "Archivos de registro"
},
{
"id": "Number out of allowed range",
"message": "Number out of allowed range",
"translation": "Número fuera del rango permitido"
},
{
"id": "Please enter a number greater than {Intt}.",
"message": "Please enter a number greater than {Intt}.",
"translation": "Ingrese un número mayor que {Intt}.",
"placeholders": [
{
"id": "Intt",
"string": "%[1]d",
"type": "int",
"underlyingType": "int",
"argNum": 1,
"expr": "int(t)"
}
]
},
{
"id": "Password mismatch",
"message": "Password mismatch",
"translation": "Contraseña no coincide"
},
{
"id": "Please check and try again.",
"message": "Please check and try again.",
"translation": "Por favor revisa e intenta de nuevo."
},
{
"id": "Not a number",
"message": "Not a number",
"translation": "No un número"
},
{
"id": "Please enter a valid number.",
"message": "Please enter a valid number.",
"translation": "Por favor ingrese un número valido."
},
{
"id": "New Version!",
"message": "New Version!",
@ -994,6 +1034,16 @@
"message": "Add",
"translation": "Agregar"
},
{
"id": "Port",
"message": "Port",
"translation": "Puerto"
},
{
"id": "Open Port",
"message": "Open Port",
"translation": "Puerto abierto"
},
{
"id": "Preferences",
"message": "Preferences",
@ -1099,11 +1149,6 @@
"message": "HTTP File Server",
"translation": "Servidor de archivos HTTP"
},
{
"id": "SOCKS5 Proxy",
"message": "SOCKS5 Proxy",
"translation": "Proxy SOCKS5"
},
{
"id": "Enable",
"message": "Enable",
@ -1194,11 +1239,6 @@
"message": "Disable",
"translation": "Deshabilitar"
},
{
"id": "Port",
"message": "Port",
"translation": "Puerto"
},
{
"id": "Passive Port Range",
"message": "Passive Port Range",
@ -1266,6 +1306,73 @@
"id": "The password is incorrect. Re-enter password.",
"message": "The password is incorrect. Re-enter password.",
"translation": "La contraseña es incorrecta. Escriba la contraseña otra vez."
},
{
"id": "Invalid Input",
"message": "Invalid Input",
"translation": "Entrada invalida"
},
{
"id": "Please enter a number from {Number} to {Number_1}.",
"message": "Please enter a number from {Number} to {Number_1}.",
"translation": "Ingrese un número de {Number} a {Number_1}.",
"placeholders": [
{
"id": "Number",
"string": "%.[1]f",
"type": "",
"underlyingType": "float64",
"argNum": 1
},
{
"id": "Number_1",
"string": "%.[2]f",
"type": "",
"underlyingType": "float64",
"argNum": 2
}
]
},
{
"id": "Please enter a number from {Arg_1} to {Arg_2}.",
"message": "Please enter a number from {Arg_1} to {Arg_2}.",
"translation": "Ingrese un número de {Arg_1} a {Arg_2}.",
"placeholders": [
{
"id": "Arg_1",
"string": "%[1]s",
"type": "",
"underlyingType": "string",
"argNum": 1
},
{
"id": "Arg_2",
"string": "%[2]s",
"type": "",
"underlyingType": "string",
"argNum": 2
}
]
},
{
"id": "The text does not match the required pattern.",
"message": "The text does not match the required pattern.",
"translation": "El texto no coincide con el patrón requerido."
},
{
"id": "Selection Required",
"message": "Selection Required",
"translation": "Selección requerida"
},
{
"id": "Please select one of the provided options.",
"message": "Please select one of the provided options.",
"translation": "Seleccione una de las opciones proporcionadas."
},
{
"id": "A selection is required.",
"message": "A selection is required.",
"translation": "Se requiere una selección."
}
]
}

View File

@ -71,6 +71,46 @@
"message": "Log Files",
"translation": "ログファイル"
},
{
"id": "Number out of allowed range",
"message": "Number out of allowed range",
"translation": "許容範囲外の数値"
},
{
"id": "Please enter a number greater than {Intt}.",
"message": "Please enter a number greater than {Intt}.",
"translation": "{Intt} より大きい数値を入力してください。",
"placeholders": [
{
"id": "Intt",
"string": "%[1]d",
"type": "int",
"underlyingType": "int",
"argNum": 1,
"expr": "int(t)"
}
]
},
{
"id": "Password mismatch",
"message": "Password mismatch",
"translation": "パスワードの不一致"
},
{
"id": "Please check and try again.",
"message": "Please check and try again.",
"translation": "もう一度確認してください。"
},
{
"id": "Not a number",
"message": "Not a number",
"translation": "数字ではありません"
},
{
"id": "Please enter a valid number.",
"message": "Please enter a valid number.",
"translation": "有効な数値を入力してください。"
},
{
"id": "New Version!",
"message": "New Version!",
@ -1004,6 +1044,16 @@
"message": "Add",
"translation": "追加"
},
{
"id": "Port",
"message": "Port",
"translation": "ポート"
},
{
"id": "Open Port",
"message": "Open Port",
"translation": "ポート開放"
},
{
"id": "Preferences",
"message": "Preferences",
@ -1109,11 +1159,6 @@
"message": "HTTP File Server",
"translation": "HTTP ファイルサーバー"
},
{
"id": "SOCKS5 Proxy",
"message": "SOCKS5 Proxy",
"translation": "SOCKS5 プロキシ"
},
{
"id": "Enable",
"message": "Enable",
@ -1204,11 +1249,6 @@
"message": "Disable",
"translation": "無効"
},
{
"id": "Port",
"message": "Port",
"translation": "ポート"
},
{
"id": "Passive Port Range",
"message": "Passive Port Range",
@ -1276,6 +1316,73 @@
"id": "The password is incorrect. Re-enter password.",
"message": "The password is incorrect. Re-enter password.",
"translation": "パスワードが正しくありません。 パスワード再入力。"
},
{
"id": "Invalid Input",
"message": "Invalid Input",
"translation": "無効入力"
},
{
"id": "Please enter a number from {Number} to {Number_1}.",
"message": "Please enter a number from {Number} to {Number_1}.",
"translation": "{Number} から {Number_1} までの数字を入力してください。",
"placeholders": [
{
"id": "Number",
"string": "%.[1]f",
"type": "",
"underlyingType": "float64",
"argNum": 1
},
{
"id": "Number_1",
"string": "%.[2]f",
"type": "",
"underlyingType": "float64",
"argNum": 2
}
]
},
{
"id": "Please enter a number from {Arg_1} to {Arg_2}.",
"message": "Please enter a number from {Arg_1} to {Arg_2}.",
"translation": "{Arg_1} から {Arg_2} までの数値を入力してください。",
"placeholders": [
{
"id": "Arg_1",
"string": "%[1]s",
"type": "",
"underlyingType": "string",
"argNum": 1
},
{
"id": "Arg_2",
"string": "%[2]s",
"type": "",
"underlyingType": "string",
"argNum": 2
}
]
},
{
"id": "The text does not match the required pattern.",
"message": "The text does not match the required pattern.",
"translation": "テキストが必要なパターンと一致しません。"
},
{
"id": "Selection Required",
"message": "Selection Required",
"translation": "選択必須"
},
{
"id": "Please select one of the provided options.",
"message": "Please select one of the provided options.",
"translation": "提供されたオプションのいずれかを選択してください。"
},
{
"id": "A selection is required.",
"message": "A selection is required.",
"translation": "選択が必要です。"
}
]
}

View File

@ -71,6 +71,46 @@
"message": "Log Files",
"translation": "로그 파일"
},
{
"id": "Number out of allowed range",
"message": "Number out of allowed range",
"translation": "허용 범위를 벗어난 숫자"
},
{
"id": "Please enter a number greater than {Intt}.",
"message": "Please enter a number greater than {Intt}.",
"translation": "{Intt}보다 큰 숫자를 입력하세요.",
"placeholders": [
{
"id": "Intt",
"string": "%[1]d",
"type": "int",
"underlyingType": "int",
"argNum": 1,
"expr": "int(t)"
}
]
},
{
"id": "Password mismatch",
"message": "Password mismatch",
"translation": "암호 불일치"
},
{
"id": "Please check and try again.",
"message": "Please check and try again.",
"translation": "확인하고 다시 시도해 주세요."
},
{
"id": "Not a number",
"message": "Not a number",
"translation": "숫자가 아님"
},
{
"id": "Please enter a valid number.",
"message": "Please enter a valid number.",
"translation": "유효한 숫자를 입력하세요."
},
{
"id": "New Version!",
"message": "New Version!",
@ -994,6 +1034,16 @@
"message": "Add",
"translation": "추가하다"
},
{
"id": "Port",
"message": "Port",
"translation": "포트"
},
{
"id": "Open Port",
"message": "Open Port",
"translation": "오픈 포트"
},
{
"id": "Preferences",
"message": "Preferences",
@ -1099,11 +1149,6 @@
"message": "HTTP File Server",
"translation": "HTTP 파일 서버"
},
{
"id": "SOCKS5 Proxy",
"message": "SOCKS5 Proxy",
"translation": "SOCKS5 프록시"
},
{
"id": "Enable",
"message": "Enable",
@ -1194,11 +1239,6 @@
"message": "Disable",
"translation": "폐쇄"
},
{
"id": "Port",
"message": "Port",
"translation": "포트"
},
{
"id": "Passive Port Range",
"message": "Passive Port Range",
@ -1266,6 +1306,73 @@
"id": "The password is incorrect. Re-enter password.",
"message": "The password is incorrect. Re-enter password.",
"translation": "비밀번호가 올바르지 않습니다. 비밀번호를 다시 입력하세요."
},
{
"id": "Invalid Input",
"message": "Invalid Input",
"translation": "잘못된 입력"
},
{
"id": "Please enter a number from {Number} to {Number_1}.",
"message": "Please enter a number from {Number} to {Number_1}.",
"translation": "{Number}에서 {Number_1}까지의 숫자를 입력하세요.",
"placeholders": [
{
"id": "Number",
"string": "%.[1]f",
"type": "",
"underlyingType": "float64",
"argNum": 1
},
{
"id": "Number_1",
"string": "%.[2]f",
"type": "",
"underlyingType": "float64",
"argNum": 2
}
]
},
{
"id": "Please enter a number from {Arg_1} to {Arg_2}.",
"message": "Please enter a number from {Arg_1} to {Arg_2}.",
"translation": "{Arg_1}에서 {Arg_2} 사이의 숫자를 입력하십시오.",
"placeholders": [
{
"id": "Arg_1",
"string": "%[1]s",
"type": "",
"underlyingType": "string",
"argNum": 1
},
{
"id": "Arg_2",
"string": "%[2]s",
"type": "",
"underlyingType": "string",
"argNum": 2
}
]
},
{
"id": "The text does not match the required pattern.",
"message": "The text does not match the required pattern.",
"translation": "텍스트가 필수 패턴과 일치하지 않습니다."
},
{
"id": "Selection Required",
"message": "Selection Required",
"translation": "선택 필수"
},
{
"id": "Please select one of the provided options.",
"message": "Please select one of the provided options.",
"translation": "제공된 옵션 중 하나를 선택하십시오."
},
{
"id": "A selection is required.",
"message": "A selection is required.",
"translation": "선택이 필요합니다."
}
]
}

View File

@ -71,6 +71,46 @@
"message": "Log Files",
"translation": "日志文件"
},
{
"id": "Number out of allowed range",
"message": "Number out of allowed range",
"translation": "数值超出允许范围"
},
{
"id": "Please enter a number greater than {Intt}.",
"message": "Please enter a number greater than {Intt}.",
"translation": "请输入一个大于 {Intt} 的数字。",
"placeholders": [
{
"id": "Intt",
"string": "%[1]d",
"type": "int",
"underlyingType": "int",
"argNum": 1,
"expr": "int(t)"
}
]
},
{
"id": "Password mismatch",
"message": "Password mismatch",
"translation": "密码不匹配"
},
{
"id": "Please check and try again.",
"message": "Please check and try again.",
"translation": "请检查并重试。"
},
{
"id": "Not a number",
"message": "Not a number",
"translation": "不是数字"
},
{
"id": "Please enter a valid number.",
"message": "Please enter a valid number.",
"translation": "请输入一个有效的数字。"
},
{
"id": "New Version!",
"message": "New Version!",
@ -994,6 +1034,16 @@
"message": "Add",
"translation": "添加"
},
{
"id": "Port",
"message": "Port",
"translation": "端口"
},
{
"id": "Open Port",
"message": "Open Port",
"translation": "打开端口"
},
{
"id": "Preferences",
"message": "Preferences",
@ -1099,11 +1149,6 @@
"message": "HTTP File Server",
"translation": "HTTP 文件服务"
},
{
"id": "SOCKS5 Proxy",
"message": "SOCKS5 Proxy",
"translation": "SOCKS5 代理"
},
{
"id": "Enable",
"message": "Enable",
@ -1194,11 +1239,6 @@
"message": "Disable",
"translation": "禁用"
},
{
"id": "Port",
"message": "Port",
"translation": "端口"
},
{
"id": "Passive Port Range",
"message": "Passive Port Range",
@ -1266,6 +1306,73 @@
"id": "The password is incorrect. Re-enter password.",
"message": "The password is incorrect. Re-enter password.",
"translation": "密码错误。请重新输入。"
},
{
"id": "Invalid Input",
"message": "Invalid Input",
"translation": "输入无效"
},
{
"id": "Please enter a number from {Number} to {Number_1}.",
"message": "Please enter a number from {Number} to {Number_1}.",
"translation": "请输入一个从 {Number} 到 {Number_1} 的数字。",
"placeholders": [
{
"id": "Number",
"string": "%.[1]f",
"type": "",
"underlyingType": "float64",
"argNum": 1
},
{
"id": "Number_1",
"string": "%.[2]f",
"type": "",
"underlyingType": "float64",
"argNum": 2
}
]
},
{
"id": "Please enter a number from {Arg_1} to {Arg_2}.",
"message": "Please enter a number from {Arg_1} to {Arg_2}.",
"translation": "请输入一个从 {Arg_1} 到 {Arg_2} 的数字。",
"placeholders": [
{
"id": "Arg_1",
"string": "%[1]s",
"type": "",
"underlyingType": "string",
"argNum": 1
},
{
"id": "Arg_2",
"string": "%[2]s",
"type": "",
"underlyingType": "string",
"argNum": 2
}
]
},
{
"id": "The text does not match the required pattern.",
"message": "The text does not match the required pattern.",
"translation": "文本与要求的模式不匹配。"
},
{
"id": "Selection Required",
"message": "Selection Required",
"translation": "必填项"
},
{
"id": "Please select one of the provided options.",
"message": "Please select one of the provided options.",
"translation": "请选择其中一个选项。"
},
{
"id": "A selection is required.",
"message": "A selection is required.",
"translation": "需要选择。"
}
]
}

View File

@ -71,6 +71,46 @@
"message": "Log Files",
"translation": "日誌文件"
},
{
"id": "Number out of allowed range",
"message": "Number out of allowed range",
"translation": "數值超出允許範圍"
},
{
"id": "Please enter a number greater than {Intt}.",
"message": "Please enter a number greater than {Intt}.",
"translation": "請輸入一個大於 {Intt} 的數字。",
"placeholders": [
{
"id": "Intt",
"string": "%[1]d",
"type": "int",
"underlyingType": "int",
"argNum": 1,
"expr": "int(t)"
}
]
},
{
"id": "Password mismatch",
"message": "Password mismatch",
"translation": "密碼不匹配"
},
{
"id": "Please check and try again.",
"message": "Please check and try again.",
"translation": "請檢查並重試。"
},
{
"id": "Not a number",
"message": "Not a number",
"translation": "不是數字"
},
{
"id": "Please enter a valid number.",
"message": "Please enter a valid number.",
"translation": "請輸入一個有效的數字。"
},
{
"id": "New Version!",
"message": "New Version!",
@ -994,6 +1034,16 @@
"message": "Add",
"translation": "添加"
},
{
"id": "Port",
"message": "Port",
"translation": "端口"
},
{
"id": "Open Port",
"message": "Open Port",
"translation": "打開端口"
},
{
"id": "Preferences",
"message": "Preferences",
@ -1099,11 +1149,6 @@
"message": "HTTP File Server",
"translation": "HTTP 文件服務"
},
{
"id": "SOCKS5 Proxy",
"message": "SOCKS5 Proxy",
"translation": "SOCKS5 代理"
},
{
"id": "Enable",
"message": "Enable",
@ -1194,11 +1239,6 @@
"message": "Disable",
"translation": "禁用"
},
{
"id": "Port",
"message": "Port",
"translation": "端口"
},
{
"id": "Passive Port Range",
"message": "Passive Port Range",
@ -1266,6 +1306,73 @@
"id": "The password is incorrect. Re-enter password.",
"message": "The password is incorrect. Re-enter password.",
"translation": "密碼錯誤。請重新輸入。"
},
{
"id": "Invalid Input",
"message": "Invalid Input",
"translation": "輸入無效"
},
{
"id": "Please enter a number from {Number} to {Number_1}.",
"message": "Please enter a number from {Number} to {Number_1}.",
"translation": "請輸入一個從 {Number} 到 {Number_1} 的數字。",
"placeholders": [
{
"id": "Number",
"string": "%.[1]f",
"type": "",
"underlyingType": "float64",
"argNum": 1
},
{
"id": "Number_1",
"string": "%.[2]f",
"type": "",
"underlyingType": "float64",
"argNum": 2
}
]
},
{
"id": "Please enter a number from {Arg_1} to {Arg_2}.",
"message": "Please enter a number from {Arg_1} to {Arg_2}.",
"translation": "請輸入一個從 {Arg_1} 到 {Arg_2} 的數字。",
"placeholders": [
{
"id": "Arg_1",
"string": "%[1]s",
"type": "",
"underlyingType": "string",
"argNum": 1
},
{
"id": "Arg_2",
"string": "%[2]s",
"type": "",
"underlyingType": "string",
"argNum": 2
}
]
},
{
"id": "The text does not match the required pattern.",
"message": "The text does not match the required pattern.",
"translation": "文本與要求的模式不匹配。"
},
{
"id": "Selection Required",
"message": "Selection Required",
"translation": "必填項"
},
{
"id": "Please select one of the provided options.",
"message": "Please select one of the provided options.",
"translation": "請選擇其中一個選項。"
},
{
"id": "A selection is required.",
"message": "A selection is required.",
"translation": "需要選擇。"
}
]
}

View File

@ -2,6 +2,7 @@ package consts
import (
"github.com/koho/frpmgr/i18n"
"github.com/koho/frpmgr/pkg/validators"
"github.com/lxn/walk"
. "github.com/lxn/walk/declarative"
"github.com/lxn/win"
@ -44,7 +45,7 @@ const (
IconFtp = 137
IconHttpFile = 69
IconHttpProxy = 114
IconSocks5 = 146
IconOpenPort = 135
IconVpn = 47
IconNewVersion1 = -1028
IconNewVersion2 = 1
@ -96,9 +97,10 @@ var (
// Validators
var (
ValidateNonEmpty = Regexp{Pattern: "[^\\s]+"}
ValidateRequireInteger = Regexp{Pattern: "^\\d+$"}
ValidateInteger = Regexp{Pattern: "^\\d*$"}
ValidateNonEmpty = validators.Regexp{Pattern: "[^\\s]+"}
ValidateRequireInteger = validators.Regexp{Pattern: "^\\d+$"}
ValidateInteger = validators.Regexp{Pattern: "^\\d*$"}
ValidatePortRange = []Validator{ValidateRequireInteger, validators.Range{Min: 0, Max: 65535}}
)
// Dialogs

View File

@ -1,20 +0,0 @@
package validators
import "github.com/lxn/walk"
// TextEqual compares to the text of a LineEdit.
type TextEqual struct {
Target **walk.LineEdit
}
func (t TextEqual) Create() (walk.Validator, error) {
return &TextEqual{t.Target}, nil
}
func (t *TextEqual) Validate(v interface{}) error {
text := v.(string)
if (*t.Target).Text() == text {
return nil
}
return walk.NewValidationError("Text mismatch", "")
}

40
pkg/validators/gte.go Normal file
View File

@ -0,0 +1,40 @@
package validators
import (
"github.com/koho/frpmgr/i18n"
"github.com/lxn/walk"
)
type GTEValidator struct {
Value **walk.LineEdit
}
func (g *GTEValidator) Validate(v interface{}) error {
text := v.(string)
f, err := walk.ParseFloat(text)
if err != nil {
return nanErr
}
val := (*g.Value).Text()
if val == "" {
return silentErr
}
t, err := walk.ParseFloat(val)
if err != nil {
return nanErr
}
if f >= t {
return nil
}
return walk.NewValidationError(i18n.Sprintf("Number out of allowed range"),
i18n.Sprintf("Please enter a number greater than %d.", int(t)))
}
// GTE checks whether the input value is greater than or equal to the target value.
type GTE struct {
Value **walk.LineEdit
}
func (g GTE) Create() (walk.Validator, error) {
return &GTEValidator{g.Value}, nil
}

30
pkg/validators/passwd.go Normal file
View File

@ -0,0 +1,30 @@
package validators
import (
"github.com/koho/frpmgr/i18n"
"github.com/lxn/walk"
)
type PasswordValidator struct {
Password **walk.LineEdit
}
func (p *PasswordValidator) Validate(v interface{}) error {
text := v.(string)
if text == "" {
return silentErr
}
if (*p.Password).Text() == text {
return nil
}
return walk.NewValidationError(i18n.Sprintf("Password mismatch"), i18n.Sprintf("Please check and try again."))
}
// ConfirmPassword checks whether the input text is equal to the password field.
type ConfirmPassword struct {
Password **walk.LineEdit
}
func (c ConfirmPassword) Create() (walk.Validator, error) {
return &PasswordValidator{c.Password}, nil
}

View File

@ -0,0 +1,40 @@
package validators
import (
"errors"
"github.com/koho/frpmgr/i18n"
"github.com/lxn/walk"
)
var (
nanErr = walk.NewValidationError(i18n.Sprintf("Not a number"), i18n.Sprintf("Please enter a valid number."))
silentErr = errors.New("")
)
type ToolTipErrorPresenter struct {
*walk.ToolTipErrorPresenter
}
func NewToolTipErrorPresenter() (*ToolTipErrorPresenter, error) {
p, err := walk.NewToolTipErrorPresenter()
if err != nil {
return nil, err
}
return &ToolTipErrorPresenter{p}, nil
}
func (ttep *ToolTipErrorPresenter) PresentError(err error, widget walk.Widget) {
if errors.Is(err, silentErr) {
ttep.ToolTipErrorPresenter.PresentError(nil, widget)
} else {
ttep.ToolTipErrorPresenter.PresentError(err, widget)
}
}
// SilentToolTipErrorPresenter hides the tooltip when the input value is empty.
type SilentToolTipErrorPresenter struct {
}
func (SilentToolTipErrorPresenter) Create() (walk.ErrorPresenter, error) {
return NewToolTipErrorPresenter()
}

46
pkg/validators/range.go Normal file
View File

@ -0,0 +1,46 @@
package validators
import (
"github.com/lxn/walk"
)
type RangeValidator struct {
*walk.RangeValidator
}
func NewRangeValidator(min, max float64) (*RangeValidator, error) {
validator, err := walk.NewRangeValidator(min, max)
if err != nil {
return nil, err
}
return &RangeValidator{validator}, nil
}
func (rv *RangeValidator) Validate(v interface{}) error {
var value float64
switch v.(type) {
case string:
f, err := walk.ParseFloat(v.(string))
if err != nil {
return nanErr
}
value = f
case float64:
value = v.(float64)
default:
panic("Unsupported type")
}
return rv.RangeValidator.Validate(value)
}
// Range checks whether the input value is between Min and Max value.
// Supported widgets: NumberEdit, LineEdit.
type Range struct {
Min float64
Max float64
}
func (r Range) Create() (walk.Validator, error) {
return NewRangeValidator(r.Min, r.Max)
}

34
pkg/validators/regexp.go Normal file
View File

@ -0,0 +1,34 @@
package validators
import (
"github.com/lxn/walk"
)
type RegexpValidator struct {
*walk.RegexpValidator
}
func NewRegexpValidator(pattern string) (*RegexpValidator, error) {
re, err := walk.NewRegexpValidator(pattern)
if err != nil {
return nil, err
}
return &RegexpValidator{re}, nil
}
func (rv *RegexpValidator) Validate(v interface{}) error {
err := rv.RegexpValidator.Validate(v)
if str, ok := v.(string); ok && str == "" && err != nil {
return silentErr
}
return err
}
type Regexp struct {
Pattern string
}
func (re Regexp) Create() (walk.Validator, error) {
return NewRegexpValidator(re.Pattern)
}

View File

@ -5,6 +5,7 @@ import (
"github.com/koho/frpmgr/i18n"
"github.com/koho/frpmgr/pkg/config"
"github.com/koho/frpmgr/pkg/consts"
"github.com/koho/frpmgr/pkg/validators"
"github.com/lxn/walk"
. "github.com/lxn/walk/declarative"
)
@ -35,9 +36,18 @@ func NewPluginProxyDialog(title string, icon *walk.Icon, plugin string) *PluginP
func (pp *PluginProxyDialog) Run(owner walk.Form) (int, error) {
widgets := []Widget{
Label{Text: i18n.SprintfColon("Remote Port")},
LineEdit{Text: Bind("RemotePort", consts.ValidateRequireInteger)},
LineEdit{Text: Bind("RemotePort", consts.ValidatePortRange...)},
}
switch pp.plugin {
case consts.PluginHttpProxy, consts.PluginSocks5:
pp.binder.Plugin = consts.PluginHttpProxy
widgets = append([]Widget{
Label{Text: i18n.SprintfColon("Type")},
NewRadioButtonGroup("Plugin", nil, []RadioButton{
{Text: "HTTP", Value: consts.PluginHttpProxy},
{Text: "SOCKS5", Value: consts.PluginSocks5},
}),
}, widgets...)
case consts.PluginStaticFile:
// Make the dialog wider
remoteView := widgets[1].(LineEdit)
@ -50,12 +60,10 @@ func (pp *PluginProxyDialog) Run(owner walk.Form) (int, error) {
)
}
return NewBasicDialog(&pp.Dialog, fmt.Sprintf("%s %s", i18n.Sprintf("Add"), pp.title), pp.icon, DataBinder{
AssignTo: &pp.db,
DataSource: pp.binder,
}, pp.onSave, Composite{
Layout: Grid{Columns: 2, MarginsZero: true},
Children: widgets,
}, VSpacer{}).Run(owner)
AssignTo: &pp.db,
DataSource: pp.binder,
ErrorPresenter: validators.SilentToolTipErrorPresenter{},
}, pp.onSave, append(widgets, VSpacer{})...).Run(owner)
}
func (pp *PluginProxyDialog) GetProxies() []*config.Proxy {
@ -66,6 +74,9 @@ func (pp *PluginProxyDialog) onSave() {
if err := pp.db.Submit(); err != nil {
return
}
if pp.binder.Plugin != "" {
pp.plugin = pp.binder.Plugin
}
pp.Proxies = append(pp.Proxies, &config.Proxy{
BaseProxyConf: config.BaseProxyConf{
Name: fmt.Sprintf("%s_%s", pp.plugin, pp.binder.RemotePort),

104
ui/portproxy.go Normal file
View File

@ -0,0 +1,104 @@
package ui
import (
"fmt"
"github.com/koho/frpmgr/i18n"
"github.com/koho/frpmgr/pkg/config"
"github.com/koho/frpmgr/pkg/consts"
"github.com/koho/frpmgr/pkg/validators"
"github.com/lxn/walk"
. "github.com/lxn/walk/declarative"
)
type portProxyBinder struct {
quickAddBinder
Name string
TCP bool
UDP bool
}
type PortProxyDialog struct {
*walk.Dialog
Proxies []*config.Proxy
binder *portProxyBinder
db *walk.DataBinder
}
func NewPortProxyDialog() *PortProxyDialog {
dlg := new(PortProxyDialog)
dlg.binder = &portProxyBinder{
quickAddBinder: quickAddBinder{
LocalAddr: "127.0.0.1",
},
TCP: true,
UDP: true,
}
return dlg
}
func (pp *PortProxyDialog) Run(owner walk.Form) (int, error) {
widgets := []Widget{
Label{Text: i18n.SprintfColon("Name"), ColumnSpan: 2},
LineEdit{Text: Bind("Name"), CueBanner: "open_xxx", ColumnSpan: 2},
Label{Text: i18n.SprintfColon("Remote Port"), ColumnSpan: 2},
LineEdit{Text: Bind("RemotePort", consts.ValidatePortRange...), ColumnSpan: 2},
Label{Text: i18n.SprintfColon("Protocol"), ColumnSpan: 2},
Composite{
Layout: HBox{MarginsZero: true},
ColumnSpan: 2,
Children: []Widget{
CheckBox{Text: "TCP", Checked: Bind("TCP")},
CheckBox{Text: "UDP", Checked: Bind("UDP")},
},
},
Label{Text: i18n.SprintfColon("Local Address")},
Label{Text: i18n.SprintfColon("Port")},
LineEdit{Text: Bind("LocalAddr", consts.ValidateNonEmpty), StretchFactor: 2},
LineEdit{Text: Bind("LocalPort", consts.ValidatePortRange...), StretchFactor: 1},
}
return NewBasicDialog(&pp.Dialog, i18n.Sprintf("Open Port"), loadSysIcon("shell32", consts.IconOpenPort, 32), DataBinder{
AssignTo: &pp.db,
DataSource: pp.binder,
ErrorPresenter: validators.SilentToolTipErrorPresenter{},
}, pp.onSave, Composite{
Layout: Grid{Columns: 2, MarginsZero: true},
MinSize: Size{Width: 280},
Children: widgets,
}, VSpacer{}).Run(owner)
}
func (pp *PortProxyDialog) GetProxies() []*config.Proxy {
return pp.Proxies
}
func (pp *PortProxyDialog) onSave() {
if err := pp.db.Submit(); err != nil {
return
}
name := pp.binder.Name
if name == "" {
name = fmt.Sprintf("open_%s", pp.binder.RemotePort)
}
proxy := config.Proxy{
BaseProxyConf: config.BaseProxyConf{
Name: name,
LocalIP: pp.binder.LocalAddr,
LocalPort: pp.binder.LocalPort,
},
RemotePort: pp.binder.RemotePort,
}
if pp.binder.TCP {
tcpProxy := proxy
tcpProxy.Name += "_tcp"
tcpProxy.Type = consts.ProxyTypeTCP
pp.Proxies = append(pp.Proxies, &tcpProxy)
}
if pp.binder.UDP {
udpProxy := proxy
udpProxy.Name += "_udp"
udpProxy.Type = consts.ProxyTypeUDP
pp.Proxies = append(pp.Proxies, &udpProxy)
}
pp.Accept()
}

View File

@ -178,14 +178,18 @@ func (pp *PrefPage) changePassword() string {
Password string
}
NewBasicDialog(nil, i18n.Sprintf("Master password"), loadResourceIcon(consts.IconKey, 32),
DataBinder{AssignTo: &db, DataSource: &vm}, nil, Composite{
DataBinder{
AssignTo: &db,
DataSource: &vm,
ErrorPresenter: validators.SilentToolTipErrorPresenter{},
}, nil, Composite{
Layout: VBox{MarginsZero: true},
MinSize: Size{Width: 280},
Children: []Widget{
Label{Text: i18n.SprintfColon("New master password")},
LineEdit{AssignTo: &pwdEdit, Text: Bind("Password", consts.ValidateNonEmpty), PasswordMode: true},
Label{Text: i18n.SprintfColon("Re-enter password")},
LineEdit{Text: Bind("", validators.TextEqual{Target: &pwdEdit}), PasswordMode: true},
LineEdit{Text: Bind("", validators.ConfirmPassword{Password: &pwdEdit}), PasswordMode: true},
},
}, VSpacer{}).Run(pp.Form())
if vm.Password != "" {

View File

@ -22,6 +22,7 @@ type ProxyView struct {
// Actions
newAction *walk.Action
portAction *walk.Action
rdAction *walk.Action
sshAction *walk.Action
webAction *walk.Action
@ -30,7 +31,6 @@ type ProxyView struct {
ftpAction *walk.Action
httpFileAction *walk.Action
httpProxyAction *walk.Action
socks5Action *walk.Action
vpnAction *walk.Action
editAction *walk.Action
deleteAction *walk.Action
@ -106,6 +106,14 @@ func (pv *ProxyView) createToolbar() ToolBar {
Text: i18n.Sprintf("Quick Add"),
Image: loadSysIcon("imageres", consts.IconQuickAdd, 16),
Items: []MenuItem{
Action{
AssignTo: &pv.portAction,
Text: i18n.Sprintf("Open Port"),
Image: loadSysIcon("shell32", consts.IconOpenPort, 16),
OnTriggered: func() {
pv.onQuickAdd(NewPortProxyDialog())
},
},
Action{
AssignTo: &pv.rdAction,
Text: i18n.Sprintf("Remote Desktop"),
@ -155,6 +163,15 @@ func (pv *ProxyView) createToolbar() ToolBar {
"dns", []string{consts.ProxyTypeUDP}, systemDns+":53"))
},
},
Action{
AssignTo: &pv.vpnAction,
Text: "OpenVPN",
Image: loadSysIcon("shell32", consts.IconVpn, 16),
OnTriggered: func() {
pv.onQuickAdd(NewSimpleProxyDialog("OpenVPN", loadSysIcon("shell32", consts.IconVpn, 32),
"openvpn", []string{consts.ProxyTypeTCP, consts.ProxyTypeUDP}, ":1194"))
},
},
Action{
AssignTo: &pv.ftpAction,
Text: "FTP",
@ -182,24 +199,6 @@ func (pv *ProxyView) createToolbar() ToolBar {
consts.PluginHttpProxy))
},
},
Action{
AssignTo: &pv.socks5Action,
Text: i18n.Sprintf("SOCKS5 Proxy"),
Image: loadSysIcon("imageres", consts.IconSocks5, 16),
OnTriggered: func() {
pv.onQuickAdd(NewPluginProxyDialog(i18n.Sprintf("SOCKS5 Proxy"), loadSysIcon("imageres", consts.IconSocks5, 32),
consts.PluginSocks5))
},
},
Action{
AssignTo: &pv.vpnAction,
Text: "OpenVPN",
Image: loadSysIcon("shell32", consts.IconVpn, 16),
OnTriggered: func() {
pv.onQuickAdd(NewSimpleProxyDialog("OpenVPN", loadSysIcon("shell32", consts.IconVpn, 32),
"openvpn", []string{consts.ProxyTypeTCP, consts.ProxyTypeUDP}, ":1194"))
},
},
},
},
Action{
@ -271,16 +270,16 @@ func (pv *ProxyView) createProxyTable() TableView {
Text: i18n.Sprintf("Quick Add"),
Image: loadSysIcon("imageres", consts.IconQuickAdd, 16),
Items: []MenuItem{
ActionRef{&pv.portAction},
ActionRef{&pv.rdAction},
ActionRef{&pv.vncAction},
ActionRef{&pv.sshAction},
ActionRef{&pv.webAction},
ActionRef{&pv.dnsAction},
ActionRef{&pv.vpnAction},
ActionRef{&pv.ftpAction},
ActionRef{&pv.httpFileAction},
ActionRef{&pv.httpProxyAction},
ActionRef{&pv.socks5Action},
ActionRef{&pv.vpnAction},
},
},
Action{

View File

@ -13,6 +13,7 @@ type quickAddBinder struct {
LocalPortMin string
LocalPortMax string
Dir string
Plugin string
}
// QuickAdd is the interface that must be implemented to build a quick-add dialog

View File

@ -6,6 +6,7 @@ import (
"github.com/koho/frpmgr/pkg/config"
"github.com/koho/frpmgr/pkg/consts"
"github.com/koho/frpmgr/pkg/util"
"github.com/koho/frpmgr/pkg/validators"
"github.com/lxn/walk"
. "github.com/lxn/walk/declarative"
)
@ -45,26 +46,28 @@ func NewSimpleProxyDialog(title string, icon *walk.Icon, service string, types [
func (sp *SimpleProxyDialog) Run(owner walk.Form) (int, error) {
widgets := []Widget{
Label{Text: i18n.SprintfColon("Remote Port"), ColumnSpan: 2},
LineEdit{Text: Bind("RemotePort", consts.ValidateRequireInteger), ColumnSpan: 2},
LineEdit{Text: Bind("RemotePort", consts.ValidatePortRange...), ColumnSpan: 2},
Label{Text: i18n.SprintfColon("Local Address")},
Label{Text: i18n.SprintfColon("Port")},
LineEdit{Text: Bind("LocalAddr", consts.ValidateNonEmpty), StretchFactor: 2},
LineEdit{Text: Bind("LocalPort", consts.ValidateRequireInteger), StretchFactor: 1},
LineEdit{Text: Bind("LocalPort", consts.ValidatePortRange...), StretchFactor: 1},
}
switch sp.service {
case "ftp":
var lPortMinEdit *walk.LineEdit
widgets = append(widgets, Label{Text: i18n.SprintfColon("Passive Port Range"), ColumnSpan: 2}, Composite{
Layout: HBox{MarginsZero: true},
Children: []Widget{
LineEdit{Text: Bind("LocalPortMin", consts.ValidateRequireInteger)},
LineEdit{AssignTo: &lPortMinEdit, Text: Bind("LocalPortMin", consts.ValidatePortRange...)},
Label{Text: "-"},
LineEdit{Text: Bind("LocalPortMax", consts.ValidateRequireInteger)},
LineEdit{Text: Bind("LocalPortMax", append(consts.ValidatePortRange, validators.GTE{Value: &lPortMinEdit})...)},
},
})
}
return NewBasicDialog(&sp.Dialog, fmt.Sprintf("%s %s", i18n.Sprintf("Add"), sp.title), sp.icon, DataBinder{
AssignTo: &sp.db,
DataSource: sp.binder,
AssignTo: &sp.db,
DataSource: sp.binder,
ErrorPresenter: validators.SilentToolTipErrorPresenter{},
}, sp.onSave, Composite{
Layout: Grid{Columns: 2, MarginsZero: true},
MinSize: Size{Width: 280},

View File

@ -36,6 +36,11 @@ type View interface {
func init() {
rand.Seed(time.Now().UnixNano())
walk.SetTranslationFunc(func(source string, context ...string) string {
translation := i18n.Sprintf(source)
s1 := strings.ReplaceAll(translation, "%!f(MISSING)", "%.f")
return strings.ReplaceAll(s1, "%!f(BADINDEX)", "%.f")
})
}
type FRPManager struct {