635 lines
17 KiB
Go
635 lines
17 KiB
Go
package ui
|
|
|
|
import (
|
|
_ "embed"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"termdoku/internal/achievements"
|
|
"termdoku/internal/config"
|
|
"termdoku/internal/generator"
|
|
"termdoku/internal/stats"
|
|
"termdoku/internal/theme"
|
|
|
|
tea "github.com/charmbracelet/bubbletea"
|
|
"github.com/charmbracelet/lipgloss"
|
|
)
|
|
|
|
//go:embed assets/banner.txt
|
|
var bannerArt string
|
|
|
|
type appState int
|
|
|
|
const (
|
|
stateMenu appState = iota
|
|
stateGame
|
|
stateStats
|
|
stateAchievements
|
|
stateLeaderboard
|
|
stateProfile
|
|
stateProfileSubmenu
|
|
stateDatabase
|
|
)
|
|
|
|
type App struct {
|
|
state appState
|
|
cfg config.Config
|
|
th theme.Theme
|
|
styles UIStyles
|
|
stats stats.Stats
|
|
achievements *achievements.Manager
|
|
|
|
menuItems []string
|
|
selectedIdx int
|
|
autoCheck bool
|
|
timerEnabled bool
|
|
|
|
width int
|
|
height int
|
|
|
|
currentDiff string
|
|
game Model
|
|
|
|
// Profile submenu
|
|
profileMenuItems []string
|
|
profileSelectedIdx int
|
|
}
|
|
|
|
func NewApp(cfg config.Config) App {
|
|
th := theme.GetTheme(cfg.Theme)
|
|
st, _ := stats.Load()
|
|
ach, _ := achievements.Load()
|
|
return App{
|
|
state: stateMenu,
|
|
cfg: cfg,
|
|
th: th,
|
|
styles: BuildStyles(th),
|
|
stats: st,
|
|
achievements: ach,
|
|
menuItems: []string{"Easy", "Normal", "Hard", "Expert", "Lunatic", "Daily", "Profile"},
|
|
selectedIdx: 1,
|
|
autoCheck: cfg.AutoCheck,
|
|
timerEnabled: cfg.TimerEnabled,
|
|
profileMenuItems: []string{"Achievements", "Leaderboard"},
|
|
profileSelectedIdx: 0,
|
|
}
|
|
}
|
|
|
|
func (a App) Init() tea.Cmd { return nil }
|
|
|
|
func (a App) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|
switch a.state {
|
|
case stateMenu:
|
|
switch m := msg.(type) {
|
|
case tea.KeyMsg:
|
|
s := m.String()
|
|
switch s {
|
|
case "up", "k":
|
|
a.selectedIdx = clamp(a.selectedIdx-1, 0, len(a.menuItems)-1)
|
|
case "down", "j":
|
|
a.selectedIdx = clamp(a.selectedIdx+1, 0, len(a.menuItems)-1)
|
|
case "left", "h":
|
|
a.selectedIdx = clamp(a.selectedIdx-1, 0, len(a.menuItems)-1)
|
|
case "right", "l":
|
|
a.selectedIdx = clamp(a.selectedIdx+1, 0, len(a.menuItems)-1)
|
|
case "a":
|
|
a.autoCheck = !a.autoCheck
|
|
case "t":
|
|
a.timerEnabled = !a.timerEnabled
|
|
case "enter":
|
|
sel := a.menuItems[a.selectedIdx]
|
|
switch sel {
|
|
case "Achievements":
|
|
a.state = stateAchievements
|
|
return a, nil
|
|
case "Leaderboard":
|
|
a.state = stateLeaderboard
|
|
return a, nil
|
|
case "Profile":
|
|
a.state = stateProfile
|
|
return a, nil
|
|
default:
|
|
gm, cmd := a.startGame()
|
|
a.game = gm
|
|
a.state = stateGame
|
|
return a, cmd
|
|
}
|
|
case "q", "esc", "ctrl+c":
|
|
return a, tea.Quit
|
|
}
|
|
case tea.WindowSizeMsg:
|
|
a.width, a.height = m.Width, m.Height
|
|
}
|
|
return a, nil
|
|
case stateStats:
|
|
switch m := msg.(type) {
|
|
case tea.KeyMsg:
|
|
s := m.String()
|
|
switch s {
|
|
case "m", "q", "esc", "enter":
|
|
a.state = stateMenu
|
|
return a, nil
|
|
}
|
|
case tea.WindowSizeMsg:
|
|
a.width, a.height = m.Width, m.Height
|
|
}
|
|
return a, nil
|
|
case stateGame:
|
|
// Check if game was just completed
|
|
wasCompleted := a.game.completed
|
|
|
|
// intercept main menu key
|
|
if kmsg, isKey := msg.(tea.KeyMsg); isKey {
|
|
if kmsg.String() == "m" {
|
|
// Record game if it was completed but not yet recorded
|
|
if a.game.completed && !wasCompleted {
|
|
a.recordGameCompletion()
|
|
}
|
|
a.state = stateMenu
|
|
return a, nil
|
|
}
|
|
}
|
|
|
|
gm, cmd := a.game.Update(msg)
|
|
if v, ok := gm.(Model); ok {
|
|
// Check if game just became completed
|
|
if v.completed && !wasCompleted {
|
|
a.game = v
|
|
a.recordGameCompletion()
|
|
return a, cmd
|
|
}
|
|
a.game = v
|
|
}
|
|
return a, cmd
|
|
case stateAchievements:
|
|
switch m := msg.(type) {
|
|
case tea.KeyMsg:
|
|
s := m.String()
|
|
switch s {
|
|
case "m", "q", "esc", "enter":
|
|
a.state = stateMenu
|
|
return a, nil
|
|
}
|
|
case tea.WindowSizeMsg:
|
|
a.width, a.height = m.Width, m.Height
|
|
}
|
|
return a, nil
|
|
case stateLeaderboard:
|
|
switch m := msg.(type) {
|
|
case tea.KeyMsg:
|
|
s := m.String()
|
|
switch s {
|
|
case "m", "q", "esc", "enter":
|
|
a.state = stateMenu
|
|
return a, nil
|
|
}
|
|
case tea.WindowSizeMsg:
|
|
a.width, a.height = m.Width, m.Height
|
|
}
|
|
return a, nil
|
|
case stateProfile:
|
|
switch m := msg.(type) {
|
|
case tea.KeyMsg:
|
|
s := m.String()
|
|
switch s {
|
|
case "m", "q", "esc", "enter":
|
|
a.state = stateMenu
|
|
return a, nil
|
|
case "s":
|
|
a.state = stateProfileSubmenu
|
|
return a, nil
|
|
case "d":
|
|
a.state = stateDatabase
|
|
return a, nil
|
|
}
|
|
case tea.WindowSizeMsg:
|
|
a.width, a.height = m.Width, m.Height
|
|
}
|
|
return a, nil
|
|
case stateProfileSubmenu:
|
|
switch m := msg.(type) {
|
|
case tea.KeyMsg:
|
|
s := m.String()
|
|
switch s {
|
|
case "m", "q", "esc":
|
|
a.state = stateProfile
|
|
return a, nil
|
|
case "up", "k":
|
|
a.profileSelectedIdx = clamp(a.profileSelectedIdx-1, 0, len(a.profileMenuItems)-1)
|
|
case "down", "j":
|
|
a.profileSelectedIdx = clamp(a.profileSelectedIdx+1, 0, len(a.profileMenuItems)-1)
|
|
case "enter":
|
|
sel := a.profileMenuItems[a.profileSelectedIdx]
|
|
switch sel {
|
|
case "Stats":
|
|
a.state = stateStats
|
|
return a, nil
|
|
case "Achievements":
|
|
a.state = stateAchievements
|
|
return a, nil
|
|
case "Leaderboard":
|
|
a.state = stateLeaderboard
|
|
return a, nil
|
|
}
|
|
}
|
|
case tea.WindowSizeMsg:
|
|
a.width, a.height = m.Width, m.Height
|
|
}
|
|
return a, nil
|
|
case stateDatabase:
|
|
switch m := msg.(type) {
|
|
case tea.KeyMsg:
|
|
s := m.String()
|
|
switch s {
|
|
case "m", "q", "esc", "enter":
|
|
a.state = stateMenu
|
|
return a, nil
|
|
case "p":
|
|
a.state = stateProfile
|
|
return a, nil
|
|
}
|
|
case tea.WindowSizeMsg:
|
|
a.width, a.height = m.Width, m.Height
|
|
}
|
|
return a, nil
|
|
}
|
|
return a, nil
|
|
}
|
|
|
|
func (a App) View() string {
|
|
switch a.state {
|
|
case stateMenu:
|
|
return a.viewMenu()
|
|
case stateGame:
|
|
return a.viewGame()
|
|
case stateAchievements:
|
|
return a.viewAchievements()
|
|
case stateProfile:
|
|
return a.viewProfile()
|
|
case stateProfileSubmenu:
|
|
return a.viewProfileSubmenu()
|
|
case stateDatabase:
|
|
return a.viewDatabaseInfo()
|
|
case stateLeaderboard:
|
|
return a.viewLeaderboard()
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (a App) recordGameCompletion() {
|
|
record := stats.GameRecord{
|
|
Difficulty: a.currentDiff,
|
|
Completed: a.game.completed,
|
|
Time: int(a.game.elapsed.Seconds()),
|
|
HintsUsed: a.game.hintsUsed,
|
|
Date: time.Now(),
|
|
IsDaily: a.currentDiff == "Daily",
|
|
}
|
|
if a.currentDiff == "Daily" {
|
|
record.DailySeed = time.Now().Format("2006-01-02")
|
|
}
|
|
a.stats.RecordGame(record)
|
|
_ = stats.Save(a.stats)
|
|
|
|
if a.game.completed {
|
|
a.achievements.CheckAndUnlock("first_win", a.stats.CompletedGames)
|
|
|
|
if a.game.hintsUsed == 0 {
|
|
a.achievements.CheckAndUnlock("perfectionist", 1)
|
|
}
|
|
|
|
if a.currentDiff == "Easy" && int(a.game.elapsed.Seconds()) < 180 {
|
|
a.achievements.CheckAndUnlock("speed_demon", 1)
|
|
}
|
|
|
|
if a.currentDiff == "Hard" && !a.autoCheck {
|
|
a.achievements.CheckAndUnlock("no_mistakes", 1)
|
|
}
|
|
|
|
a.achievements.CheckAndUnlock("streak_master", a.stats.CurrentStreak)
|
|
a.achievements.CheckAndUnlock("century", a.stats.CompletedGames)
|
|
|
|
if a.currentDiff == "Lunatic" {
|
|
lunaticCount := a.stats.CompletionCounts["Lunatic"]
|
|
a.achievements.CheckAndUnlock("lunatic_legend", lunaticCount)
|
|
}
|
|
|
|
if a.currentDiff == "Daily" {
|
|
dailyCount := len(a.stats.DailyHistory)
|
|
a.achievements.CheckAndUnlock("daily_devotee", dailyCount)
|
|
}
|
|
|
|
_ = achievements.Save(a.achievements)
|
|
}
|
|
}
|
|
|
|
func (a *App) startGame() (Model, tea.Cmd) {
|
|
var g generator.Grid
|
|
var err error
|
|
sel := a.menuItems[a.selectedIdx]
|
|
switch sel {
|
|
case "Daily":
|
|
g, err = generator.GenerateDaily(time.Now())
|
|
case "Easy":
|
|
g, err = generator.Generate(generator.Easy, "")
|
|
case "Normal":
|
|
g, err = generator.Generate(generator.Normal, "")
|
|
case "Hard":
|
|
g, err = generator.Generate(generator.Hard, "")
|
|
case "Expert":
|
|
g, err = generator.Generate(generator.Expert, "")
|
|
case "Lunatic":
|
|
g, err = generator.Generate(generator.Lunatic, "")
|
|
}
|
|
if err != nil {
|
|
return a.game, nil
|
|
}
|
|
cfg := a.cfg
|
|
cfg.AutoCheck = a.autoCheck
|
|
cfg.TimerEnabled = a.timerEnabled
|
|
a.currentDiff = sel
|
|
m := New(g, a.th, cfg)
|
|
m.difficulty = sel
|
|
adaptiveColors := theme.NewAdaptiveColors(a.th)
|
|
diffColors := adaptiveColors.GetDifficultyColors()
|
|
hex := diffColors[sel]
|
|
if hex == "" {
|
|
hex = a.th.Palette.Accent
|
|
}
|
|
style := lipgloss.NewStyle().Foreground(lipgloss.Color(hex))
|
|
m.styles.RowSep = style
|
|
m.styles.ColSep = style
|
|
m.styles.CellFixed = m.styles.CellFixed.Foreground(lipgloss.Color(hex))
|
|
return m, m.Init()
|
|
}
|
|
|
|
func (a App) viewMenu() string {
|
|
banner := bannerArt
|
|
|
|
// Options
|
|
optAC := fmt.Sprintf("Auto-Check (a): %s", boolText(a.styles, a.autoCheck))
|
|
optTM := fmt.Sprintf("Timer (t): %s", boolText(a.styles, a.timerEnabled))
|
|
|
|
// Adaptive colors
|
|
adaptiveColors := theme.NewAdaptiveColors(a.th)
|
|
accentColors := adaptiveColors.GetAccentColors()
|
|
|
|
// Display all menu items
|
|
var items []string
|
|
selectedStyle := lipgloss.NewStyle().Foreground(lipgloss.Color(accentColors["selected"])).Bold(true)
|
|
for i, name := range a.menuItems {
|
|
prefix := " "
|
|
if i == a.selectedIdx {
|
|
prefix := "✭ "
|
|
label := prefix + name
|
|
items = append(items, selectedStyle.Render(label))
|
|
} else {
|
|
label := prefix + name
|
|
items = append(items, a.styles.MenuItem.Render(label))
|
|
}
|
|
}
|
|
gap := strings.Repeat(" ", 2)
|
|
diffRow := strings.Join(items, gap)
|
|
|
|
// Adaptive gradient colors
|
|
gradientColors := adaptiveColors.GetGradientColors()
|
|
bannerGrad := gradientColors["banner"]
|
|
leftHex := bannerGrad[0]
|
|
rightHex := bannerGrad[1]
|
|
|
|
title := gradientText("Select option", leftHex, rightHex)
|
|
box := renderGradientBox(diffRow, 2, leftHex, rightHex)
|
|
// Gradient banner (line by line)
|
|
var gb strings.Builder
|
|
for i, l := range strings.Split(strings.TrimRight(banner, "\n"), "\n") {
|
|
gb.WriteString(gradientText(l, leftHex, rightHex))
|
|
if i < len(strings.Split(strings.TrimRight(banner, "\n"), "\n"))-1 {
|
|
gb.WriteString("\n")
|
|
}
|
|
}
|
|
gradientBanner := gb.String()
|
|
|
|
// Compose content with explicit 2-line top/bottom padding
|
|
content := "\n\n" + gradientBanner + "\n\n\n" + optAC + "\n" + optTM + "\n\n\n" + title + "\n" + box + "\n\n"
|
|
panel := a.styles.Panel.Render(content)
|
|
if a.width > 0 && a.height > 0 {
|
|
return a.styles.App.Render(lipgloss.Place(a.width, a.height, lipgloss.Center, lipgloss.Center, panel))
|
|
}
|
|
return a.styles.App.Render(panel)
|
|
}
|
|
|
|
func boolText(s UIStyles, v bool) string {
|
|
if v {
|
|
return s.BoolTrue.Render("ON")
|
|
}
|
|
return s.BoolFalse.Render("OFF")
|
|
}
|
|
|
|
func (a App) viewGame() string {
|
|
innerWidth := 58
|
|
|
|
boardAndStatus := Render(a.game)
|
|
|
|
label := a.currentDiff
|
|
if a.currentDiff == "Daily" {
|
|
label = "Daily Seed"
|
|
}
|
|
headerText := label + " Mode"
|
|
// Adaptive colors for headers
|
|
adaptiveColors := theme.NewAdaptiveColors(a.th)
|
|
gradientColors := adaptiveColors.GetGradientColors()
|
|
|
|
var header string
|
|
switch a.currentDiff {
|
|
case "Easy":
|
|
easyGrad := gradientColors["easy"]
|
|
header = gradientText(headerText, easyGrad[0], easyGrad[1])
|
|
case "Normal":
|
|
normalGrad := gradientColors["normal"]
|
|
header = gradientText(headerText, normalGrad[0], normalGrad[1])
|
|
case "Hard":
|
|
hardGrad := gradientColors["hard"]
|
|
header = gradientText(headerText, hardGrad[0], hardGrad[1])
|
|
case "Lunatic":
|
|
lunaticGrad := gradientColors["lunatic"]
|
|
header = gradientText(headerText, lunaticGrad[0], lunaticGrad[1])
|
|
case "Daily":
|
|
dailyGrad := gradientColors["daily"]
|
|
header = gradientText(headerText, dailyGrad[0], dailyGrad[1])
|
|
default:
|
|
header = lipgloss.NewStyle().Foreground(lipgloss.Color(a.th.Palette.Accent)).Bold(true).Render(headerText)
|
|
}
|
|
|
|
headerCentered := lipgloss.PlaceHorizontal(innerWidth, lipgloss.Center, header)
|
|
centered := lipgloss.PlaceHorizontal(innerWidth, lipgloss.Center, boardAndStatus)
|
|
body := "\n" + headerCentered + "\n\n" + centered + "\n"
|
|
panel := a.styles.Panel.Render(body)
|
|
if a.width > 0 && a.height > 0 {
|
|
return a.styles.App.Render(lipgloss.Place(a.width, a.height, lipgloss.Center, lipgloss.Center, panel))
|
|
}
|
|
return a.styles.App.Render(panel)
|
|
}
|
|
|
|
// Helpers: gradient text and gradient bordered box
|
|
func renderGradientBox(content string, padX int, leftHex, rightHex string) string {
|
|
w := lipgloss.Width(content) + padX*2
|
|
top := lipgloss.NewStyle().Foreground(lipgloss.Color(leftHex)).Render("╭") + gradientLine("─", w, leftHex, rightHex) + lipgloss.NewStyle().Foreground(lipgloss.Color(rightHex)).Render("╮")
|
|
bottom := lipgloss.NewStyle().Foreground(lipgloss.Color(leftHex)).Render("╰") + gradientLine("─", w, leftHex, rightHex) + lipgloss.NewStyle().Foreground(lipgloss.Color(rightHex)).Render("╯")
|
|
left := lipgloss.NewStyle().Foreground(lipgloss.Color(leftHex)).Render("│")
|
|
right := lipgloss.NewStyle().Foreground(lipgloss.Color(rightHex)).Render("│")
|
|
middle := left + strings.Repeat(" ", padX) + content + strings.Repeat(" ", padX) + right
|
|
return strings.Join([]string{top, middle, bottom}, "\n")
|
|
}
|
|
|
|
func gradientLine(ch string, width int, fromHex, toHex string) string {
|
|
colors := gradientColors(fromHex, toHex, width)
|
|
var b strings.Builder
|
|
for i := 0; i < width; i++ {
|
|
b.WriteString(lipgloss.NewStyle().Foreground(lipgloss.Color(colors[i])).Render(ch))
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
func gradientText(text, leftHex, rightHex string) string {
|
|
colors := gradientColors(leftHex, rightHex, len(text))
|
|
var b strings.Builder
|
|
idx := 0
|
|
for _, ch := range text { // rune-safe
|
|
b.WriteString(lipgloss.NewStyle().Foreground(lipgloss.Color(colors[idx])).Bold(true).Render(string(ch)))
|
|
idx++
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
func gradientColors(fromHex, toHex string, steps int) []string {
|
|
r1, g1, b1 := hexToRGB(fromHex)
|
|
r2, g2, b2 := hexToRGB(toHex)
|
|
out := make([]string, steps)
|
|
for i := 0; i < steps; i++ {
|
|
if steps == 1 {
|
|
out[i] = fromHex
|
|
continue
|
|
}
|
|
t := float64(i) / float64(steps-1)
|
|
r := int(float64(r1) + (float64(r2)-float64(r1))*t)
|
|
g := int(float64(g1) + (float64(g2)-float64(g1))*t)
|
|
b := int(float64(b1) + (float64(b2)-float64(b1))*t)
|
|
out[i] = fmt.Sprintf("#%02x%02x%02x", r, g, b)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func hexToRGB(hex string) (int, int, int) {
|
|
h := strings.TrimPrefix(hex, "#")
|
|
if len(h) != 6 {
|
|
return 255, 255, 255
|
|
}
|
|
var r, g, b int
|
|
fmt.Sscanf(h, "%02x%02x%02x", &r, &g, &b)
|
|
return r, g, b
|
|
}
|
|
|
|
func (a App) viewAchievements() string {
|
|
adaptiveColors := theme.NewAdaptiveColors(a.th)
|
|
gradientColors := adaptiveColors.GetGradientColors()
|
|
achGrad := gradientColors["banner"]
|
|
|
|
title := gradientText("Achievements", achGrad[0], achGrad[1])
|
|
|
|
var content strings.Builder
|
|
content.WriteString(title + "\n\n")
|
|
|
|
unlockedCount := a.achievements.GetUnlockedCount()
|
|
totalCount := a.achievements.GetTotalCount()
|
|
progressText := fmt.Sprintf("Unlocked: %d/%d\n\n", unlockedCount, totalCount)
|
|
content.WriteString(a.styles.Status.Render(progressText))
|
|
|
|
achievementOrder := []string{
|
|
"first_win", "perfectionist", "speed_demon", "no_mistakes",
|
|
"streak_master", "lunatic_legend", "daily_devotee", "century",
|
|
}
|
|
|
|
for _, id := range achievementOrder {
|
|
if ach, ok := a.achievements.Achievements[id]; ok {
|
|
var line string
|
|
if ach.Unlocked {
|
|
line = fmt.Sprintf("%s %s - %s",
|
|
ach.Icon,
|
|
lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("#10b981")).Render(ach.Name),
|
|
ach.Description)
|
|
} else {
|
|
progressBar := ""
|
|
if ach.Target > 1 {
|
|
progressBar = fmt.Sprintf(" [%d/%d]", ach.Progress, ach.Target)
|
|
}
|
|
line = fmt.Sprintf("%s %s - %s%s",
|
|
lipgloss.NewStyle().Foreground(lipgloss.Color("#6b7280")).Render("🔒"),
|
|
lipgloss.NewStyle().Foreground(lipgloss.Color("#9ca3af")).Render(ach.Name),
|
|
lipgloss.NewStyle().Foreground(lipgloss.Color("#6b7280")).Render(ach.Description),
|
|
progressBar)
|
|
}
|
|
content.WriteString(line + "\n")
|
|
}
|
|
}
|
|
|
|
content.WriteString("\n" + a.styles.Status.Render("Press 'm' or Enter to return to menu"))
|
|
|
|
panel := a.styles.Panel.Render(content.String())
|
|
if a.width > 0 && a.height > 0 {
|
|
return a.styles.App.Render(lipgloss.Place(a.width, a.height, lipgloss.Center, lipgloss.Center, panel))
|
|
}
|
|
return a.styles.App.Render(panel)
|
|
}
|
|
|
|
func (a App) viewLeaderboard() string {
|
|
adaptiveColors := theme.NewAdaptiveColors(a.th)
|
|
gradientColors := adaptiveColors.GetGradientColors()
|
|
leaderGrad := gradientColors["banner"]
|
|
|
|
title := gradientText("Leaderboard", leaderGrad[0], leaderGrad[1])
|
|
|
|
var content strings.Builder
|
|
content.WriteString(title + "\n\n")
|
|
|
|
difficulties := []string{"Easy", "Normal", "Hard", "Lunatic"}
|
|
for _, diff := range difficulties {
|
|
diffColors := adaptiveColors.GetDifficultyColors()
|
|
diffColor := diffColors[diff]
|
|
diffHeader := lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color(diffColor)).Render(diff)
|
|
content.WriteString(diffHeader + ":\n")
|
|
|
|
leaderboard := a.stats.GetLeaderboard(diff, 5)
|
|
if len(leaderboard) == 0 {
|
|
content.WriteString(a.styles.Status.Render(" No records yet\n\n"))
|
|
} else {
|
|
for i, record := range leaderboard {
|
|
medal := " "
|
|
switch i {
|
|
case 0:
|
|
medal = "🥇"
|
|
case 1:
|
|
medal = "🥈"
|
|
case 2:
|
|
medal = "🥉"
|
|
}
|
|
timeStr := stats.FormatTime(record.Time)
|
|
line := fmt.Sprintf("%s %d. %s", medal, i+1, timeStr)
|
|
if record.HintsUsed > 0 {
|
|
line += fmt.Sprintf(" (%d hints)", record.HintsUsed)
|
|
}
|
|
content.WriteString(a.styles.Status.Render(line + "\n"))
|
|
}
|
|
content.WriteString("\n")
|
|
}
|
|
}
|
|
|
|
content.WriteString(a.styles.Status.Render("Press 'm' or Enter to return to menu"))
|
|
|
|
panel := a.styles.Panel.Render(content.String())
|
|
if a.width > 0 && a.height > 0 {
|
|
return a.styles.App.Render(lipgloss.Place(a.width, a.height, lipgloss.Center, lipgloss.Center, panel))
|
|
}
|
|
return a.styles.App.Render(panel)
|
|
}
|