Path: blob/main/internal/messages/composer/uploads.go
366 views
package composer12import (3"context"4"fmt"5"html"6"path/filepath"7"slices"8"strings"910"github.com/diamondburned/gotk4-adwaita/pkg/adw"11"github.com/diamondburned/gotk4/pkg/core/glib"12"github.com/diamondburned/gotk4/pkg/gio/v2"13"github.com/diamondburned/gotk4/pkg/gtk/v4"14"github.com/diamondburned/gotk4/pkg/pango"15"github.com/diamondburned/gotkit/app"16"github.com/diamondburned/gotkit/app/locale"17"github.com/diamondburned/gotkit/gtkutil/cssutil"18"github.com/dustin/go-humanize"19)2021const (22spoiledDisabledIcon = "visibility-symbolic"23spoiledEnabledIcon = "visibility-off-symbolic"24)2526func fileSpoilerIcon(file *File) string {27if file.IsSpoiler() {28return spoiledEnabledIcon29}30return spoiledDisabledIcon31}3233func mimeIsText(mime string) bool {34// How is utf8_string a valid MIME type? GTK, what the fuck?35return strings.HasPrefix(mime, "text") || mime == "utf8_string"36}3738// UploadTray is the tray holding files to be uploaded.39type UploadTray struct {40*gtk.Box41files []uploadFile42maxUploadSize int6443}4445type uploadFile struct {46gtk.Widgetter47box *gtk.CenterBox48bin *adw.BreakpointBin4950icon *gtk.Image51name *gtk.Label52size *gtk.Label53spoiler *gtk.ToggleButton54delete *gtk.Button5556file *File57}5859var uploadTrayCSS = cssutil.Applier("composer-upload-tray", `60.composer-upload-item {61margin: 0.25em 0.65em;62margin-top: 0;63}64.composer-upload-file-name {65font-size: 0.9em;66}67.composer-upload-file-icon {68margin: 0 0.5em;69margin-bottom: 1px;70}71.composer-upload-file-size {72font-size: 0.75em;73opacity: 0.85;74margin: 0 0.25em;75}76`)7778// NewUploadTray creates a new UploadTray.79func NewUploadTray() *UploadTray {80t := UploadTray{}81t.Box = gtk.NewBox(gtk.OrientationVertical, 0)82uploadTrayCSS(t.Box)83return &t84}8586// SetMaxUploadSize sets the maximum upload size for the tray.87// If the size is set to 0, it will not limit the upload size.88// If a file exceeds this size, it will not be added to the tray, and an alert89// will be shown to the user.90func (t *UploadTray) SetMaxUploadSize(size int64) {91t.maxUploadSize = size92}9394// AddFile adds a file into the tray.95func (t *UploadTray) AddFile(ctx context.Context, f *File) {96if t.maxUploadSize > 0 && f.Size > t.maxUploadSize {97// Ellipsize the file name if it is too long. Preserve the file98// extension for convenience.99shortName := f.Name100if len(shortName) > 50 {101shortName = f.Name[:45] + "…" + strings.TrimPrefix(filepath.Ext(f.Name), ".")102}103104alert := adw.NewAlertDialog(105locale.Get("File Too Large"),106fmt.Sprintf(107"%s\n%s",108locale.Sprintf(109"<i>%s</i> (%s) is larger than %s.",110html.EscapeString(shortName),111humanize.IBytes(uint64(f.Size)),112humanize.IBytes(uint64(t.maxUploadSize)),113),114locale.Get("Discord will not allow you to upload this file."),115))116alert.SetBodyUseMarkup(true)117alert.SetPreferWideLayout(true)118119alert.AddResponse("continue", locale.Get("Continue (dangerous)"))120alert.AddResponse("skip", locale.Get("Skip"))121alert.SetResponseAppearance("continue", adw.ResponseDestructive)122alert.SetResponseAppearance("skip", adw.ResponseDefault)123alert.SetDefaultResponse("skip")124alert.SetCloseResponse("skip")125// Disable the continue response by default unless users start126// complaining in our issues otherwise.127alert.SetResponseEnabled("continue", false)128129alert.Choose(ctx, app.WindowFromContext(ctx), func(res gio.AsyncResulter) {130response := alert.ChooseFinish(res)131switch response {132case "skip":133// do nothing134case "continue":135t.addFile(f)136}137})138139return140}141142t.addFile(f)143}144145func (t *UploadTray) addFile(f *File) {146u := uploadFile{file: f}147148u.icon = gtk.NewImageFromIconName(mimeIcon(f.Type))149u.icon.AddCSSClass("composer-upload-file-icon")150151u.name = gtk.NewLabel(f.Name)152u.name.AddCSSClass("composer-upload-file-name")153u.name.SetEllipsize(pango.EllipsizeEnd)154u.name.SetVExpand(true)155u.name.SetVAlign(gtk.AlignBaseline)156157u.size = gtk.NewLabel(humanize.IBytes(uint64(f.Size)))158u.size.AddCSSClass("composer-upload-file-size")159u.size.SetVisible(f.Size > 0)160u.size.SetVExpand(true)161u.size.SetVAlign(gtk.AlignBaseline)162163u.spoiler = gtk.NewToggleButton()164u.spoiler.AddCSSClass("composer-upload-toggle-spoiler")165u.spoiler.SetIconName(fileSpoilerIcon(f))166u.spoiler.SetHasFrame(false)167u.spoiler.SetTooltipText(locale.Get("Spoiler"))168169u.delete = gtk.NewButtonFromIconName("edit-clear-all-symbolic")170u.delete.AddCSSClass("composer-upload-delete")171u.delete.SetHasFrame(false)172u.delete.SetTooltipText(locale.Get("Remove File"))173174start := gtk.NewBox(gtk.OrientationHorizontal, 0)175start.Append(u.icon)176start.Append(u.name)177start.Append(u.size)178179end := gtk.NewBox(gtk.OrientationHorizontal, 0)180end.Append(u.spoiler)181end.Append(u.delete)182183u.box = gtk.NewCenterBox()184u.box.AddCSSClass("composer-upload-item")185u.box.SetHExpand(true)186u.box.SetStartWidget(start)187u.box.SetEndWidget(end)188189smallBreakpoint := adw.NewBreakpoint(adw.NewBreakpointConditionLength(190adw.BreakpointConditionMaxWidth,191275, adw.LengthUnitSp,192))193194// Hide the size and icon on small screens.195smallBreakpoint.AddSetter(u.size, "visible", false)196smallBreakpoint.AddSetter(u.icon, "visible", false)197198u.bin = adw.NewBreakpointBin()199u.bin.SetSizeRequest(100, 1)200u.bin.AddBreakpoint(smallBreakpoint)201u.bin.SetChild(u.box)202203u.Widgetter = u.bin204205t.Box.Append(u)206t.files = append(t.files, u)207208u.delete.ConnectClicked(func() {209t.files = slices.DeleteFunc(t.files, func(searching uploadFile) bool {210if glib.ObjectEq(searching, u) {211t.Box.Remove(u)212return true213}214return false215})216})217218u.spoiler.ConnectClicked(func() {219spoiler := !f.IsSpoiler()220f.SetSpoiler(spoiler)221222u.name.SetText(f.Name)223u.spoiler.SetActive(spoiler)224u.spoiler.SetIconName(fileSpoilerIcon(u.file))225})226}227228func mimeIcon(mime string) string {229if mime == "" {230return "text-x-generic-symbolic"231}232233switch strings.SplitN(mime, "/", 2)[0] {234case "image":235return "image-x-generic-symbolic"236case "video":237return "video-x-generic-symbolic"238case "audio":239return "audio-x-generic-symbolic"240default:241return "text-x-generic-symbolic"242}243}244245// Files returns the list of files in the tray.246func (t *UploadTray) Files() []*File {247files := make([]*File, len(t.files))248for i, file := range t.files {249files[i] = file.file250}251return files252}253254// Clear clears the tray and returns the list of paths that it held.255func (t *UploadTray) Clear() []*File {256files := make([]*File, len(t.files))257for i, file := range t.files {258files[i] = file.file259t.Remove(file)260}261t.files = nil262return files263}264265266