mirror of
https://github.com/koho/frpmgr.git
synced 2025-10-20 16:03:47 +08:00
302 lines
6.8 KiB
Go
302 lines
6.8 KiB
Go
package ui
|
|
|
|
import (
|
|
"path/filepath"
|
|
"slices"
|
|
"sort"
|
|
"strings"
|
|
"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"
|
|
)
|
|
|
|
type LogPage struct {
|
|
*walk.TabPage
|
|
|
|
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 {
|
|
paths []string
|
|
maxLines int
|
|
}
|
|
|
|
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 {
|
|
return TabPage{
|
|
AssignTo: &lp.TabPage,
|
|
Title: i18n.Sprintf("Log"),
|
|
Layout: VBox{},
|
|
Children: []Widget{
|
|
Composite{
|
|
Layout: HBox{MarginsZero: true},
|
|
Children: []Widget{
|
|
ComboBox{
|
|
AssignTo: &lp.nameView,
|
|
StretchFactor: 2,
|
|
DisplayMember: "Name",
|
|
OnCurrentIndexChanged: lp.switchLogName,
|
|
},
|
|
ComboBox{
|
|
AssignTo: &lp.dateView,
|
|
StretchFactor: 1,
|
|
DisplayMember: "Title",
|
|
Format: time.DateOnly,
|
|
OnCurrentIndexChanged: lp.switchLogDate,
|
|
},
|
|
},
|
|
},
|
|
TableView{
|
|
Name: "log",
|
|
AssignTo: &lp.logView,
|
|
AlternatingRowBG: true,
|
|
LastColumnStretched: true,
|
|
HeaderHidden: true,
|
|
Columns: []TableViewColumn{{}},
|
|
MultiSelection: true,
|
|
ContextMenuItems: []MenuItem{
|
|
Action{
|
|
Text: i18n.Sprintf("Copy"),
|
|
Enabled: Bind("log.SelectedCount > 0"),
|
|
OnTriggered: func() {
|
|
if indexes := lp.logView.SelectedIndexes(); len(indexes) > 0 && lp.logModel != nil {
|
|
walk.Clipboard().SetText(strings.Join(
|
|
lo.Map(indexes, func(item int, index int) string {
|
|
return lp.logModel.Value(item, 0).(string)
|
|
}), "\n"))
|
|
}
|
|
},
|
|
},
|
|
Action{
|
|
Text: i18n.Sprintf("Select all"),
|
|
Enabled: Bind("log.SelectedCount < log.ItemCount"),
|
|
OnTriggered: func() {
|
|
lp.logView.SetSelectedIndexes([]int{-1})
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Composite{
|
|
Layout: HBox{MarginsZero: true},
|
|
Children: []Widget{
|
|
HSpacer{},
|
|
PushButton{
|
|
AssignTo: &lp.openView,
|
|
MinSize: Size{Width: 150},
|
|
Text: i18n.Sprintf("Open Log Folder"),
|
|
Enabled: false,
|
|
OnClicked: func() {
|
|
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
|
|
}
|
|
}
|
|
}
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func (lp *LogPage) OnCreate() {
|
|
lp.VisibleChanged().Attach(lp.onVisibleChanged)
|
|
go func() {
|
|
// Due to the file caching mechanism, new logs may not be written to
|
|
// the disk immediately, and therefore no write events will be received.
|
|
// It is still necessary to read files regularly.
|
|
ticker := time.NewTicker(time.Second * 5)
|
|
defer ticker.Stop()
|
|
var path string
|
|
var watch bool
|
|
for {
|
|
select {
|
|
case event, ok := <-lp.watcher.Events:
|
|
if !ok {
|
|
return
|
|
}
|
|
if path != event.Name {
|
|
continue
|
|
}
|
|
if event.Has(fsnotify.Write) {
|
|
lp.refreshLog()
|
|
} 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)
|
|
}
|
|
})
|
|
}
|
|
case logs := <-lp.ch:
|
|
// Try to avoid duplicate operations
|
|
if path != "" && len(logs.paths) > 0 && logs.paths[0] == path {
|
|
continue
|
|
}
|
|
if path != "" {
|
|
if watch {
|
|
lp.watcher.Remove(filepath.Dir(path))
|
|
}
|
|
path = ""
|
|
watch = false
|
|
}
|
|
var model *LogModel
|
|
var ok bool
|
|
if len(logs.paths) > 0 {
|
|
path = logs.paths[0]
|
|
watch = logs.maxLines > 0
|
|
if watch {
|
|
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)
|
|
}
|
|
})
|
|
case <-ticker.C:
|
|
if path != "" && watch {
|
|
lp.refreshLog()
|
|
}
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
func (lp *LogPage) refreshLog() {
|
|
lp.logView.Synchronize(func() {
|
|
if lp.logModel != nil {
|
|
scroll := lp.logModel.RowCount() == 0 || (lp.logView.ItemVisible(lp.logModel.RowCount()-1) && len(lp.logView.SelectedIndexes()) <= 1)
|
|
if n, err := lp.logModel.ReadMore(); err == nil && n > 0 && scroll {
|
|
lp.scrollToBottom()
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
func (lp *LogPage) onVisibleChanged() {
|
|
if lp.Visible() {
|
|
// Try to avoid duplicate operations
|
|
if lp.nameView.CurrentIndex() >= 0 {
|
|
return
|
|
}
|
|
// Refresh config name list
|
|
lp.nameModel = getConfList()
|
|
lp.nameView.SetModel(lp.nameModel)
|
|
if len(lp.nameModel) == 0 {
|
|
return
|
|
}
|
|
// Switch to current config log first
|
|
if conf := getCurrentConf(); conf != nil {
|
|
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)
|
|
lp.nameView.SetModel(nil)
|
|
lp.nameModel = nil
|
|
}
|
|
}
|
|
|
|
func (lp *LogPage) scrollToBottom() {
|
|
if count := lp.logModel.RowCount(); count > 0 {
|
|
lp.logView.EnsureItemVisible(count - 1)
|
|
}
|
|
}
|
|
|
|
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 {
|
|
cleanup()
|
|
return
|
|
}
|
|
files, dates, err := util.FindLogFiles(lp.nameModel[index].Data.LogFile)
|
|
if err != nil {
|
|
cleanup()
|
|
return
|
|
}
|
|
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() {
|
|
index := lp.dateView.CurrentIndex()
|
|
if index < 0 || lp.dateModel == nil {
|
|
return
|
|
}
|
|
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()
|
|
}
|