diff --git a/internal/ui/keypad.go b/internal/ui/keypad.go new file mode 100644 index 0000000..a1644b9 --- /dev/null +++ b/internal/ui/keypad.go @@ -0,0 +1,58 @@ +package ui + +import ( + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/widget" +) + +func keyButton(label string, entry *widget.Entry) *widget.Button { + return widget.NewButton(label, func() { + entry.Append(label) + }) +} + +func entButton(entry *widget.Entry) *widget.Button { + return widget.NewButton("ENT", func() { + entry.Append("\n") + }) +} + +func clearButton(label string, entry *widget.Entry) *widget.Button { + return widget.NewButton(label, func() { + entry.SetText("") + }) +} + +func newIPKeypad(entry *widget.Entry) fyne.CanvasObject { + presets := container.NewHBox( + keyButton("192.168.", entry), + keyButton("169.254.", entry), + keyButton("10.", entry), + keyButton("100.", entry), + ) + numpad := container.NewGridWithColumns(3, + keyButton("1", entry), keyButton("2", entry), keyButton("3", entry), + keyButton("4", entry), keyButton("5", entry), keyButton("6", entry), + keyButton("7", entry), keyButton("8", entry), keyButton("9", entry), + entButton(entry), keyButton("0", entry), keyButton(".", entry), + keyButton("/", entry), keyButton("-", entry), clearButton("清空IP", entry), + ) + return container.NewVBox(presets, numpad) +} + +func newPortKeypad(entry *widget.Entry) fyne.CanvasObject { + presets := container.NewGridWithColumns(3, + keyButton("1-65535", entry), keyButton("80;443", entry), keyButton("21", entry), + keyButton("22", entry), keyButton("23", entry), keyButton("3389", entry), + keyButton("445", entry), keyButton("25565", entry), keyButton("25;587", entry), + ) + numpad := container.NewGridWithColumns(3, + keyButton("1", entry), keyButton("2", entry), keyButton("3", entry), + keyButton("4", entry), keyButton("5", entry), keyButton("6", entry), + keyButton("7", entry), keyButton("8", entry), keyButton("9", entry), + keyButton("0", entry), keyButton(";", entry), keyButton("-", entry), + entButton(entry), clearButton("清空端口", entry), + ) + return container.NewVBox(presets, numpad) +} diff --git a/internal/ui/scanform.go b/internal/ui/scanform.go new file mode 100644 index 0000000..602e462 --- /dev/null +++ b/internal/ui/scanform.go @@ -0,0 +1,175 @@ +package ui + +import ( + "fmt" + "net/url" + "strconv" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/layout" + "fyne.io/fyne/v2/widget" +) + +const appVersion = "V1.5.1" + +const ( + ipPlaceholder = "例子:\n127.0.0.1 - 单IP\n192.168.0.0/24 - 掩码IP段\n0.0.0.0-255.255.255.255 - IP段\ndomain.com - 单域名" + portPlaceholder = "例子:\n22 - 单端口\n80;443 - 多个不连续端口\n3000-65535 - 连续的端口范围" +) + +type scanForm struct { + app fyne.App + + ipList *widget.Entry + portList *widget.Entry + timeout *widget.Entry + threads *widget.Entry + output *widget.Entry + progress *widget.ProgressBar + + scanBtn *widget.Button + pingBtn *widget.Button + scanning bool +} + +func newScanForm(app fyne.App) *scanForm { + f := &scanForm{app: app} + + f.ipList = widget.NewMultiLineEntry() + f.ipList.SetPlaceHolder(ipPlaceholder) + f.ipList.SetMinRowsVisible(4) + + f.portList = widget.NewMultiLineEntry() + f.portList.SetPlaceHolder(portPlaceholder) + f.portList.SetMinRowsVisible(4) + + f.timeout = newIntEntry(100, 1, 10000) + f.threads = newIntEntry(64, 1, 1024) + + f.output = widget.NewMultiLineEntry() + f.output.SetMinRowsVisible(6) + + f.progress = widget.NewProgressBar() + + f.scanBtn = widget.NewButton("开始扫描", f.toggleScan) + f.pingBtn = widget.NewButton("仅Ping", f.togglePing) + + return f +} + +func (f *scanForm) build() fyne.CanvasObject { + ipSection := container.NewVBox( + widget.NewLabel("IP"), + f.ipList, + newIPKeypad(f.ipList), + ) + portSection := container.NewVBox( + widget.NewLabel("端口"), + f.portList, + newPortKeypad(f.portList), + ) + + topRow := container.New(layout.NewGridLayoutWithColumns(2), ipSection, portSection) + controlRow := f.buildControlRow() + topArea := container.NewVBox(topRow, controlRow) + + footer := container.NewHBox( + widget.NewLabel(appVersion), + layout.NewSpacer(), + widget.NewButton("Blog", func() { f.openURL("https://wnfed.com/") }), + widget.NewButton("GitLab", func() { f.openURL("https://git.lmve.net/kevin/tcp_ip_scan") }), + widget.NewButton("GitHub", func() { f.openURL("https://github.com/wuwenfengmi1998/tcp_ip_scan") }), + ) + + return container.NewBorder(topArea, footer, nil, nil, f.output) +} + +func (f *scanForm) buildControlRow() fyne.CanvasObject { + left := container.NewHBox( + widget.NewButton("清空输出", func() { f.output.SetText("") }), + widget.NewButton("保存输出", f.saveOutput), + container.NewHBox(widget.NewLabel("超时ms"), f.timeout), + container.NewHBox(widget.NewLabel("线程"), f.threads), + widget.NewButton("清空IP", func() { f.ipList.SetText("") }), + f.pingBtn, + ) + right := container.NewHBox( + widget.NewButton("清空端口", func() { f.portList.SetText("") }), + f.scanBtn, + ) + return container.NewBorder(nil, nil, left, right, f.progress) +} + +func (f *scanForm) toggleScan() { + if !f.scanning { + f.scanning = true + f.scanBtn.SetText("停止扫描") + f.pingBtn.Disable() + f.setInputsEnabled(false) + f.progress.SetValue(0) + f.output.Append("开始扫描... (TODO: 接入扫描逻辑)\n") + } else { + f.scanning = false + f.scanBtn.SetText("开始扫描") + f.pingBtn.Enable() + f.setInputsEnabled(true) + f.output.Append("已停止扫描\n") + } +} + +func (f *scanForm) togglePing() { + if !f.scanning { + f.scanning = true + f.pingBtn.SetText("停止Ping") + f.scanBtn.Disable() + f.setInputsEnabled(false) + f.progress.SetValue(0) + f.output.Append("开始Ping... (TODO: 接入扫描逻辑)\n") + } else { + f.scanning = false + f.pingBtn.SetText("仅Ping") + f.scanBtn.Enable() + f.setInputsEnabled(true) + f.output.Append("已停止Ping\n") + } +} + +func (f *scanForm) setInputsEnabled(enabled bool) { + if enabled { + f.ipList.Enable() + f.portList.Enable() + f.timeout.Enable() + f.threads.Enable() + } else { + f.ipList.Disable() + f.portList.Disable() + f.timeout.Disable() + f.threads.Disable() + } +} + +func (f *scanForm) saveOutput() { + f.output.Append("(TODO: 保存输出到文件)\n") +} + +func (f *scanForm) openURL(urlStr string) { + u, err := url.Parse(urlStr) + if err != nil { + return + } + _ = f.app.OpenURL(u) +} + +func newIntEntry(val, min, max int) *widget.Entry { + e := widget.NewEntry() + e.SetText(strconv.Itoa(val)) + e.Validator = func(s string) error { + n, err := strconv.Atoi(s) + if err != nil || n < min || n > max { + return fmt.Errorf("需为 %d-%d", min, max) + } + return nil + } + return e +} diff --git a/internal/ui/window.go b/internal/ui/window.go index d93995b..af7fdbc 100644 --- a/internal/ui/window.go +++ b/internal/ui/window.go @@ -3,24 +3,19 @@ package ui import ( "fyne.io/fyne/v2" "fyne.io/fyne/v2/app" - "fyne.io/fyne/v2/container" - "fyne.io/fyne/v2/widget" ) const ( - appTitle = "IP Scanner" - appWidth = 800 - appHeight = 600 + appTitle = "TCP端口扫描工具" + appWidth = 850 + appHeight = 500 ) func Run() { a := app.New() + f := newScanForm(a) w := a.NewWindow(appTitle) - - w.SetContent(container.NewVBox( - widget.NewLabel("Hello Fyne!"), - )) - + w.SetContent(f.build()) w.Resize(fyne.NewSize(appWidth, appHeight)) w.ShowAndRun() }