Merge pull request #5 from acaloiaro/add-status-bar

Add status bar
This commit is contained in:
Adriano 2020-03-25 20:41:00 -04:00 committed by GitHub
commit c27b6450c4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 144 additions and 61 deletions

View file

@ -1,8 +1,8 @@
package app
import (
"fmt"
"io/ioutil"
"log"
"net/http"
"time"
@ -27,8 +27,8 @@ func PlayChannel(chn *components.ChannelItem, ctx *context.AppContext) {
client := &http.Client{}
req, _ := http.NewRequest("GET", chn.Playlist, nil)
resp, err := client.Do(req)
if err != nil {
log.Fatal(err)
if err != nil || resp.StatusCode != 200 {
ctx.SetStatusMessage(fmt.Sprintf("Unable to stream channel: %s", chn.Name))
}
defer resp.Body.Close()
@ -65,6 +65,7 @@ func TogglePause(ctx *context.AppContext) {
ctx.IsPlaying = !ctx.IsPlaying
}
// UpdateNowPlaying updates the application's now playing view with the currently playing channel
func UpdateNowPlaying(chn *components.ChannelItem, ctx *context.AppContext) {
ctx.CurrentChannel = chn
cp := difm.GetCurrentlyPlaying(ctx)

View file

@ -30,3 +30,9 @@ type Track struct {
Duration float64 `json:"duration"`
StartTime time.Time `json:"start_time"`
}
// StatusMessage is a message to display in the application for a fixed period of time
type StatusMessage struct {
Message string
Duration time.Duration
}

View file

@ -1,6 +1,8 @@
package context
import (
"time"
"github.com/acaloiaro/di-tui/components"
"github.com/acaloiaro/di-tui/views"
"github.com/faiface/beep"
@ -8,17 +10,40 @@ import (
// CreateAppContext creates the application context
func CreateAppContext(view *views.ViewContext) *AppContext {
ctx := &AppContext{View: view}
ctx := &AppContext{
View: view,
StatusChannel: make(chan components.StatusMessage, 1),
}
return ctx
}
// AppContext is a shared context to be shared across the application
// AudioStream - The raw audio stream used by the beep library to stream audio
// CurrentChannel - The ChannelItem representing the currently playing di.fm channel
// DfmToken - The token used to authenticate to di.fm
// IsPlaying - Is there audio playing?
// SpeakerInitialized - Has the speaker been initialized with a bitrate?
// View - The view context
// Status - Gets and sets current application status messages
type AppContext struct {
AudioStream beep.StreamSeekCloser
CurrentChannel *components.ChannelItem
DifmToken string
IsPlaying bool
ShowStatus bool // The status pane will be visible when true
SpeakerInitialized bool
StatusChannel chan components.StatusMessage
View *views.ViewContext
}
// SetStatusMessage sets the application's status message for three seconds.
func (c *AppContext) SetStatusMessage(msg string) {
c.SetStatusMessageTimed(msg, 5*time.Second)
}
// SetStatusMessageTimed sets the application's status message for a fixed period of time.
func (c *AppContext) SetStatusMessageTimed(msg string, d time.Duration) {
c.ShowStatus = true
c.StatusChannel <- components.StatusMessage{Message: msg, Duration: d}
}

View file

@ -39,7 +39,7 @@ func main() {
os.Exit(1)
}
ctx = context.CreateAppContext(views.CreateAppView())
ctx = context.CreateAppContext(views.CreateViewContext())
ctx.DifmToken = token
run()
@ -47,20 +47,19 @@ func main() {
func run() {
configureEventHandling()
updateScreenLayout()
configureUIComponents()
layout := buildUILayout()
err := ctx.View.App.
SetRoot(layout, true).
SetFocus(ctx.View.FavoriteList).
Run()
err := ctx.View.App.Run()
if err != nil {
panic(err)
}
}
func buildUILayout() *tview.Flex {
func updateScreenLayout() {
focusView := ctx.View.App.GetFocus()
main := tview.NewFlex()
main.SetDirection(tview.FlexRow)
@ -69,20 +68,29 @@ func buildUILayout() *tview.Flex {
AddItem(ctx.View.ChannelList, 0, 2, false).
SetDirection(tview.FlexRow)
flex := tview.NewFlex()
flex.
primaryView := tview.NewFlex()
primaryView.
AddItem(favsAndChannels, 30, 0, false).
AddItem(ctx.View.NowPlaying, 0, 4, false)
if ctx.ShowStatus {
main.AddItem(ctx.View.Status, 4, 0, false)
}
main.
AddItem(flex, 0, 3, false).
AddItem(primaryView, 0, 3, false).
AddItem(ctx.View.Keybindings, 4, 0, false)
return main
if focusView == nil {
focusView = ctx.View.FavoriteList
}
ctx.View.App.
SetRoot(main, true).
SetFocus(focusView)
}
func configureEventHandling() {
ctx.View.App.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
focus := ctx.View.App.GetFocus().(*tview.List)
@ -130,12 +138,26 @@ func configureEventHandling() {
ctx.View.App.QueueUpdateDraw(func() {})
}
}()
// display the status pane when new status messages arrive
go func() {
for {
status := <-ctx.StatusChannel
ctx.View.Status.Message = status.Message
updateScreenLayout() // add the status pane to the screen
<-time.Tick(status.Duration)
ctx.ShowStatus = false
updateScreenLayout() // remove the status pane from the screen
}
}()
}
func configureUIComponents() {
// configure the channel list
channels := difm.ListChannels()
channels := difm.ListChannels(ctx)
for _, chn := range channels {
ctx.View.ChannelList.AddItem(chn.Name, "", 0, func() {
chn := channels[ctx.View.ChannelList.GetCurrentItem()]

View file

@ -81,16 +81,17 @@ func GetCurrentlyPlaying(ctx *context.AppContext) (currentlyPlaying components.C
client := &http.Client{}
req, _ := http.NewRequest("GET", "https://www.di.fm/_papi/v1/di/currently_playing", nil)
resp, err := client.Do(req)
if err != nil {
if err != nil || resp.StatusCode != 200 {
ctx.SetStatusMessage("Unable to fetch currently playing track info.")
return
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
// TODO: Don't exit here. Once there's a status message area in the app, populate it with the error
log.Println("unable to list channels", err.Error())
os.Exit(1)
if err != nil || resp.StatusCode != 200 {
ctx.SetStatusMessage("Unable to fetch currently playing track info.")
return
}
var currentlyPlayingStations []components.CurrentlyPlaying
@ -106,7 +107,7 @@ func GetCurrentlyPlaying(ctx *context.AppContext) (currentlyPlaying components.C
}
// ListChannels lists all premium MP3 channels
func ListChannels() (channels []components.ChannelItem) {
func ListChannels(ctx *context.AppContext) (channels []components.ChannelItem) {
client := &http.Client{}
req, _ := http.NewRequest("GET", "http://listen.di.fm/premium_high", nil)
resp, err := client.Do(req)
@ -117,15 +118,16 @@ func ListChannels() (channels []components.ChannelItem) {
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
// TODO: Don't exit here. Once there's a status message area in the app, populate it with the error
log.Println("unable to list channels", err.Error())
os.Exit(1)
msg := fmt.Sprintf("Unable to fetch the list of channels: %s", err.Error())
ctx.SetStatusMessage(msg)
return
}
err = json.Unmarshal(body, &channels)
if err != nil {
// TODO: Don't exit here. Once there's a status message area in the app, populate it with the error
log.Panicf("unable to fetch channel list: %e", err)
msg := fmt.Sprintf("Unable to fetch the list of channels: %s", err.Error())
ctx.SetStatusMessage(msg)
return
}
return
@ -146,8 +148,8 @@ func ListFavorites(ctx *context.AppContext) (favorites []components.FavoriteItem
body, err := ioutil.ReadAll(resp.Body)
cfg, err := ini.Load(body)
if err != nil {
// TODO: show an message in a status UI element of the app
fmt.Printf("favorite list parsing failed: %v\n", err.Error())
msg := fmt.Sprintf("There was a problem fetching your favorites: %s", err.Error())
ctx.SetStatusMessage(msg)
return
}
@ -172,16 +174,16 @@ func Stream(url string, ctx *context.AppContext) (format beep.Format) {
req, _ := http.NewRequest("GET", u, nil)
resp, err := client.Do(req)
if err != nil {
// TODO: Don't exit here. Once there's a status message area in the app, populate it with the error
log.Println("unable to stream channel", err.Error())
os.Exit(1)
msg := fmt.Sprintf("There was a problem streaming this channel channels: %s", err.Error())
ctx.SetStatusMessage(msg)
return
}
ctx.AudioStream, format, err = mp3.Decode(resp.Body)
if err != nil {
// TODO: Don't exit here. Once there's a status message area in the app, populate it with the error
log.Println("unable to stream channel:", resp.StatusCode)
os.Exit(1)
msg := fmt.Sprintf("There was a problem streaming this channel channels: %s", err.Error())
ctx.SetStatusMessage(msg)
return
}
return

View file

@ -13,8 +13,9 @@ type ViewContext struct {
App *tview.Application
ChannelList *tview.List
FavoriteList *tview.List
NowPlaying *NowPlayingView
Keybindings *KeybindingView
NowPlaying *NowPlayingView
Status *StatusView
}
// KeybindingView is a custom view for dispalying the keyboard bindings available to users
@ -25,41 +26,35 @@ type KeybindingView struct {
// UIKeybinding is a helper struct for building a ControlsView
type UIKeybinding struct {
Shortcut string // a keybinding to bind to this control
Description string // a description of what the keybinding does
Func func() // the funcion to execute when the binding is pressed
}
// CreateAppView creates the primary application view of di-tui
func CreateAppView() *ViewContext {
return &ViewContext{
App: tview.NewApplication(),
ChannelList: createChannelList(),
FavoriteList: createFavoriteList(),
NowPlaying: newNowPlaying(),
Keybindings: createKeybindings(),
}
Shortcut string // a keybinding to bind to this control
}
// NowPlayingView is a custom view for dispalying the currently playing channel
type NowPlayingView struct {
*tview.Box
Channel *components.ChannelItem
Track components.Track
Elapsed float64
Track components.Track
}
func newNowPlaying() *NowPlayingView {
np := &NowPlayingView{
Box: tview.NewBox(),
Channel: &components.ChannelItem{},
Elapsed: 0.0,
// StatusView shows temporary status messages in the application
type StatusView struct {
*tview.Box
Message string
}
// CreateViewContext creates the primary application view of di-tui
func CreateViewContext() *ViewContext {
return &ViewContext{
App: tview.NewApplication(),
ChannelList: createChannelList(),
FavoriteList: createFavoriteList(),
Keybindings: createKeybindings(),
NowPlaying: createNowPlaying(),
Status: createStatusView(),
}
np.SetTitle(" Now Playing ")
np.SetBorder(true)
return np
}
// Draw draws a NowPlayingView onto the scren
@ -86,6 +81,15 @@ func (n *NowPlayingView) Draw(screen tcell.Screen) {
tview.Print(screen, line, x, y+3, width, tview.AlignLeft, tcell.ColorBlue)
}
// Draw draws a NowPlayingView onto the scren
func (s *StatusView) Draw(screen tcell.Screen) {
s.Box.Draw(screen)
x, y, width, _ := s.GetInnerRect()
line := fmt.Sprintf("%s[white] %s", "Message:", s.Message)
tview.Print(screen, line, x, y, width, tview.AlignLeft, tcell.ColorBlue)
}
// Draw draws the key bindings view on to the screen
func (k *KeybindingView) Draw(screen tcell.Screen) {
k.Box.Draw(screen)
@ -96,7 +100,6 @@ func (k *KeybindingView) Draw(screen tcell.Screen) {
line := fmt.Sprintf("(%s)[white] %s", bnd.Shortcut, bnd.Description)
tview.Print(screen, line, x+previousWidth, y, width, tview.AlignLeft, tcell.ColorBlue)
previousWidth += len(bnd.Shortcut) + len(bnd.Description) + 4
// virtically separate playback controls from ui controls
// yes, this is hacky, but there's a comment, so it's ok, right?
if j == 4 {
@ -134,3 +137,27 @@ func createKeybindings() *KeybindingView {
return kbv
}
func createNowPlaying() *NowPlayingView {
np := &NowPlayingView{
Box: tview.NewBox(),
Channel: &components.ChannelItem{},
Elapsed: 0.0,
}
np.SetTitle(" Now Playing ")
np.SetBorder(true)
return np
}
func createStatusView() *StatusView {
sv := &StatusView{
Box: tview.NewBox(),
Message: "Ready to Play",
}
sv.SetTitle(" Status ")
sv.SetBorder(true)
return sv
}