Files
MujicaUI/chat/file_message.go
T

125 lines
3.8 KiB
Go

package chat
import (
"fmt"
"log/slog"
"path"
"strings"
"github.com/egoist/mygo/ui"
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/core"
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/icons"
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/internal"
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/theme"
)
// FileMessageOptions describe a file sent in a chat.
type FileMessageOptions struct {
// Name is the file name; required.
Name string
// Size is the length in bytes.
Size int64
// Type is the MIME type; empty guesses from the name's extension.
Type string
}
// FileMessageResult is a FileMessage's element and this frame's actions.
type FileMessageResult struct {
Element *ui.Element
opened, downloaded bool
}
// Opened reports a click or Enter on the file.
func (r FileMessageResult) Opened() bool { return r.opened }
// Downloaded reports a click on the download button.
func (r FileMessageResult) Downloaded() bool { return r.downloaded }
// FileMessage shows a file's type icon, name and size, with open and download actions.
func FileMessage(c *ui.Context, opts FileMessageOptions) FileMessageResult {
if opts.Name == "" {
panic("mujica: FileMessage needs a Name")
}
if opts.Size < 0 {
panic("mujica: FileMessage Size must not be negative")
}
k := core.Tokens(c)
u := core.Density(c).Unit()
size := fileMessageSize(c, opts.Size)
var r FileMessageResult
r.Element = ui.Row(c).Gap(u).AlignItems(ui.Center).Padding(u, u, u, u*2).MaxWidth(360).
Radius(theme.CardRadius).Background(k.Surface).Border(theme.BorderWidth, k.Border)
disabled := r.Element.IsDisabled()
r.Element.Children(func() {
open := ui.ButtonBase(c).Grow(1).MinWidth(0).Gap(u * 3).Padding(u).Justify(ui.Start).Radius(theme.ControlRadius).
Label(fmt.Sprintf(core.Msg(c, "file_message.open"), opts.Name, size))
internal.Ring(open, k.Focus, theme.ControlRadius)
if open.Hovered() {
open.Background(k.SurfaceHover)
}
open.Children(func() {
ui.Icon(c, icons.Must(fileMessageIcon(opts.Name, opts.Type))).Size(24, 24).Shrink(0).TextColor(k.AccentText)
ui.Column(c).MinWidth(0).Grow(1).Children(func() {
ui.Text(c, opts.Name).SingleLine().FontWeight(600)
ui.Text(c, size).FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).SingleLine()
})
})
if open.Clicked() && !disabled {
r.opened = true
slog.Debug("mujica: file_message open")
}
if core.IconAction(c, icons.Must("download"), fmt.Sprintf(core.Msg(c, "file_message.download"), opts.Name)).Clicked() && !disabled {
r.downloaded = true
slog.Debug("mujica: file_message download")
}
})
return r
}
// fileMessageSize formats n bytes in binary units.
func fileMessageSize(c *ui.Context, n int64) string {
units := []string{"B", "KB", "MB", "GB", "TB"}
v, i := float64(n), 0
for v >= 1024 && i < len(units)-1 {
v /= 1024
i++
}
if i == 0 {
return fmt.Sprintf(core.Msg(c, "file_message.bytes"), n)
}
return fmt.Sprintf("%.1f %s", v, units[i])
}
// fileMessageKinds maps extensions to type icons.
var fileMessageKinds = func() map[string]string {
m := map[string]string{}
for icon, exts := range map[string]string{
"image": ".png .jpg .jpeg .gif .webp .svg",
"file-archive": ".zip .tar .gz .tgz .7z .rar",
"file-code": ".go .js .ts .py .json .sh .sql .html .css .rs .java .c .h",
"file-text": ".txt .md .pdf .doc .docx .rtf",
} {
for _, e := range strings.Fields(exts) {
m[e] = icon
}
}
return m
}()
// fileMessageIcon picks a type icon from the MIME type, else the extension.
func fileMessageIcon(name, mime string) string {
switch {
case strings.HasPrefix(mime, "image/"):
return "image"
case strings.Contains(mime, "zip"):
return "file-archive"
case strings.HasPrefix(mime, "text/"):
return "file-text"
}
if icon, ok := fileMessageKinds[strings.ToLower(path.Ext(name))]; ok {
return icon
}
return "file"
}