diff --git a/internal/commands/commands.go b/internal/commands/commands.go index 870ef04..846d088 100644 --- a/internal/commands/commands.go +++ b/internal/commands/commands.go @@ -64,6 +64,14 @@ func ArtistAlbums(ctx *gctx.Context, client *spotify.Client, artist spotify.ID, return albums, nil } +func Search(ctx *gctx.Context, client *spotify.Client, search string, page int) (*spotify.SearchResult, error) { + result, err := client.Search(ctx, search, spotify.SearchTypeAlbum|spotify.SearchTypeArtist|spotify.SearchTypeTrack|spotify.SearchTypePlaylist, spotify.Limit(50), spotify.Offset((page-1)*50)) + if err != nil { + return nil, err + } + return result, nil +} + func AlbumTracks(ctx *gctx.Context, client *spotify.Client, album spotify.ID, page int) (*spotify.SimpleTrackPage, error) { tracks, err := client.GetAlbumTracks(ctx, album, spotify.Limit(50), spotify.Offset((page-1)*50), spotify.Market(spotify.CountryUSA)) if err != nil { diff --git a/internal/tui/main.go b/internal/tui/main.go index 79bf320..438e15d 100644 --- a/internal/tui/main.go +++ b/internal/tui/main.go @@ -8,6 +8,7 @@ import ( "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/bubbles/list" + "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" "github.com/zmb3/spotify/v2" @@ -36,12 +37,14 @@ func (i mainItem) FilterValue() string { return i.Title() + i.Desc } type mainModel struct { list list.Model + input textinput.Model ctx *gctx.Context client *spotify.Client mode string playlist spotify.SimplePlaylist artist spotify.SimpleArtist album spotify.SimpleAlbum + search string fromArtist bool } @@ -70,10 +73,14 @@ func (m *mainModel) Tick() { } func (m mainModel) View() string { - return DocStyle.Render(m.list.View()) + if m.input.Focused() { + return DocStyle.Render(m.list.View() + "\n" + m.input.View()) + } + return DocStyle.Render(m.list.View() + "\n") } func (m mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + search := false m.list.NewStatusMessage(currentlyPlaying) select { case update := <-main_updates: @@ -88,19 +95,54 @@ func (m mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } switch msg := msg.(type) { case tea.KeyMsg: - if msg.String() == "d" { - m.mode = "devices" - new_items, err := DeviceView(m.ctx, m.client) - if err != nil { - fmt.Println(err.Error()) - return m, tea.Quit + if m.input.Focused() { + if msg.String() == "enter" { + m.list.NewStatusMessage("Setting view to search for " + m.input.Value()) + items, err := SearchView(m.ctx, m.client, m.input.Value()) + if err != nil { + fmt.Println(err.Error()) + return m, tea.Quit + } + m.search = m.input.Value() + m.list.SetItems(items) + m.list.ResetSelected() + m.input.SetValue("") + m.input.Blur() + search = true + } + m.input, _ = m.input.Update(msg) + } + if msg.String() == "s" { + m.input.Focus() + } + if msg.String() == "d" { + if !m.input.Focused() { + m.mode = "devices" + new_items, err := DeviceView(m.ctx, m.client) + if err != nil { + fmt.Println(err.Error()) + return m, tea.Quit + } + m.list.SetItems(new_items) + m.list.ResetSelected() + m.list.NewStatusMessage("Setting view to devices") } - m.list.SetItems(new_items) - m.list.ResetSelected() - m.list.NewStatusMessage("Setting view to devices") } if msg.String() == "backspace" || msg.String() == "esc" || msg.String() == "q" { - if m.mode == "album" { + if m.input.Focused() { + if msg.String() == "esc" { + m.input.SetValue("") + m.input.Blur() + m.list.SetShowPagination(true) + m.mode = "main" + m.list.NewStatusMessage("Setting view to main") + new_items, err := MainView(m.ctx, m.client) + if err != nil { + fmt.Println(err.Error()) + } + m.list.SetItems(new_items) + } + } else if m.mode == "album" { if m.fromArtist { m.mode = "albums" m.fromArtist = true @@ -163,6 +205,51 @@ func (m mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } if msg.String() == "enter" || msg.String() == "spacebar" { switch m.mode { + case "search": + switch m.list.SelectedItem().(mainItem).SpotifyItem.(type) { + case *spotify.FullArtistPage: + m.mode = "searchartists" + m.list.NewStatusMessage("Setting view to artists") + new_items, err := SearchArtistsView(m.ctx, m.client, m.list.SelectedItem().(mainItem).SpotifyItem.(*spotify.FullArtistPage)) + if err != nil { + fmt.Println(err.Error()) + return m, tea.Quit + } + m.list.SetItems(new_items) + m.list.ResetSelected() + case *spotify.SimpleAlbumPage: + m.mode = "searchalbums" + m.list.NewStatusMessage("Setting view to albums") + new_items, err := SearchAlbumsView(m.ctx, m.client, m.list.SelectedItem().(mainItem).SpotifyItem.(*spotify.SimpleAlbumPage)) + if err != nil { + fmt.Println(err.Error()) + return m, tea.Quit + } + m.list.SetItems(new_items) + m.list.ResetSelected() + case *spotify.SimplePlaylistPage: + m.mode = "searchplaylist" + playlists := m.list.SelectedItem().(mainItem).SpotifyItem.(*spotify.SimplePlaylistPage) + m.list.NewStatusMessage("Setting view to playlist") + new_items, err := SearchPlaylistsView(m.ctx, m.client, playlists) + if err != nil { + fmt.Println(err.Error()) + return m, tea.Quit + } + m.list.SetItems(new_items) + m.list.ResetSelected() + case *spotify.FullTrackPage: + m.mode = "searchtracks" + m.list.NewStatusMessage("Setting view to tracks") + new_items, err := SearchTracksView(m.ctx, m.client, m.list.SelectedItem().(mainItem).SpotifyItem.(*spotify.FullTrackPage)) + if err != nil { + fmt.Println(err.Error()) + return m, tea.Quit + } + m.list.SetItems(new_items) + m.list.ResetSelected() + m.list.NewStatusMessage("Setting view to tracks") + } case "main": switch m.list.SelectedItem().(mainItem).SpotifyItem.(type) { case *spotify.FullArtistCursorPage: @@ -296,9 +383,11 @@ func (m mainModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } case tea.WindowSizeMsg: h, v := DocStyle.GetFrameSize() - m.list.SetSize(msg.Width-h, msg.Height-v) + m.list.SetSize(msg.Width-h, msg.Height-v-1) + } + if search { + m.mode = "search" } - var cmd tea.Cmd m.list, cmd = m.list.Update(msg) return m, cmd @@ -353,5 +442,11 @@ func InitMain(ctx *gctx.Context, client *spotify.Client, mode string) (tea.Model key.NewBinding(key.WithKeys("d"), key.WithHelp("d", "select device")), } } + input := textinput.New() + input.Prompt = "$ " + input.Placeholder = "Search..." + input.CharLimit = 250 + input.Width = 50 + m.input = input return m, nil } diff --git a/internal/tui/views.go b/internal/tui/views.go index 1949c53..9f2fa00 100644 --- a/internal/tui/views.go +++ b/internal/tui/views.go @@ -63,6 +63,49 @@ func ArtistsView(ctx *gctx.Context, client *spotify.Client) ([]list.Item, error) return items, nil } +func SearchArtistsView(ctx *gctx.Context, client *spotify.Client, artists *spotify.FullArtistPage) ([]list.Item, error) { + items := []list.Item{} + for _, artist := range artists.Artists { + items = append(items, mainItem{ + Name: artist.Name, + ID: artist.ID, + Desc: fmt.Sprintf("%d followers, genres: %s, popularity: %d", artist.Followers.Count, artist.Genres, artist.Popularity), + SpotifyItem: artist.SimpleArtist, + }) + } + return items, nil +} + +func SearchView(ctx *gctx.Context, client *spotify.Client, search string) ([]list.Item, error) { + items := []list.Item{} + + result, err := commands.Search(ctx, client, search, 1) + if err != nil { + return nil, err + } + items = append(items, mainItem{ + Name: "Tracks", + Desc: "Search results", + SpotifyItem: result.Tracks, + }) + items = append(items, mainItem{ + Name: "Albums", + Desc: "Search results", + SpotifyItem: result.Albums, + }) + items = append(items, mainItem{ + Name: "Artists", + Desc: "Search results", + SpotifyItem: result.Artists, + }) + items = append(items, mainItem{ + Name: "Playlists", + Desc: "Search results", + SpotifyItem: result.Playlists, + }) + return items, nil +} + func AlbumsView(ctx *gctx.Context, client *spotify.Client) ([]list.Item, error) { items := []list.Item{} albums, err := commands.UserAlbums(ctx, client, 1) @@ -80,6 +123,31 @@ func AlbumsView(ctx *gctx.Context, client *spotify.Client) ([]list.Item, error) return items, nil } +func SearchPlaylistsView(ctx *gctx.Context, client *spotify.Client, playlists *spotify.SimplePlaylistPage) ([]list.Item, error) { + items := []list.Item{} + for _, playlist := range playlists.Playlists { + items = append(items, mainItem{ + Name: playlist.Name, + Desc: playlist.Description, + SpotifyItem: playlist, + }) + } + return items, nil +} + +func SearchAlbumsView(ctx *gctx.Context, client *spotify.Client, albums *spotify.SimpleAlbumPage) ([]list.Item, error) { + items := []list.Item{} + for _, album := range albums.Albums { + items = append(items, mainItem{ + Name: album.Name, + ID: album.ID, + Desc: fmt.Sprintf("%s, %d", album.Artists[0].Name, album.ReleaseDateTime()), + SpotifyItem: album, + }) + } + return items, nil +} + func ArtistAlbumsView(ctx *gctx.Context, album spotify.ID, client *spotify.Client) ([]list.Item, error) { items := []list.Item{} albums, err := commands.ArtistAlbums(ctx, client, album, 1) @@ -115,6 +183,20 @@ func AlbumTracksView(ctx *gctx.Context, album spotify.ID, client *spotify.Client return items, err } +func SearchTracksView(ctx *gctx.Context, client *spotify.Client, tracks *spotify.FullTrackPage) ([]list.Item, error) { + items := []list.Item{} + for _, track := range tracks.Tracks { + items = append(items, mainItem{ + Name: track.Name, + Artist: track.Artists[0], + Duration: track.TimeDuration().Round(time.Second).String(), + ID: track.ID, + Desc: track.Artists[0].Name + " - " + track.TimeDuration().Round(time.Second).String(), + }) + } + return items, nil +} + func SavedTracksView(ctx *gctx.Context, client *spotify.Client) ([]list.Item, error) { items := []list.Item{} tracks, err := commands.TrackList(ctx, client, 1)