UI: animasi dan branding tambahan

- Garis aksen gradien berdenyut terus (indigo <-> cyan)
- Kartu shortcut: transisi warna halus saat disorot mouse (hoverCard)
- Kartu baru berkedip warna brand saat daftar dimuat / setelah install
- Logo LazyRun + brand mark di dialog, footer 'LazyRun vX · oxpoe'
- Thumbnail shortcut bersudut membulat
- Versi logo fade-in dibuang: canvas.Image.Translucency tidak ter-render di Fyne 2.8
This commit is contained in:
oxpoe committed 2026-10-13 08:00:12 +07:00
1 parent b12efefd6a
commit 70534bf6e2
6 files changed
+131 -16

No files matched your search

+87
View File
@@ -0,0 +1,87 @@
package ui
import (
"image/color"
"time"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/driver/desktop"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
)
// Animasi di sini sengaja pendek dan tidak menyentuh posisi/ukuran objek,
// supaya tidak bertabrakan dengan layout container.
// pulseGradient membuat garis aksen bergerak bolak-balik antara indigo dan cyan.
func pulseGradient(bar *canvas.LinearGradient) {
anim := canvas.NewColorRGBAAnimation(brandIndigo, brandCyan, 3*time.Second, func(c color.Color) {
bar.EndColor = c
canvas.Refresh(bar)
})
anim.AutoReverse = true
anim.RepeatCount = fyne.AnimationRepeatForever
anim.Start()
}
// hoverCard adalah kartu yang warnanya berubah halus saat disorot mouse,
// dan bisa dipakai untuk animasi kedatangan baris baru.
type hoverCard struct {
widget.BaseWidget
bg *canvas.Rectangle
content fyne.CanvasObject
hover bool
}
func newHoverCard(content fyne.CanvasObject) *hoverCard {
c := &hoverCard{
bg: canvas.NewRectangle(theme.Color(colorNameCard)),
content: content,
}
c.bg.CornerRadius = 12
c.ExtendBaseWidget(c)
return c
}
func (c *hoverCard) CreateRenderer() fyne.WidgetRenderer {
return widget.NewSimpleRenderer(container.NewStack(c.bg, container.NewPadded(c.content)))
}
func (c *hoverCard) MouseIn(*desktop.MouseEvent) { c.setHover(true) }
func (c *hoverCard) MouseMoved(*desktop.MouseEvent) {}
func (c *hoverCard) MouseOut() { c.setHover(false) }
func (c *hoverCard) setHover(on bool) {
if c.hover == on {
return
}
c.hover = on
c.animate(theme.Color(pick(on, colorNameCardHover, colorNameCard)), 150*time.Millisecond)
}
// flash membuat kartu berkedip warna brand sebentar (baris baru muncul).
func (c *hoverCard) flash(delay time.Duration) {
start := func() { c.animate(theme.Color(colorNameCard), 450*time.Millisecond) }
if delay <= 0 {
c.bg.FillColor = theme.Color(colorNameChipMenu)
start()
return
}
time.AfterFunc(delay, func() {
fyne.Do(func() {
c.bg.FillColor = theme.Color(colorNameChipMenu)
start()
})
})
}
// animate memindahkan warna latar kartu secara halus dari warna sekarang.
func (c *hoverCard) animate(goal color.Color, d time.Duration) {
from := c.bg.FillColor
canvas.NewColorRGBAAnimation(from, goal, d, func(col color.Color) {
c.bg.FillColor = col
canvas.Refresh(c.bg)
}).Start()
}
+1 -1
View File
@@ -96,7 +96,7 @@ func (ui *app) showExecutableDialog() {
menu,
desktop,
muted(T("exec_hint")),
container.NewHBox(spacer(),
container.NewHBox(logoBox(newLogo(), 20), spacer(),
widget.NewButton(T("cancel"), func() { d.Hide() }),
install),
)
+1 -1
View File
@@ -102,7 +102,7 @@ func (ui *app) showWebAppDialog() {
menu,
desktop,
muted(T("web_hint")),
container.NewHBox(spacer(),
container.NewHBox(logoBox(newLogo(), 20), spacer(),
widget.NewButton(T("cancel"), func() { d.Hide() }),
install),
)
+30 -10
View File
@@ -6,6 +6,7 @@ import (
"os/exec"
"path/filepath"
"strconv"
"time"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
@@ -26,6 +27,7 @@ type app struct {
store *store.Store
browsers []browser.Browser
rows *fyne.Container
cards map[string]*hoverCard
count *canvas.Text // angka jumlah shortcut di header daftar
version string
}
@@ -34,6 +36,7 @@ type app struct {
func Run(a fyne.App, version string) {
ui, loadErr := build(a, version)
ui.win.Show()
ui.flashCards()
if loadErr != nil {
dialog.ShowError(loadErr, ui.win)
}
@@ -61,7 +64,7 @@ func build(a fyne.App, version string) (*app, error) {
addExec.Importance = widget.HighImportance
addWeb := widget.NewButtonWithIcon(T("btn_webapp"), theme.ContentAddIcon(), ui.showWebAppDialog)
leftHeader := container.NewHBox(logoImage(40), container.NewVBox(
leftHeader := container.NewHBox(logoBox(newLogo(), 40), container.NewVBox(
heading("LazyRun"),
caption(T("tagline")),
))
@@ -75,10 +78,12 @@ func build(a fyne.App, version string) (*app, error) {
footer := container.NewBorder(
widget.NewSeparator(), nil,
container.NewHBox(update, github, donate),
caption("v"+version),
caption("LazyRun v"+version+" · oxpoe"),
)
top := container.NewVBox(header, accentBar(),
accent := accentBar()
pulseGradient(accent)
top := container.NewVBox(header, accent,
container.NewHBox(sectionLabel(T("installed")), countChip))
body := container.NewBorder(
top,
@@ -106,12 +111,15 @@ func (ui *app) reload() {
if len(ui.store.Items) == 0 {
ui.rows.Add(emptyState())
}
ui.cards = map[string]*hoverCard{}
for _, sc := range ui.store.Items {
ui.rows.Add(ui.row(sc))
row := ui.row(sc)
ui.rows.Add(row)
ui.cards[sc.Slug] = row
}
}
func (ui *app) row(sc store.Shortcut) fyne.CanvasObject {
func (ui *app) row(sc store.Shortcut) *hoverCard {
info := container.NewVBox(
subheading(sc.Name),
caption(metaText(sc)),
@@ -137,11 +145,9 @@ func (ui *app) row(sc store.Shortcut) fyne.CanvasObject {
}))
actions.Add(iconButton(theme.DeleteIcon(), func() { ui.confirmDelete(sc) }))
return container.NewVBox(
card(container.NewBorder(nil, nil,
return newHoverCard(container.NewBorder(nil, nil,
container.NewHBox(thumb(sc.IconPath, fallbackIcon(sc)), info),
actions)),
)
actions))
}
// metaText menampilkan tipe shortcut dan penanda singkatnya.
@@ -169,12 +175,23 @@ func emptyState() fyne.CanvasObject {
hint := muted(T("empty_hint"))
hint.Alignment = fyne.TextAlignCenter
return container.NewCenter(container.NewVBox(
container.NewCenter(logoImage(88)),
container.NewCenter(logoBox(newLogo(), 88)),
subheading(T("empty_title")),
hint,
))
}
// flashCards memberi kilatan berurutan saat daftar dimuat/diubah.
func (ui *app) flashCards() {
for i, sc := range ui.store.Items {
card, ok := ui.cards[sc.Slug]
if !ok {
continue
}
card.flash(time.Duration(min(i, 6)) * 70 * time.Millisecond)
}
}
func (ui *app) confirmDelete(sc store.Shortcut) {
content := container.NewVBox(widget.NewLabel(fmt.Sprintf(T("confirm_delete_msg"), sc.Name)))
@@ -231,6 +248,9 @@ func (ui *app) added(sc store.Shortcut, err error, closeDialog func()) {
}
closeDialog()
ui.reload()
if card, ok := ui.cards[sc.Slug]; ok {
card.flash(0)
}
dialog.ShowInformation(T("success_title"), fmt.Sprintf(T("success_msg"), sc.Name), ui.win)
}
+9 -4
View File
@@ -76,7 +76,7 @@ func chipLabel(text string, bg fyne.ThemeColorName) (fyne.CanvasObject, *canvas.
}
// accentBar adalah garis gradien brand di bawah header.
func accentBar() fyne.CanvasObject {
func accentBar() *canvas.LinearGradient {
bar := canvas.NewLinearGradient(brandIndigo, brandCyan, 90)
bar.SetMinSize(fyne.NewSize(0, 3))
return bar
@@ -101,15 +101,20 @@ func thumb(path string, fallback fyne.Resource) fyne.CanvasObject {
image = canvas.NewImageFromResource(theme.NewThemedResource(fallback))
}
image.FillMode = canvas.ImageFillContain
image.CornerRadius = 8
image.SetMinSize(fyne.NewSize(36, 36))
return container.NewGridWrap(fyne.NewSize(36, 36), image)
}
// logoImage menampilkan ikon LazyRun pada ukuran tertentu.
func logoImage(size float32) fyne.CanvasObject {
// newLogo mengembalikan ikon LazyRun (dipakai untuk keperluan animasi).
func newLogo() *canvas.Image {
image := canvas.NewImageFromResource(fyne.NewStaticResource("lazyrun.png", assets.Icon))
image.FillMode = canvas.ImageFillContain
image.SetMinSize(fyne.NewSize(size, size))
return image
}
// logoBox menampilkan ikon LazyRun pada ukuran tetap.
func logoBox(image *canvas.Image, size float32) fyne.CanvasObject {
return container.NewGridWrap(fyne.NewSize(size, size), image)
}
+3
View File
@@ -16,6 +16,7 @@ var (
// Nama warna tambahan di luar tema standar Fyne.
const (
colorNameCard fyne.ThemeColorName = "lazyrunCard"
colorNameCardHover fyne.ThemeColorName = "lazyrunCardHover"
colorNameMuted fyne.ThemeColorName = "lazyrunMuted"
colorNameChipMenu fyne.ThemeColorName = "lazyrunChipMenu"
colorNameChipDesk fyne.ThemeColorName = "lazyrunChipDesktop"
@@ -34,6 +35,8 @@ func (b brand) Color(name fyne.ThemeColorName, variant fyne.ThemeVariant) color.
return pick(dark, color.NRGBA{R: 0x13, G: 0x13, B: 0x1B, A: 0xFF}, color.NRGBA{R: 0xF5, G: 0xF5, B: 0xFA, A: 0xFF})
case colorNameCard:
return pick(dark, color.NRGBA{R: 0x1D, G: 0x1D, B: 0x28, A: 0xFF}, color.NRGBA{R: 0xFF, G: 0xFF, B: 0xFF, A: 0xFF})
case colorNameCardHover:
return withAlpha(brandIndigo, pick[uint8](dark, 0x2E, 0x14))
case theme.ColorNameInputBackground:
return pick(dark, color.NRGBA{R: 0x1A, G: 0x1A, B: 0x24, A: 0xFF}, color.NRGBA{R: 0xFF, G: 0xFF, B: 0xFF, A: 0xFF})
case theme.ColorNameButton, theme.ColorNameMenuBackground: