188 lines
6.1 KiB
Go
188 lines
6.1 KiB
Go
package mujica
|
|
|
|
import (
|
|
"cmp"
|
|
"fmt"
|
|
"log/slog"
|
|
|
|
"github.com/egoist/mygo/ui"
|
|
|
|
"mujicaui/icons"
|
|
"mujicaui/internal"
|
|
"mujicaui/theme"
|
|
)
|
|
|
|
// Notification is one in-app message. The caller owns and persists the list.
|
|
type Notification struct {
|
|
// ID is required and unique within the list.
|
|
ID string
|
|
Title string
|
|
Body string
|
|
// Time shows as given, as "5 min ago".
|
|
Time string
|
|
Read bool
|
|
// Action labels a button; empty shows none.
|
|
Action string
|
|
}
|
|
|
|
// NotificationCenterOptions configure a NotificationCenter.
|
|
type NotificationCenterOptions struct {
|
|
// Title uses the built-in "Notifications" when empty.
|
|
Title string
|
|
}
|
|
|
|
// NotificationCenterResult reports the frame's events by notification ID.
|
|
type NotificationCenterResult struct {
|
|
Element *ui.Element
|
|
Action string
|
|
// Toggled changed its Read state; Opened was clicked and is now read.
|
|
Toggled string
|
|
Opened string
|
|
Removed string
|
|
// MarkedAllRead and ClearedAll report the header buttons.
|
|
MarkedAllRead bool
|
|
ClearedAll bool
|
|
}
|
|
|
|
// NotificationCenter lists *items and edits them in place: read, unread, remove, clear.
|
|
func NotificationCenter(c *ui.Context, items *[]Notification, opts NotificationCenterOptions) NotificationCenterResult {
|
|
if items == nil {
|
|
panic("mujica: NotificationCenter needs items")
|
|
}
|
|
seen := map[string]bool{}
|
|
unread := 0
|
|
for _, n := range *items {
|
|
if n.ID == "" || seen[n.ID] {
|
|
panic(fmt.Sprintf("mujica: notification ID %q empty or repeated", n.ID))
|
|
}
|
|
seen[n.ID] = true
|
|
if !n.Read {
|
|
unread++
|
|
}
|
|
}
|
|
k := Tokens(c)
|
|
u := Density(c).Unit()
|
|
title := cmp.Or(opts.Title, msg(c, "notification_center.title"))
|
|
var res NotificationCenterResult
|
|
panel := ui.Column(c).FillWidth().Shrink(1).MinHeight(0).Radius(theme.CardRadius).Background(k.Surface).Border(theme.BorderWidth, k.Border).
|
|
Role(ui.RoleGroup).Label(title).Clip()
|
|
panel.Children(func() {
|
|
ui.Row(c).AlignItems(ui.Center).Gap(u*2).Padding(u*3, u*3, u*3, u*4).Children(func() {
|
|
ui.Text(c, title).Font(theme.SerifFont).FontSize(FontSize(c, theme.H3Size)).TextColor(k.Text)
|
|
if unread > 0 {
|
|
ui.Text(c, fmt.Sprintf(msg(c, "notification_center.unread"), unread)).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.AccentText)
|
|
}
|
|
ui.Spacer(c)
|
|
if Button(c, msg(c, "notification_center.mark_all_read"), ButtonOptions{Variant: Ghost, Disabled: unread == 0}).Clicked() {
|
|
for i := range *items {
|
|
(*items)[i].Read = true
|
|
}
|
|
res.MarkedAllRead = true
|
|
slog.Debug("mujica: notification_center mark all read", "count", unread)
|
|
}
|
|
if Button(c, msg(c, "notification_center.clear_all"), ButtonOptions{Variant: Ghost, Disabled: len(*items) == 0}).Clicked() {
|
|
slog.Debug("mujica: notification_center clear all", "count", len(*items))
|
|
*items = (*items)[:0]
|
|
res.ClearedAll = true
|
|
}
|
|
})
|
|
// Ornament hairline under the panel title.
|
|
ui.Box(c).FillWidth().Height(1).Background(k.Ornament).Role(ui.RoleNone)
|
|
if len(*items) == 0 && !res.ClearedAll {
|
|
EmptyState(c, EmptyStateOptions{
|
|
Title: msg(c, "notification_center.empty"),
|
|
Description: msg(c, "notification_center.empty_hint"),
|
|
Icon: icons.Must("bell"),
|
|
})
|
|
return
|
|
}
|
|
list := ui.Scroll(c).Shrink(1).FillWidth().Role(ui.RoleList).FocusGroup(ui.Vertical | ui.Horizontal)
|
|
list.Dividers(theme.BorderWidth, k.Border)
|
|
list.Children(func() {
|
|
for i := range *items {
|
|
notificationRow(c, k, u, &(*items)[i], &res)
|
|
}
|
|
})
|
|
})
|
|
if res.Removed != "" {
|
|
for i, n := range *items {
|
|
if n.ID == res.Removed {
|
|
*items = append((*items)[:i], (*items)[i+1:]...)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
res.Element = panel
|
|
return res
|
|
}
|
|
|
|
func notificationRow(c *ui.Context, k theme.Tokens, u float32, n *Notification, res *NotificationCenterResult) {
|
|
name := n.Title
|
|
if !n.Read {
|
|
name = msg(c, "notification_center.unread_mark") + ": " + n.Title
|
|
}
|
|
row := ui.Row(c).Key(n.ID).FillWidth().AlignItems(ui.Start).Gap(u*2).Padding(u*3, u*3, u*3, u*2).
|
|
Role(ui.RoleListItem)
|
|
if row.Hovered() {
|
|
row.Background(k.SurfaceHover)
|
|
}
|
|
row.Children(func() {
|
|
// Unread mark: a diamond and bold title, not color alone.
|
|
ui.Box(c).Size(12, 20).Shrink(0).Role(ui.RoleNone).Draw(func(p *ui.Painter, r ui.Rect) {
|
|
if !n.Read {
|
|
p.FillPath(internal.Diamond(r.X+r.W/2, r.Y+r.H/2, 8), k.AccentText)
|
|
}
|
|
})
|
|
open := ui.ButtonBase(c).MinHeight(theme.MinHitSize).Grow(1).Shrink(1).Column().AlignItems(ui.Start).Justify(ui.Start).Gap(u).
|
|
Radius(theme.ControlRadius).Label(name)
|
|
internal.Ring(open, k.Focus, theme.ControlRadius)
|
|
open.Children(func() {
|
|
ui.Row(c).FillWidth().Gap(u * 2).Children(func() {
|
|
t := ui.Text(c, n.Title).TextColor(k.Text).Grow(1).Shrink(1)
|
|
if !n.Read {
|
|
t.Bold()
|
|
}
|
|
if n.Time != "" {
|
|
ui.Text(c, n.Time).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).SingleLine()
|
|
}
|
|
})
|
|
if n.Body != "" {
|
|
ui.Text(c, n.Body).TextColor(k.TextMuted).LineHeight(theme.LineHeight)
|
|
}
|
|
})
|
|
if open.Clicked() && !n.Read {
|
|
n.Read, res.Opened = true, n.ID
|
|
slog.Debug("mujica: notification_center opened", "id", n.ID)
|
|
}
|
|
ui.Row(c).Gap(u).AlignItems(ui.Center).Children(func() {
|
|
if n.Action != "" && Button(c, n.Action, ButtonOptions{Variant: Outline}).Clicked() {
|
|
res.Action = n.ID
|
|
slog.Debug("mujica: notification_center action", "id", n.ID)
|
|
}
|
|
toggle, icon := "notification_center.mark_read", icons.Must("check")
|
|
if n.Read {
|
|
toggle, icon = "notification_center.mark_unread", icons.Must("circle")
|
|
}
|
|
if notificationIconButton(c, k, u, icon, msg(c, toggle)).Clicked() {
|
|
n.Read, res.Toggled = !n.Read, n.ID
|
|
slog.Debug("mujica: notification_center toggled", "id", n.ID, "read", n.Read)
|
|
}
|
|
if notificationIconButton(c, k, u, icons.Must("x"), msg(c, "notification_center.clear")).Clicked() {
|
|
res.Removed = n.ID
|
|
}
|
|
})
|
|
})
|
|
if row.Shortcut(0, ui.KeyDelete) || row.Shortcut(0, ui.KeyBackspace) {
|
|
res.Removed = n.ID
|
|
}
|
|
if res.Removed == n.ID {
|
|
slog.Debug("mujica: notification_center removed", "id", n.ID)
|
|
}
|
|
}
|
|
|
|
func notificationIconButton(c *ui.Context, k theme.Tokens, u float32, icon *ui.SVG, label string) *ui.Element {
|
|
b := Button(c, "", ButtonOptions{Variant: Ghost, Icon: icon, Label: label})
|
|
b.TextColor(k.TextMuted).Padding(0, u).Tooltip(label)
|
|
return b
|
|
}
|