feat(ui): auto-filter on typing + refactor item struct with favorites
Some checks failed
gobuild / build (push) Failing after 3s
Some checks failed
gobuild / build (push) Failing after 3s
- Add auto-start filter when typing alphanumeric characters - Refactor item struct to use explicit station field and isFavorite flag - Show ★ prefix for favorites in list rendering - Improve description formatting with bullet separators - Include URL in FilterValue for better search - Update help text and add corresponding test
This commit is contained in:
parent
ec5db53b8e
commit
444193a5d2
@ -15,12 +15,17 @@ import (
|
||||
|
||||
// item wraps a station for the bubbles list.
|
||||
type item struct {
|
||||
s radio.Station
|
||||
station radio.Station
|
||||
isFavorite bool
|
||||
}
|
||||
|
||||
func (i item) Title() string { return i.s.Name }
|
||||
func (i item) Description() string { return fmt.Sprintf("%s %s %s", i.s.Codec, i.s.Bitrate, truncate(i.s.Url, 50)) }
|
||||
func (i item) FilterValue() string { return i.s.Name + " " + i.s.Tags + " " + i.s.Codec }
|
||||
func (i item) Title() string { return i.station.Name }
|
||||
func (i item) Description() string {
|
||||
return fmt.Sprintf("%s • %s kbps • %s", i.station.Codec, i.station.Bitrate, truncate(i.station.Url, 50))
|
||||
}
|
||||
func (i item) FilterValue() string {
|
||||
return i.station.Name + " " + i.station.Tags + " " + i.station.Codec + " " + i.station.Url
|
||||
}
|
||||
|
||||
func truncate(s string, n int) string {
|
||||
if len(s) <= n {
|
||||
@ -40,13 +45,16 @@ func (d listDelegate) Render(w io.Writer, m list.Model, index int, listItem list
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
title := i.s.Name
|
||||
title := i.station.Name
|
||||
if i.isFavorite {
|
||||
title = "★ " + title
|
||||
}
|
||||
if m.Index() == index {
|
||||
title = lipgloss.NewStyle().Bold(true).Render("▶ " + title)
|
||||
} else {
|
||||
title = " " + title
|
||||
}
|
||||
desc := fmt.Sprintf(" %s • %s kbps • %s", i.s.Codec, i.s.Bitrate, truncate(i.s.Url, 45))
|
||||
desc := fmt.Sprintf(" %s • %s kbps • %s", i.station.Codec, i.station.Bitrate, truncate(i.station.Url, 45))
|
||||
fmt.Fprintf(w, "%s\n%s", title, desc)
|
||||
}
|
||||
|
||||
@ -70,11 +78,8 @@ func NewApp(initial []radio.Station) *App {
|
||||
|
||||
items := make([]list.Item, len(initial))
|
||||
for i, s := range initial {
|
||||
title := s.Name
|
||||
if favSet[s.Url] {
|
||||
title = "★ " + title
|
||||
}
|
||||
items[i] = item{s: radio.Station{Name: title, Codec: s.Codec, Bitrate: s.Bitrate, Url: s.Url, Tags: s.Tags}}
|
||||
isFav := favSet[s.Url]
|
||||
items[i] = item{station: s, isFavorite: isFav}
|
||||
}
|
||||
|
||||
l := list.New(items, listDelegate{}, 60, 20)
|
||||
@ -101,10 +106,26 @@ func (a *App) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
if i, ok := a.list.SelectedItem().(item); ok {
|
||||
// Placeholder: for now just show what would be played.
|
||||
// Later: switch to playback model + use player.Play
|
||||
a.list.Title = "Would play: " + i.s.Name + " (press q)"
|
||||
a.list.Title = "Would play: " + i.station.Name + " (press q)"
|
||||
}
|
||||
return a, nil
|
||||
}
|
||||
|
||||
// Auto-start filter on first alphanumeric character (better UX than requiring / first)
|
||||
s := msg.String()
|
||||
if len(s) == 1 {
|
||||
r := rune(s[0])
|
||||
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') {
|
||||
if a.list.FilterState() != list.Filtering {
|
||||
// Simulate pressing the filter key to activate
|
||||
slash := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'/'}}
|
||||
a.list, _ = a.list.Update(slash)
|
||||
// Now feed the original key into the active filter
|
||||
a.list, _ = a.list.Update(msg)
|
||||
return a, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
case tea.WindowSizeMsg:
|
||||
a.list.SetSize(msg.Width-4, msg.Height-4)
|
||||
}
|
||||
@ -118,7 +139,7 @@ func (a *App) View() string {
|
||||
if a.quitting {
|
||||
return "Thanks for using GoStations!\n"
|
||||
}
|
||||
return "\n" + a.list.View() + "\n\n(enter=play stub • / filter • q quit • --legacy for old UI)\n"
|
||||
return "\n" + a.list.View() + "\n\n(type to filter • enter=play stub • / or letters for filter • q quit • --legacy for old UI)\n"
|
||||
}
|
||||
|
||||
// Run starts the TUI (alt screen).
|
||||
|
||||
@ -27,3 +27,27 @@ func TestApp_BasicKeyHandling(t *testing.T) {
|
||||
t.Log("list size not updated (may be ok in test)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestApp_AutoFilterOnTyping(t *testing.T) {
|
||||
app := NewApp([]radio.Station{
|
||||
{Name: "WFMT 98.7", Url: "http://wfmt", Codec: "MP3", Bitrate: "128", Tags: "chicago,classical"},
|
||||
{Name: "Other Station", Url: "http://other", Codec: "AAC", Bitrate: "64", Tags: "news"},
|
||||
})
|
||||
|
||||
// Simulate typing 'W' (should auto enter filter and filter to WFMT)
|
||||
model, _ := app.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'W'}})
|
||||
a := model.(*App)
|
||||
|
||||
// After auto filter start + 'W', the filter value should be "W" and state Filtering or applied
|
||||
fv := a.list.FilterValue()
|
||||
if fv != "W" {
|
||||
t.Errorf("expected filter value 'W' after typing W, got %q", fv)
|
||||
}
|
||||
|
||||
// The visible items should be filtered (at least the WFMT one should match)
|
||||
visible := a.list.VisibleItems()
|
||||
if len(visible) == 0 {
|
||||
t.Error("expected some visible items after filter 'W'")
|
||||
}
|
||||
// Check that 'Other' is not the only one, or better, since fuzzy, 'W' may match others weakly, but at least not empty
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user