Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
diamondburned
GitHub Repository: diamondburned/gtkcord4
Path: blob/main/internal/messages/composer/uploads.go
366 views
1
package composer
2
3
import (
4
"context"
5
"fmt"
6
"html"
7
"path/filepath"
8
"slices"
9
"strings"
10
11
"github.com/diamondburned/gotk4-adwaita/pkg/adw"
12
"github.com/diamondburned/gotk4/pkg/core/glib"
13
"github.com/diamondburned/gotk4/pkg/gio/v2"
14
"github.com/diamondburned/gotk4/pkg/gtk/v4"
15
"github.com/diamondburned/gotk4/pkg/pango"
16
"github.com/diamondburned/gotkit/app"
17
"github.com/diamondburned/gotkit/app/locale"
18
"github.com/diamondburned/gotkit/gtkutil/cssutil"
19
"github.com/dustin/go-humanize"
20
)
21
22
const (
23
spoiledDisabledIcon = "visibility-symbolic"
24
spoiledEnabledIcon = "visibility-off-symbolic"
25
)
26
27
func fileSpoilerIcon(file *File) string {
28
if file.IsSpoiler() {
29
return spoiledEnabledIcon
30
}
31
return spoiledDisabledIcon
32
}
33
34
func mimeIsText(mime string) bool {
35
// How is utf8_string a valid MIME type? GTK, what the fuck?
36
return strings.HasPrefix(mime, "text") || mime == "utf8_string"
37
}
38
39
// UploadTray is the tray holding files to be uploaded.
40
type UploadTray struct {
41
*gtk.Box
42
files []uploadFile
43
maxUploadSize int64
44
}
45
46
type uploadFile struct {
47
gtk.Widgetter
48
box *gtk.CenterBox
49
bin *adw.BreakpointBin
50
51
icon *gtk.Image
52
name *gtk.Label
53
size *gtk.Label
54
spoiler *gtk.ToggleButton
55
delete *gtk.Button
56
57
file *File
58
}
59
60
var uploadTrayCSS = cssutil.Applier("composer-upload-tray", `
61
.composer-upload-item {
62
margin: 0.25em 0.65em;
63
margin-top: 0;
64
}
65
.composer-upload-file-name {
66
font-size: 0.9em;
67
}
68
.composer-upload-file-icon {
69
margin: 0 0.5em;
70
margin-bottom: 1px;
71
}
72
.composer-upload-file-size {
73
font-size: 0.75em;
74
opacity: 0.85;
75
margin: 0 0.25em;
76
}
77
`)
78
79
// NewUploadTray creates a new UploadTray.
80
func NewUploadTray() *UploadTray {
81
t := UploadTray{}
82
t.Box = gtk.NewBox(gtk.OrientationVertical, 0)
83
uploadTrayCSS(t.Box)
84
return &t
85
}
86
87
// SetMaxUploadSize sets the maximum upload size for the tray.
88
// If the size is set to 0, it will not limit the upload size.
89
// If a file exceeds this size, it will not be added to the tray, and an alert
90
// will be shown to the user.
91
func (t *UploadTray) SetMaxUploadSize(size int64) {
92
t.maxUploadSize = size
93
}
94
95
// AddFile adds a file into the tray.
96
func (t *UploadTray) AddFile(ctx context.Context, f *File) {
97
if t.maxUploadSize > 0 && f.Size > t.maxUploadSize {
98
// Ellipsize the file name if it is too long. Preserve the file
99
// extension for convenience.
100
shortName := f.Name
101
if len(shortName) > 50 {
102
shortName = f.Name[:45] + "…" + strings.TrimPrefix(filepath.Ext(f.Name), ".")
103
}
104
105
alert := adw.NewAlertDialog(
106
locale.Get("File Too Large"),
107
fmt.Sprintf(
108
"%s\n%s",
109
locale.Sprintf(
110
"<i>%s</i> (%s) is larger than %s.",
111
html.EscapeString(shortName),
112
humanize.IBytes(uint64(f.Size)),
113
humanize.IBytes(uint64(t.maxUploadSize)),
114
),
115
locale.Get("Discord will not allow you to upload this file."),
116
))
117
alert.SetBodyUseMarkup(true)
118
alert.SetPreferWideLayout(true)
119
120
alert.AddResponse("continue", locale.Get("Continue (dangerous)"))
121
alert.AddResponse("skip", locale.Get("Skip"))
122
alert.SetResponseAppearance("continue", adw.ResponseDestructive)
123
alert.SetResponseAppearance("skip", adw.ResponseDefault)
124
alert.SetDefaultResponse("skip")
125
alert.SetCloseResponse("skip")
126
// Disable the continue response by default unless users start
127
// complaining in our issues otherwise.
128
alert.SetResponseEnabled("continue", false)
129
130
alert.Choose(ctx, app.WindowFromContext(ctx), func(res gio.AsyncResulter) {
131
response := alert.ChooseFinish(res)
132
switch response {
133
case "skip":
134
// do nothing
135
case "continue":
136
t.addFile(f)
137
}
138
})
139
140
return
141
}
142
143
t.addFile(f)
144
}
145
146
func (t *UploadTray) addFile(f *File) {
147
u := uploadFile{file: f}
148
149
u.icon = gtk.NewImageFromIconName(mimeIcon(f.Type))
150
u.icon.AddCSSClass("composer-upload-file-icon")
151
152
u.name = gtk.NewLabel(f.Name)
153
u.name.AddCSSClass("composer-upload-file-name")
154
u.name.SetEllipsize(pango.EllipsizeEnd)
155
u.name.SetVExpand(true)
156
u.name.SetVAlign(gtk.AlignBaseline)
157
158
u.size = gtk.NewLabel(humanize.IBytes(uint64(f.Size)))
159
u.size.AddCSSClass("composer-upload-file-size")
160
u.size.SetVisible(f.Size > 0)
161
u.size.SetVExpand(true)
162
u.size.SetVAlign(gtk.AlignBaseline)
163
164
u.spoiler = gtk.NewToggleButton()
165
u.spoiler.AddCSSClass("composer-upload-toggle-spoiler")
166
u.spoiler.SetIconName(fileSpoilerIcon(f))
167
u.spoiler.SetHasFrame(false)
168
u.spoiler.SetTooltipText(locale.Get("Spoiler"))
169
170
u.delete = gtk.NewButtonFromIconName("edit-clear-all-symbolic")
171
u.delete.AddCSSClass("composer-upload-delete")
172
u.delete.SetHasFrame(false)
173
u.delete.SetTooltipText(locale.Get("Remove File"))
174
175
start := gtk.NewBox(gtk.OrientationHorizontal, 0)
176
start.Append(u.icon)
177
start.Append(u.name)
178
start.Append(u.size)
179
180
end := gtk.NewBox(gtk.OrientationHorizontal, 0)
181
end.Append(u.spoiler)
182
end.Append(u.delete)
183
184
u.box = gtk.NewCenterBox()
185
u.box.AddCSSClass("composer-upload-item")
186
u.box.SetHExpand(true)
187
u.box.SetStartWidget(start)
188
u.box.SetEndWidget(end)
189
190
smallBreakpoint := adw.NewBreakpoint(adw.NewBreakpointConditionLength(
191
adw.BreakpointConditionMaxWidth,
192
275, adw.LengthUnitSp,
193
))
194
195
// Hide the size and icon on small screens.
196
smallBreakpoint.AddSetter(u.size, "visible", false)
197
smallBreakpoint.AddSetter(u.icon, "visible", false)
198
199
u.bin = adw.NewBreakpointBin()
200
u.bin.SetSizeRequest(100, 1)
201
u.bin.AddBreakpoint(smallBreakpoint)
202
u.bin.SetChild(u.box)
203
204
u.Widgetter = u.bin
205
206
t.Box.Append(u)
207
t.files = append(t.files, u)
208
209
u.delete.ConnectClicked(func() {
210
t.files = slices.DeleteFunc(t.files, func(searching uploadFile) bool {
211
if glib.ObjectEq(searching, u) {
212
t.Box.Remove(u)
213
return true
214
}
215
return false
216
})
217
})
218
219
u.spoiler.ConnectClicked(func() {
220
spoiler := !f.IsSpoiler()
221
f.SetSpoiler(spoiler)
222
223
u.name.SetText(f.Name)
224
u.spoiler.SetActive(spoiler)
225
u.spoiler.SetIconName(fileSpoilerIcon(u.file))
226
})
227
}
228
229
func mimeIcon(mime string) string {
230
if mime == "" {
231
return "text-x-generic-symbolic"
232
}
233
234
switch strings.SplitN(mime, "/", 2)[0] {
235
case "image":
236
return "image-x-generic-symbolic"
237
case "video":
238
return "video-x-generic-symbolic"
239
case "audio":
240
return "audio-x-generic-symbolic"
241
default:
242
return "text-x-generic-symbolic"
243
}
244
}
245
246
// Files returns the list of files in the tray.
247
func (t *UploadTray) Files() []*File {
248
files := make([]*File, len(t.files))
249
for i, file := range t.files {
250
files[i] = file.file
251
}
252
return files
253
}
254
255
// Clear clears the tray and returns the list of paths that it held.
256
func (t *UploadTray) Clear() []*File {
257
files := make([]*File, len(t.files))
258
for i, file := range t.files {
259
files[i] = file.file
260
t.Remove(file)
261
}
262
t.files = nil
263
return files
264
}
265
266