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 }