From 8c209e0ed32d0ed1e98643fe1c9ff52795f8c27a Mon Sep 17 00:00:00 2001 From: szktkfm Date: Mon, 22 Jan 2024 22:46:32 +0900 Subject: [PATCH] Added the device selection feature. (#2) --- README.md | 1 + animtext.go | 4 +-- auth.go | 1 + bar.go | 4 +++ help.go | 17 ++++++++---- list.go | 2 +- spotify.go | 14 ++++++++++ style.go | 6 +--- tab.go | 78 +++++++++++++++++++++++++++++++++++++++++++--------- textinput.go | 5 +--- util.go | 5 +--- 11 files changed, 103 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 9de806a..00d15cb 100644 --- a/README.md +++ b/README.md @@ -43,4 +43,5 @@ Here are the key bindings for sptui: | `:pause` | Pause playback | | `:next` | Next track | | `:prev` | Previous track | +| `:device` | Select a device | diff --git a/animtext.go b/animtext.go index 08646b2..6e41be5 100644 --- a/animtext.go +++ b/animtext.go @@ -15,9 +15,9 @@ type AnimTextModel struct { } func (m AnimTextModel) UpdateAnimText(msg tea.Msg) (AnimTextModel, tea.Cmd) { - switch msg.(type) { + switch msg := msg.(type) { case animTextTickMsg: - if m.id != msg.(animTextTickMsg).id { + if m.id != msg.id { return m, nil } m.offset = (m.offset + 1) % len(m.text) diff --git a/auth.go b/auth.go index ae383a7..2c34444 100644 --- a/auth.go +++ b/auth.go @@ -32,6 +32,7 @@ var ( spotifyauth.WithScopes( spotifyauth.ScopeUserReadPrivate, spotifyauth.ScopeUserModifyPlaybackState, + spotifyauth.ScopeUserReadPlaybackState, spotifyauth.ScopeUserLibraryRead, spotifyauth.ScopePlaylistReadCollaborative, spotifyauth.ScopePlaylistReadPrivate, diff --git a/bar.go b/bar.go index 79554da..ae96f05 100644 --- a/bar.go +++ b/bar.go @@ -118,3 +118,7 @@ func tickCmd(id string) tea.Cmd { } }) } + +func (m BarModel) PositionMs() int { + return int(1000 / m.deltaDur * m.percent) +} diff --git a/help.go b/help.go index 571eb73..0871d98 100644 --- a/help.go +++ b/help.go @@ -6,11 +6,12 @@ import ( ) type KeyMap struct { - Play key.Binding - Pause key.Binding - Next key.Binding - Prev key.Binding - Help key.Binding + Play key.Binding + Pause key.Binding + Next key.Binding + Prev key.Binding + Help key.Binding + Device key.Binding } type HelpModel struct { @@ -45,6 +46,10 @@ func NewHelp() HelpModel { key.WithKeys(""), key.WithHelp(":help", ""), ), + Device: key.NewBinding( + key.WithKeys(""), + key.WithHelp(":device", ""), + ), //TODO: add more keybindings }, } @@ -59,6 +64,7 @@ func (m HelpModel) ShortHelp() []key.Binding { m.KeyMap.Pause, m.KeyMap.Next, m.KeyMap.Prev, + m.KeyMap.Device, } } @@ -69,6 +75,7 @@ func (m HelpModel) FullHelp() [][]key.Binding { m.KeyMap.Pause, m.KeyMap.Next, m.KeyMap.Prev, + m.KeyMap.Device, m.KeyMap.Help, }, } diff --git a/list.go b/list.go index a476fa7..5c0ba7d 100644 --- a/list.go +++ b/list.go @@ -30,7 +30,7 @@ func (d itemDelegate) Render(w io.Writer, m list.Model, index int, listItem list return } - str := CleanString(fmt.Sprintf("%s", i)) + str := CleanString(string(i)) var fn func(strs ...string) string if index == m.Index() { diff --git a/spotify.go b/spotify.go index 412fbcd..921dbeb 100644 --- a/spotify.go +++ b/spotify.go @@ -39,6 +39,10 @@ type CurrentlyPlayingMsg struct { Track *spotify.CurrentlyPlaying } +type PlayerDevicesMsg struct { + PlayerDevices []spotify.PlayerDevice +} + type PlaybackMsg struct { } @@ -114,6 +118,16 @@ func GetCurrentlyPlayingTrackCmd(client *spotify.Client) tea.Cmd { } } +func GetAvailableDevicesCmd(client *spotify.Client) tea.Cmd { + return func() tea.Msg { + device, err := client.PlayerDevices(context.Background()) + if err != nil { + return ErrMsg{Err: err} + } + return PlayerDevicesMsg{PlayerDevices: device} + } +} + func StartPlaybackCmd(client *spotify.Client, opts *spotify.PlayOptions) tea.Cmd { return func() tea.Msg { err := client.PlayOpt(context.Background(), diff --git a/style.go b/style.go index f86b271..5fc12be 100644 --- a/style.go +++ b/style.go @@ -11,11 +11,7 @@ var ( inactiveTabStyle = lipgloss.NewStyle().Border(inactiveTabBorder, true). BorderForeground(highlightColor).Padding(0, 1) activeTabStyle = inactiveTabStyle.Copy().Border(activeTabBorder, true) - borderStyle = lipgloss.NewStyle(). - Border(lipgloss.NormalBorder(), false, false, false, true). - BorderForeground(lipgloss.Color("5")) - - windowStyle = lipgloss.NewStyle(). + windowStyle = lipgloss.NewStyle(). BorderForeground(highlightColor). Padding(1, 5). Align(lipgloss.Left). diff --git a/tab.go b/tab.go index 6bc8403..2f4d21d 100644 --- a/tab.go +++ b/tab.go @@ -42,7 +42,7 @@ type TabModel struct { tabs []string activeTab int tabContents []ListModel - trackList ListModel + listView ListModel progress BarModel @@ -65,6 +65,9 @@ type TabModel struct { selectedPlaylist *spotify.FullPlaylist currentlyPlaying *spotify.CurrentlyPlaying + currentDevice *spotify.PlayerDevice + devices []spotify.PlayerDevice + deviceMode bool } func (m TabModel) Init() tea.Cmd { @@ -132,6 +135,10 @@ func (m TabModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return execTxtCommand(m) } + if m.deviceMode { + return playOnDevice(m) + } + if m.depth == TRACKLIST { return playTrack(m) } @@ -156,6 +163,14 @@ func (m TabModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, tea.Batch(tickCmd(tickID), AnimTextTickCmd(tickID, 2000*time.Millisecond)) + case PlayerDevicesMsg: + m.listView = NewListModel(playerDeviceToItemList(msg.PlayerDevices), + WithTitle("Select Device"), + ) + m.deviceMode = true + m.devices = msg.PlayerDevices + m.depth = TRACKLIST + case PlaybackMsg: //TODO: Fix time.Sleep(500 * time.Millisecond) @@ -180,6 +195,22 @@ func (m TabModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } +func playOnDevice(m TabModel) (tea.Model, tea.Cmd) { + m.currentDevice = &m.devices[m.listView.list.Index()] + m.deviceMode = false + m.depth = TOP + if m.currentlyPlaying != nil && m.currentlyPlaying.Playing { + return m, StartPlaybackCmd(m.client, + &spotify.PlayOptions{ + DeviceID: &m.currentDevice.ID, + URIs: []spotify.URI{m.currentlyPlaying.Item.URI}, + PositionMs: m.progress.PositionMs(), + }, + ) + } + return m, nil +} + func execTxtCommand(m TabModel) (tea.Model, tea.Cmd) { txtCmd := m.textInput.textInput.Value() m.textMode = NONE @@ -193,18 +224,31 @@ func execTxtCommand(m TabModel) (tea.Model, tea.Cmd) { if m.progress.IsPlaying { return m, nil } - return m, StartPlaybackCmd(m.client, - &spotify.PlayOptions{ + + var opt *spotify.PlayOptions + if m.devices != nil { + opt = &spotify.PlayOptions{ + DeviceID: &m.currentDevice.ID, URIs: []spotify.URI{m.currentlyPlaying.Item.URI}, PositionMs: m.currentlyPlaying.Progress, - }, - ) + } + + } else { + opt = &spotify.PlayOptions{ + URIs: []spotify.URI{m.currentlyPlaying.Item.URI}, + PositionMs: m.currentlyPlaying.Progress, + } + } + return m, StartPlaybackCmd(m.client, opt) + case "pause": return m, PausePlaybackCmd(m.client) case "next": return m, NextPlaybackCmd(m.client) case "prev": return m, PreviousPlaybackCmd(m.client) + case "device": + return m, GetAvailableDevicesCmd(m.client) default: return m, nil @@ -212,7 +256,7 @@ func execTxtCommand(m TabModel) (tea.Model, tea.Cmd) { } func playTrack(m TabModel) (tea.Model, tea.Cmd) { - selected := m.trackList.list.Index() + selected := m.listView.list.Index() switch m.activeTab { case PLAYLIST: return m, StartPlaybackCmd(m.client, @@ -250,20 +294,20 @@ func listUpdate(m TabModel, msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case AlbumDetailMsg: m.selectedAlbum = msg.Album - m.trackList = NewListModel( + m.listView = NewListModel( albumTracksToItemList(msg.Album.Tracks.Tracks), WithTitle(msg.Album.Name+" ("+msg.Album.Artists[0].Name+")"), ) case ShowDetailMsg: m.episodes = msg.Show.Episodes.Episodes - m.trackList = NewListModel(episodesToItemList(m.episodes), + m.listView = NewListModel(episodesToItemList(m.episodes), WithTitle(msg.Show.Name), ) case PlaylistDetailMsg: m.selectedPlaylist = msg.Playlist - m.trackList = NewListModel(playlistTracksToItemList(msg.Playlist.Tracks.Tracks), + m.listView = NewListModel(playlistTracksToItemList(msg.Playlist.Tracks.Tracks), WithTitle(msg.Playlist.Name), ) @@ -275,8 +319,8 @@ func listUpdate(m TabModel, msg tea.Msg) (tea.Model, tea.Cmd) { } } - newListModel, cmd := m.trackList.UpdateList(msg, m.depth) - m.trackList = newListModel + newListModel, cmd := m.listView.UpdateList(msg, m.depth) + m.listView = newListModel return m, cmd } @@ -383,7 +427,7 @@ func tabUpdate(msg tea.Msg, m TabModel) (tea.Model, tea.Cmd) { func getTracks(m TabModel) (tea.Model, tea.Cmd) { m.depth = TRACKLIST - m.trackList = NewListModel([]list.Item{item(loading)}) + m.listView = NewListModel([]list.Item{item(loading)}) selected := m.tabContents[m.activeTab].list.Index() switch m.activeTab { @@ -398,6 +442,14 @@ func getTracks(m TabModel) (tea.Model, tea.Cmd) { } } +func playerDeviceToItemList(devices []spotify.PlayerDevice) []list.Item { + var itemList []list.Item + for _, d := range devices { + itemList = append(itemList, item(d.Name)) + } + return itemList +} + func playlistTracksToItemList(tracks []spotify.PlaylistTrack) []list.Item { var itemList []list.Item for _, t := range tracks { @@ -522,7 +574,7 @@ func tracksView(m TabModel) string { doc.WriteString( windowStyleDtl. - Render(m.trackList.View(m.depth))) + Render(m.listView.View(m.depth))) return docStyle.Render(doc.String()) } diff --git a/textinput.go b/textinput.go index d40ce75..1cd2784 100644 --- a/textinput.go +++ b/textinput.go @@ -1,7 +1,6 @@ package sptui import ( - "fmt" "strings" "github.com/charmbracelet/bubbles/textinput" @@ -56,9 +55,7 @@ func (m TextModel) UpdateText(msg tea.Msg) (TextModel, tea.Cmd) { func (m TextModel) ViewText(textMode int) string { pad := strings.Repeat(" ", padding) if textMode == INPUT || textMode == ERROR { - return pad + fmt.Sprintf( - m.textInput.View(), - ) + return pad + m.textInput.View() } return "" } diff --git a/util.go b/util.go index f2cebdc..e83e92b 100644 --- a/util.go +++ b/util.go @@ -1,7 +1,6 @@ package sptui import ( - "fmt" "strings" "github.com/mattn/go-runewidth" @@ -24,8 +23,6 @@ const ( empty = "" ) -var replacer = strings.NewReplacer(string(ZWSP), empty) - func RemoveZeroWidthSpace(s string) string { return strings.Replace(s, string(ZWSP), empty, -1) } @@ -63,7 +60,7 @@ func WrapText(s string, width int, line int) string { func PadOrTruncate(s string, n int) string { if runewidth.StringWidth(s) > listWidth { - return fmt.Sprintf("%s", runewidth.Truncate(s, n, "")) + return runewidth.Truncate(s, n, "") } else { return s + strings.Repeat(" ", listWidth-runewidth.StringWidth(s)) }