1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
|
package main
import (
"fmt"
"os"
"path/filepath"
"strings"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/widget"
"nymdrop/internal/reader"
)
// submissionEntry is one row in the submissions list: enough to render the
// row (label) and to load the full body on demand (path) without keeping
// every decrypted body in memory at once.
type submissionEntry struct {
label string
path string
}
// runGUI is the non-technical front-end for nymdrop-reader: a journalist
// running this from a USB stick should never need a terminal. It drives the
// same reader.Instance the CLI uses, through the same Hooks contract, so
// there is exactly one implementation of the receive/decrypt/save logic.
func runGUI(cfg reader.Config) {
a := app.NewWithID("art.virebent.nymdrop-reader")
w := a.NewWindow("NymDrop Reader")
w.Resize(fyne.NewSize(900, 560))
statusLabel := widget.NewLabel("Starting...")
statusLabel.Wrapping = fyne.TextWrapWord
addrEntry := widget.NewEntry()
addrEntry.Disable()
addrEntry.SetPlaceHolder("Nym inbox address will appear here once connected")
outDirLabel := widget.NewLabel("")
var submissions []submissionEntry
content := widget.NewMultiLineEntry()
content.Wrapping = fyne.TextWrapWord
content.Disable()
list := widget.NewList(
func() int { return len(submissions) },
func() fyne.CanvasObject { return widget.NewLabel("template") },
func(id widget.ListItemID, obj fyne.CanvasObject) {
obj.(*widget.Label).SetText(submissions[id].label)
},
)
list.OnSelected = func(id widget.ListItemID) {
if id < 0 || id >= len(submissions) {
return
}
data, err := os.ReadFile(submissions[id].path)
if err != nil {
content.SetText(fmt.Sprintf("error reading %s: %v", submissions[id].path, err))
return
}
content.SetText(string(data))
}
hooks := reader.Hooks{
Log: func(format string, args ...any) {
msg := fmt.Sprintf(format, args...)
fyne.Do(func() { statusLabel.SetText(msg) })
},
SelfAddress: func(addr string) {
fyne.Do(func() {
addrEntry.SetText(addr)
statusLabel.SetText("Connected -- waiting for submissions.")
})
},
Submission: func(path, preview string) {
ts := strings.TrimSuffix(filepath.Base(path), ".txt")
oneLine := strings.ReplaceAll(preview, "\n", " ")
if len(oneLine) > 80 {
oneLine = oneLine[:80] + "..."
}
entry := submissionEntry{label: ts + " " + oneLine, path: path}
fyne.Do(func() {
// Newest first: a journalist checking the box wants the
// latest tip on top, not scrolled to the bottom.
submissions = append([]submissionEntry{entry}, submissions...)
list.Refresh()
statusLabel.SetText(fmt.Sprintf("%d submission(s) received.", len(submissions)))
})
},
Attachment: func(path string, size int) {
fyne.Do(func() {
statusLabel.SetText(fmt.Sprintf("attachment saved: %s (%d bytes)", path, size))
})
},
Error: func(err error) {
fyne.Do(func() { statusLabel.SetText("error: " + err.Error()) })
},
}
in, err := reader.Start(cfg, hooks)
if err != nil {
statusLabel.SetText("failed to start: " + err.Error())
} else {
outDirLabel.SetText("Submissions saved to: " + in.OutDir())
go in.ReceiveLoop()
}
top := container.NewVBox(
widget.NewLabelWithStyle("NymDrop Reader", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
statusLabel,
widget.NewForm(widget.NewFormItem("Nym inbox address", addrEntry)),
outDirLabel,
)
split := container.NewHSplit(
container.NewBorder(widget.NewLabel("Submissions"), nil, nil, nil, list),
container.NewBorder(widget.NewLabel("Content"), nil, nil, nil, content),
)
split.Offset = 0.35
w.SetContent(container.NewBorder(top, nil, nil, nil, split))
w.SetCloseIntercept(func() {
if in != nil {
in.Close()
}
w.Close()
})
w.ShowAndRun()
}
|