Allow setting default configuration format (#127)

* Allow setting default configuration format

* Fix spaces
This commit is contained in:
Gerhard Tan
2024-03-11 20:53:36 +08:00
committed by GitHub
parent 77453e1448
commit 3981230629
29 changed files with 327 additions and 245 deletions

View File

@ -1 +0,0 @@
[defaults]

View File

@ -1,19 +0,0 @@
# Language Setting File
#
# Specify your preferred Language ID on this setting file.
# The text messages will be displayed in the specified language.
#
# Please note that you must restart program to apply the modification of
# this setting file.
#
# Only one line is acceptable. Any other lines are ignored.
#
# Available Language IDs are:
# zh-CN: Simplified Chinese (简体中文)
# zh-TW: Traditional Chinese (繁體中文)
# en-US: English (English)
# ja-JP: Japanese (日本語)
# ko-KR: Korean (한국어)
# es-ES: Spanish (Español)
# Specify a Language ID here.

View File

@ -3,16 +3,15 @@ package i18n
//go:generate go run golang.org/x/text/cmd/gotext -srclang=en-US update -out=catalog.go -lang=en-US,zh-CN,zh-TW,ja-JP,ko-KR,es-ES ../cmd/frpmgr
import (
"bufio"
"encoding/json"
"os"
"strings"
"golang.org/x/sys/windows"
"golang.org/x/text/language"
"golang.org/x/text/message"
)
const LangFile = "lang.config"
"github.com/koho/frpmgr/pkg/config"
)
var (
printer *message.Printer
@ -43,19 +42,17 @@ func GetLanguage() string {
// langInConfig returns the UI language code in config file
func langInConfig() string {
langFile, err := os.Open(LangFile)
b, err := os.ReadFile(config.DefaultAppFile)
if err != nil {
return ""
}
defer langFile.Close()
scanner := bufio.NewScanner(langFile)
for scanner.Scan() {
if text := strings.TrimSpace(scanner.Text()); text != "" && !strings.HasPrefix(text, "#") {
return text
}
var s struct {
Lang string `json:"lang"`
}
return ""
if err = json.Unmarshal(b, &s); err != nil {
return ""
}
return s.Lang
}
// lang returns the user preferred UI language.

View File

@ -166,6 +166,14 @@ namespace actions
{
ForceDeleteDirectory(Path.Combine(installPath, "profiles"));
ForceDeleteDirectory(Path.Combine(installPath, "logs"));
try
{
File.Delete(Path.Combine(installPath, "app.json"));
}
catch (Exception e)
{
session.Log(e.Message);
}
}
return ActionResult.Success;
}
@ -210,8 +218,8 @@ namespace actions
public static ActionResult SetLangConfig(Session session)
{
session.Log("Set language config");
string langPath = session["CustomActionData"];
if (string.IsNullOrEmpty(langPath))
string installPath = session["CustomActionData"];
if (string.IsNullOrEmpty(installPath))
{
return ActionResult.Failure;
}
@ -220,7 +228,18 @@ namespace actions
{
return ActionResult.Failure;
}
File.AppendAllText(langPath, name.ToString() + Environment.NewLine, Encoding.UTF8);
string cfgPath = Path.Combine(installPath, "app.json");
if (!File.Exists(cfgPath))
{
try
{
File.WriteAllText(cfgPath, "{\n \"lang\": \"" + name.ToString() + "\"\n}");
}
catch (Exception e)
{
session.Log(e.Message);
}
}
return ActionResult.Success;
}

View File

@ -82,12 +82,6 @@
<!-- A dummy to make WiX create ServiceControl table for us. -->
<ServiceControl Id="DummyService.E3F2D6BE_38C7_4654_9C1B_C667A1F9040A" Name="DummyService.E3F2D6BE_38C7_4654_9C1B_C667A1F9040A" />
</Component>
<Component Guid="{4D11B79E-74E2-4F35-81EA-8B5CC7225A78}" Id="LangConfig" Win64="$(var.Win64)">
<File Id="LangConfig" Source="..\cmd\frpmgr\lang.config" KeyPath="yes"/>
</Component>
<Component Guid="{B73203B2-5C06-4C45-BC6C-1C98CE11A292}" Id="AppConfig" Win64="$(var.Win64)">
<File Id="AppConfig" Source="..\cmd\frpmgr\app.config" KeyPath="yes"/>
</Component>
</ComponentGroup>
</Fragment>
@ -99,7 +93,7 @@
<CustomAction Id="KillProcesses.SetProperty" Return="check" Property="KillProcesses" Value="[#MainApplication]" />
<CustomAction Id="EvaluateFrpServices.SetProperty" Return="check" Property="EvaluateFrpServices" Value="[#MainApplication]" />
<CustomAction Id="RemoveFrpFiles.SetProperty" Return="check" Property="RemoveFrpFiles" Value="[INSTALLFOLDER]" />
<CustomAction Id="SetLangConfig.SetProperty" Return="check" Property="SetLangConfig" Value="[#LangConfig]" />
<CustomAction Id="SetLangConfig.SetProperty" Return="check" Property="SetLangConfig" Value="[INSTALLFOLDER]" />
<CustomAction Id="MoveFrpProfiles.SetProperty" Return="check" Property="MoveFrpProfiles" Value="[INSTALLFOLDER]" />
<CustomAction Id="RemoveOldFrpServices.SetProperty" Return="check" Property="RemoveOldFrpServices" Value="[#MainApplication]" />

View File

@ -48,23 +48,40 @@ static int Cleanup(void) {
static Language *GetPreferredLang(TCHAR *folder) {
TCHAR langPath[MAX_PATH];
if (PathCombine(langPath, folder, L"lang.config") == NULL) {
if (PathCombine(langPath, folder, L"app.json") == NULL) {
return NULL;
}
FILE *file;
if (_wfopen_s(&file, langPath, L"r") != 0) {
if (_wfopen_s(&file, langPath, L"rb") != 0) {
return NULL;
}
char lang[sizeof(languages[0].code)];
while (fgets(lang, sizeof(lang), file) != NULL) {
for (int i = 0; i < sizeof(languages) / sizeof(languages[0]); i++) {
if (strncmp(lang, languages[i].code, strlen(languages[i].code)) == 0) {
fclose(file);
return &languages[i];
}
fseek(file, 0L, SEEK_END);
long fileSize = ftell(file);
fseek(file, 0L, SEEK_SET);
char *buf = malloc(fileSize + 1);
size_t size = fread(buf, 1, fileSize, file);
buf[size] = 0;
fclose(file);
const char *p1 = strstr(buf, "\"lang\"");
if (p1 == NULL) {
goto cleanup;
}
const char *p2 = strstr(p1, ":");
if (p2 == NULL) {
goto cleanup;
}
const char *p3 = strstr(p2, "\"");
if (p3 == NULL) {
goto cleanup;
}
for (int i = 0; i < sizeof(languages) / sizeof(languages[0]); i++) {
if (strncmp(p3 + 1, languages[i].code, strlen(languages[i].code)) == 0) {
free(buf);
return &languages[i];
}
}
fclose(file);
cleanup:
free(buf);
return NULL;
}

View File

@ -1,34 +1,72 @@
package config
import (
"gopkg.in/ini.v1"
"encoding/json"
"os"
"github.com/koho/frpmgr/pkg/consts"
)
const DefaultAppFile = "app.config"
const DefaultAppFile = "app.json"
type App struct {
Password string `ini:"password,omitempty"`
Defaults ClientCommon `ini:"defaults"`
Lang string `json:"lang,omitempty"`
Password string `json:"password,omitempty"`
Defaults DefaultValue `json:"defaults"`
}
func UnmarshalAppConfFromIni(source any, dst *App) error {
cfg, err := ini.LoadSources(ini.LoadOptions{
IgnoreInlineComment: true,
AllowBooleanKeys: true,
}, source)
type DefaultValue struct {
Protocol string `json:"protocol,omitempty"`
User string `json:"user,omitempty"`
LogLevel string `json:"logLevel"`
LogMaxDays int64 `json:"logMaxDays"`
DeleteAfterDays int64 `json:"deleteAfterDays,omitempty"`
DNSServer string `json:"dnsServer,omitempty"`
NatHoleSTUNServer string `json:"natHoleStunServer,omitempty"`
ConnectServerLocalIP string `json:"connectServerLocalIP,omitempty"`
TCPMux bool `json:"tcpMux"`
TLSEnable bool `json:"tls"`
ManualStart bool `json:"manualStart,omitempty"`
LegacyFormat bool `json:"legacyFormat,omitempty"`
}
func (dv *DefaultValue) AsClientConfig() ClientCommon {
conf := ClientCommon{
ServerPort: consts.DefaultServerPort,
Protocol: dv.Protocol,
User: dv.User,
LogLevel: dv.LogLevel,
LogMaxDays: dv.LogMaxDays,
DNSServer: dv.DNSServer,
NatHoleSTUNServer: dv.NatHoleSTUNServer,
ConnectServerLocalIP: dv.ConnectServerLocalIP,
TCPMux: dv.TCPMux,
TLSEnable: dv.TLSEnable,
ManualStart: dv.ManualStart,
LegacyFormat: dv.LegacyFormat,
DisableCustomTLSFirstByte: true,
}
if dv.DeleteAfterDays > 0 {
conf.AutoDelete = AutoDelete{
DeleteMethod: consts.DeleteRelative,
DeleteAfterDays: dv.DeleteAfterDays,
}
}
return conf
}
func UnmarshalAppConf(path string, dst *App) error {
b, err := os.ReadFile(path)
if err != nil {
return err
}
if err = cfg.MapTo(dst); err != nil {
return err
}
return nil
return json.Unmarshal(b, dst)
}
func (conf *App) Save(path string) error {
cfg := ini.Empty()
if err := cfg.ReflectFrom(conf); err != nil {
b, err := json.MarshalIndent(conf, "", " ")
if err != nil {
return err
}
return cfg.SaveTo(path)
return os.WriteFile(path, b, 0666)
}

View File

@ -1,43 +1,44 @@
package config
import (
"os"
"reflect"
"testing"
)
func TestUnmarshalAppConfFromIni(t *testing.T) {
input := `
password = abcde
[defaults]
server_port = 7000
log_level = info
log_max_days = 5
protocol = kcp
login_fail_exit = false
user = user
tcp_mux = true
frpmgr_manual_start = true
frpmgr_delete_after_days = 1
input := `{
"password": "abcde",
"defaults": {
"logLevel": "info",
"logMaxDays": 5,
"protocol": "kcp",
"user": "user",
"tcpMux": true,
"manualStart": true,
"deleteAfterDays": 1,
"legacyFormat": true
}
}
`
if err := os.WriteFile(DefaultAppFile, []byte(input), 0666); err != nil {
t.Fatal(err)
}
expected := App{
Password: "abcde",
Defaults: ClientCommon{
ServerPort: 7000,
LogLevel: "info",
LogMaxDays: 5,
Protocol: "kcp",
LoginFailExit: false,
User: "user",
TCPMux: true,
ManualStart: true,
AutoDelete: AutoDelete{
DeleteAfterDays: 1,
},
Defaults: DefaultValue{
LogLevel: "info",
LogMaxDays: 5,
Protocol: "kcp",
User: "user",
TCPMux: true,
ManualStart: true,
DeleteAfterDays: 1,
LegacyFormat: true,
},
}
var actual App
if err := UnmarshalAppConfFromIni([]byte(input), &actual); err != nil {
if err := UnmarshalAppConf(DefaultAppFile, &actual); err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(actual, expected) {

View File

@ -428,7 +428,19 @@ func (conf *ClientConfig) saveTOML(path string) error {
func (conf *ClientConfig) Complete(read bool) {
// Common config
if conf.AuthMethod == "" {
conf.AuthMethod = consts.AuthToken
}
conf.ClientAuth = conf.ClientAuth.Complete()
if conf.ServerPort == 0 {
conf.ServerPort = consts.DefaultServerPort
}
if conf.LogLevel == "" {
conf.LogLevel = consts.LogLevelInfo
}
if conf.LogMaxDays == 0 {
conf.LogMaxDays = consts.DefaultLogMaxDays
}
if conf.AdminPort == 0 {
conf.AdminUser = ""
conf.AdminPwd = ""
@ -436,6 +448,9 @@ func (conf *ClientConfig) Complete(read bool) {
conf.AdminTLS = v1.TLSConfig{}
conf.PprofEnable = false
}
if conf.DeleteMethod == "" {
conf.DeleteMethod = consts.DeleteRelative
}
conf.AutoDelete = conf.AutoDelete.Complete()
if !conf.TCPMux {
conf.TCPMuxKeepaliveInterval = 0
@ -603,11 +618,11 @@ func UnmarshalClientConf(source interface{}) (*ClientConfig, error) {
if err = config.LoadConfigure(b, &cfg, false); err != nil {
return nil, err
}
var r ClientConfig
r.ClientCommon = ClientCommonFromV1(&cfg.ClientCommonConfig)
r.ManualStart = cfg.Mgr.ManualStart
r.SVCBEnable = cfg.Mgr.SVCBEnable
r.AutoDelete = cfg.Mgr.AutoDelete
var conf ClientConfig
conf.ClientCommon = ClientCommonFromV1(&cfg.ClientCommonConfig)
conf.ManualStart = cfg.Mgr.ManualStart
conf.SVCBEnable = cfg.Mgr.SVCBEnable
conf.AutoDelete = cfg.Mgr.AutoDelete
// Proxies
ignore := make(map[string]struct{})
proxies := make([]*Proxy, len(cfg.Proxies))
@ -622,26 +637,29 @@ func UnmarshalClientConf(source interface{}) (*ClientConfig, error) {
}
proxies[i] = p
}
r.Proxies = lo.Filter(proxies, func(item *Proxy, index int) bool {
conf.Proxies = lo.Filter(proxies, func(item *Proxy, index int) bool {
_, ok := ignore[item.Name]
return !ok
})
// Visitors
for _, v := range cfg.Visitors {
r.Proxies = append(r.Proxies, ClientVisitorFromV1(v))
conf.Proxies = append(conf.Proxies, ClientVisitorFromV1(v))
}
return &r, nil
conf.Complete(true)
return &conf, nil
}
func NewDefaultClientConfig() *ClientConfig {
return &ClientConfig{
ClientCommon: ClientCommon{
ClientAuth: ClientAuth{AuthMethod: consts.AuthToken},
ServerPort: 7000,
LogLevel: "info",
ServerPort: consts.DefaultServerPort,
LogLevel: consts.LogLevelInfo,
LogMaxDays: consts.DefaultLogMaxDays,
TCPMux: true,
TLSEnable: true,
DisableCustomTLSFirstByte: true,
LoginFailExit: true,
AutoDelete: AutoDelete{DeleteMethod: consts.DeleteRelative},
},
Proxies: make([]*Proxy, 0),

View File

@ -6,12 +6,19 @@ import (
"time"
)
func TestExpiry(t *testing.T) {
func init() {
if err := os.MkdirAll("testdata", 0750); err != nil {
panic(err)
}
if err := os.Chdir("testdata"); err != nil {
panic(err)
}
}
func TestExpiry(t *testing.T) {
if err := os.WriteFile("example.ini", []byte("test"), 0666); err != nil {
t.Fatal(err)
}
os.Chdir("testdata")
os.WriteFile("example.ini", []byte("test"), 0666)
tests := []struct {
input AutoDelete
expected time.Duration

View File

@ -3,6 +3,7 @@ package consts
const (
RangePrefix = "range:"
DefaultSTUNServer = "stun.easyvoip.com:3478"
DefaultServerPort = 7000
)
// Protocols
@ -83,3 +84,5 @@ const (
)
var LogLevels = []string{LogLevelTrace, LogLevelDebug, LogLevelInfo, LogLevelWarn, LogLevelError}
const DefaultLogMaxDays = 3

View File

@ -1,4 +1,4 @@
package consts
package res
import (
"fmt"

View File

@ -10,7 +10,7 @@ import (
. "github.com/lxn/walk/declarative"
"github.com/koho/frpmgr/i18n"
"github.com/koho/frpmgr/pkg/consts"
"github.com/koho/frpmgr/pkg/res"
"github.com/koho/frpmgr/pkg/version"
)
@ -37,7 +37,7 @@ type aboutViewModel struct {
func NewAboutPage() *AboutPage {
ap := new(AboutPage)
ap.viewModel.TabIcon = loadShieldIcon(16)
ap.viewModel.UpdateIcon = loadIcon(consts.IconUpdate, 32)
ap.viewModel.UpdateIcon = loadIcon(res.IconUpdate, 32)
return ap
}
@ -56,7 +56,7 @@ func (ap *AboutPage) Page() TabPage {
Composite{
Layout: VBox{Margins: Margins{Left: 12}},
Children: []Widget{
Label{Text: AppName, Font: consts.TextLarge, TextColor: consts.ColorDarkBlue},
Label{Text: AppName, Font: res.TextLarge, TextColor: res.ColorDarkBlue},
Label{Text: i18n.Sprintf("Version: %s", version.Number)},
Label{Text: i18n.Sprintf("FRP version: %s", version.FRPVersion)},
Label{Text: i18n.Sprintf("Built on: %s", version.BuildDate)},
@ -67,7 +67,7 @@ func (ap *AboutPage) Page() TabPage {
i18n.Sprintf("Download updates"), i18n.Sprintf("Checking for updates"),
i18n.Sprintf("Check for updates"),
)),
Font: consts.TextMedium,
Font: res.TextMedium,
OnClicked: func() {
if ap.viewModel.NewVersion {
openPath(ap.viewModel.HtmlUrl)
@ -90,7 +90,7 @@ func (ap *AboutPage) Page() TabPage {
Label{Text: i18n.Sprintf("For comments or to report bugs, please visit the project page:")},
LinkLabel{
Alignment: AlignHNearVCenter,
Text: fmt.Sprintf(`<a id="home" href="%s">%s</a>`, consts.ProjectURL, consts.ProjectURL),
Text: fmt.Sprintf(`<a id="home" href="%s">%s</a>`, res.ProjectURL, res.ProjectURL),
OnLinkActivated: func(link *walk.LinkLabelLink) {
openPath(link.URL())
},
@ -99,7 +99,7 @@ func (ap *AboutPage) Page() TabPage {
Label{Text: i18n.Sprintf("For FRP configuration documentation, please visit the FRP project page:")},
LinkLabel{
Alignment: AlignHNearVCenter,
Text: fmt.Sprintf(`<a id="frp" href="%s">%s</a>`, consts.FRPProjectURL, consts.FRPProjectURL),
Text: fmt.Sprintf(`<a id="frp" href="%s">%s</a>`, res.FRPProjectURL, res.FRPProjectURL),
OnLinkActivated: func(link *walk.LinkLabelLink) {
openPath(link.URL())
},
@ -121,7 +121,7 @@ func (ap *AboutPage) checkUpdate(showErr bool) {
ap.db.Reset()
go func() {
var body []byte
resp, err := http.Get(consts.UpdateURL)
resp, err := http.Get(res.UpdateURL)
if err != nil {
goto Fin
}

View File

@ -5,7 +5,7 @@ import (
. "github.com/lxn/walk/declarative"
"github.com/koho/frpmgr/i18n"
"github.com/koho/frpmgr/pkg/consts"
"github.com/koho/frpmgr/pkg/res"
)
// NewBrowseLineEdit places a tool button at the tail of a LineEdit, and opens a file dialog when the button is clicked
@ -49,7 +49,7 @@ func NewBasicDialog(assignTo **walk.Dialog, title string, icon Property, db Data
Icon: icon,
Title: title,
Layout: VBox{},
Font: consts.TextRegular,
Font: res.TextRegular,
DefaultButton: &acceptPB,
CancelButton: &cancelPB,
DataBinder: db,
@ -144,7 +144,7 @@ func NewAttributeTable(m *AttributeModel, nameWidth, valueWidth int) Composite {
func NewAttributeDialog(title string, data *map[string]string) Dialog {
var p *walk.Dialog
m := NewAttributeModel(*data)
dlg := NewBasicDialog(&p, title, loadIcon(consts.IconFile, 32), DataBinder{}, func() {
dlg := NewBasicDialog(&p, title, loadIcon(res.IconFile, 32), DataBinder{}, func() {
*data = m.AsMap()
p.Accept()
},

View File

@ -78,13 +78,11 @@ func (conf *Conf) Save() error {
}
var (
appConf = config.App{Defaults: config.ClientCommon{
ServerPort: 7000,
LogLevel: "info",
LogMaxDays: 3,
TCPMux: true,
TLSEnable: true,
DisableCustomTLSFirstByte: true,
appConf = config.App{Defaults: config.DefaultValue{
LogLevel: consts.LogLevelInfo,
LogMaxDays: consts.DefaultLogMaxDays,
TCPMux: true,
TLSEnable: true,
}}
// The config list contains all the loaded configs
confList []*Conf
@ -93,7 +91,7 @@ var (
)
func loadAllConfs() error {
_ = config.UnmarshalAppConfFromIni(config.DefaultAppFile, &appConf)
_ = config.UnmarshalAppConf(config.DefaultAppFile, &appConf)
// Find all config files in `profiles` directory
files, err := filepath.Glob(PathOfConf("*.conf"))
if err != nil {
@ -191,7 +189,7 @@ func commitConf(conf *Conf, flag runFlag) {
func newDefaultClientConfig() *config.ClientConfig {
return &config.ClientConfig{
ClientCommon: appConf.Defaults,
ClientCommon: appConf.Defaults.AsClientConfig(),
}
}

View File

@ -18,6 +18,7 @@ import (
"github.com/koho/frpmgr/pkg/config"
"github.com/koho/frpmgr/pkg/consts"
"github.com/koho/frpmgr/pkg/layout"
"github.com/koho/frpmgr/pkg/res"
"github.com/koho/frpmgr/pkg/util"
)
@ -119,7 +120,7 @@ func (cv *ConfView) View() Widget {
}
conf := cv.model.items[row]
if !conf.Data.AutoStart() {
style.TextColor = consts.ColorBlue
style.TextColor = res.ColorBlue
}
margin := cv.listView.IntFrom96DPI(1)
bitmapWidth := cv.listView.IntFrom96DPI(16)
@ -161,27 +162,27 @@ func (cv *ConfView) View() Widget {
Menu{
OnTriggered: cv.editNew,
Text: i18n.Sprintf("New Config"),
Image: loadIcon(consts.IconNewConf, 16),
Image: loadIcon(res.IconNewConf, 16),
Items: []MenuItem{
Action{
AssignTo: &cv.tbAddAction,
Text: i18n.Sprintf("Manual Settings"),
Image: loadIcon(consts.IconCreate, 16),
Image: loadIcon(res.IconCreate, 16),
OnTriggered: cv.editNew,
},
Action{
Text: i18n.SprintfEllipsis("Import from File"),
Image: loadIcon(consts.IconFileImport, 16),
Image: loadIcon(res.IconFileImport, 16),
OnTriggered: cv.onFileImport,
},
Action{
Text: i18n.SprintfEllipsis("Import from URL"),
Image: loadIcon(consts.IconURLImport, 16),
Image: loadIcon(res.IconURLImport, 16),
OnTriggered: cv.onURLImport,
},
Action{
Text: i18n.Sprintf("Import from Clipboard"),
Image: loadIcon(consts.IconClipboard, 16),
Image: loadIcon(res.IconClipboard, 16),
OnTriggered: cv.onClipboardImport,
},
},
@ -190,14 +191,14 @@ func (cv *ConfView) View() Widget {
Action{
Enabled: Bind("conf.Selected"),
AssignTo: &cv.tbDeleteAction,
Image: loadIcon(consts.IconDelete, 16),
Image: loadIcon(res.IconDelete, 16),
OnTriggered: cv.onDelete,
},
Separator{},
Action{
Enabled: Bind("conf.Selected"),
AssignTo: &cv.tbExportAction,
Image: loadIcon(consts.IconExport, 16),
Image: loadIcon(res.IconExport, 16),
OnTriggered: cv.onExport,
},
},
@ -321,7 +322,7 @@ checkName:
func (cv *ConfView) onFileImport() {
dlg := walk.FileDialog{
Filter: consts.FilterConfig + consts.FilterAllFiles,
Filter: res.FilterConfig + res.FilterAllFiles,
Title: i18n.Sprintf("Import from File"),
}
@ -356,7 +357,7 @@ func (cv *ConfView) ImportFiles(files []string) {
subTotal, subImported := cv.importZip(path, nil, false)
total += subTotal
imported += subImported
} else if slices.Contains(consts.SupportedConfigFormats, ext) {
} else if slices.Contains(res.SupportedConfigFormats, ext) {
total++
newPath, ok := cv.checkConfName(path, false)
if !ok {
@ -431,7 +432,7 @@ func (cv *ConfView) importZip(path string, data []byte, rename bool) (total, imp
if file.FileInfo().IsDir() {
continue
}
if !slices.Contains(consts.SupportedConfigFormats, strings.ToLower(filepath.Ext(file.Name))) {
if !slices.Contains(res.SupportedConfigFormats, strings.ToLower(filepath.Ext(file.Name))) {
continue
}
total++
@ -454,8 +455,8 @@ func (cv *ConfView) onClipboardImport() {
}
var name string
// Check for a share link
if strings.HasPrefix(text, consts.ShareLinkScheme) {
text = strings.TrimPrefix(text, consts.ShareLinkScheme)
if strings.HasPrefix(text, res.ShareLinkScheme) {
text = strings.TrimPrefix(text, res.ShareLinkScheme)
content, err := base64.StdEncoding.DecodeString(text)
if err != nil {
showError(err, cv.Form())
@ -484,7 +485,7 @@ func (cv *ConfView) onCopyShareLink() {
}
// Insert the config name in the first line
content = append([]byte("# "+conf.Name+"\n"), content...)
walk.Clipboard().SetText(consts.ShareLinkScheme + base64.StdEncoding.EncodeToString(content))
walk.Clipboard().SetText(res.ShareLinkScheme + base64.StdEncoding.EncodeToString(content))
}
}
@ -522,7 +523,7 @@ func (cv *ConfView) onDelete() {
func (cv *ConfView) onExport() {
dlg := walk.FileDialog{
Filter: consts.FilterZip,
Filter: res.FilterZip,
Title: i18n.Sprintf("Export All Configs to ZIP"),
}

View File

@ -16,6 +16,7 @@ import (
"github.com/koho/frpmgr/i18n"
"github.com/koho/frpmgr/pkg/config"
"github.com/koho/frpmgr/pkg/consts"
"github.com/koho/frpmgr/pkg/res"
"github.com/koho/frpmgr/pkg/util"
"github.com/koho/frpmgr/services"
)
@ -86,7 +87,7 @@ func (cd *EditClientDialog) View() Dialog {
if cd.Conf.Name != "" {
title = i18n.Sprintf("Edit Client - %s", cd.Conf.Name)
}
dlg := NewBasicDialog(&cd.Dialog, title, loadIcon(consts.IconEditDialog, 32), DataBinder{
dlg := NewBasicDialog(&cd.Dialog, title, loadIcon(res.IconEditDialog, 32), DataBinder{
AssignTo: &cd.db,
Name: "common",
DataSource: cd.binder,
@ -109,7 +110,7 @@ func (cd *EditClientDialog) basicConfPage() TabPage {
Layout: Grid{Columns: 2},
Children: []Widget{
Label{Text: i18n.SprintfColon("Name")},
LineEdit{AssignTo: &cd.nameView, Text: Bind("Name", consts.ValidateNonEmpty), OnTextChanged: func() {
LineEdit{AssignTo: &cd.nameView, Text: Bind("Name", res.ValidateNonEmpty), OnTextChanged: func() {
if name := cd.nameView.Text(); name != "" {
curLog := strings.TrimSpace(cd.logFileView.Text())
// Automatically change the log file if it's empty or using the default log directory
@ -119,7 +120,7 @@ func (cd *EditClientDialog) basicConfPage() TabPage {
}
}},
Label{Text: i18n.SprintfColon("Server Address")},
LineEdit{Text: Bind("ServerAddress", consts.ValidateNonEmpty)},
LineEdit{Text: Bind("ServerAddress", res.ValidateNonEmpty)},
Label{Text: i18n.SprintfColon("Server Port")},
Composite{
Layout: HBox{MarginsZero: true},
@ -197,7 +198,7 @@ func (cd *EditClientDialog) logConfPage() TabPage {
VSpacer{Size: 2, ColumnSpan: 2},
Label{Text: i18n.SprintfColon("Log File")},
NewBrowseLineEdit(&cd.logFileView, true, true, Bind("LogFile"),
i18n.Sprintf("Select Log File"), consts.FilterLog, true),
i18n.Sprintf("Select Log File"), res.FilterLog, true),
Label{Text: i18n.SprintfColon("Level")},
ComboBox{
Value: Bind("LogLevel"),
@ -231,7 +232,7 @@ func (cd *EditClientDialog) adminConfPage() TabPage {
},
ToolButton{
Enabled: Bind("adminPort.Value > 0 && !legacyFormat.Checked"),
Image: loadIcon(consts.IconLock, 16),
Image: loadIcon(res.IconLock, 16),
ToolTipText: "TLS", OnClicked: func() {
cd.adminTLSDialog().Run(cd.Form())
},
@ -332,13 +333,13 @@ func (cd *EditClientDialog) tlsConfPage() TabPage {
LineEdit{Visible: Bind("tlsCheck.Checked"), Text: Bind("TLSServerName")},
Label{Visible: Bind("tlsCheck.Checked"), Text: i18n.SprintfColon("Certificate")},
NewBrowseLineEdit(nil, Bind("tlsCheck.Checked"), true, Bind("TLSCertFile"),
i18n.Sprintf("Select Certificate File"), consts.FilterCert, true),
i18n.Sprintf("Select Certificate File"), res.FilterCert, true),
Label{Visible: Bind("tlsCheck.Checked"), Text: i18n.SprintfColon("Certificate Key"), AlwaysConsumeSpace: true},
NewBrowseLineEdit(nil, Bind("tlsCheck.Checked"), true, Bind("TLSKeyFile"),
i18n.Sprintf("Select Certificate Key File"), consts.FilterKey, true),
i18n.Sprintf("Select Certificate Key File"), res.FilterKey, true),
Label{Visible: Bind("tlsCheck.Checked"), Text: i18n.SprintfColon("Trusted CA"), AlwaysConsumeSpace: true},
NewBrowseLineEdit(nil, Bind("tlsCheck.Checked"), true, Bind("TLSTrustedCaFile"),
i18n.Sprintf("Select Trusted CA File"), consts.FilterCert, true),
i18n.Sprintf("Select Trusted CA File"), res.FilterCert, true),
Label{Visible: Bind("tlsCheck.Checked"), Text: i18n.SprintfColon("Other Options")},
CheckBox{Visible: Bind("tlsCheck.Checked"), Text: i18n.Sprintf("Disable custom first byte"), Checked: Bind("DisableCustomTLSFirstByte")},
},
@ -423,7 +424,7 @@ func (cd *EditClientDialog) advancedConfPage() TabPage {
func (cd *EditClientDialog) experimentDialog() Dialog {
dlg := NewBasicDialog(nil, i18n.Sprintf("Experimental Features"),
loadIcon(consts.IconExperiment, 32), DataBinder{DataSource: cd.binder}, nil,
loadIcon(res.IconExperiment, 32), DataBinder{DataSource: cd.binder}, nil,
Label{Text: i18n.Sprintf("* The following features may affect the stability of the service.")},
CheckBox{Checked: Bind("SVCBEnable"), Text: i18n.Sprintf("Use server SVCB records"), Alignment: AlignHNearVNear},
VSpacer{},
@ -436,19 +437,19 @@ func (cd *EditClientDialog) experimentDialog() Dialog {
func (cd *EditClientDialog) adminTLSDialog() Dialog {
var widgets [4]*walk.LineEdit
dlg := NewBasicDialog(nil, "TLS",
loadIcon(consts.IconLock, 32),
loadIcon(res.IconLock, 32),
DataBinder{DataSource: &cd.binder.AdminTLS}, nil,
Label{Text: i18n.SprintfColon("Host Name")},
LineEdit{AssignTo: &widgets[0], Text: Bind("ServerName")},
Label{Text: i18n.SprintfColon("Certificate")},
NewBrowseLineEdit(&widgets[1], true, true, Bind("CertFile"),
i18n.Sprintf("Select Certificate File"), consts.FilterCert, true),
i18n.Sprintf("Select Certificate File"), res.FilterCert, true),
Label{Text: i18n.SprintfColon("Certificate Key")},
NewBrowseLineEdit(&widgets[2], true, true, Bind("KeyFile"),
i18n.Sprintf("Select Certificate Key File"), consts.FilterKey, true),
i18n.Sprintf("Select Certificate Key File"), res.FilterKey, true),
Label{Text: i18n.SprintfColon("Trusted CA")},
NewBrowseLineEdit(&widgets[3], true, true, Bind("TrustedCaFile"),
i18n.Sprintf("Select Trusted CA File"), consts.FilterCert, true),
i18n.Sprintf("Select Trusted CA File"), res.FilterCert, true),
VSpacer{Size: 4},
)
dlg.MinSize = Size{Width: 350}

View File

@ -15,6 +15,7 @@ import (
"github.com/koho/frpmgr/i18n"
"github.com/koho/frpmgr/pkg/config"
"github.com/koho/frpmgr/pkg/consts"
"github.com/koho/frpmgr/pkg/res"
)
type EditProxyDialog struct {
@ -106,7 +107,7 @@ func (pd *EditProxyDialog) View() Dialog {
if pd.exist && pd.Proxy.Name != "" {
title = i18n.Sprintf("Edit Proxy - %s", pd.Proxy.Name)
}
dlg := NewBasicDialog(&pd.Dialog, title, loadIcon(consts.IconEditDialog, 32), DataBinder{
dlg := NewBasicDialog(&pd.Dialog, title, loadIcon(res.IconEditDialog, 32), DataBinder{
AssignTo: &pd.vmDB,
Name: "vm",
DataSource: &pd.viewModel,
@ -125,8 +126,8 @@ func (pd *EditProxyDialog) View() Dialog {
Composite{
Layout: HBox{MarginsZero: true},
Children: []Widget{
LineEdit{AssignTo: &pd.nameView, Text: Bind("Name", consts.ValidateNonEmpty)},
PushButton{Text: i18n.SprintfLSpace("Random"), Image: loadIcon(consts.IconRandom, 16), OnClicked: func() {
LineEdit{AssignTo: &pd.nameView, Text: Bind("Name", res.ValidateNonEmpty)},
PushButton{Text: i18n.SprintfLSpace("Random"), Image: loadIcon(res.IconRandom, 16), OnClicked: func() {
pd.nameView.SetText(lo.RandomString(8, lo.AlphanumericCharset))
}},
},
@ -348,10 +349,10 @@ func (pd *EditProxyDialog) pluginProxyPage() TabPage {
},
Label{Visible: Bind("vm.PluginCertVisible"), Text: i18n.SprintfColon("Certificate")},
NewBrowseLineEdit(nil, Bind("vm.PluginCertVisible"), true, Bind("PluginCrtPath"),
i18n.Sprintf("Select Certificate File"), consts.FilterCert, true),
i18n.Sprintf("Select Certificate File"), res.FilterCert, true),
Label{Visible: Bind("vm.PluginCertVisible"), Text: i18n.SprintfColon("Certificate Key")},
NewBrowseLineEdit(nil, Bind("vm.PluginCertVisible"), true, Bind("PluginKeyPath"),
i18n.Sprintf("Select Certificate Key File"), consts.FilterKey, true),
i18n.Sprintf("Select Certificate Key File"), res.FilterKey, true),
Label{Visible: Bind("vm.PluginHTTPFwdVisible"), Text: i18n.SprintfColon("Host Rewrite")},
LineEdit{Visible: Bind("vm.PluginHTTPFwdVisible"), Text: Bind("PluginHostHeaderRewrite")},
},

View File

@ -4,11 +4,12 @@ import (
"github.com/lxn/walk"
"github.com/koho/frpmgr/pkg/consts"
"github.com/koho/frpmgr/pkg/res"
)
var cachedIconsForWidthAndId = make(map[widthAndId]*walk.Icon)
func loadIcon(id consts.Icon, size int) (icon *walk.Icon) {
func loadIcon(id res.Icon, size int) (icon *walk.Icon) {
icon = cachedIconsForWidthAndId[widthAndId{size, id}]
if icon != nil {
return
@ -27,7 +28,7 @@ func loadIcon(id consts.Icon, size int) (icon *walk.Icon) {
type widthAndId struct {
width int
icon consts.Icon
icon res.Icon
}
type widthAndState struct {
@ -44,24 +45,24 @@ func iconForState(state consts.ServiceState, size int) (icon *walk.Icon) {
}
switch state {
case consts.StateStarted:
icon = loadIcon(consts.IconStateRunning, size)
icon = loadIcon(res.IconStateRunning, size)
case consts.StateStopped, consts.StateUnknown:
icon = loadIcon(consts.IconStateStopped, size)
icon = loadIcon(res.IconStateStopped, size)
default:
icon = loadIcon(consts.IconStateWorking, size)
icon = loadIcon(res.IconStateWorking, size)
}
cachedIconsForWidthAndState[widthAndState{size, state}] = icon
return
}
func loadLogoIcon(size int) *walk.Icon {
return loadIcon(consts.IconLogo, size)
return loadIcon(res.IconLogo, size)
}
func loadShieldIcon(size int) (icon *walk.Icon) {
icon = loadIcon(consts.IconNewVersion1, size)
icon = loadIcon(res.IconNewVersion1, size)
if icon == nil {
icon = loadIcon(consts.IconNewVersion2, size)
icon = loadIcon(res.IconNewVersion2, size)
}
return
}

View File

@ -8,7 +8,7 @@ import (
. "github.com/lxn/walk/declarative"
"github.com/koho/frpmgr/i18n"
"github.com/koho/frpmgr/pkg/consts"
"github.com/koho/frpmgr/pkg/res"
)
type NATDiscoveryDialog struct {
@ -27,7 +27,7 @@ func NewNATDiscoveryDialog(serverAddr string) *NATDiscoveryDialog {
}
func (nd *NATDiscoveryDialog) Run(owner walk.Form) (int, error) {
dlg := NewBasicDialog(&nd.Dialog, i18n.Sprintf("NAT Discovery"), loadIcon(consts.IconNat, 32),
dlg := NewBasicDialog(&nd.Dialog, i18n.Sprintf("NAT Discovery"), loadIcon(res.IconNat, 32),
DataBinder{}, nil,
VSpacer{Size: 1},
Composite{

View File

@ -10,6 +10,7 @@ import (
"github.com/koho/frpmgr/i18n"
"github.com/koho/frpmgr/pkg/config"
"github.com/koho/frpmgr/pkg/consts"
"github.com/koho/frpmgr/pkg/res"
"github.com/koho/frpmgr/services"
)
@ -37,7 +38,7 @@ func NewPanelView() *PanelView {
func (pv *PanelView) View() Widget {
var cpIcon *walk.CustomWidget
cpIconColor := consts.ColorDarkGray
cpIconColor := res.ColorDarkGray
setCopyIconColor := func(button walk.MouseButton, color walk.Color) {
if button == walk.LeftButton {
cpIconColor = color
@ -79,10 +80,10 @@ func (pv *PanelView) View() Widget {
return drawCopyIcon(canvas, cpIconColor)
},
OnMouseDown: func(x, y int, button walk.MouseButton) {
setCopyIconColor(button, consts.ColorLightBlue)
setCopyIconColor(button, res.ColorLightBlue)
},
OnMouseUp: func(x, y int, button walk.MouseButton) {
setCopyIconColor(button, consts.ColorDarkGray)
setCopyIconColor(button, res.ColorDarkGray)
bounds := cpIcon.ClientBoundsPixels()
if x >= 0 && x <= bounds.Right() && y >= 0 && y <= bounds.Bottom() {
walk.Clipboard().SetText(pv.addressText.Text())

View File

@ -9,6 +9,7 @@ import (
"github.com/koho/frpmgr/i18n"
"github.com/koho/frpmgr/pkg/config"
"github.com/koho/frpmgr/pkg/consts"
"github.com/koho/frpmgr/pkg/res"
"github.com/koho/frpmgr/pkg/validators"
)
@ -38,7 +39,7 @@ 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.ValidatePortRange...), MinSize: Size{Width: 280}},
LineEdit{Text: Bind("RemotePort", res.ValidatePortRange...), MinSize: Size{Width: 280}},
}
switch pp.plugin {
case consts.PluginHttpProxy, consts.PluginSocks5:
@ -53,7 +54,7 @@ func (pp *PluginProxyDialog) Run(owner walk.Form) (int, error) {
case consts.PluginStaticFile:
widgets = append(widgets,
Label{Text: i18n.SprintfColon("Local Directory")},
NewBrowseLineEdit(nil, true, true, Bind("Dir", consts.ValidateNonEmpty),
NewBrowseLineEdit(nil, true, true, Bind("Dir", res.ValidateNonEmpty),
i18n.Sprintf("Select a folder for directory listing."), "", false),
)
}

View File

@ -9,6 +9,7 @@ import (
"github.com/koho/frpmgr/i18n"
"github.com/koho/frpmgr/pkg/config"
"github.com/koho/frpmgr/pkg/consts"
"github.com/koho/frpmgr/pkg/res"
"github.com/koho/frpmgr/pkg/validators"
)
@ -44,7 +45,7 @@ func (pp *PortProxyDialog) Run(owner walk.Form) (int, error) {
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},
LineEdit{Text: Bind("RemotePort", res.ValidatePortRange...), ColumnSpan: 2},
Label{Text: i18n.SprintfColon("Protocol"), ColumnSpan: 2},
Composite{
Layout: HBox{MarginsZero: true},
@ -56,10 +57,10 @@ func (pp *PortProxyDialog) Run(owner walk.Form) (int, error) {
},
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},
LineEdit{Text: Bind("LocalAddr", res.ValidateNonEmpty), StretchFactor: 2},
LineEdit{Text: Bind("LocalPort", res.ValidatePortRange...), StretchFactor: 1},
}
return NewBasicDialog(&pp.Dialog, i18n.Sprintf("Open Port"), loadIcon(consts.IconOpenPort, 32), DataBinder{
return NewBasicDialog(&pp.Dialog, i18n.Sprintf("Open Port"), loadIcon(res.IconOpenPort, 32), DataBinder{
AssignTo: &pp.db,
DataSource: pp.binder,
ErrorPresenter: validators.SilentToolTipErrorPresenter{},

View File

@ -1,7 +1,6 @@
package ui
import (
"os"
"sort"
"github.com/lxn/walk"
@ -10,6 +9,7 @@ import (
"github.com/koho/frpmgr/i18n"
"github.com/koho/frpmgr/pkg/consts"
"github.com/koho/frpmgr/pkg/res"
"github.com/koho/frpmgr/pkg/sec"
"github.com/koho/frpmgr/pkg/validators"
)
@ -47,7 +47,7 @@ func (pp *PrefPage) passwordSection() GroupBox {
Title: i18n.Sprintf("Master password"),
Layout: Grid{Alignment: AlignHNearVCenter, Columns: 2},
Children: []Widget{
ImageView{Image: loadIcon(consts.IconKey, 32)},
ImageView{Image: loadIcon(res.IconKey, 32)},
Label{Text: i18n.Sprintf("You can set a password to restrict access to this program.\nYou will be asked to enter it the next time you use this program.")},
CheckBox{
AssignTo: &pp.usePassword,
@ -93,7 +93,7 @@ func (pp *PrefPage) languageSection() GroupBox {
Title: i18n.Sprintf("Languages"),
Layout: Grid{Alignment: AlignHNearVCenter, Columns: 2},
Children: []Widget{
ImageView{Image: loadIcon(consts.IconLanguage, 32)},
ImageView{Image: loadIcon(res.IconLanguage, 32)},
Composite{
Layout: VBox{MarginsZero: true},
Children: []Widget{
@ -136,7 +136,7 @@ func (pp *PrefPage) defaultSection() GroupBox {
Title: i18n.Sprintf("Defaults"),
Layout: Grid{Alignment: AlignHNearVCenter, Columns: 2, Spacing: 10, Margins: Margins{Left: 9, Top: 9, Right: 9, Bottom: 16}},
Children: []Widget{
ImageView{Image: loadIcon(consts.IconDefaults, 32)},
ImageView{Image: loadIcon(res.IconDefaults, 32)},
Label{Text: i18n.Sprintf("Define the default value when creating a new configuration.\nThe value here will not affect the existing configuration.")},
Composite{
Row: 1, Column: 1,
@ -179,7 +179,7 @@ func (pp *PrefPage) changePassword() string {
var vm struct {
Password string
}
NewBasicDialog(nil, i18n.Sprintf("Master password"), loadIcon(consts.IconKey, 32),
NewBasicDialog(nil, i18n.Sprintf("Master password"), loadIcon(res.IconKey, 32),
DataBinder{
AssignTo: &db,
DataSource: &vm,
@ -189,7 +189,7 @@ func (pp *PrefPage) changePassword() string {
MinSize: Size{Width: 280},
Children: []Widget{
Label{Text: i18n.SprintfColon("New master password")},
LineEdit{AssignTo: &pwdEdit, Text: Bind("Password", consts.ValidateNonEmpty), PasswordMode: true},
LineEdit{AssignTo: &pwdEdit, Text: Bind("Password", res.ValidateNonEmpty), PasswordMode: true},
Label{Text: i18n.SprintfColon("Re-enter password")},
LineEdit{Text: Bind("", validators.ConfirmPassword{Password: &pwdEdit}), PasswordMode: true},
},
@ -208,14 +208,15 @@ func (pp *PrefPage) changePassword() string {
}
func (pp *PrefPage) switchLanguage(lc string) {
if err := os.WriteFile(i18n.LangFile, []byte(lc), 0660); err != nil {
appConf.Lang = lc
if err := saveAppConfig(); err != nil {
showError(err, pp.Form())
}
}
func (pp *PrefPage) setDefaultValue() (int, error) {
dlg := NewBasicDialog(nil, i18n.Sprintf("Defaults"),
loadIcon(consts.IconDefaults, 32),
loadIcon(res.IconDefaults, 32),
DataBinder{DataSource: &appConf.Defaults}, nil, Composite{
Layout: Grid{Columns: 2, MarginsZero: true},
Children: []Widget{
@ -254,7 +255,8 @@ func (pp *PrefPage) setDefaultValue() (int, error) {
Children: []Widget{
CheckBox{Text: i18n.Sprintf("TCP Mux"), Checked: Bind("TCPMux")},
CheckBox{Text: "TLS", Checked: Bind("TLSEnable")},
CheckBox{Text: i18n.Sprintf("Disable auto-start at boot"), Checked: Bind("ManualStart")},
CheckBox{Text: i18n.Sprintf("Disable auto-start at boot"), Checked: Bind("ManualStart"), ColumnSpan: 2},
CheckBox{Text: i18n.Sprintf("Use legacy format config file"), Checked: Bind("LegacyFormat"), ColumnSpan: 2},
},
},
},

View File

@ -15,6 +15,7 @@ import (
"github.com/koho/frpmgr/i18n"
"github.com/koho/frpmgr/pkg/config"
"github.com/koho/frpmgr/pkg/consts"
"github.com/koho/frpmgr/pkg/res"
"github.com/koho/frpmgr/pkg/util"
)
@ -102,19 +103,19 @@ func (pv *ProxyView) createToolbar() ToolBar {
Action{
AssignTo: &pv.newAction,
Text: i18n.Sprintf("Add"),
Image: loadIcon(consts.IconCreate, 16),
Image: loadIcon(res.IconCreate, 16),
OnTriggered: func() {
pv.onEdit(false, nil)
},
},
Menu{
Text: i18n.Sprintf("Quick Add"),
Image: loadIcon(consts.IconQuickAdd, 16),
Image: loadIcon(res.IconQuickAdd, 16),
Items: []MenuItem{
Action{
AssignTo: &pv.portAction,
Text: i18n.Sprintf("Open Port"),
Image: loadIcon(consts.IconOpenPort, 16),
Image: loadIcon(res.IconOpenPort, 16),
OnTriggered: func() {
pv.onQuickAdd(NewPortProxyDialog())
},
@ -122,85 +123,85 @@ func (pv *ProxyView) createToolbar() ToolBar {
Action{
AssignTo: &pv.rdAction,
Text: i18n.Sprintf("Remote Desktop"),
Image: loadIcon(consts.IconRemote, 16),
Image: loadIcon(res.IconRemote, 16),
OnTriggered: func() {
pv.onQuickAdd(NewSimpleProxyDialog(i18n.Sprintf("Remote Desktop"), loadIcon(consts.IconRemote, 32),
pv.onQuickAdd(NewSimpleProxyDialog(i18n.Sprintf("Remote Desktop"), loadIcon(res.IconRemote, 32),
"rdp", []string{consts.ProxyTypeTCP, consts.ProxyTypeUDP}, ":3389"))
},
},
Action{
AssignTo: &pv.vncAction,
Text: "VNC",
Image: loadIcon(consts.IconVNC, 16),
Image: loadIcon(res.IconVNC, 16),
OnTriggered: func() {
pv.onQuickAdd(NewSimpleProxyDialog("VNC", loadIcon(consts.IconVNC, 32),
pv.onQuickAdd(NewSimpleProxyDialog("VNC", loadIcon(res.IconVNC, 32),
"vnc", []string{consts.ProxyTypeTCP, consts.ProxyTypeUDP}, ":5900"))
},
},
Action{
AssignTo: &pv.sshAction,
Text: "SSH",
Image: loadIcon(consts.IconSSH, 16),
Image: loadIcon(res.IconSSH, 16),
OnTriggered: func() {
pv.onQuickAdd(NewSimpleProxyDialog("SSH", loadIcon(consts.IconSSH, 32),
pv.onQuickAdd(NewSimpleProxyDialog("SSH", loadIcon(res.IconSSH, 32),
"ssh", []string{consts.ProxyTypeTCP}, ":22"))
},
},
Action{
AssignTo: &pv.webAction,
Text: "Web",
Image: loadIcon(consts.IconWeb, 16),
Image: loadIcon(res.IconWeb, 16),
OnTriggered: func() {
pv.onQuickAdd(NewSimpleProxyDialog("Web", loadIcon(consts.IconWeb, 32),
pv.onQuickAdd(NewSimpleProxyDialog("Web", loadIcon(res.IconWeb, 32),
"web", []string{consts.ProxyTypeTCP}, ":80"))
},
},
Action{
AssignTo: &pv.dnsAction,
Text: "DNS",
Image: loadIcon(consts.IconDns, 16),
Image: loadIcon(res.IconDns, 16),
OnTriggered: func() {
systemDns := util.GetSystemDnsServer()
if systemDns == "" {
systemDns = "114.114.114.114"
}
pv.onQuickAdd(NewSimpleProxyDialog("DNS", loadIcon(consts.IconDns, 32),
pv.onQuickAdd(NewSimpleProxyDialog("DNS", loadIcon(res.IconDns, 32),
"dns", []string{consts.ProxyTypeUDP}, systemDns+":53"))
},
},
Action{
AssignTo: &pv.vpnAction,
Text: "OpenVPN",
Image: loadIcon(consts.IconLock, 16),
Image: loadIcon(res.IconLock, 16),
OnTriggered: func() {
pv.onQuickAdd(NewSimpleProxyDialog("OpenVPN", loadIcon(consts.IconLock, 32),
pv.onQuickAdd(NewSimpleProxyDialog("OpenVPN", loadIcon(res.IconLock, 32),
"openvpn", []string{consts.ProxyTypeTCP, consts.ProxyTypeUDP}, ":1194"))
},
},
Action{
AssignTo: &pv.ftpAction,
Text: "FTP",
Image: loadIcon(consts.IconFtp, 16),
Image: loadIcon(res.IconFtp, 16),
OnTriggered: func() {
pv.onQuickAdd(NewSimpleProxyDialog("FTP", loadIcon(consts.IconFtp, 32),
pv.onQuickAdd(NewSimpleProxyDialog("FTP", loadIcon(res.IconFtp, 32),
"ftp", []string{consts.ProxyTypeTCP}, ":21"))
},
},
Action{
AssignTo: &pv.httpFileAction,
Text: i18n.Sprintf("HTTP File Server"),
Image: loadIcon(consts.IconHttpFile, 16),
Image: loadIcon(res.IconHttpFile, 16),
OnTriggered: func() {
pv.onQuickAdd(NewPluginProxyDialog(i18n.Sprintf("HTTP File Server"), loadIcon(consts.IconHttpFile, 32),
pv.onQuickAdd(NewPluginProxyDialog(i18n.Sprintf("HTTP File Server"), loadIcon(res.IconHttpFile, 32),
consts.PluginStaticFile))
},
},
Action{
AssignTo: &pv.httpProxyAction,
Text: i18n.Sprintf("HTTP Proxy"),
Image: loadIcon(consts.IconHttpProxy, 16),
Image: loadIcon(res.IconHttpProxy, 16),
OnTriggered: func() {
pv.onQuickAdd(NewPluginProxyDialog(i18n.Sprintf("HTTP Proxy"), loadIcon(consts.IconHttpProxy, 32),
pv.onQuickAdd(NewPluginProxyDialog(i18n.Sprintf("HTTP Proxy"), loadIcon(res.IconHttpProxy, 32),
consts.PluginHttpProxy))
},
},
@ -208,7 +209,7 @@ func (pv *ProxyView) createToolbar() ToolBar {
},
Action{
AssignTo: &pv.editAction,
Image: loadIcon(consts.IconEdit, 16),
Image: loadIcon(res.IconEdit, 16),
Text: i18n.Sprintf("Edit"),
Enabled: Bind("proxy.CurrentIndex >= 0"),
OnTriggered: func() {
@ -217,21 +218,21 @@ func (pv *ProxyView) createToolbar() ToolBar {
},
Action{
AssignTo: &pv.toggleAction,
Image: loadIcon(consts.IconDisable, 16),
Image: loadIcon(res.IconDisable, 16),
Text: i18n.Sprintf("Disable"),
Enabled: Bind("proxy.CurrentIndex >= 0 && switchable(proxy.CurrentIndex)"),
OnTriggered: pv.onToggleProxy,
},
Action{
AssignTo: &pv.deleteAction,
Image: loadIcon(consts.IconDelete, 16),
Image: loadIcon(res.IconDelete, 16),
Text: i18n.Sprintf("Delete"),
Enabled: Bind("proxy.CurrentIndex >= 0"),
OnTriggered: pv.onDelete,
},
Menu{
Text: i18n.Sprintf("Open Config"),
Image: loadIcon(consts.IconOpen, 16),
Image: loadIcon(res.IconOpen, 16),
Items: []MenuItem{
Action{
AssignTo: &pv.openConfAction,
@ -273,7 +274,7 @@ func (pv *ProxyView) createProxyTable() TableView {
ActionRef{Action: &pv.newAction},
Menu{
Text: i18n.Sprintf("Quick Add"),
Image: loadIcon(consts.IconQuickAdd, 16),
Image: loadIcon(res.IconQuickAdd, 16),
Items: []MenuItem{
ActionRef{Action: &pv.portAction},
ActionRef{Action: &pv.rdAction},
@ -289,19 +290,19 @@ func (pv *ProxyView) createProxyTable() TableView {
},
Action{
Text: i18n.Sprintf("Import from Clipboard"),
Image: loadIcon(consts.IconClipboard, 16),
Image: loadIcon(res.IconClipboard, 16),
OnTriggered: pv.onClipboardImport,
},
Separator{},
Action{
Enabled: Bind("proxy.CurrentIndex >= 0"),
Text: i18n.Sprintf("Copy Access Address"),
Image: loadIcon(consts.IconSysCopy, 16),
Image: loadIcon(res.IconSysCopy, 16),
OnTriggered: pv.onCopyAccessAddr,
},
Menu{
Text: i18n.Sprintf("Open Config"),
Image: loadIcon(consts.IconOpen, 16),
Image: loadIcon(res.IconOpen, 16),
Items: []MenuItem{
ActionRef{Action: &pv.openConfAction},
ActionRef{Action: &pv.showConfAction},
@ -317,11 +318,11 @@ func (pv *ProxyView) createProxyTable() TableView {
if _, proxy := pv.getConfigProxy(style.Row()); proxy != nil {
if proxy.Disabled {
// Disabled proxy
style.TextColor = consts.ColorGray
style.BackgroundColor = consts.ColorGrayBG
style.TextColor = res.ColorGray
style.BackgroundColor = res.ColorGrayBG
} else if proxy.IsVisitor() {
// Visitor proxy
style.TextColor = consts.ColorBlue
style.TextColor = res.ColorBlue
}
// Normal proxy is default black text
}
@ -532,10 +533,10 @@ func (pv *ProxyView) switchToggleAction() {
}
if proxy.Disabled {
pv.toggleAction.SetText(i18n.Sprintf("Enable"))
pv.toggleAction.SetImage(loadIcon(consts.IconEnable, 16))
pv.toggleAction.SetImage(loadIcon(res.IconEnable, 16))
} else {
pv.toggleAction.SetText(i18n.Sprintf("Disable"))
pv.toggleAction.SetImage(loadIcon(consts.IconDisable, 16))
pv.toggleAction.SetImage(loadIcon(res.IconDisable, 16))
}
}

View File

@ -8,7 +8,7 @@ import (
"github.com/koho/frpmgr/i18n"
"github.com/koho/frpmgr/pkg/config"
"github.com/koho/frpmgr/pkg/consts"
"github.com/koho/frpmgr/pkg/res"
"github.com/koho/frpmgr/pkg/util"
"github.com/koho/frpmgr/pkg/validators"
)
@ -48,11 +48,11 @@ 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.ValidatePortRange...), ColumnSpan: 2},
LineEdit{Text: Bind("RemotePort", res.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.ValidatePortRange...), StretchFactor: 1},
LineEdit{Text: Bind("LocalAddr", res.ValidateNonEmpty), StretchFactor: 2},
LineEdit{Text: Bind("LocalPort", res.ValidatePortRange...), StretchFactor: 1},
}
switch sp.service {
case "ftp":
@ -60,9 +60,9 @@ func (sp *SimpleProxyDialog) Run(owner walk.Form) (int, error) {
widgets = append(widgets, Label{Text: i18n.SprintfColon("Passive Port Range"), ColumnSpan: 2}, Composite{
Layout: HBox{MarginsZero: true},
Children: []Widget{
LineEdit{AssignTo: &lPortMinEdit, Text: Bind("LocalPortMin", consts.ValidatePortRange...)},
LineEdit{AssignTo: &lPortMinEdit, Text: Bind("LocalPortMin", res.ValidatePortRange...)},
Label{Text: "-"},
LineEdit{Text: Bind("LocalPortMax", append(consts.ValidatePortRange, validators.GTE{Value: &lPortMinEdit})...)},
LineEdit{Text: Bind("LocalPortMax", append(res.ValidatePortRange, validators.GTE{Value: &lPortMinEdit})...)},
},
})
}

View File

@ -12,7 +12,7 @@ import (
"golang.org/x/sys/windows"
"github.com/koho/frpmgr/i18n"
"github.com/koho/frpmgr/pkg/consts"
"github.com/koho/frpmgr/pkg/res"
"github.com/koho/frpmgr/pkg/util"
"github.com/koho/frpmgr/services"
)
@ -77,7 +77,7 @@ func RunUI() error {
Persistent: true,
Visible: false,
Layout: VBox{Margins: Margins{Left: 5, Top: 5, Right: 5, Bottom: 5}},
Font: consts.TextRegular,
Font: res.TextRegular,
Children: []Widget{
TabWidget{
AssignTo: &fm.tabs,
@ -158,7 +158,7 @@ func openFolder(path string) {
// openFileDialog shows a file dialog to choose file or directory and sends the selected path to the LineEdit view
func openFileDialog(receiver *walk.LineEdit, title string, filter string, file bool) error {
dlg := walk.FileDialog{
Filter: filter + consts.FilterAllFiles,
Filter: filter + res.FilterAllFiles,
Title: title,
}
var ok bool

View File

@ -13,7 +13,7 @@ import (
"github.com/samber/lo"
"github.com/koho/frpmgr/i18n"
"github.com/koho/frpmgr/pkg/consts"
"github.com/koho/frpmgr/pkg/res"
"github.com/koho/frpmgr/pkg/util"
)
@ -54,12 +54,12 @@ func NewURLImportDialog() *URLImportDialog {
}
func (ud *URLImportDialog) Run(owner walk.Form) (int, error) {
return NewBasicDialog(&ud.Dialog, i18n.Sprintf("Import from URL"), loadIcon(consts.IconURLImport, 32),
return NewBasicDialog(&ud.Dialog, i18n.Sprintf("Import from URL"), loadIcon(res.IconURLImport, 32),
DataBinder{AssignTo: &ud.db, DataSource: &ud.viewModel, Name: "vm"}, ud.onImport,
Label{Text: i18n.Sprintf("* Support batch import, one link per line.")},
TextEdit{
Enabled: Bind("!vm.Working"),
Text: Bind("URLs", consts.ValidateNonEmpty),
Text: Bind("URLs", res.ValidateNonEmpty),
VScroll: true,
MinSize: Size{Width: 430, Height: 130},
},

View File

@ -11,7 +11,7 @@ import (
"golang.org/x/sys/windows"
"github.com/koho/frpmgr/i18n"
"github.com/koho/frpmgr/pkg/consts"
"github.com/koho/frpmgr/pkg/res"
"github.com/koho/frpmgr/pkg/sec"
)
@ -27,7 +27,7 @@ func NewValidateDialog() *ValidateDialog {
}
func (vd *ValidateDialog) Run() (int, error) {
name, err := syscall.UTF16PtrFromString(consts.DialogValidate)
name, err := syscall.UTF16PtrFromString(res.DialogValidate)
if err != nil {
return -1, err
}
@ -44,9 +44,9 @@ func (vd *ValidateDialog) proc(h win.HWND, msg uint32, wp, lp uintptr) uintptr {
switch msg {
case win.WM_INITDIALOG:
SetWindowText(h, fmt.Sprintf("%s - %s", i18n.Sprintf("Enter Password"), AppLocalName))
SetWindowText(win.GetDlgItem(h, consts.DialogTitle), i18n.Sprintf("You must enter an administration password to operate the %s.", AppLocalName))
SetWindowText(win.GetDlgItem(h, consts.DialogStatic1), i18n.Sprintf("Enter Administration Password"))
SetWindowText(win.GetDlgItem(h, consts.DialogStatic2), i18n.SprintfColon("Password"))
SetWindowText(win.GetDlgItem(h, res.DialogTitle), i18n.Sprintf("You must enter an administration password to operate the %s.", AppLocalName))
SetWindowText(win.GetDlgItem(h, res.DialogStatic1), i18n.Sprintf("Enter Administration Password"))
SetWindowText(win.GetDlgItem(h, res.DialogStatic2), i18n.SprintfColon("Password"))
SetWindowText(win.GetDlgItem(h, win.IDOK), i18n.Sprintf("OK"))
SetWindowText(win.GetDlgItem(h, win.IDCANCEL), i18n.Sprintf("Cancel"))
vd.setIcon(h, int(win.GetDpiForWindow(h)))
@ -54,11 +54,11 @@ func (vd *ValidateDialog) proc(h win.HWND, msg uint32, wp, lp uintptr) uintptr {
case win.WM_COMMAND:
switch win.LOWORD(uint32(wp)) {
case win.IDOK:
passwd := GetWindowText(win.GetDlgItem(h, consts.DialogEdit))
passwd := GetWindowText(win.GetDlgItem(h, res.DialogEdit))
if sec.EncryptPassword(passwd) != appConf.Password {
win.MessageBox(h, windows.StringToUTF16Ptr(i18n.Sprintf("The password is incorrect. Re-enter password.")),
windows.StringToUTF16Ptr(AppLocalName), windows.MB_ICONERROR)
win.SetFocus(win.GetDlgItem(h, consts.DialogEdit))
win.SetFocus(win.GetDlgItem(h, res.DialogEdit))
} else {
win.EndDialog(h, win.IDOK)
}
@ -80,7 +80,7 @@ func (vd *ValidateDialog) setIcon(h win.HWND, dpi int) error {
if err != nil {
return err
}
iconFile, err := syscall.UTF16PtrFromString(filepath.Join(system32, consts.IconKey.Dll+".dll"))
iconFile, err := syscall.UTF16PtrFromString(filepath.Join(system32, res.IconKey.Dll+".dll"))
if err != nil {
return err
}
@ -89,10 +89,10 @@ func (vd *ValidateDialog) setIcon(h win.HWND, dpi int) error {
vd.hIcon = 0
}
size := walk.SizeFrom96DPI(walk.Size{Width: 32, Height: 32}, dpi)
win.SHDefExtractIcon(iconFile, int32(consts.IconKey.Index),
win.SHDefExtractIcon(iconFile, int32(res.IconKey.Index),
0, nil, &vd.hIcon, win.MAKELONG(0, uint16(size.Width)))
if vd.hIcon != 0 {
win.SendDlgItemMessage(h, consts.DialogIcon, stmSetIcon, uintptr(vd.hIcon), 0)
win.SendDlgItemMessage(h, res.DialogIcon, stmSetIcon, uintptr(vd.hIcon), 0)
}
return nil
}