135 lines
3.5 KiB
Go
135 lines
3.5 KiB
Go
package app
|
||
|
||
import (
|
||
"fmt"
|
||
"strings"
|
||
"time"
|
||
|
||
tea "charm.land/bubbletea/v2"
|
||
"charm.land/lipgloss/v2"
|
||
|
||
"github.com/oxpoe/tia/internal/audio"
|
||
"github.com/oxpoe/tia/internal/ui/animation"
|
||
"github.com/oxpoe/tia/internal/ui/components"
|
||
)
|
||
|
||
// Model root sementara (T-0.2–T-0.4, Phase 0).
|
||
// Membuktikan Bubble Tea v2 (View struct, mouse, resize, ticker 30 FPS),
|
||
// audio engine, dan toast animasi sebelum layout penuh Phase 1.
|
||
type Model struct {
|
||
width, height int
|
||
clicks int
|
||
lastClick string
|
||
sound *audio.Engine
|
||
|
||
tickID int64
|
||
animating bool
|
||
phase float64
|
||
toast components.Toast
|
||
frames int
|
||
fps int
|
||
lastFPSAt time.Time
|
||
}
|
||
|
||
func New() Model {
|
||
return Model{
|
||
lastClick: "belum ada klik",
|
||
sound: audio.New(audio.Options{Enabled: true, Volume: 70, MuteOnSSH: true}),
|
||
tickID: animation.NewID(),
|
||
animating: true,
|
||
toast: components.NewToast(),
|
||
}
|
||
}
|
||
|
||
func (m Model) Init() tea.Cmd { return animation.Next(m.tickID) }
|
||
|
||
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||
switch msg := msg.(type) {
|
||
case animation.TickMsg:
|
||
if msg.ID != m.tickID || !m.animating {
|
||
return m, nil // tick basi / animasi dimatikan oleh reduced-motion
|
||
}
|
||
m.phase += 0.9
|
||
m.toast.Update(animation.FrameInterval)
|
||
m.frames++
|
||
if m.lastFPSAt.IsZero() {
|
||
m.lastFPSAt = msg.Time
|
||
}
|
||
if d := msg.Time.Sub(m.lastFPSAt); d >= time.Second {
|
||
m.fps = int(float64(m.frames) / d.Seconds())
|
||
m.frames = 0
|
||
m.lastFPSAt = msg.Time
|
||
}
|
||
return m, animation.Next(m.tickID)
|
||
|
||
case tea.WindowSizeMsg:
|
||
m.width, m.height = msg.Width, msg.Height
|
||
|
||
case tea.KeyPressMsg:
|
||
switch msg.String() {
|
||
case "q", "ctrl+c", "esc":
|
||
m.sound.Play(audio.ModalClose)
|
||
return m, tea.Quit
|
||
case "a": // reduced motion: hentikan/nyalakan ticker
|
||
m.animating = !m.animating
|
||
if m.animating {
|
||
m.tickID = animation.NewID()
|
||
return m, animation.Next(m.tickID)
|
||
}
|
||
return m, nil
|
||
case "t":
|
||
m.toast.Show("✔ [ SUKSES ] Konversi Berhasil Disimpan",
|
||
"Stack 'web-app' tersimpan di ~/.tia/stacks/web-app/")
|
||
m.sound.Play(audio.NotifySuccess)
|
||
case "j", "down", "k", "up":
|
||
m.phase += 6
|
||
m.sound.Play(audio.Navigate)
|
||
}
|
||
|
||
case tea.MouseClickMsg:
|
||
if msg.Button == tea.MouseLeft {
|
||
m.clicks++
|
||
m.lastClick = fmt.Sprintf("klik #%d di (%d, %d)", m.clicks, msg.X, msg.Y)
|
||
if m.toast.Active() {
|
||
m.toast = components.NewToast() // klik = tutup toast
|
||
}
|
||
m.sound.Play(audio.Click)
|
||
}
|
||
}
|
||
return m, nil
|
||
}
|
||
|
||
func (m Model) View() tea.View {
|
||
title := lipgloss.NewStyle().
|
||
Bold(true).
|
||
Foreground(lipgloss.Color("#cba6f7")).
|
||
Render("tia — Terminal Is A My Way")
|
||
|
||
fps := "—"
|
||
if m.animating {
|
||
fps = fmt.Sprintf("%d", m.fps)
|
||
}
|
||
statusline := lipgloss.NewStyle().
|
||
Foreground(lipgloss.Color("#a6adc8")).
|
||
Render(fmt.Sprintf("Just Work · Phase 0 · animasi %s fps · audio: %s", fps, m.sound.Status()))
|
||
hint := lipgloss.NewStyle().
|
||
Foreground(lipgloss.Color("#6c7086")).
|
||
Render("[t] toast · [a] reduced-motion · klik / j / k = bunyi · q = keluar")
|
||
|
||
box := lipgloss.NewStyle().
|
||
Border(lipgloss.RoundedBorder()).
|
||
BorderForeground(lipgloss.Color("#6c7086")).
|
||
Padding(0, 2).
|
||
Render(fmt.Sprintf("%s\nukuran jendela: %dx%d", m.lastClick, m.width, m.height))
|
||
|
||
parts := []string{title, "", animation.RenderLogo(m.phase), "", statusline, hint, "", box}
|
||
if t := m.toast.View(max(m.width, 20)); t != "" {
|
||
parts = append([]string{t, ""}, parts...)
|
||
}
|
||
|
||
v := tea.NewView(strings.Join(parts, "\n"))
|
||
v.AltScreen = true
|
||
v.MouseMode = tea.MouseModeCellMotion
|
||
return v
|
||
}
|