diff --git a/client.go b/client/client.go similarity index 74% rename from client.go rename to client/client.go index da8c3fe..fece67c 100644 --- a/client.go +++ b/client/client.go @@ -1,8 +1,11 @@ -package main +package client import ( "errors" "fmt" + + "github.com/fhs/gompd/mpd" + "strings" "github.com/aditya-K2/gomp/utils" @@ -10,10 +13,23 @@ import ( ) var ( + CONN *mpd.Client + ArtistTree map[string]map[string]map[string]string + NotificationServer interface { + Send(string) + } WHITE_AND_BOLD string = "[#ffffff::b]" ) -func togglePlayBack() error { +func SetConnection(c *mpd.Client) { + CONN = c +} + +func SetNotificationServer(n interface{ Send(string) }) { + NotificationServer = n +} + +func TogglePlayBack() error { status, err := CONN.Status() if status["state"] == "play" && err == nil { CONN.Pause(true) @@ -39,32 +55,30 @@ func UpdatePlaylist(inputTable *tview.Table) { } } -/* - The GenerateContentSlice returns a slice of the content to be displayed on the Search View. The Slice is generated - because the random nature of maps as they return values randomly hence the draw function keeps changing the order - in which the results. -*/ +// The GenerateContentSlice returns a slice of the content to be displayed on the Search View. The Slice is generated +// because the random nature of maps as they return values randomly hence the draw function keeps changing the order +// in which the results appear. func GenerateContentSlice(selectedSuggestion string) ([]interface{}, error) { var ContentSlice []interface{} if strings.TrimRight(selectedSuggestion, " ") == "" { - NOTIFICATION_SERVER.Send("Empty Search!") + NotificationServer.Send("Empty Search!") return nil, errors.New("empty Search String Provided") } - if _, ok := ARTIST_TREE[selectedSuggestion]; ok { + if _, ok := ArtistTree[selectedSuggestion]; ok { ContentSlice = append(ContentSlice, WHITE_AND_BOLD+"Artists :") ContentSlice = append(ContentSlice, selectedSuggestion) ContentSlice = append(ContentSlice, WHITE_AND_BOLD+"Artist Albums :") - for albumName := range ARTIST_TREE[selectedSuggestion] { + for albumName := range ArtistTree[selectedSuggestion] { ContentSlice = append(ContentSlice, [2]string{albumName, selectedSuggestion}) } ContentSlice = append(ContentSlice, WHITE_AND_BOLD+"Artist Tracks :") - for albumName, trackList := range ARTIST_TREE[selectedSuggestion] { + for albumName, trackList := range ArtistTree[selectedSuggestion] { for track := range trackList { ContentSlice = append(ContentSlice, [3]string{track, selectedSuggestion, albumName}) } } } - if aMap := QueryArtistTreeForAlbums(ARTIST_TREE, selectedSuggestion); len(aMap) != 0 { + if aMap := QueryArtistTreeForAlbums(ArtistTree, selectedSuggestion); len(aMap) != 0 { ContentSlice = append(ContentSlice, WHITE_AND_BOLD+"Albums :") for mSlice := range aMap { ContentSlice = append(ContentSlice, mSlice) @@ -76,7 +90,7 @@ func GenerateContentSlice(selectedSuggestion string) ([]interface{}, error) { } } } - if tMap := QueryArtistTreeForTracks(ARTIST_TREE, selectedSuggestion); len(tMap) != 0 { + if tMap := QueryArtistTreeForTracks(ArtistTree, selectedSuggestion); len(tMap) != 0 { ContentSlice = append(ContentSlice, WHITE_AND_BOLD+"Tracks :") for mSlice := range tMap { ContentSlice = append(ContentSlice, mSlice) @@ -85,11 +99,9 @@ func GenerateContentSlice(selectedSuggestion string) ([]interface{}, error) { return ContentSlice, nil } -/* - UpdateSearchView as the name suggests Updates the Search View the idea is to basically keep a fourth option called - Search in the Navigation bar which will render things from a global ContentSlice at least in the context of the main - function this will also help in persisting the Search Results. -*/ +// UpdateSearchView as the name suggests Updates the Search View the idea is to basically keep a fourth option called +// Search in the Navigation bar which will render things from a global ContentSlice at least in the context of the main +// function this will also help in persisting the Search Results. func UpdateSearchView(inputTable *tview.Table, c []interface{}) { inputTable.Clear() _, _, width, _ := inputTable.GetInnerRect() @@ -122,8 +134,8 @@ func UpdateSearchView(inputTable *tview.Table, c []interface{}) { func Update(f []FileNode, inputTable *tview.Table) { inputTable.Clear() for i, j := range f { - if len(j.children) == 0 { - _songAttributes, err := CONN.ListAllInfo(j.absolutePath) + if len(j.Children) == 0 { + _songAttributes, err := CONN.ListAllInfo(j.AbsolutePath) if err == nil && _songAttributes[0]["Title"] != "" { _, _, w, _ := inputTable.GetInnerRect() inputTable.SetCell(i, 0, @@ -140,19 +152,21 @@ func Update(f []FileNode, inputTable *tview.Table) { } else if _songAttributes[0]["Title"] == "" { inputTable.SetCell(i, 0, - tview.NewTableCell("[blue]"+j.path). + tview.NewTableCell("[blue]"+j.Path). SetAlign(tview.AlignLeft)) } } else { inputTable.SetCell(i, 0, - tview.NewTableCell("[yellow::b]"+j.path). + tview.NewTableCell("[yellow::b]"+j.Path). SetAlign(tview.AlignLeft)) } } } +// GenerateArtistTree Artist Tree is a map of Artist to their Album Map +// Album Tree is a map of the tracks in that particular album. func GenerateArtistTree() (map[string]map[string]map[string]string, error) { - ArtistTree := make(map[string]map[string]map[string]string) + ArtistTree = make(map[string]map[string]map[string]string) AllInfo, err := CONN.ListAllInfo("/") if err == nil { for _, i := range AllInfo { @@ -184,56 +198,50 @@ func PrintArtistTree(a map[string]map[string]map[string]string) { } } -/* - Adds All tracks from a specified album to a playlist -*/ +// Adds All tracks from a specified album to a playlist func AddAlbum(a map[string]map[string]map[string]string, alb string, artist string) { for _, v := range a[artist][alb] { err := CONN.Add(v) if err != nil { - NOTIFICATION_SERVER.Send("Could Not Add Song : " + v) + NotificationServer.Send("Could Not Add Song : " + v) } } - NOTIFICATION_SERVER.Send("Album Added : " + alb) + NotificationServer.Send("Album Added : " + alb) } -/* - Adds All tracks from a specified artist to a playlist -*/ +// Adds All tracks from a specified artist to a playlist func AddArtist(a map[string]map[string]map[string]string, artist string) { if val, ok := a[artist]; ok { for _, v := range val { for _, path := range v { err := CONN.Add(path) if err != nil { - NOTIFICATION_SERVER.Send("Could Not Add Song : " + path) + NotificationServer.Send("Could Not Add Song : " + path) } } } - NOTIFICATION_SERVER.Send("Artist Added : " + artist) + NotificationServer.Send("Artist Added : " + artist) } } -/* - Adds Specified Track to the Playlist -*/ +// Adds Specified Track to the Playlist func AddTitle(a map[string]map[string]map[string]string, artist, alb, track string, addAndPlay bool) { if addAndPlay { id, err := CONN.AddId(a[artist][alb][track], -1) CONN.PlayId(id) if err != nil { - NOTIFICATION_SERVER.Send("Could Not Add Track : " + track) + NotificationServer.Send("Could Not Add Track : " + track) } } else { err := CONN.Add(a[artist][alb][track]) if err != nil { - NOTIFICATION_SERVER.Send("Could Not Add Track : " + track) + NotificationServer.Send("Could Not Add Track : " + track) } } - NOTIFICATION_SERVER.Send("Track Added : " + track) + NotificationServer.Send("Track Added : " + track) } -/* Querys the Artist Tree for a track and returns a TrackMap (i.e [3]string{artist, album, track} -> path) which will help us +/* Querys the Artist Tree for a track and returns a TrackMap (i.e [3]string{artist, album, track} -> Path) which will help us to add tracks to the playlist */ func QueryArtistTreeForTracks(a map[string]map[string]map[string]string, track string) map[[3]string]string { TrackMap := make(map[[3]string]string) @@ -249,7 +257,7 @@ func QueryArtistTreeForTracks(a map[string]map[string]map[string]string, track s return TrackMap } -/* Querys the Artist Tree for an album and returns a AlbumMap (i.e [3]string{artist, album } ->[]path of songs in the album) +/* Querys the Artist Tree for an album and returns a AlbumMap (i.e [3]string{artist, album } ->[]Path of songs in the album) which will help us to add all album tracks to the playlist */ func QueryArtistTreeForAlbums(a map[string]map[string]map[string]string, album string) map[[2]string][][2]string { AlbumMap := make(map[[2]string][][2]string) @@ -272,17 +280,17 @@ func AddToPlaylist(a interface{}, addAndPlay bool) { case [3]string: { b := a.([3]string) - AddTitle(ARTIST_TREE, b[1], b[2], b[0], addAndPlay) + AddTitle(ArtistTree, b[1], b[2], b[0], addAndPlay) } case [2]string: { b := a.([2]string) - AddAlbum(ARTIST_TREE, b[0], b[1]) + AddAlbum(ArtistTree, b[0], b[1]) } case string: { b := a.(string) - AddArtist(ARTIST_TREE, b) + AddArtist(ArtistTree, b) } } } diff --git a/client/files.go b/client/files.go new file mode 100644 index 0000000..71cac40 --- /dev/null +++ b/client/files.go @@ -0,0 +1,69 @@ +package client + +import ( + "fmt" + "strings" +) + +type FileNode struct { + Children []FileNode + Path string + Parent *FileNode + AbsolutePath string +} + +func (f *FileNode) AddChildren(path string) { + if f.Path != "" { + f.Children = append(f.Children, FileNode{Children: make([]FileNode, 0), Path: path, Parent: f, AbsolutePath: f.AbsolutePath + "/" + path}) + } else { + f.Children = append(f.Children, FileNode{Children: make([]FileNode, 0), Path: path, Parent: f, AbsolutePath: f.AbsolutePath + path}) + } +} + +func (f *FileNode) AddChildNode(m FileNode) { + m.Parent = f + f.Children = append(f.Children, m) +} + +func GenerateDirectoryTree(path []string) *FileNode { + var head *FileNode = new(FileNode) + var head1 *FileNode = head + for i := range path { + sepPaths := strings.Split(path[i], "/") + for j := range sepPaths { + if len(head.Children) == 0 { + head.AddChildren(sepPaths[j]) + head = &(head.Children[len(head.Children)-1]) + } else { + var headIsChanged = false + for k := range head.Children { + if head.Children[k].Path == sepPaths[j] { + head = &(head.Children[k]) + headIsChanged = true + break + } + } + if !headIsChanged { + head.AddChildren(sepPaths[j]) + head = &(head.Children[len(head.Children)-1]) + } + } + } + head = head1 + } + return head +} + +func (f FileNode) Print(count int) { + if len(f.Children) == 0 { + return + } else { + for i := range f.Children { + for j := 0; j < count; j++ { + fmt.Print("---") + } + fmt.Println(f.Children[i].AbsolutePath) + f.Children[i].Print(count + 1) + } + } +} diff --git a/fileBrowser.go b/fileBrowser.go deleted file mode 100644 index fa1eebc..0000000 --- a/fileBrowser.go +++ /dev/null @@ -1,69 +0,0 @@ -package main - -import ( - "fmt" - "strings" -) - -type FileNode struct { - children []FileNode - path string - parent *FileNode - absolutePath string -} - -func (f *FileNode) addChildren(path string) { - if f.path != "" { - f.children = append(f.children, FileNode{children: make([]FileNode, 0), path: path, parent: f, absolutePath: f.absolutePath + "/" + path}) - } else { - f.children = append(f.children, FileNode{children: make([]FileNode, 0), path: path, parent: f, absolutePath: f.absolutePath + path}) - } -} - -func (f *FileNode) addChildNode(m FileNode) { - m.parent = f - f.children = append(f.children, m) -} - -func generateDirectoryTree(path []string) *FileNode { - var head *FileNode = new(FileNode) - var head1 *FileNode = head - for i := range path { - sepPaths := strings.Split(path[i], "/") - for j := range sepPaths { - if len(head.children) == 0 { - head.addChildren(sepPaths[j]) - head = &(head.children[len(head.children)-1]) - } else { - var headIsChanged = false - for k := range head.children { - if head.children[k].path == sepPaths[j] { - head = &(head.children[k]) - headIsChanged = true - break - } - } - if !headIsChanged { - head.addChildren(sepPaths[j]) - head = &(head.children[len(head.children)-1]) - } - } - } - head = head1 - } - return head -} - -func (f FileNode) Print(count int) { - if len(f.children) == 0 { - return - } else { - for i := range f.children { - for j := 0; j < count; j++ { - fmt.Print("---") - } - fmt.Println(f.children[i].absolutePath) - f.children[i].Print(count + 1) - } - } -} diff --git a/main.go b/main.go index 0dc677e..5ca3447 100644 --- a/main.go +++ b/main.go @@ -4,6 +4,12 @@ import ( "strconv" "time" + "github.com/aditya-K2/gomp/ui/notify" + + "github.com/aditya-K2/gomp/render" + "github.com/aditya-K2/gomp/ui" + + "github.com/aditya-K2/gomp/client" "github.com/aditya-K2/gomp/utils" "github.com/aditya-K2/fuzzy" @@ -14,92 +20,110 @@ import ( "github.com/spf13/viper" ) -var ( - CONN *mpd.Client - UI *Application - NOTIFICATION_SERVER *NotificationServer - Volume int64 - Random bool - Repeat bool - InsidePlaylist bool = true - InsideSearchView bool = false - ARTIST_TREE map[string]map[string]map[string]string -) - func main() { config.ReadConfig() - // Connect to MPD server var mpdConnectionError error - CONN, mpdConnectionError = mpd.Dial("tcp", "localhost:"+viper.GetString("MPD_PORT")) + CONN, mpdConnectionError := mpd.Dial("tcp", "localhost:"+viper.GetString("MPD_PORT")) if mpdConnectionError != nil { panic(mpdConnectionError) } defer CONN.Close() + + ui.GenerateFocusMap() + + client.SetConnection(CONN) + ui.SetConnection(CONN) + render.SetConnection(CONN) + cache.SetCacheDir(viper.GetString("CACHE_DIR")) - r := newRenderer() + + Renderer := render.NewRenderer() + // Connecting the Renderer to the Main UI + ui.ConnectRenderer(Renderer) + c, _ := CONN.CurrentSong() if len(c) != 0 { - r.Start(c["file"]) + Renderer.Start(c["file"]) } else { - r.Start("stop") + Renderer.Start("stop") } - UI = newApplication(r) + UI := ui.NewApplication() + + // Connecting the Notification Server to the Main UI + notify.ConnectUI(UI) fileMap, err := CONN.GetFiles() - dirTree := generateDirectoryTree(fileMap) - UpdatePlaylist(UI.ExpandedView) + // Generating the Directory Tree for File Navigation. + dirTree := client.GenerateDirectoryTree(fileMap) + + // Default View upon Opening is of Playlist. + client.UpdatePlaylist(UI.ExpandedView) _v, _ := CONN.Status() - Volume, _ = strconv.ParseInt(_v["volume"], 10, 64) - Random, _ = strconv.ParseBool(_v["random"]) - Repeat, _ = strconv.ParseBool(_v["repeat"]) + // Setting Volume, Random and Repeat Values + Volume, _ := strconv.ParseInt(_v["volume"], 10, 64) + Random, _ := strconv.ParseBool(_v["random"]) + Repeat, _ := strconv.ParseBool(_v["repeat"]) - ARTIST_TREE, err = GenerateArtistTree() - ARTIST_TREE_CONTENT := utils.ConvertToArray(ARTIST_TREE) - NOTIFICATION_SERVER = NewNotificationServer() - NOTIFICATION_SERVER.Start() + ArtistTree, err := client.GenerateArtistTree() - var SEARCH_CONTENT_SLICE []interface{} + // Used for Fuzzy Searching + ArtistTreeContent := utils.ConvertToArray(ArtistTree) + Notify := notify.NewNotificationServer() + Notify.Start() + + // Connecting Notification Server to Client and Rendering Module so that they can send Notifications + client.SetNotificationServer(Notify) + render.SetNotificationServer(Notify) + + // This is the Slice that is used to Display Content in the SearchView + var SearchContentSlice []interface{} + + // This Function Is Responsible for Changing the Focus it uses the Focus Map and Based on it Chooses + // the Draw Function UI.ExpandedView.SetDrawFunc(func(s tcell.Screen, x, y, width, height int) (int, int, int, int) { - if InsidePlaylist { - UpdatePlaylist(UI.ExpandedView) - } else if InsideSearchView { - UpdateSearchView(UI.ExpandedView, SEARCH_CONTENT_SLICE) - } else { - Update(dirTree.children, UI.ExpandedView) + if ui.HasFocus("Playlist") { + client.UpdatePlaylist(UI.ExpandedView) + } else if ui.HasFocus("SearchView") { + client.UpdateSearchView(UI.ExpandedView, SearchContentSlice) + } else if ui.HasFocus("FileBrowser") { + client.Update(dirTree.Children, UI.ExpandedView) } return UI.ExpandedView.GetInnerRect() }) - var FUNC_MAP = map[string]func(){ + // Function Maps is used For Mapping Keys According to the Value mapped to the Key the respective Function is called + // For e.g. in the config if the User Maps T to togglePlayBack then whenever in the input handler the T is received + // the respective function in this case togglePlayBack is called. + var FuncMap = map[string]func(){ "showChildrenContent": func() { r, _ := UI.ExpandedView.GetSelection() - if !InsidePlaylist && !InsideSearchView { - if len(dirTree.children[r].children) == 0 { - id, _ := CONN.AddId(dirTree.children[r].absolutePath, -1) + if ui.HasFocus("FileBrowser") { + if len(dirTree.Children[r].Children) == 0 { + id, _ := CONN.AddId(dirTree.Children[r].AbsolutePath, -1) CONN.PlayId(id) } else { - Update(dirTree.children[r].children, UI.ExpandedView) - dirTree = &dirTree.children[r] + client.Update(dirTree.Children[r].Children, UI.ExpandedView) + dirTree = &dirTree.Children[r] } - } else if InsidePlaylist { + } else if ui.HasFocus("Playlist") { CONN.Play(r) - } else if InsideSearchView { + } else if ui.HasFocus("SearchView") { r, _ := UI.ExpandedView.GetSelection() - AddToPlaylist(SEARCH_CONTENT_SLICE[r], true) + client.AddToPlaylist(SearchContentSlice[r], true) } }, "togglePlayBack": func() { - togglePlayBack() + client.TogglePlayBack() }, "showParentContent": func() { - if !InsidePlaylist && !InsideSearchView { - if dirTree.parent != nil { - Update(dirTree.parent.children, UI.ExpandedView) - dirTree = dirTree.parent + if ui.HasFocus("FileBrowser") { + if dirTree.Parent != nil { + client.Update(dirTree.Parent.Children, UI.ExpandedView) + dirTree = dirTree.Parent } } }, @@ -108,18 +132,18 @@ func main() { }, "clearPlaylist": func() { CONN.Clear() - NOTIFICATION_SERVER.Send("PlayList Cleared") + Notify.Send("Playlist Cleared") }, "previousSong": func() { CONN.Previous() }, "addToPlaylist": func() { - if !InsidePlaylist && !InsideSearchView { + if ui.HasFocus("FileBrowser") { r, _ := UI.ExpandedView.GetSelection() - CONN.Add(dirTree.children[r].absolutePath) - } else if InsideSearchView { + CONN.Add(dirTree.Children[r].AbsolutePath) + } else if ui.HasFocus("SearchView") { r, _ := UI.ExpandedView.GetSelection() - AddToPlaylist(SEARCH_CONTENT_SLICE[r], false) + client.AddToPlaylist(SearchContentSlice[r], false) } }, "toggleRandom": func() { @@ -151,25 +175,20 @@ func main() { CONN.SetVolume(int(Volume)) }, "navigateToFiles": func() { - InsidePlaylist = false - InsideSearchView = false + ui.SetFocus("FileBrowser") UI.Navbar.Select(1, 0) - Update(dirTree.children, UI.ExpandedView) + client.Update(dirTree.Children, UI.ExpandedView) }, "navigateToPlaylist": func() { - InsidePlaylist = true - InsideSearchView = false + ui.SetFocus("Playlist") UI.Navbar.Select(0, 0) - UpdatePlaylist(UI.ExpandedView) + client.UpdatePlaylist(UI.ExpandedView) }, "navigateToMostPlayed": func() { - InsideSearchView = false - InsidePlaylist = false UI.Navbar.Select(2, 0) }, "navigateToSearch": func() { - InsideSearchView = true - InsidePlaylist = false + ui.SetFocus("SearchView") UI.Navbar.Select(3, 0) }, "quit": func() { @@ -177,17 +196,17 @@ func main() { }, "stop": func() { CONN.Stop() - NOTIFICATION_SERVER.Send("Playback Stopped") + Notify.Send("Playback Stopped") }, "updateDB": func() { _, err = CONN.Update("") if err != nil { panic(err) } - NOTIFICATION_SERVER.Send("Database Updated") + Notify.Send("Database Updated") }, "deleteSongFromPlaylist": func() { - if InsidePlaylist { + if ui.HasFocus("Playlist") { r, _ := UI.ExpandedView.GetSelection() CONN.Delete(r, -1) } @@ -197,12 +216,15 @@ func main() { }, } - config.GenerateKeyMap(FUNC_MAP) + // Generating the Key Map Based on the Function Map Here Basically the Values will be flipped + // In the config if togglePlayBack is mapped to [ T , P, SPACE ] then here Basically we will receive a map + // for each event T, P, SPACE mapped to the same function togglePlayBack + config.GenerateKeyMap(FuncMap) UI.SearchBar.SetAutocompleteFunc(func(c string) []string { if c != "" && c != " " && c != " " { _, _, w, _ := UI.SearchBar.GetRect() - matches := fuzzy.Find(c, ARTIST_TREE_CONTENT) + matches := fuzzy.Find(c, ArtistTreeContent) var suggestions []string for i, match := range matches { if i == 10 { @@ -216,9 +238,10 @@ func main() { } }) + // Input Handler UI.ExpandedView.SetInputCapture(func(e *tcell.EventKey) *tcell.EventKey { if val, ok := config.KEY_MAP[int(e.Rune())]; ok { - FUNC_MAP[val]() + FuncMap[val]() return nil } else { return e @@ -228,12 +251,11 @@ func main() { UI.SearchBar.SetDoneFunc(func(e tcell.Key) { if e == tcell.KeyEnter { UI.ExpandedView.Select(0, 0) - InsideSearchView = true - InsidePlaylist = false - SEARCH_CONTENT_SLICE = nil - SEARCH_CONTENT_SLICE, err = GenerateContentSlice(UI.SearchBar.GetText()) + ui.SetFocus("SearchView") + SearchContentSlice = nil + SearchContentSlice, err = client.GenerateContentSlice(UI.SearchBar.GetText()) if err != nil { - NOTIFICATION_SERVER.Send("Could Not Retrieve the Results") + Notify.Send("Could Not Retrieve the Results") } else { UI.SearchBar.SetText("") UI.App.SetFocus(UI.ExpandedView) @@ -241,7 +263,7 @@ func main() { } } if e == tcell.KeyEscape { - InsideSearchView = false + ui.FocusMap["SearchView"] = false UI.App.SetFocus(UI.ExpandedView) } }) diff --git a/render.go b/render.go deleted file mode 100644 index 3d28ee1..0000000 --- a/render.go +++ /dev/null @@ -1,122 +0,0 @@ -package main - -import ( - "image" - "os" - - "github.com/aditya-K2/gomp/cache" - "github.com/aditya-K2/gomp/utils" - "github.com/nfnt/resize" - "github.com/spf13/viper" - "gitlab.com/diamondburned/ueberzug-go" -) - -/* - Renderer is just a channel on which we will send the Path to the song whose - Image is to be Rendered. This channel is passed to the openImage which in turn is called - by the Start() function as a go routine. -*/ -type Renderer struct { - c chan string -} - -/* - Returns a new Renderer with a string channel -*/ -func newRenderer() *Renderer { - c := make(chan string) - return &Renderer{ - c: c, - } -} - -/* - Send Image Path to Renderer -*/ -func (self *Renderer) Send(path string) { - self.c <- path -} - -/* - - Go Routine that will Be Called and will listen on the channel c - for changes and on getting a string over the channel will open the Image and - keep listening again. This will keep the image blocked ( i.e no need to use time.Sleep() etc. ) - and saves resources too. - -*/ -func openImage(path string, c chan string) { - fw, fh := utils.GetFontWidth() - var im *ueberzug.Image - if path != "stop" { - extractedImage := getImagePath(path) - img2, _ := GetImg(extractedImage) - im, _ = ueberzug.NewImage(img2, int(float32(IMG_X)*fw)+viper.GetInt("ADDITIONAL_PADDING_X"), int(float32(IMG_Y)*fh)+viper.GetInt("ADDITIONAL_PADDING_Y")) - } - d := <-c - if im != nil { - im.Clear() - } - if d != "stop" { - openImage(d, c) - } else { - openImage("stop", c) - } -} - -/* - Initialises the Renderer and calls the go routine openImage and passes the channel - as argument. -*/ -func (self *Renderer) Start(path string) { - go openImage(path, self.c) -} - -/* - This Function returns the path to the image that is to be rendered it checks first for the image in the cache - else it adds the image to the cache and then extracts it and renders it. -*/ -func getImagePath(path string) string { - a, err := CONN.ListInfo(path) - var extractedImage string - if err == nil && len(a) != 0 { - if cache.Exists(a[0]["artist"], a[0]["album"]) { - extractedImage = cache.GenerateName(a[0]["artist"], a[0]["album"]) - } else { - imagePath := cache.GenerateName(a[0]["artist"], a[0]["album"]) - absPath := utils.CheckDirectoryFmt(viper.GetString("MUSIC_DIRECTORY")) + path - extractedImage = utils.ExtractImageFromFile(absPath, imagePath) - if extractedImage == viper.GetString("DEFAULT_IMAGE_PATH") && viper.GetString("GET_COVER_ART_FROM_LAST_FM") == "TRUE" { - downloadedImage, err := getImageFromLastFM(a[0]["artist"], a[0]["album"], imagePath) - if err == nil { - NOTIFICATION_SERVER.Send("Image From LastFM") - extractedImage = downloadedImage - } else { - NOTIFICATION_SERVER.Send("Falling Back to Default Image.") - } - } else { - NOTIFICATION_SERVER.Send("Extracted Image Successfully") - } - } - } - return extractedImage -} - -func GetImg(uri string) (image.Image, error) { - f, err := os.Open(uri) - if err != nil { - return nil, err - } - defer f.Close() - img, _, err := image.Decode(f) - if err != nil { - return nil, err - } - fw, fh := utils.GetFontWidth() - img = resize.Resize( - uint(float32(IMG_W)*(fw+float32(viper.GetFloat64("IMAGE_WIDTH_EXTRA_X")))), uint(float32(IMG_H)*(fh+float32(viper.GetFloat64("IMAGE_WIDTH_EXTRA_Y")))), - img, - resize.Bilinear, - ) - return img, nil -} diff --git a/utils/imageUtils.go b/render/imageUtils.go similarity index 88% rename from utils/imageUtils.go rename to render/imageUtils.go index ce3dfee..8c802c2 100644 --- a/utils/imageUtils.go +++ b/render/imageUtils.go @@ -1,6 +1,7 @@ -package utils +package render import ( + "github.com/aditya-K2/gomp/utils" "os" "strings" @@ -67,17 +68,17 @@ func ExtractImageFromFile(uri string, imagePath string) string { if strings.HasSuffix(uri, ".mp3") { imagePath := GetMp3Image(uri, imagePath) if imagePath == "" { - Copy(viper.GetString("DEFAULT_IMAGE_PATH"), _i) + utils.Copy(viper.GetString("DEFAULT_IMAGE_PATH"), _i) return viper.GetString("DEFAULT_IMAGE_PATH") } } else if strings.HasSuffix(uri, ".flac") { imagePath := GetFlacImage(uri, imagePath) if imagePath == "" { - Copy(viper.GetString("DEFAULT_IMAGE_PATH"), _i) + utils.Copy(viper.GetString("DEFAULT_IMAGE_PATH"), _i) return viper.GetString("DEFAULT_IMAGE_PATH") } } else { - Copy(viper.GetString("DEFAULT_IMAGE_PATH"), _i) + utils.Copy(viper.GetString("DEFAULT_IMAGE_PATH"), _i) return viper.GetString("DEFAULT_IMAGE_PATH") } return imagePath diff --git a/lastfm.go b/render/lastfm.go similarity index 98% rename from lastfm.go rename to render/lastfm.go index c1e9b86..4185114 100644 --- a/lastfm.go +++ b/render/lastfm.go @@ -1,4 +1,4 @@ -package main +package render import ( "errors" diff --git a/render/render.go b/render/render.go new file mode 100644 index 0000000..0515437 --- /dev/null +++ b/render/render.go @@ -0,0 +1,124 @@ +package render + +import ( + "image" + "os" + + "github.com/aditya-K2/gomp/ui" + "github.com/fhs/gompd/mpd" + + "github.com/aditya-K2/gomp/cache" + "github.com/aditya-K2/gomp/utils" + "github.com/nfnt/resize" + "github.com/spf13/viper" + "gitlab.com/diamondburned/ueberzug-go" +) + +var ( + CONN *mpd.Client + Notify interface{ Send(string) } +) + +func SetConnection(c *mpd.Client) { + CONN = c +} + +func SetNotificationServer(n interface{ Send(string) }) { + Notify = n +} + +// Renderer is just a channel on which we will send the Path to the song whose +// Image is to be Rendered. This channel is passed to the OpenImage which in turn is called +// by the Start() function as a go routine. +type Renderer struct { + c chan string +} + +// Returns a new Renderer with a string channel +func NewRenderer() *Renderer { + c := make(chan string) + return &Renderer{ + c: c, + } +} + +// Send Image Path to Renderer +func (self *Renderer) Send(path string) { + self.c <- path +} + +// Go Routine that will Be Called and will listen on the channel c +// for changes and on getting a string over the channel will open the Image and +// keep listening again. This will keep the image blocked ( i.e no need to use time.Sleep() etc. ) +// and saves resources too. +func OpenImage(path string, c chan string) { + fw, fh := utils.GetFontWidth() + var im *ueberzug.Image + if path != "stop" { + extractedImage := GetImagePath(path) + img2, _ := GetImg(extractedImage) + im, _ = ueberzug.NewImage(img2, int(float32(ui.IMG_X)*fw)+viper.GetInt("ADDITIONAL_PADDING_X"), int(float32(ui.IMG_Y)*fh)+viper.GetInt("ADDITIONAL_PADDING_Y")) + } + d := <-c + if im != nil { + im.Clear() + } + if d != "stop" { + OpenImage(d, c) + } else { + OpenImage("stop", c) + } +} + +// Initialises the Renderer and calls the go routine OpenImage and passes the channel +// as argument. +func (self *Renderer) Start(path string) { + go OpenImage(path, self.c) +} + +// This Function returns the path to the image that is to be rendered it checks first for the image in the cache +// else it adds the image to the cache and then extracts it and renders it. +func GetImagePath(path string) string { + a, err := CONN.ListInfo(path) + var extractedImage string + if err == nil && len(a) != 0 { + if cache.Exists(a[0]["artist"], a[0]["album"]) { + extractedImage = cache.GenerateName(a[0]["artist"], a[0]["album"]) + } else { + imagePath := cache.GenerateName(a[0]["artist"], a[0]["album"]) + absPath := utils.CheckDirectoryFmt(viper.GetString("MUSIC_DIRECTORY")) + path + extractedImage = ExtractImageFromFile(absPath, imagePath) + if extractedImage == viper.GetString("DEFAULT_IMAGE_PATH") && viper.GetString("GET_COVER_ART_FROM_LAST_FM") == "TRUE" { + downloadedImage, err := getImageFromLastFM(a[0]["artist"], a[0]["album"], imagePath) + if err == nil { + Notify.Send("Image From LastFM") + extractedImage = downloadedImage + } else { + Notify.Send("Falling Back to Default Image.") + } + } else { + Notify.Send("Extracted Image Successfully") + } + } + } + return extractedImage +} + +func GetImg(uri string) (image.Image, error) { + f, err := os.Open(uri) + if err != nil { + return nil, err + } + defer f.Close() + img, _, err := image.Decode(f) + if err != nil { + return nil, err + } + fw, fh := utils.GetFontWidth() + img = resize.Resize( + uint(float32(ui.IMG_W)*(fw+float32(viper.GetFloat64("IMAGE_WIDTH_EXTRA_X")))), uint(float32(ui.IMG_H)*(fh+float32(viper.GetFloat64("IMAGE_WIDTH_EXTRA_Y")))), + img, + resize.Bilinear, + ) + return img, nil +} diff --git a/app.go b/ui/app.go similarity index 92% rename from app.go rename to ui/app.go index 13c2383..bb86485 100644 --- a/app.go +++ b/ui/app.go @@ -1,4 +1,4 @@ -package main +package ui import ( "github.com/aditya-K2/tview" @@ -16,9 +16,9 @@ type Application struct { Pages *tview.Pages } -func newApplication(r *Renderer) *Application { +func NewApplication() *Application { - var pBar *progressBar = newProgressBar(r) + var pBar *progressBar = newProgressBar() expandedView := tview.NewTable() Navbar := tview.NewTable() searchBar := tview.NewInputField() @@ -60,7 +60,7 @@ func newApplication(r *Renderer) *Application { AddItem(searchBar, 3, 1, false). AddItem(sNavExpViewFlex, 0, 1, false) - mainFlex := tview.NewFlex().SetDirection(tview.FlexRow). + lex := tview.NewFlex().SetDirection(tview.FlexRow). AddItem(searchBarFlex, 0, 8, false). AddItem(pBar.t, 5, 1, false) @@ -68,7 +68,7 @@ func newApplication(r *Renderer) *Application { expandedView.SetSelectable(true, false) rootPages := tview.NewPages() - rootPages.AddPage("Main", mainFlex, true, true) + rootPages.AddPage("Main", lex, true, true) App := tview.NewApplication() App.SetRoot(rootPages, true).SetFocus(expandedView) diff --git a/ui/focus.go b/ui/focus.go new file mode 100644 index 0000000..b71c377 --- /dev/null +++ b/ui/focus.go @@ -0,0 +1,24 @@ +package ui + +// The Focus Map Helps to keep track of which UI Element Currently Has the Focus It can be queried to get the Current +// UI Element with Focus and also can set UI Focus keep in mind that it isn't Focus Map that is Responsible to change +// the Focus that is Done through the Update Function of UI.ExpandedView */ +var FocusMap map[string]bool + +func GenerateFocusMap() { + FocusMap = make(map[string]bool) + FocusMap["Playlist"] = true + FocusMap["FileBrowser"] = false + FocusMap["SearchView"] = false +} + +func HasFocus(s string) bool { + return FocusMap[s] +} + +func SetFocus(s string) { + for k := range FocusMap { + FocusMap[k] = false + } + FocusMap[s] = true +} diff --git a/notification.go b/ui/notify/notification.go similarity index 95% rename from notification.go rename to ui/notify/notification.go index 584434a..e05554f 100644 --- a/notification.go +++ b/ui/notify/notification.go @@ -1,6 +1,7 @@ -package main +package notify import ( + "github.com/aditya-K2/gomp/ui" "time" "github.com/aditya-K2/gomp/utils" @@ -9,6 +10,14 @@ import ( "github.com/gdamore/tcell/v2" ) +var ( + UI *ui.Application +) + +func ConnectUI(a *ui.Application) { + UI = a +} + /* Notification Primitive */ type Notification struct { *tview.Box diff --git a/progressBar.go b/ui/progressBar.go similarity index 71% rename from progressBar.go rename to ui/progressBar.go index 677a9ac..103db80 100644 --- a/progressBar.go +++ b/ui/progressBar.go @@ -1,16 +1,30 @@ -package main +package ui import ( "fmt" "strconv" + "github.com/fhs/gompd/mpd" + "github.com/aditya-K2/gomp/utils" "github.com/aditya-K2/tview" "github.com/gdamore/tcell/v2" ) -var CurrentSong string +var ( + CurrentSong string + CONN *mpd.Client + RENDERER interface{ Send(string) } +) + +func SetConnection(c *mpd.Client) { + CONN = c +} + +func ConnectRenderer(r interface{ Send(string) }) { + RENDERER = r +} // The progressBar is just a string which is separated by the color formatting String // for e.g @@ -26,7 +40,7 @@ type progressBar struct { // This Function returns a progressBar with a table of two rows // the First row will contain information about the current Song // and the Second one will contain the progressBar -func newProgressBar(r *Renderer) *progressBar { +func newProgressBar() *progressBar { p := progressBar{} a := tview.NewTable(). @@ -38,7 +52,7 @@ func newProgressBar(r *Renderer) *progressBar { a.SetBorder(true) a.SetDrawFunc(func(s tcell.Screen, x, y, width, height int) (int, int, int, int) { - p.updateTitle(r) + p.updateTitle() p.updateProgress() return p.t.GetInnerRect() }) @@ -49,16 +63,16 @@ func newProgressBar(r *Renderer) *progressBar { return &p } -func (s *progressBar) updateTitle(r *Renderer) { +func (s *progressBar) updateTitle() { _currentAttributes, err := CONN.CurrentSong() if err == nil { song := "[green::bi]" + _currentAttributes["Title"] + "[-:-:-] - " + "[blue::b]" + _currentAttributes["Artist"] + "\n" s.t.GetCell(0, 0).Text = song if len(_currentAttributes) == 0 && CurrentSong != "" { CurrentSong = "" - r.Send("stop") + RENDERER.Send("stop") } else if song != CurrentSong && len(_currentAttributes) != 0 { - r.Send(_currentAttributes["file"]) + RENDERER.Send(_currentAttributes["file"]) CurrentSong = song } } @@ -72,14 +86,28 @@ func (s *progressBar) updateProgress() { if du != 0 { percentage := el / du * 100 if err == nil && err1 == nil { - s.t.SetTitle(fmt.Sprintf("[[::i] %s [-:-:-]Shuffle: %s Repeat: %s Volume: %s ]", utils.FormatString(_status["state"]), utils.FormatString(_status["random"]), utils.FormatString(_status["repeat"]), _status["volume"])).SetTitleAlign(tview.AlignRight) + s.t.SetTitle(fmt.Sprintf("[[::i] %s [-:-:-]Shuffle: %s Repeat: %s Volume: %s ]", + utils.FormatString(_status["state"]), + utils.FormatString(_status["random"]), + utils.FormatString(_status["repeat"]), + _status["volume"])). + SetTitleAlign(tview.AlignRight) s.t.GetCell(2, 0).Text = utils.GetText(float64(_width), percentage, utils.StrTime(el)+"/"+utils.StrTime(du)+"("+strconv.FormatFloat(percentage, 'f', 2, 32)+"%"+")") } else { - s.t.SetTitle(fmt.Sprintf("[[::i] %s [-:-:-]Shuffle: %s Repeat: %s]", utils.FormatString(_status["state"]), utils.FormatString(_status["random"]), utils.FormatString(_status["repeat"]))).SetTitleAlign(tview.AlignRight) + s.t.SetTitle(fmt.Sprintf("[[::i] %s [-:-:-]Shuffle: %s Repeat: %s]", + utils.FormatString(_status["state"]), + utils.FormatString(_status["random"]), + utils.FormatString(_status["repeat"]))). + SetTitleAlign(tview.AlignRight) s.t.GetCell(2, 0).Text = "" } } else { - s.t.SetTitle(fmt.Sprintf("[[::i] %s [-:-:-]Shuffle: %s Repeat: %s Volume: %s ]", utils.FormatString(_status["state"]), utils.FormatString(_status["random"]), utils.FormatString(_status["repeat"]), _status["volume"])).SetTitleAlign(tview.AlignRight) + s.t.SetTitle(fmt.Sprintf("[[::i] %s [-:-:-]Shuffle: %s Repeat: %s Volume: %s ]", + utils.FormatString(_status["state"]), + utils.FormatString(_status["random"]), + utils.FormatString(_status["repeat"]), + _status["volume"])). + SetTitleAlign(tview.AlignRight) s.t.GetCell(2, 0).Text = utils.GetText(float64(_width), 0, " ---:---") } }