369 lines
8.3 KiB
Go
369 lines
8.3 KiB
Go
package ui
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"fyne.io/fyne/v2"
|
|
fyneapp "fyne.io/fyne/v2/app"
|
|
"fyne.io/fyne/v2/container"
|
|
"fyne.io/fyne/v2/dialog"
|
|
"fyne.io/fyne/v2/layout"
|
|
"fyne.io/fyne/v2/storage"
|
|
"fyne.io/fyne/v2/widget"
|
|
appcore "github.com/veggiedefender/torrent-client/internal/app"
|
|
"github.com/veggiedefender/torrent-client/internal/torrent"
|
|
"github.com/veggiedefender/torrent-client/internal/torrentfile"
|
|
)
|
|
|
|
type desktopUI struct {
|
|
controller *appcore.Controller
|
|
app fyne.App
|
|
window fyne.Window
|
|
|
|
pathEntry *widget.Entry
|
|
progress *widget.ProgressBar
|
|
phaseLabel *widget.Label
|
|
phaseMetricLabel *widget.Label
|
|
messageLabel *widget.Label
|
|
errorLabel *widget.Label
|
|
nameLabel *widget.Label
|
|
sizeLabel *widget.Label
|
|
pieceLabel *widget.Label
|
|
pieceCountLabel *widget.Label
|
|
peerCountLabel *widget.Label
|
|
|
|
mu sync.RWMutex
|
|
trackers []torrent.TrackerStatus
|
|
peers []torrent.PeerStatus
|
|
files []torrentfile.File
|
|
|
|
trackersTable *widget.Table
|
|
peersTable *widget.Table
|
|
filesTable *widget.Table
|
|
|
|
stopRefresh chan struct{}
|
|
}
|
|
|
|
func Start(controller *appcore.Controller) {
|
|
desktop := newDesktopUI(controller)
|
|
desktop.window.ShowAndRun()
|
|
}
|
|
|
|
func newDesktopUI(controller *appcore.Controller) *desktopUI {
|
|
a := fyneapp.NewWithID("github.com/veggiedefender/ztorrent")
|
|
w := a.NewWindow("Ztorrent")
|
|
w.Resize(fyne.NewSize(1200, 760))
|
|
|
|
ui := &desktopUI{
|
|
controller: controller,
|
|
app: a,
|
|
window: w,
|
|
stopRefresh: make(chan struct{}),
|
|
pathEntry: widget.NewEntry(),
|
|
progress: widget.NewProgressBar(),
|
|
phaseLabel: widget.NewLabel("Phase: idle"),
|
|
phaseMetricLabel: widget.NewLabel("idle"),
|
|
messageLabel: widget.NewLabel(""),
|
|
errorLabel: widget.NewLabel(""),
|
|
nameLabel: widget.NewLabel("-"),
|
|
sizeLabel: widget.NewLabel("-"),
|
|
pieceLabel: widget.NewLabel("-"),
|
|
pieceCountLabel: widget.NewLabel("-"),
|
|
peerCountLabel: widget.NewLabel("0"),
|
|
}
|
|
|
|
ui.pathEntry.SetPlaceHolder("/path/to/file.torrent")
|
|
|
|
browseButton := widget.NewButton("Browse", ui.onBrowse)
|
|
loadButton := widget.NewButton("Load Torrent", ui.onLoad)
|
|
topRow := container.NewBorder(nil, nil, nil, container.NewHBox(browseButton, loadButton), ui.pathEntry)
|
|
|
|
metaGrid := container.NewGridWithColumns(3,
|
|
wrapMetric("Name", ui.nameLabel),
|
|
wrapMetric("Size (bytes)", ui.sizeLabel),
|
|
wrapMetric("Piece length", ui.pieceLabel),
|
|
wrapMetric("Pieces", ui.pieceCountLabel),
|
|
wrapMetric("Discovered peers", ui.peerCountLabel),
|
|
wrapMetric("Phase", ui.phaseMetricLabel),
|
|
)
|
|
|
|
ui.trackersTable = ui.newTrackersTable()
|
|
ui.peersTable = ui.newPeersTable()
|
|
ui.filesTable = ui.newFilesTable()
|
|
|
|
tabs := container.NewAppTabs(
|
|
container.NewTabItem("Trackers", container.NewMax(ui.trackersTable)),
|
|
container.NewTabItem("Peers", container.NewMax(ui.peersTable)),
|
|
container.NewTabItem("Files", container.NewMax(ui.filesTable)),
|
|
)
|
|
|
|
statusColumn := container.NewVBox(
|
|
ui.phaseLabel,
|
|
ui.progress,
|
|
ui.messageLabel,
|
|
ui.errorLabel,
|
|
)
|
|
|
|
content := container.NewBorder(
|
|
container.NewVBox(topRow),
|
|
nil,
|
|
nil,
|
|
nil,
|
|
container.NewVBox(
|
|
statusColumn,
|
|
layout.NewSpacer(),
|
|
metaGrid,
|
|
layout.NewSpacer(),
|
|
tabs,
|
|
),
|
|
)
|
|
|
|
w.SetContent(content)
|
|
w.SetOnClosed(func() {
|
|
close(ui.stopRefresh)
|
|
})
|
|
|
|
if len(os.Args) > 1 {
|
|
ui.pathEntry.SetText(os.Args[1])
|
|
ui.onLoad()
|
|
}
|
|
|
|
go ui.refreshLoop()
|
|
return ui
|
|
}
|
|
|
|
func wrapMetric(name string, value fyne.CanvasObject) fyne.CanvasObject {
|
|
return container.NewVBox(widget.NewLabel(name), value)
|
|
}
|
|
|
|
func (ui *desktopUI) onBrowse() {
|
|
filter := storage.NewExtensionFileFilter([]string{".torrent"})
|
|
fd := dialog.NewFileOpen(func(reader fyne.URIReadCloser, err error) {
|
|
if err != nil {
|
|
ui.setError(err.Error())
|
|
return
|
|
}
|
|
if reader == nil {
|
|
return
|
|
}
|
|
defer reader.Close()
|
|
|
|
ui.pathEntry.SetText(reader.URI().Path())
|
|
}, ui.window)
|
|
fd.SetFilter(filter)
|
|
fd.Show()
|
|
}
|
|
|
|
func (ui *desktopUI) onLoad() {
|
|
path := strings.TrimSpace(ui.pathEntry.Text)
|
|
if path == "" {
|
|
ui.setError("select a .torrent file first")
|
|
return
|
|
}
|
|
|
|
ui.setMessage("Loading torrent...")
|
|
ui.setError("")
|
|
|
|
go func() {
|
|
err := ui.controller.StartTorrent(path)
|
|
status := ui.controller.Status()
|
|
ui.updateFromStatus(status)
|
|
if err != nil {
|
|
ui.setError(err.Error())
|
|
} else {
|
|
ui.setMessage("Loaded " + filepath.Base(path))
|
|
}
|
|
}()
|
|
}
|
|
|
|
func (ui *desktopUI) refreshLoop() {
|
|
ticker := time.NewTicker(1200 * time.Millisecond)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-ui.stopRefresh:
|
|
return
|
|
case <-ticker.C:
|
|
ui.updateFromStatus(ui.controller.Status())
|
|
}
|
|
}
|
|
}
|
|
|
|
func (ui *desktopUI) updateFromStatus(status torrent.Status) {
|
|
fyne.Do(func() {
|
|
ui.progress.SetValue(status.Progress)
|
|
ui.phaseLabel.SetText("Phase: " + status.Phase)
|
|
ui.phaseMetricLabel.SetText(status.Phase)
|
|
ui.nameLabel.SetText(status.Name)
|
|
ui.sizeLabel.SetText(strconv.Itoa(status.Length))
|
|
ui.pieceLabel.SetText(strconv.Itoa(status.PieceLength))
|
|
ui.pieceCountLabel.SetText(strconv.Itoa(status.PieceCount))
|
|
ui.peerCountLabel.SetText(strconv.Itoa(status.PeerCount))
|
|
|
|
ui.mu.Lock()
|
|
ui.trackers = append(ui.trackers[:0], status.Trackers...)
|
|
ui.peers = append(ui.peers[:0], status.Peers...)
|
|
ui.files = append(ui.files[:0], status.Files...)
|
|
ui.mu.Unlock()
|
|
|
|
ui.trackersTable.Refresh()
|
|
ui.peersTable.Refresh()
|
|
ui.filesTable.Refresh()
|
|
|
|
if status.LastError != "" {
|
|
ui.errorLabel.SetText(status.LastError)
|
|
}
|
|
})
|
|
}
|
|
|
|
func (ui *desktopUI) setMessage(msg string) {
|
|
fyne.Do(func() {
|
|
ui.messageLabel.SetText(msg)
|
|
})
|
|
}
|
|
|
|
func (ui *desktopUI) setError(msg string) {
|
|
fyne.Do(func() {
|
|
if msg == "" {
|
|
ui.errorLabel.SetText("")
|
|
return
|
|
}
|
|
ui.errorLabel.SetText("Error: " + msg)
|
|
})
|
|
}
|
|
|
|
func (ui *desktopUI) newTrackersTable() *widget.Table {
|
|
table := widget.NewTable(
|
|
func() (int, int) {
|
|
ui.mu.RLock()
|
|
defer ui.mu.RUnlock()
|
|
return len(ui.trackers) + 1, 4
|
|
},
|
|
func() fyne.CanvasObject {
|
|
return widget.NewLabel("")
|
|
},
|
|
func(id widget.TableCellID, obj fyne.CanvasObject) {
|
|
label := obj.(*widget.Label)
|
|
if id.Row == 0 {
|
|
switch id.Col {
|
|
case 0:
|
|
label.SetText("Tracker")
|
|
case 1:
|
|
label.SetText("State")
|
|
case 2:
|
|
label.SetText("Peers")
|
|
case 3:
|
|
label.SetText("Error")
|
|
}
|
|
return
|
|
}
|
|
|
|
ui.mu.RLock()
|
|
defer ui.mu.RUnlock()
|
|
tr := ui.trackers[id.Row-1]
|
|
switch id.Col {
|
|
case 0:
|
|
label.SetText(tr.URL)
|
|
case 1:
|
|
label.SetText(tr.State)
|
|
case 2:
|
|
label.SetText(strconv.Itoa(tr.PeerCount))
|
|
case 3:
|
|
label.SetText(tr.Error)
|
|
}
|
|
},
|
|
)
|
|
table.SetColumnWidth(0, 420)
|
|
table.SetColumnWidth(1, 90)
|
|
table.SetColumnWidth(2, 70)
|
|
table.SetColumnWidth(3, 420)
|
|
return table
|
|
}
|
|
|
|
func (ui *desktopUI) newPeersTable() *widget.Table {
|
|
table := widget.NewTable(
|
|
func() (int, int) {
|
|
ui.mu.RLock()
|
|
defer ui.mu.RUnlock()
|
|
return len(ui.peers) + 1, 3
|
|
},
|
|
func() fyne.CanvasObject {
|
|
return widget.NewLabel("")
|
|
},
|
|
func(id widget.TableCellID, obj fyne.CanvasObject) {
|
|
label := obj.(*widget.Label)
|
|
if id.Row == 0 {
|
|
switch id.Col {
|
|
case 0:
|
|
label.SetText("Address")
|
|
case 1:
|
|
label.SetText("Port")
|
|
case 2:
|
|
label.SetText("Source tracker")
|
|
}
|
|
return
|
|
}
|
|
|
|
ui.mu.RLock()
|
|
defer ui.mu.RUnlock()
|
|
peer := ui.peers[id.Row-1]
|
|
switch id.Col {
|
|
case 0:
|
|
label.SetText(peer.Address)
|
|
case 1:
|
|
label.SetText(strconv.Itoa(int(peer.Port)))
|
|
case 2:
|
|
label.SetText(peer.Source)
|
|
}
|
|
},
|
|
)
|
|
table.SetColumnWidth(0, 180)
|
|
table.SetColumnWidth(1, 80)
|
|
table.SetColumnWidth(2, 700)
|
|
return table
|
|
}
|
|
|
|
func (ui *desktopUI) newFilesTable() *widget.Table {
|
|
table := widget.NewTable(
|
|
func() (int, int) {
|
|
ui.mu.RLock()
|
|
defer ui.mu.RUnlock()
|
|
return len(ui.files) + 1, 2
|
|
},
|
|
func() fyne.CanvasObject {
|
|
return widget.NewLabel("")
|
|
},
|
|
func(id widget.TableCellID, obj fyne.CanvasObject) {
|
|
label := obj.(*widget.Label)
|
|
if id.Row == 0 {
|
|
switch id.Col {
|
|
case 0:
|
|
label.SetText("File")
|
|
case 1:
|
|
label.SetText("Size")
|
|
}
|
|
return
|
|
}
|
|
|
|
ui.mu.RLock()
|
|
defer ui.mu.RUnlock()
|
|
file := ui.files[id.Row-1]
|
|
switch id.Col {
|
|
case 0:
|
|
label.SetText(file.Path)
|
|
case 1:
|
|
label.SetText(fmt.Sprintf("%d", file.Length))
|
|
}
|
|
},
|
|
)
|
|
table.SetColumnWidth(0, 780)
|
|
table.SetColumnWidth(1, 120)
|
|
return table
|
|
}
|