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:
1 parent
b12efefd6a
commit
70534bf6e2
6 files changed
+132
-17
No files matched your search
@@ -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()
|
||||
}
|
||||
@@ -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),
|
||||
)
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
|
||||
+31
-11
@@ -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,
|
||||
container.NewHBox(thumb(sc.IconPath, fallbackIcon(sc)), info),
|
||||
actions)),
|
||||
)
|
||||
return newHoverCard(container.NewBorder(nil, nil,
|
||||
container.NewHBox(thumb(sc.IconPath, fallbackIcon(sc)), info),
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in new issue
Block a user