diff --git a/go.mod b/go.mod index c2ef1d0a..2424c900 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.22.0 require ( github.com/fatedier/frp v0.61.1 github.com/fatedier/golib v0.5.0 + github.com/fsnotify/fsnotify v1.8.0 github.com/go-ole/go-ole v1.3.0 github.com/lxn/walk v0.0.0-20210112085537-c389da54e794 github.com/lxn/win v0.0.0-20210218163916-a377121e959e diff --git a/go.sum b/go.sum index 90203612..287db6db 100644 --- a/go.sum +++ b/go.sum @@ -20,6 +20,8 @@ github.com/fatedier/frp v0.61.1 h1:o9Kqxe9axV5HVsdXs1ruBfd/UrplpVHN/dlQqHJNgdw= github.com/fatedier/frp v0.61.1/go.mod h1:Ua8s7hyXQhgVidDMnSPbAiODMWa6x6aYfc3HN/Xluqo= github.com/fatedier/golib v0.5.0 h1:hNcH7hgfIFqVWbP+YojCCAj4eO94pPf4dEF8lmq2jWs= github.com/fatedier/golib v0.5.0/go.mod h1:W6kIYkIFxHsTzbgqg5piCxIiDo4LzwgTY6R5W8l9NFQ= +github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= +github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/go-jose/go-jose/v4 v4.0.1 h1:QVEPDE3OluqXBQZDcnNvQrInro2h0e4eqNbnZSWqS6U= github.com/go-jose/go-jose/v4 v4.0.1/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= diff --git a/pkg/util/file.go b/pkg/util/file.go index 5570d5fb..cd1214c6 100644 --- a/pkg/util/file.go +++ b/pkg/util/file.go @@ -9,6 +9,7 @@ import ( "path/filepath" "regexp" "strings" + "time" ) // SplitExt splits the path into base name and file extension @@ -31,16 +32,13 @@ func FileExists(path string) bool { } // FindLogFiles returns the files and dates archived by date -func FindLogFiles(path string) ([]string, []string, error) { +func FindLogFiles(path string) ([]string, []time.Time, error) { if path == "" || path == "console" { return nil, nil, os.ErrInvalid } - if !FileExists(path) { - return nil, nil, os.ErrNotExist - } fileDir, fileName := filepath.Split(path) baseName, ext := SplitExt(fileName) - pattern := regexp.MustCompile(`^\.\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$`) + pattern := regexp.MustCompile(`^\.\d{4}(0[1-9]|1[0-2])(0[1-9]|[12][0-9]|3[01])-([0-1][0-9]|2[0-3])([0-5][0-9])([0-5][0-9])$`) if fileDir == "" { fileDir = "." } @@ -48,17 +46,17 @@ func FindLogFiles(path string) ([]string, []string, error) { if err != nil { return nil, nil, err } - logs := make([]string, 0) - dates := make([]string, 0) - logs = append(logs, path) - dates = append(dates, "") + logs := []string{filepath.Clean(path)} + dates := []time.Time{{}} for _, file := range files { if strings.HasPrefix(file.Name(), baseName) && strings.HasSuffix(file.Name(), ext) { tailPart := strings.TrimPrefix(file.Name(), baseName) datePart := strings.TrimSuffix(tailPart, ext) if pattern.MatchString(datePart) { - logs = append(logs, filepath.Join(fileDir, file.Name())) - dates = append(dates, datePart[1:]) + if date, err := time.Parse("20060102-150405", datePart[1:]); err == nil { + logs = append(logs, filepath.Join(fileDir, file.Name())) + dates = append(dates, date) + } } } } @@ -82,26 +80,42 @@ func RenameFiles(old []string, new []string) { } } -// ReadFileLines reads all lines in a file and returns a slice of string -func ReadFileLines(path string) ([]string, error) { +// ReadFileLines reads the last n lines in a file starting at a given offset +func ReadFileLines(path string, offset int64, n int) ([]string, int, int64, error) { file, err := os.Open(path) if err != nil { - return nil, err + return nil, -1, 0, err } defer file.Close() - + _, err = file.Seek(offset, io.SeekStart) + if err != nil { + return nil, -1, 0, err + } reader := bufio.NewReader(file) var line string lines := make([]string, 0) + i := -1 for { line, err = reader.ReadString('\n') if err != nil { break } - lines = append(lines, line) + if n < 0 || len(lines) < n { + lines = append(lines, line) + } else { + i = (i + 1) % n + lines[i] = line + } + } + offset, err = file.Seek(0, io.SeekCurrent) + if err != nil { + return nil, -1, 0, err + } + if i >= 0 { + i = (i + 1) % n } - return lines, nil + return lines, i, offset, nil } // ZipFiles compresses the given file list to a zip file diff --git a/ui/editclient.go b/ui/editclient.go index fe64cfb9..62bbc1c2 100644 --- a/ui/editclient.go +++ b/ui/editclient.go @@ -569,11 +569,11 @@ func (cd *EditClientDialog) onSave() { // Rename old log files // The service should be stopped first cd.shutdownService(true) - util.RenameFiles(logs, lo.Map(dates, func(item string, i int) string { - if item == "" { + util.RenameFiles(logs, lo.Map(dates, func(item time.Time, i int) string { + if item.IsZero() { return newConf.LogFile } else { - return filepath.Join(filepath.Dir(newConf.LogFile), baseName+"."+item+ext) + return filepath.Join(filepath.Dir(newConf.LogFile), baseName+"."+item.Format("20060102-150405")+ext) } })) } diff --git a/ui/editproxy.go b/ui/editproxy.go index 19b55bd0..89b0413c 100644 --- a/ui/editproxy.go +++ b/ui/editproxy.go @@ -269,8 +269,7 @@ func (pd *EditProxyDialog) basicProxyPage() TabPage { } func (pd *EditProxyDialog) advancedProxyPage() TabPage { - bandwidthMode := NewStringPairModel(consts.BandwidthMode, - []string{i18n.Sprintf("Client"), i18n.Sprintf("Server")}, "") + bandwidthMode := NewListModel(consts.BandwidthMode, i18n.Sprintf("Client"), i18n.Sprintf("Server")) var xtcpVisitor = Bind("proxyType.Value == 'xtcp' && vm.ServerNameVisible") return TabPage{ Title: i18n.Sprintf("Advanced"), @@ -293,8 +292,8 @@ func (pd *EditProxyDialog) advancedProxyPage() TabPage { Label{Text: "@"}, ComboBox{ Model: bandwidthMode, - BindingMember: "Name", - DisplayMember: "DisplayName", + BindingMember: "Value", + DisplayMember: "Title", Value: Bind("BandwidthLimitMode"), }, }, @@ -302,17 +301,17 @@ func (pd *EditProxyDialog) advancedProxyPage() TabPage { Label{Visible: Bind("vm.PluginEnable"), Text: i18n.SprintfColon("Proxy Protocol")}, ComboBox{ Visible: Bind("vm.PluginEnable"), - Model: NewDefaultListModel([]string{"v1", "v2"}, "", i18n.Sprintf("auto")), - BindingMember: "Name", - DisplayMember: "DisplayName", + Model: NewListModel([]string{"", "v1", "v2"}, i18n.Sprintf("auto")), + BindingMember: "Value", + DisplayMember: "Title", Value: Bind("ProxyProtocolVersion"), }, Label{Visible: xtcpVisitor, Text: i18n.SprintfColon("Protocol")}, ComboBox{ Visible: xtcpVisitor, - Model: NewDefaultListModel([]string{consts.ProtoQUIC, consts.ProtoKCP}, "", i18n.Sprintf("default")), - BindingMember: "Name", - DisplayMember: "DisplayName", + Model: NewListModel([]string{"", consts.ProtoQUIC, consts.ProtoKCP}, i18n.Sprintf("default")), + BindingMember: "Value", + DisplayMember: "Title", Value: Bind("Protocol"), }, Composite{ @@ -376,10 +375,10 @@ func (pd *EditProxyDialog) pluginProxyPage() TabPage { ComboBox{ AssignTo: &pd.pluginView, Enabled: Bind("vm.PluginEnable"), - Model: NewDefaultListModel(consts.PluginTypes, "", i18n.Sprintf("None")), + Model: NewListModel(append([]string{""}, consts.PluginTypes...), i18n.Sprintf("None")), Value: Bind("Plugin"), - BindingMember: "Name", - DisplayMember: "DisplayName", + BindingMember: "Value", + DisplayMember: "Title", OnCurrentIndexChanged: pd.switchType, Greedy: true, }, diff --git a/ui/logpage.go b/ui/logpage.go index 21f48d88..bed520d0 100644 --- a/ui/logpage.go +++ b/ui/logpage.go @@ -1,13 +1,15 @@ package ui import ( + "path/filepath" "slices" "sort" - "sync" "time" + "github.com/fsnotify/fsnotify" "github.com/lxn/walk" . "github.com/lxn/walk/declarative" + "github.com/samber/lo" "github.com/koho/frpmgr/i18n" "github.com/koho/frpmgr/pkg/util" @@ -15,31 +17,35 @@ import ( type LogPage struct { *walk.TabPage - sync.Mutex - nameModel *ListModel - dateModel []*StringPair - logModel *LogModel - db *walk.DataBinder - logFileChan chan logSelect + nameModel []*Conf + dateModel ListModel + logModel *LogModel + ch chan logSelect + watcher *fsnotify.Watcher // Views logView *walk.TableView nameView *walk.ComboBox dateView *walk.ComboBox + openView *walk.PushButton } type logSelect struct { - path string - // main defines whether the log file is used by config now. - main bool + paths []string + maxLines int } -func NewLogPage() *LogPage { - v := new(LogPage) - v.logFileChan = make(chan logSelect) - v.logModel = NewLogModel("") - return v +func NewLogPage() (*LogPage, error) { + lp := &LogPage{ + ch: make(chan logSelect), + } + watcher, err := fsnotify.NewWatcher() + if err != nil { + return nil, err + } + lp.watcher = watcher + return lp, nil } func (lp *LogPage) Page() TabPage { @@ -54,12 +60,14 @@ func (lp *LogPage) Page() TabPage { ComboBox{ AssignTo: &lp.nameView, StretchFactor: 2, + DisplayMember: "Name", OnCurrentIndexChanged: lp.switchLogName, }, ComboBox{ AssignTo: &lp.dateView, StretchFactor: 1, - DisplayMember: "DisplayName", + DisplayMember: "Title", + Format: time.DateOnly, OnCurrentIndexChanged: lp.switchLogDate, }, }, @@ -69,24 +77,29 @@ func (lp *LogPage) Page() TabPage { AlternatingRowBG: true, LastColumnStretched: true, HeaderHidden: true, - Columns: []TableViewColumn{{DataMember: "Text"}}, + Columns: []TableViewColumn{{}}, }, Composite{ - DataBinder: DataBinder{ - AssignTo: &lp.db, - DataSource: &struct { - HasLogFile func() bool - }{lp.hasLogFile}, - }, Layout: HBox{MarginsZero: true}, Children: []Widget{ HSpacer{}, PushButton{ - MinSize: Size{Width: 150}, - Enabled: Bind("HasLogFile"), - Text: i18n.Sprintf("Open Log Folder"), + AssignTo: &lp.openView, + MinSize: Size{Width: 150}, + Text: i18n.Sprintf("Open Log Folder"), OnClicked: func() { - openFolder(lp.logModel.path) + if i := lp.dateView.CurrentIndex(); i >= 0 && i < len(lp.dateModel) { + paths := lp.dateModel[i : i+1] + if i == 0 { + paths = lp.dateModel + } + for _, path := range paths { + if util.FileExists(path.Value) { + openFolder(path.Value) + break + } + } + } }, }, }, @@ -97,82 +110,62 @@ func (lp *LogPage) Page() TabPage { func (lp *LogPage) OnCreate() { lp.VisibleChanged().Attach(lp.onVisibleChanged) - ticker := time.NewTicker(time.Second * 5) go func() { - defer ticker.Stop() - var lastLog string + var path string for { select { - case logFile := <-lp.logFileChan: - // CurrentIndexChanged event may be triggered multiple times. - // Try to avoid duplicate operations. - if lastLog != "" && logFile.path == lastLog { + case event, ok := <-lp.watcher.Events: + if !ok { + return + } + if path != event.Name { continue } - lastLog = logFile.path - - lp.Lock() - lp.logModel = NewLogModel(logFile.path) - lp.logModel.Reset() - // A change of main log name. - // The available date list need to be updated. - if logFile.main { - fl, dl, _ := util.FindLogFiles(logFile.path) - lp.dateModel = NewStringPairModel(fl, dl, i18n.Sprintf("Latest")) - sort.SliceStable(lp.dateModel, func(i, j int) bool { - t1, err := time.Parse("2006-01-02", lp.dateModel[i].DisplayName) - if err != nil { - // Put non-date string at top. - return true + if event.Has(fsnotify.Write) { + lp.logView.Synchronize(func() { + if lp.logModel != nil { + scroll := lp.logModel.RowCount() == 0 || lp.logView.ItemVisible(lp.logModel.RowCount()-1) + if err := lp.logModel.ReadMore(); err == nil && scroll { + lp.scrollToBottom() + } } - t2, err := time.Parse("2006-01-02", lp.dateModel[j].DisplayName) - if err != nil { - // Put non-date string at top. - return false + }) + } else if event.Has(fsnotify.Create) { + lp.logView.Synchronize(func() { + if lp.logModel != nil { + lp.logModel.Reset() + } + if !lp.openView.Enabled() { + lp.openView.SetEnabled(true) } - return t1.After(t2) }) } - lp.Unlock() - - lp.Synchronize(func() { - lp.Lock() - defer lp.Unlock() - lp.db.Reset() - lp.logView.SetModel(lp.logModel) - if logFile.main { - // A change of main log name always reads the latest log file. - // So there's no need to trigger another same operation. - // Moreover, the update of model in date view will trigger - // CurrentIndexChanged event which may cause a deadlock. - // Thus, we must disable this event first. - lp.dateView.CurrentIndexChanged().Detach(0) - lp.dateView.SetModel(lp.dateModel) - if len(lp.dateModel) > 0 { - lp.dateView.SetCurrentIndex(0) - } - // We are safe to restore the event now. - lp.dateView.CurrentIndexChanged().Attach(lp.switchLogDate) - } - lp.scrollToBottom() - }) - case <-ticker.C: - // We should only read log when the log page is visible. - // Also, there's no need to reload the backup log. - if !lp.Visible() || lp.dateView.CurrentIndex() > 0 { + case logs := <-lp.ch: + // Try to avoid duplicate operations + if path != "" && len(logs.paths) > 0 && logs.paths[0] == path { continue } - lp.Lock() - err := lp.logModel.Reset() - lp.Unlock() - if err == nil { - lp.Synchronize(func() { - lp.Lock() - defer lp.Unlock() - lp.logView.SetModel(lp.logModel) - lp.scrollToBottom() - }) + if path != "" { + lp.watcher.Remove(filepath.Dir(path)) + path = "" + } + var model *LogModel + var ok bool + if len(logs.paths) > 0 { + path = logs.paths[0] + lp.watcher.Add(filepath.Dir(path)) + model, ok = NewLogModel(logs.paths, logs.maxLines) } + lp.Synchronize(func() { + lp.openView.SetEnabled(ok) + lp.logModel = model + if model != nil { + lp.logView.SetModel(model) + lp.scrollToBottom() + } else { + lp.logView.SetModel(nil) + } + }) } } }() @@ -180,57 +173,63 @@ func (lp *LogPage) OnCreate() { func (lp *LogPage) onVisibleChanged() { if lp.Visible() { - // Remember the previous selected name - var preName string - if idx := lp.nameView.CurrentIndex(); idx >= 0 && lp.nameModel != nil && idx < len(lp.nameModel.items) { - preName = lp.nameModel.items[idx].Name + // Try to avoid duplicate operations + if lp.nameView.CurrentIndex() >= 0 { + return } // Refresh config name list - lp.nameModel = NewListModel(confList) + lp.nameModel = getConfListSafe() lp.nameView.SetModel(lp.nameModel) - if len(lp.nameModel.items) == 0 { + if len(lp.nameModel) == 0 { return } // Switch to current config log first if conf := getCurrentConf(); conf != nil { - if i := slices.IndexFunc(lp.nameModel.items, func(c *Conf) bool { return c.Name == conf.Name }); i >= 0 { - lp.nameView.SetCurrentIndex(i) - return - } - } - // Select previous config log - if preName != "" { - if i := slices.IndexFunc(lp.nameModel.items, func(c *Conf) bool { return c.Name == preName }); i >= 0 { + if i := slices.Index(lp.nameModel, conf); i >= 0 { lp.nameView.SetCurrentIndex(i) return } } // Fallback to the first config log lp.nameView.SetCurrentIndex(0) + } else { + lp.nameView.SetCurrentIndex(-1) } } func (lp *LogPage) scrollToBottom() { - if len(lp.logModel.lines) > 0 { - lp.logView.EnsureItemVisible(len(lp.logModel.lines) - 1) + if count := lp.logModel.RowCount(); count > 0 { + lp.logView.EnsureItemVisible(count - 1) } } -func (lp *LogPage) hasLogFile() bool { - if lp.logModel.path != "" { - return util.FileExists(lp.logModel.path) - } - return false -} - func (lp *LogPage) switchLogName() { index := lp.nameView.CurrentIndex() + cleanup := func() { + lp.dateModel = nil + lp.dateView.SetModel(nil) + lp.ch <- logSelect{} + } if index < 0 || lp.nameModel == nil { - // No config selected, the log page should be empty - lp.logFileChan <- logSelect{"", true} + cleanup() + return + } + files, dates, err := util.FindLogFiles(lp.nameModel[index].Data.GetLogFile()) + if err != nil { + cleanup() return } - lp.logFileChan <- logSelect{lp.nameModel.items[index].Data.GetLogFile(), true} + pairs := lo.Zip2(files, dates) + sort.SliceStable(pairs[1:], func(i, j int) bool { + return pairs[i+1].B.After(pairs[j+1].B) + }) + files, dates = lo.Unzip2(pairs) + titles := lo.ToAnySlice(dates) + titles[0] = i18n.Sprintf("Latest") + lp.dateModel = NewListModel(files, titles...) + lp.dateView.SetCurrentIndex(-1) + lp.dateView.SetModel(lp.dateModel) + lp.dateView.SetCurrentIndex(0) } func (lp *LogPage) switchLogDate() { @@ -238,5 +237,18 @@ func (lp *LogPage) switchLogDate() { if index < 0 || lp.dateModel == nil { return } - lp.logFileChan <- logSelect{lp.dateModel[index].Name, false} + if index == 0 { + lp.ch <- logSelect{ + paths: lo.Map(lp.dateModel, func(item *ListItem, index int) string { + return item.Value + }), + maxLines: 2000, + } + } else { + lp.ch <- logSelect{paths: []string{lp.dateModel[index].Value}, maxLines: -1} + } +} + +func (lp *LogPage) Close() error { + return lp.watcher.Close() } diff --git a/ui/model.go b/ui/model.go index c381827c..09446f6d 100644 --- a/ui/model.go +++ b/ui/model.go @@ -2,7 +2,7 @@ package ui import ( "net" - "os" + "slices" "strconv" "strings" @@ -36,26 +36,6 @@ func (m *ConfListModel) Items() interface{} { return m.items } -type ListModel struct { - walk.ListModelBase - - items []*Conf -} - -func NewListModel(items []*Conf) *ListModel { - m := new(ListModel) - m.items = items - return m -} - -func (m *ListModel) Value(index int) interface{} { - return m.items[index].Name -} - -func (m *ListModel) ItemCount() int { - return len(m.items) -} - type ProxyModel struct { walk.ReflectTableModelBase @@ -114,71 +94,114 @@ func (m *ProxyModel) Move(i, j int) { m.PublishRowsChanged(min(i, j), max(i, j)) } -// StringPair is a simple struct to hold a pair of strings. -type StringPair struct { - Name string - DisplayName string +type ListItem struct { + Title any + Value string } -// NewDefaultListModel creates a default item at the top of the model. -func NewDefaultListModel(items []string, defaultKey string, defaultName string) []*StringPair { - listItems := make([]*StringPair, 0, len(items)+1) - listItems = append(listItems, &StringPair{Name: defaultKey, DisplayName: defaultName}) - for _, item := range items { - listItems = append(listItems, &StringPair{Name: item, DisplayName: item}) +type ListModel []*ListItem + +func NewListModel(values []string, titles ...any) ListModel { + var items []*ListItem + for i, value := range values { + var title any = value + if i < len(titles) { + title = titles[i] + } + items = append(items, &ListItem{ + Title: title, + Value: value, + }) } - return listItems + return items +} + +type LogModel struct { + walk.TableModelBase + + path string + offset int64 + maxLines int + lines []string } -// NewStringPairModel creates a slice of string pair from two string slices. -func NewStringPairModel(keys []string, values []string, defaultValue string) []*StringPair { - listItems := make([]*StringPair, 0, len(keys)) - for i, k := range keys { - pair := &StringPair{Name: k, DisplayName: values[i]} - if pair.DisplayName == "" { - pair.DisplayName = defaultValue +func NewLogModel(paths []string, maxLines int) (*LogModel, bool) { + m := &LogModel{ + path: paths[0], + maxLines: maxLines, + lines: make([]string, 0), + } + ok := false + for i, path := range paths { + lines, k, offset, err := util.ReadFileLines(path, 0, maxLines) + if err != nil { + continue + } + ok = true + if i == 0 { + m.offset = offset } - listItems = append(listItems, pair) + if k >= 0 { + for n, j := len(lines), k-1; (j+n)%n != k; j-- { + m.lines = append(m.lines, lines[(j+n)%n]) + } + m.lines = append(m.lines, lines[k]) + } else { + for j := len(lines) - 1; j >= 0; j-- { + m.lines = append(m.lines, lines[j]) + } + } + maxLines -= len(lines) + if maxLines <= 0 { + break + } + } + if len(m.lines) > 0 { + slices.Reverse(m.lines) } - return listItems + return m, ok } -type TextLine struct { - Text string +func (m *LogModel) write(lines []string, i int) { + if len(lines) == 0 { + return + } + if m.maxLines > 0 && len(m.lines) >= m.maxLines { + copy(m.lines[:], m.lines[len(lines):]) + m.lines = m.lines[:len(m.lines)-len(lines)] + m.PublishRowsRemoved(0, len(lines)-1) + m.PublishRowsChanged(0, len(m.lines)-1) + } + from := len(m.lines) + if i >= 0 { + m.lines = append(m.lines, lines[i:]...) + m.lines = append(m.lines, lines[:i]...) + } else { + m.lines = append(m.lines, lines...) + } + to := from + len(lines) - 1 + m.PublishRowsInserted(from, to) } -type LogModel struct { - walk.ReflectTableModelBase - - path string - lines []*TextLine +func (m *LogModel) Value(row, col int) any { + return m.lines[row] } -func NewLogModel(path string) *LogModel { - m := new(LogModel) - m.path = path - m.lines = make([]*TextLine, 0) - return m +func (m *LogModel) RowCount() int { + return len(m.lines) } -func (m *LogModel) Items() interface{} { - return m.lines +func (m *LogModel) Reset() { + m.offset = 0 } -// Reset reload the whole log file from disk -func (m *LogModel) Reset() error { - if m.path == "" { - return os.ErrInvalid - } - textLines, err := util.ReadFileLines(m.path) +func (m *LogModel) ReadMore() error { + lines, k, offset, err := util.ReadFileLines(m.path, m.offset, m.maxLines) if err != nil { return err } - lines := make([]*TextLine, 0) - for _, line := range textLines { - lines = append(lines, &TextLine{Text: line}) - } - m.lines = lines + m.write(lines, k) + m.offset = offset return nil } diff --git a/ui/nathole.go b/ui/nathole.go index 54ba3fe1..7d305458 100644 --- a/ui/nathole.go +++ b/ui/nathole.go @@ -43,8 +43,8 @@ func (nd *NATDiscoveryDialog) Run(owner walk.Form) (int, error) { Visible: false, AssignTo: &nd.table, Columns: []TableViewColumn{ - {Title: i18n.Sprintf("Item"), DataMember: "Name", Width: 180}, - {Title: i18n.Sprintf("Value"), DataMember: "DisplayName", Width: 180}, + {Title: i18n.Sprintf("Item"), DataMember: "Title", Width: 180}, + {Title: i18n.Sprintf("Value"), DataMember: "Value", Width: 180}, }, }, ProgressBar{AssignTo: &nd.barView, Visible: Bind("!tb.Visible"), MarqueeMode: true}, @@ -87,15 +87,15 @@ func (nd *NATDiscoveryDialog) discover() (err error) { if err != nil { return err } - items := []*StringPair{ - {Name: i18n.Sprintf("NAT Type"), DisplayName: natFeature.NatType}, - {Name: i18n.Sprintf("Behavior"), DisplayName: natFeature.Behavior}, - {Name: i18n.Sprintf("Local Address"), DisplayName: localAddr.String()}, + items := []*ListItem{ + {Title: i18n.Sprintf("NAT Type"), Value: natFeature.NatType}, + {Title: i18n.Sprintf("Behavior"), Value: natFeature.Behavior}, + {Title: i18n.Sprintf("Local Address"), Value: localAddr.String()}, } for _, addr := range addrs { - items = append(items, &StringPair{ - Name: i18n.Sprintf("External Address"), - DisplayName: addr, + items = append(items, &ListItem{ + Title: i18n.Sprintf("External Address"), + Value: addr, }) } var public string @@ -104,9 +104,9 @@ func (nd *NATDiscoveryDialog) discover() (err error) { } else { public = i18n.Sprintf("No") } - items = append(items, &StringPair{ - Name: i18n.Sprintf("Public Network"), - DisplayName: public, + items = append(items, &ListItem{ + Title: i18n.Sprintf("Public Network"), + Value: public, }) nd.table.Synchronize(func() { nd.table.SetVisible(true) diff --git a/ui/prefpage.go b/ui/prefpage.go index 2ac92d28..aa243b8a 100644 --- a/ui/prefpage.go +++ b/ui/prefpage.go @@ -7,6 +7,7 @@ import ( "github.com/lxn/walk" . "github.com/lxn/walk/declarative" "github.com/lxn/win" + "github.com/samber/lo" "github.com/koho/frpmgr/i18n" "github.com/koho/frpmgr/pkg/consts" @@ -116,10 +117,10 @@ func (pp *PrefPage) languageSection() GroupBox { Label{Text: i18n.SprintfColon("Select language")}, ComboBox{ AssignTo: &langSelect, - Model: NewStringPairModel(keys, names, ""), + Model: NewListModel(keys, lo.ToAnySlice(names)...), MinSize: Size{Width: 200}, - DisplayMember: "DisplayName", - BindingMember: "Name", + DisplayMember: "Title", + BindingMember: "Value", Value: i18n.GetLanguage(), OnCurrentIndexChanged: func() { pp.switchLanguage(keys[langSelect.CurrentIndex()]) diff --git a/ui/ui.go b/ui/ui.go index 63609e73..89d3a5de 100644 --- a/ui/ui.go +++ b/ui/ui.go @@ -53,11 +53,12 @@ type FRPManager struct { } func RunUI() error { + var err error // Make sure the config directory exists. - if err := os.MkdirAll(PathOfConf(""), os.ModePerm); err != nil { + if err = os.MkdirAll(PathOfConf(""), os.ModePerm); err != nil { return err } - if err := loadAllConfs(); err != nil { + if err = loadAllConfs(); err != nil { return err } if appConf.Password != "" { @@ -67,7 +68,10 @@ func RunUI() error { } fm := new(FRPManager) fm.confPage = NewConfPage() - fm.logPage = NewLogPage() + fm.logPage, err = NewLogPage() + if err != nil { + return err + } fm.prefPage = NewPrefPage() fm.aboutPage = NewAboutPage() mw := MainWindow{ @@ -91,7 +95,7 @@ func RunUI() error { }, OnDropFiles: fm.confPage.confView.ImportFiles, } - if err := mw.Create(); err != nil { + if err = mw.Create(); err != nil { return err } // Initialize child pages @@ -106,6 +110,7 @@ func RunUI() error { }) fm.SetVisible(true) fm.Run() + fm.logPage.Close() services.Cleanup() return nil }