CI / test (push) Successful in 39s
- Layar Pengaturan (',' atau klik chip Pengaturan beranda): bahasa, mode tema,
efek suara, volume, animasi, timestamp — semua berlaku langsung & tersimpan
ke config (config.Save), plus path config + catatan.
- Audio live: Engine.SetEnabled/SetVolume/Enabled/Volume; init idempoten
(readyOnce/started) sehingga engine yang lahir mati bisa dinyalakan tanpa
restart — komentar 'shortcut' F-024 lama dihapus.
- Beranda: kolom tengah kini CTA + hint '[m] suara: …' (klik = toggle) +
chip Pengaturan — draf dashboard lengkap.
- Tombol lama 'T'/'a'/'S' kini lewat applySetting yang sama (DRY + persist).
- TIA_LANG dihapus (tak perlu lagi): e2e memakai XDG_CONFIG_HOME temp
(config bersih per tes) → tes tidak tergantung config pengguna.
- Toast: klik di luar kotak toast tidak lagi menutupnya (hanya klik pada toast)
— tombol lain tetap bisa diklik saat notifikasi tampil.
- Tes: unit settings (apply/persist/jepit volume, baris plain==styled tanpa
ANSI) + e2e TestSettingsScreen (klik chip, toggle, bahasa→Inggris,
verifikasi config.yaml).
258 lines
7.1 KiB
Go
258 lines
7.1 KiB
Go
package app
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
tea "charm.land/bubbletea/v2"
|
|
|
|
"gitea.oxpoe.qzz.io/oxpoe/tia-love/internal/audio"
|
|
"gitea.oxpoe.qzz.io/oxpoe/tia-love/internal/config"
|
|
"gitea.oxpoe.qzz.io/oxpoe/tia-love/internal/i18n"
|
|
"gitea.oxpoe.qzz.io/oxpoe/tia-love/internal/ui/animation"
|
|
"gitea.oxpoe.qzz.io/oxpoe/tia-love/internal/ui/components"
|
|
"gitea.oxpoe.qzz.io/oxpoe/tia-love/internal/ui/theme"
|
|
)
|
|
|
|
// Layar Pengaturan (PRD §5.1.5) — dibuka dengan `,` atau tombol Pengaturan di
|
|
// beranda. Semua perubahan berlaku langsung dan tersimpan ke config (tanpa
|
|
// restart, tanpa edit file manual).
|
|
//
|
|
// shortcut: baris teks (bukan form huh) + opsi inti dulu; warna aksen,
|
|
// direktori stack, mute-SSH menyusul.
|
|
|
|
// settingsRowIDs — urutan baris pengaturan.
|
|
var settingsRowIDs = []string{"language", "theme", "sound", "volume", "animation", "timestamps"}
|
|
|
|
// settingsEnv — terjemahan label baris.
|
|
func (m Model) settingsEnv(id string) (label, value string) {
|
|
switch id {
|
|
case "language":
|
|
value = "Indonesia"
|
|
if m.cfg.UI.Language == "en" {
|
|
value = "English"
|
|
}
|
|
case "theme":
|
|
value = i18n.T("theme." + string(theme.Current().Mode))
|
|
case "sound":
|
|
value = onOffText(m.sound.Enabled())
|
|
case "volume":
|
|
value = fmt.Sprintf("%d%%", m.sound.Volume())
|
|
case "animation":
|
|
value = onOffText(m.animating)
|
|
case "timestamps":
|
|
value = onOffText(m.showTimestamps)
|
|
}
|
|
return i18n.T("settings." + id), value
|
|
}
|
|
|
|
func onOffText(on bool) string {
|
|
if on {
|
|
return i18n.T("state.on")
|
|
}
|
|
return i18n.T("state.off")
|
|
}
|
|
|
|
// settingsRowSegments — satu baris sebagai segmen PLAIN (tanpa ANSI!): satu
|
|
// sumber untuk render, teks hit-test klik, dan zona tombol (- / +). Escape
|
|
// warna tidak boleh masuk ke sini — zona klik dihitung dari teks polos.
|
|
func (m Model) settingsRowSegments(i int) []string {
|
|
label, value := m.settingsEnv(settingsRowIDs[i])
|
|
label = fmt.Sprintf("%-*s", 16, label)
|
|
if settingsRowIDs[i] == "volume" {
|
|
return []string{label, "[ - ]", value, "[ + ]"}
|
|
}
|
|
return []string{label, "[ " + value + " ]"}
|
|
}
|
|
|
|
func (m Model) settingsRowPlain(i int) string {
|
|
return strings.Join(m.settingsRowSegments(i), " ")
|
|
}
|
|
|
|
func (m Model) settingsRowLine(i, w int) string {
|
|
th := theme.Current()
|
|
sel := i == m.settingsRow
|
|
segs := m.settingsRowSegments(i)
|
|
out := make([]string, 0, len(segs))
|
|
for k, s := range segs {
|
|
c := th.Accent
|
|
switch {
|
|
case k == 0:
|
|
c = th.Subtext
|
|
case settingsRowIDs[i] == "volume" && k == 2:
|
|
c = th.Text
|
|
}
|
|
out = append(out, paintRow(sel, c, s))
|
|
}
|
|
line := strings.Join(out, " ")
|
|
if sel {
|
|
line = selPad(line, w)
|
|
}
|
|
return line
|
|
}
|
|
|
|
// settingsBody — layar penuh: panel daftar opsi + info config.
|
|
func (m Model) settingsBody() string {
|
|
w, h := m.width, m.bodyHeight()
|
|
lines := []string{}
|
|
for i := range settingsRowIDs {
|
|
lines = append(lines, m.settingsRowLine(i, w-4))
|
|
}
|
|
lines = append(lines, "",
|
|
fieldW(i18n.T("settings.config"), 16)+" "+dim(prettyPath(config.Path())),
|
|
dim(i18n.T("settings.applyNote")),
|
|
)
|
|
return panel(i18n.T("settings.title"), "esc", lines, w, h, true)
|
|
}
|
|
|
|
func (m *Model) openSettings() {
|
|
if m.view != viewSettings {
|
|
m.prevView = m.view
|
|
}
|
|
m.view = viewSettings
|
|
m.settingsRow = 0
|
|
m.sound.Play(audio.ModalOpen)
|
|
}
|
|
|
|
// applySetting — ubah satu opsi: efek langsung + simpan ke config.
|
|
// delta: 0 = aktifkan (toggle/maju), -1 = mundur (pilihan), ±10 = volume.
|
|
func (m *Model) applySetting(id string, delta int) tea.Cmd {
|
|
th := theme.Current()
|
|
switch id {
|
|
case "language":
|
|
next := "en"
|
|
if m.cfg.UI.Language == "en" {
|
|
next = "id"
|
|
}
|
|
m.cfg.UI.Language = next
|
|
i18n.SetLang(next)
|
|
name := "Indonesia"
|
|
if next == "en" {
|
|
name = "English"
|
|
}
|
|
m.toast.Show(components.LevelInfo, i18n.Tf("toast.language", name), i18n.T("theme.switchHint"))
|
|
case "theme":
|
|
next := theme.CycleMode(th.Mode)
|
|
if delta < 0 {
|
|
next = cycleBack(th.Mode)
|
|
}
|
|
m.cfg.UI.ThemeMode = string(next)
|
|
theme.Set(next, th.AccentName)
|
|
m.toast.Show(components.LevelInfo,
|
|
i18n.Tf("toast.themeMode", i18n.T("theme."+string(next))),
|
|
i18n.T("theme.switchHint"))
|
|
case "sound":
|
|
on := !m.sound.Enabled()
|
|
m.sound.SetEnabled(on)
|
|
m.cfg.UI.SoundEnabled = on
|
|
m.toast.Show(components.LevelInfo, i18n.Tf("toast.sound", onOffText(on)), "")
|
|
case "volume":
|
|
if delta == 0 {
|
|
break
|
|
}
|
|
v := min(max(m.sound.Volume()+delta, 0), 100)
|
|
m.sound.SetVolume(v)
|
|
m.cfg.UI.SoundVolume = v
|
|
m.toast.Show(components.LevelInfo, i18n.Tf("toast.volume", v), "")
|
|
case "animation":
|
|
m.animating = !m.animating
|
|
m.cfg.UI.AnimationEnabled = m.animating
|
|
m.toast.Show(components.LevelInfo, i18n.Tf("toast.animation", onOffText(m.animating)), "")
|
|
if m.animating {
|
|
m.tickID = animation.NewID()
|
|
m.saveCfg()
|
|
m.sound.Play(audio.Click)
|
|
return animation.Next(m.tickID)
|
|
}
|
|
case "timestamps":
|
|
m.showTimestamps = !m.showTimestamps
|
|
m.cfg.UI.ShowTimestamps = m.showTimestamps
|
|
m.toast.Show(components.LevelInfo, i18n.Tf("toast.timestamps", onOffText(m.showTimestamps)), "")
|
|
m.saveCfg()
|
|
m.sound.Play(audio.Click)
|
|
return m.restartData()
|
|
default:
|
|
return nil
|
|
}
|
|
m.saveCfg()
|
|
m.sound.Play(audio.Click)
|
|
return nil
|
|
}
|
|
|
|
// cycleBack — mode tema mundur (auto ← dark ← light ← terminal).
|
|
func cycleBack(mode theme.Mode) theme.Mode {
|
|
order := []theme.Mode{theme.ModeAuto, theme.ModeDark, theme.ModeLight, theme.ModeTerminal}
|
|
for i, mm := range order {
|
|
if mm == mode {
|
|
return order[(i+len(order)-1)%len(order)]
|
|
}
|
|
}
|
|
return theme.ModeAuto
|
|
}
|
|
|
|
// saveCfg — tulis config; gagal = toast (perubahan tetap hidup di sesi ini).
|
|
func (m *Model) saveCfg() {
|
|
if err := config.Save(m.cfg); err != nil {
|
|
m.toast.Show(components.LevelWarning, i18n.T("toast.configSave"), err.Error())
|
|
}
|
|
}
|
|
|
|
// settingsKey — tombol di layar Pengaturan (layar ini menangkap sendiri).
|
|
func (m *Model) settingsKey(key string) tea.Cmd {
|
|
last := len(settingsRowIDs) - 1
|
|
switch key {
|
|
case "esc", ",":
|
|
m.view = m.prevView
|
|
m.sound.Play(audio.ModalClose)
|
|
case "j", "down":
|
|
m.settingsRow = min(m.settingsRow+1, last)
|
|
m.sound.Play(audio.Navigate)
|
|
case "k", "up":
|
|
m.settingsRow = max(m.settingsRow-1, 0)
|
|
m.sound.Play(audio.Navigate)
|
|
case "enter", "space":
|
|
return m.applySetting(settingsRowIDs[m.settingsRow], 0)
|
|
case "l", "right", "]":
|
|
delta := 1
|
|
if settingsRowIDs[m.settingsRow] == "volume" {
|
|
delta = 10
|
|
}
|
|
return m.applySetting(settingsRowIDs[m.settingsRow], delta)
|
|
case "h", "left", "[":
|
|
delta := -1
|
|
if settingsRowIDs[m.settingsRow] == "volume" {
|
|
delta = -10
|
|
}
|
|
return m.applySetting(settingsRowIDs[m.settingsRow], delta)
|
|
case "q", "ctrl+c":
|
|
m.stopLogs()
|
|
if m.eventsCancel != nil {
|
|
m.eventsCancel()
|
|
}
|
|
m.sound.Play(audio.ModalClose)
|
|
return tea.Quit
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// settingsClick — klik baris: pilih + aktifkan; zona [-] [+] = volume.
|
|
func (m *Model) settingsClick(x, y int) tea.Cmd {
|
|
i := y - (m.headerRows() + 1)
|
|
if i < 0 || i >= len(settingsRowIDs) {
|
|
return nil
|
|
}
|
|
m.settingsRow = i
|
|
if label, ok := labelZoneAt(m.settingsRowPlain(i), x-2); ok {
|
|
switch label {
|
|
case "-":
|
|
return m.applySetting(settingsRowIDs[i], -10)
|
|
case "+":
|
|
return m.applySetting(settingsRowIDs[i], 10)
|
|
default:
|
|
return m.applySetting(settingsRowIDs[i], 0)
|
|
}
|
|
}
|
|
m.sound.Play(audio.Click)
|
|
return nil
|
|
}
|