Files
tia-love/internal/app/app.go
T

135 lines
3.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}