From aa2204b88fe8c6f351eb224b89d25e4237090465 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Wed, 15 Feb 2023 19:46:08 -0800 Subject: [PATCH] add shuffle button to album and playlist pages --- backend/playbackmanager.go | 24 +++++---- backend/util/util.go | 20 ++++++++ res/bundled.go | 10 ++++ res/bundled_gen.sh | 48 +++++++++--------- res/icons/{ => freepik}/disc-invert.png | Bin res/icons/{ => freepik}/disc.png | Bin res/icons/{ => freepik}/headphones-invert.png | Bin res/icons/{ => freepik}/headphones.png | Bin .../{ => freepik}/heart-filled-invert.png | Bin res/icons/{ => freepik}/heart-filled.png | Bin .../{ => freepik}/heart-outline-invert.png | Bin res/icons/{ => freepik}/heart-outline.png | Bin res/icons/{ => freepik}/icons-attrib.txt | 0 res/icons/{ => freepik}/musicnotes-invert.png | Bin res/icons/{ => freepik}/musicnotes.png | Bin res/icons/{ => freepik}/people-invert.png | Bin res/icons/{ => freepik}/people.png | Bin res/icons/{ => freepik}/playbutton.png | Bin res/icons/{ => freepik}/playlist-invert.png | Bin res/icons/{ => freepik}/playlist.png | Bin res/icons/{ => freepik}/podcast-invert.png | Bin res/icons/{ => freepik}/podcast.png | Bin .../{ => freepik}/star-filled-invert.png | Bin res/icons/{ => freepik}/star-filled.png | Bin .../{ => freepik}/star-outline-invert.png | Bin res/icons/{ => freepik}/star-outline.png | Bin .../{ => freepik}/theatermasks-invert.png | Bin res/icons/{ => freepik}/theatermasks.png | Bin res/icons/publicdomain/shuffle-invert.svg | 7 +++ res/icons/publicdomain/shuffle.svg | 29 +++++++++++ ui/browsing/albumpage.go | 14 +++-- ui/browsing/playlistpage.go | 15 +++--- 32 files changed, 123 insertions(+), 44 deletions(-) create mode 100644 backend/util/util.go rename res/icons/{ => freepik}/disc-invert.png (100%) rename res/icons/{ => freepik}/disc.png (100%) rename res/icons/{ => freepik}/headphones-invert.png (100%) rename res/icons/{ => freepik}/headphones.png (100%) rename res/icons/{ => freepik}/heart-filled-invert.png (100%) rename res/icons/{ => freepik}/heart-filled.png (100%) rename res/icons/{ => freepik}/heart-outline-invert.png (100%) rename res/icons/{ => freepik}/heart-outline.png (100%) rename res/icons/{ => freepik}/icons-attrib.txt (100%) rename res/icons/{ => freepik}/musicnotes-invert.png (100%) rename res/icons/{ => freepik}/musicnotes.png (100%) rename res/icons/{ => freepik}/people-invert.png (100%) rename res/icons/{ => freepik}/people.png (100%) rename res/icons/{ => freepik}/playbutton.png (100%) rename res/icons/{ => freepik}/playlist-invert.png (100%) rename res/icons/{ => freepik}/playlist.png (100%) rename res/icons/{ => freepik}/podcast-invert.png (100%) rename res/icons/{ => freepik}/podcast.png (100%) rename res/icons/{ => freepik}/star-filled-invert.png (100%) rename res/icons/{ => freepik}/star-filled.png (100%) rename res/icons/{ => freepik}/star-outline-invert.png (100%) rename res/icons/{ => freepik}/star-outline.png (100%) rename res/icons/{ => freepik}/theatermasks-invert.png (100%) rename res/icons/{ => freepik}/theatermasks.png (100%) create mode 100644 res/icons/publicdomain/shuffle-invert.svg create mode 100644 res/icons/publicdomain/shuffle.svg diff --git a/backend/playbackmanager.go b/backend/playbackmanager.go index 0f4d43a..c105631 100644 --- a/backend/playbackmanager.go +++ b/backend/playbackmanager.go @@ -108,42 +108,46 @@ func (p *PlaybackManager) OnPlayTimeUpdate(cb func(float64, float64)) { } // Loads the specified album into the play queue. -func (p *PlaybackManager) LoadAlbum(albumID string, appendToQueue bool) error { +func (p *PlaybackManager) LoadAlbum(albumID string, appendToQueue bool, shuffle bool) error { album, err := p.sm.Server.GetAlbum(albumID) if err != nil { return err } - return p.LoadTracks(album.Song, appendToQueue) + return p.LoadTracks(album.Song, appendToQueue, shuffle) } // Loads the specified playlist into the play queue. -func (p *PlaybackManager) LoadPlaylist(playlistID string, appendToQueue bool) error { +func (p *PlaybackManager) LoadPlaylist(playlistID string, appendToQueue bool, shuffle bool) error { playlist, err := p.sm.Server.GetPlaylist(playlistID) if err != nil { return err } - return p.LoadTracks(playlist.Entry, appendToQueue) + return p.LoadTracks(playlist.Entry, appendToQueue, shuffle) } -func (p *PlaybackManager) LoadTracks(tracks []*subsonic.Child, appendToQueue bool) error { +func (p *PlaybackManager) LoadTracks(tracks []*subsonic.Child, appendToQueue, shuffle bool) error { if !appendToQueue { p.player.Stop() p.nowPlayingIdx = 0 p.playQueue = nil } - for _, song := range tracks { - url, err := p.sm.Server.GetStreamURL(song.ID, map[string]string{}) + nums := util.Range(len(tracks)) + if shuffle { + util.ShuffleSlice(nums) + } + for _, i := range nums { + url, err := p.sm.Server.GetStreamURL(tracks[i].ID, map[string]string{}) if err != nil { return err } p.player.AppendFile(url.String()) - p.playQueue = append(p.playQueue, song) + p.playQueue = append(p.playQueue, tracks[i]) } return nil } func (p *PlaybackManager) PlayAlbum(albumID string, firstTrack int) error { - if err := p.LoadAlbum(albumID, false); err != nil { + if err := p.LoadAlbum(albumID, false, false); err != nil { return err } if firstTrack <= 0 { @@ -153,7 +157,7 @@ func (p *PlaybackManager) PlayAlbum(albumID string, firstTrack int) error { } func (p *PlaybackManager) PlayPlaylist(playlistID string, firstTrack int) error { - if err := p.LoadPlaylist(playlistID, false); err != nil { + if err := p.LoadPlaylist(playlistID, false, false); err != nil { return err } if firstTrack <= 0 { diff --git a/backend/util/util.go b/backend/util/util.go new file mode 100644 index 0000000..dc49ff7 --- /dev/null +++ b/backend/util/util.go @@ -0,0 +1,20 @@ +package util + +import ( + "math/rand" + "time" +) + +// Return a slice of range [0, n) +func Range(n int) []int { + s := make([]int, n) + for i := 0; i < n; i++ { + s[i] = i + } + return s +} + +func ShuffleSlice(a []int) { + rand.Seed(time.Now().UnixNano()) + rand.Shuffle(len(a), func(i, j int) { a[i], a[j] = a[j], a[i] }) +} diff --git a/res/bundled.go b/res/bundled.go index 7502315..f8e0be5 100644 --- a/res/bundled.go +++ b/res/bundled.go @@ -120,3 +120,13 @@ var ResTheatermasksInvertPng = &fyne.StaticResource{ StaticContent: []byte( "\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x80\x00\x00\x00\x80\b\x04\x00\x00\x00i7\xa9@\x00\x00\x00\x04gAMA\x00\x00\xb1\x8f\v\xfca\x05\x00\x00\x00 cHRM\x00\x00z&\x00\x00\x80\x84\x00\x00\xfa\x00\x00\x00\x80\xe8\x00\x00u0\x00\x00\xea`\x00\x00:\x98\x00\x00\x17p\x9c\xbaQ<\x00\x00\x00\x02bKGD\x00\xff\x87\x8f̿\x00\x00\x00\tpHYs\x00\x00\v\x13\x00\x00\v\x13\x01\x00\x9a\x9c\x18\x00\x00\x00\atIME\a\xe7\x01\x0e\x01\x19\x10\x9d_\fR\x00\x00\r\xe6IDATx\xda\xed\x9cy\x98T\xe5\x95\xc6\x7f\xb5\xf4NC\x03\xddv\xd3,\x8d \xa0\xadB#\xcbȪ :*jBB\x04\x1d\x13\x97\x8c\xbb2>q\xccdȨ\xe3\xccd\x92008σ:\x89\xc1Ђ\xb2\x04#\x19Ќ\xa2\x02J4\t\xfa\xc82\x84\xa4E\xd6 \x8d\xcd\xd2\xf4\xbeU\xbf\xf3G/Խu\xeb֭\xea\xbaU=C\xbf\xf5\xd7\xfd\xea|\xcby\xefw\xcf=\xdf\xf9\xcew=\xe2\xfc\x867\xd9\x03H6z\bH\xf6\x00\x92\x8d\x1e\x02\x92=\x80d\xa3\x87\x80d\x0f \xd98\xef\t\xf0'{\x00\x11\xe1\xc5G\x1a餑N\x06餒B\n^\xbcxhE\x04h\xa6\x99F\x1a\xa8\xa7\x91\x06\x1ai$\x80c\xff\xae\xfb\x11\xe0\xc1G\x16\xfdȧ\x90\x81\f \x9f\xfa2\x88\x8b)f\x14E\x14З\xcc.\x8e\xae\x99Z*9\xc6\x01\xfe\xc8>\xf6s\x94*ZC\xf8N\xfaZ\xc0C\x0e\x173\x9e\xbf\xe02\x06\x91\x83ϕ^Z8\xcdAv\xf3{>\xe6sj\xce=\"\xc9$\xc0K.\x13\x99\xc1\x14.\xa6O\x82\xfa\x14\xa7\xd8\xcb\xfb\xbc\xc7.*Q2\t\x18\xc1=\xdc\xc0(ғ\xd2{\r{\xf8%+9\x91,\x02JX\xce\x15\xc9\xe9\xba\x13b#\x8f&\xc7\bf\xb00\xe9ꃇ\x9b9\x92\x9c\x19p)\xefP\x90l\xfd\x018\x9e\x1cO0\x97\xecdkގ\x82\xe4\x10\xe0\xc1\x93l\xcd;F\x92\x1c\x1b\xd0J\rM\xb4\xe2\xc1O\x1a\xa9Ie )6\xa0/\x97\xd0L+^R\xe9\xc3\xe5\xdc\xce\xe8\xf3\x8b\x003F\xf0\"W\x9d\xcf\x04\xc0,~AN2:\xee.\xf1\x80\xed\xbc\x93\x9c\x8e\xbb\v\x01\r\x94R{>\x13\x00\xdb\xf8\xe0\xfc&\xa0\x9aR\x1a\x13\xdfmb\x8d\xa0\a?\xbd\xe8E\x1f\xd2(\xa7\xdc\x14\xa9\xe9\xc7\x06\xa6%\x9a\x80D8B\x1e\xb2(`\x18#\x19\xc1P\n\xe9O/\xfc\x9c`\x15\xcfQ\x19$w\x9a\x97\x99\xecR@$\xfc\xe0\\\x9c\x01\x1e\xb2(\xa2\x84\t\x940\x8c\xbc\x90\x95\x7f\x80\xc5\xfc\x03-A%\x05lb\xdc\xff\a\x02\xbc\xe41\x86\xe9L\xa6\x98\\\x9b{Z\xc1-\xfc\xd6P\xf2\x18\xff\x9e\xd8uB\xbc\t\xf0\x92\xcfD\xfe\x92i\f'Á\xfc\xf3,0X\x82\"ޤ8\x91\x04\xa0x\xfd<\xcaѵZ\xaa=j\x90s\xfcYcL\xed<\x19E\xed8 >F0\x85Q\xcc\xe6fJȊ\xb2\xe6@nc\xb7a\x1bc\x1d\xf3-\xe6@\x1d\xc7\xf11\x804\xdb\xd6\xce\xf2%\x19\x14\x84\x98\xf6\x16ʩ'\xcf\xd2\xd9\xee\xf2\x9d\xcf\xd1MZ\xa1c1߂\xfd\x1aihϫ\xd1zAuA\x12\x01\xbd\xa5\x1b4HE\x9a\xafOöӤ\x95\x9a\xa2B\r\xd7\xfd\xdao\xf8\xe7\x80\x1e\xd4E*\xd4$\xadP\x93\xb9Z\xd7&\xfd`=\xa0\xad\x86\xc1Ƃ\uf1f4\x9c\xa1GtP\xad\x92\xa4SzV\xf9\x9d\xff\x8c3)w\x0e\xabԻSj\x92\xdeS\xa3$\xa9Q[5\xb5\xb3<[+\xcc\xd5b5\x82>F\xf2\r\xe61*\x0e\xef\xed\xbd\xdc\xc8\x11S\x99\x97\x91Le g\xf8\x1d;\r\x1e\xe2\x0fXh9\xf9ob{\xd0u\x7f\xa6p\x19\x1e\xf6\xb2\x9d\x93A\xe5\x93x\x83\xbe]}\x04\xfc*\xd1\x12\x1dn\xbfC]G\xab\x16D\xd1\xfbm\nX\xb4\xf1\x99\x06:\xaa]\xa0}Ɗ\xd1\x1aA?c\xb8\x879\f\xe8\xf2}?\a\x0fw\xb0\x96\x13\x0e\xa5\xfbX\xae_2\x1c\x9a\xdfL\xf3\xcb9\x9aŐ\x8f\x12\x96\xb2\x89\x87\xe2\xaa>\xc0Xf;\x94\xcc\xe6F\xcb\xf2\x02\xaeqT\xff\x1a\x06\x9aJ\x1c\x1b\xbc\x91\xfa\x91\xfe\x1c\xa7I\x1f\x8a\xad\xea\xeb`\x14\x19z\xbaݸ\x85\u202e\x8bX\x7fF\xa8\tu\xa6~\x81\x9e\xd0g\xae)/I\r\xba5\xe2(\n\xb5\xcc\xd6\xc9:\xa2\xbb\x94\x1e\xb6v\x9a\xee\xd0\xc1\xd0J\x91\x95\xcf\xd2|\xfd\xce\xd2\xf0\xc4\x17o\xa8\x97\xad\xb71O\x1fE4\xbb5*\xd5D\xa5\x86\xd4N\xd18-W\xb5U\x15\xfbנ\x97\t\xfc-\xb3\x1dy\xf5]E\r\xf3x\xd3\xd4{6~zQ\xc4$nd\xa2\xc3}\xe4/\xd9\xc2\x7f\xb3\x93\x134\x00\xe9\xe43\x9a\xeb\x98E\xbe\xb5\xb8\x1d\x01\xf9\xdc˃\x14&@\xf96\xac\xe3N\x1a\x82\xae3X\xc4tz\x93K\xaf([\nP\xc9I\xaa\x81lr#$]\x84}\xd7_\xabm\t\x98\xf8\xc18\xad\xabL\xa3x\xd8\xfd\x11X\xbf\x06/\xe0iV3=\xc1\x11þ|\x8b\x14C\xc9\x06\xf6\xb8\xddi\xa8\x8a\x1e&\xb1\x9a\x85\xf4O\xa8\xf2m\x98M\x89\xe1\xfa\x18\xaf$\x9a\x80\f\xeeg\x1d3\x93\x14-\xce\xe7\xafL=\xbfFY\"\t\x18\xc0\x12\x962()ʷa\x0e\x97\x18\xae\x0f\xb0.q\x04\x8c\xe6e\x1eHR\xd2R\a\x860\xdfT\xb2&d\xa5\xe8\n\x01\x1ef\xf1*\xb3\x12\x12\x90l\xa1\x86*\xeaB\x93\x16\x01\xb8\x95a\x86\xeb}\xbc\xee\xe6`\xda\xfc\x00\x1fsY\x9c\x80\xa9\x7f\x8a\xdf\xf3\x1b\xf6QA3\xe9\fd\fS\x19\x1b2\xe7\xfe\x8eE\x86\xeb\xf1l\n\xe7\xc6\xc4\x01B~ݫ\x93\xae\xbf\xe5+\xb4L\x13\x95aZb\xe5\xe8\x1e\x1d7I\xee4\xad\xedS\xf43\xf7\x86\x85\xfcz@g\\W\xff7\xbaF\xfe0Nףj6\xc8\x06\xf4\xa0Ib\xbaN\xbb4\xaef4/\x01w\xff\x97\x1aj\xb3\xd0\x19\xa22\x93\xfc\x87\xca3H\xa4k\x8dK#\xab\xf5\xf2\x1d\xd7]\x9e_\xf3\b\x87l\xfe\xaf\xa4\xc2T2\x9e\xeb\r\xd7\r\x94R\xe3\xca\xd8j\xbc\xae\xa7'\x95\xf1=\xbe\xb0\x95\xc8!\xcfT\x92\u009d\xa6\xf4\xe9\x0fxߕ\xd1UzivU\xfd\x16\x9ee\xb7\xad\x84\x879\xa6\x17\x1f\xc0df\x1a\xaekX\xe1J\xf6\xc0\t/\x1f\xbaJ\xc0'\xac\x0fQ\xb8\x1f\x13\x98\xc9ed\xe3'\x8f\x87Xh\xb1\\\xcd\xe0N2\r%\uf636Q\xe3\x83C~^d\x12\xbd]#\xe0u\xd3\xf3\x9d\xca\xd7y\x84bҨ\xa2\x8c\xd3\x14Q\x1c&Qr\x06S\xd8\x1ct}\x86W\x98\n\x9c\xe6\v\x8eSA5\x01R\xe8\xcd\x05\x142 L\xac8\x12D\x19\xca\xd6F\u05ec\x7f\xa3\xae7Xs\xaf\x16X\a\xa6,\xb1\xca\x14\xdc*\xd4S\xba]\x97+Wi\xf2\xca#\xe4\x91O\x19*\xd08ݥ\x9f\xab\xcc\xf4:\x8d\x8c:\xcdAhnT\xfb\xb9ѠE\xdf0\xa80.\xaa=ē\x9abr\x9b<6/S\x9f\x8at\x8f6G\xb5QwD\x97\"\xd4W[]\"@z)\xe8.\xfa\xf4l\x94\xb5\x9f\x93\xcfaܺ\xe3\xd7[s\xb5M-\x0e\xdbߢ\xdem\xd5\xee\x8ez\xf28E\x95\x1en\x0fU\xa7\xe9\xee\xa8\xfd\xb9ú$J\x02\x10\xca\xd5\xf7B\xdckk,V\xfbb(\x9f\x8dL\x88\xc2x4r\x88?p\x803x\xe9\xcfp\x8a\x19l\nf\x9dC-o\xb3\x9dV&s}\f\xa7\x04\x9e`q\f\xc6\xcd\xcb4\x1611\x82T\x13\xf3\xd8\xd0\xc1\xda\x02\xc7\xe1\xc7*\xad\xd7\xd75H)\xedO\xa4Gi\x1a\xaa\xbb\xb4ە\x19\xb4M}b\x98\x03\b\r׆\b\xfb\be*RgTx\xb0\xf68\x1a\xd0ou\x8biE\xd7\xf1\x1b\xef\xca\xde\xd1YM\xb7\xe8\xcb#\x9fҔ\xa1Tym((\xd0z۶\x7f.\x9f:\x93I\x8e\xb2\x9a\x1fD\x982\xad\xacfa\xd8\xf8\xccǬ\xe4\x99\x18\xa6\xab=z3\xd3\xe4\x04\x17p5\xa3\x19J?\xfc4Q\xc1\xe7\xecd\x17\xc7\f\xe9vm(\xe7)\xae\xe0°\x0f\xc0&\x02\x04\xed\v\x8c\b\x9b{сR\xf5\xb7\x9dt7\x85&\xa0\xc4\x01\uf6b6̾\xa2z\x93D\xa3>\xd3\x7f\xea*\xa5Y\xbc\x1aW\x85m\xf7\x93\xb6\xbc\x93s\xfe\xd3\xfe\x10\xa7Ո-\xfc=\xa7l%2]\x89%\x17Sd\xb8>@\x95I\"\x95\x8b\xb8\x9f\r\xbc\xc0e\xa6\x90^\x80\xd3a\xdb}\xbd=#!\x88\xaf\xd16\xdb\xdf\x15\xba:\x82\xd1I\xd5K.\xdc\x7f\xa9E\xf3\r\xfd\xf4\xd3\xc7ae\xcbt\xbbR\x82d3\xf5\xeb0\x92\x87:^\xb0\xc1\xf7l/\x1b\xc2\xf2\xf5\vC\x06N\x1b\xfc\xe4Ч}!\x93\xc1}\xccu\xe1\xfe\x83\x8f1\x86\xeb\xb3\xec\x0f+;\x82\x17x,(\xc68\x9b)a$\xd7\xf3\xa7\x0e%\x82'\xcc*n\rY\x9b\x03T\xb2\xdaddҘ\xc5|F\xd2\xca\x1f؉\x9f\xe9\\gZ\xbd\xc5\x0fŤ\xd2\x144\xca\xfd6\xb2\xbdy\x86\"^\xe2\x18Y\xcc\xe6\xbba<\x8fì\xe8\x8cI\x9b\xa6q\xa9\xe5\x84\xd9a\xca\xdfH\xd7?\xaaʕ\to\x85\x9d\xca5\xf4~o\x04\xf9VUh\xaf\x0e\xdbx\xb7Ϝ[U\x18\xcdV\x13\xa5\x9c\xb5`\xacɔ\xd9?7,\xb7n\xe0\x02S\xd0\xeeX\xd0|\xb0\x82\x87\\\x8a\x19\x12\xf6(\xc0\xa7,\xa73+\xc0l\xb7?\xe2]\x8b*\xc3\fo\xd3~ܛ\x90\x94\x89\x0ed\x9b\x1e\xcb\xd3\xd4w\xa1\xb5z\x96\x04\xfb2ސ\xbfK\xa9\v\xa9T\xc0\x82Π\x89\x9f\xbb\x99\x14'\xd5>g\x19\xdf\xe5y\x0e\xdaJ\xa5\xd3\xcfp}\u0590F\x11-֚v\x9a,\x16\x94oY<5MZ\xab\xebU\xac\xa9zVg\xe3\xf4l\xbf\xa3K\xdb\xc3\x1ac\xb4\xdd\xf6\x99\xfe\x96a\x84E:\x1cs\x9f\xbbL\x99ɖ\x19\"\xb7\x87IE\xabSy\x1cM\xdfqM\f\xeas\x86\xedRٸQR\x18\xb2\x8f\xe0\x14\xa7t\x8bY[+\xdf\xed-vXN\x9e\f\xf2\xe3h\xfa6\xf3i\xd0\xd5\x0e\xdb\\\x10c\x92|\xab\x85\xdf\xef\x04M\xfc\x987̅V\x04\x9c\xe2e\xbb/\xef\xc4\x05ռj\b\xc87\x868\xb8\xe1!b\xc9\xf0\x16?\xe3\xb9P\xbd\xac\xbd\xf7\x8d\x11b\xf9]\xc7\x1bl3\\\xe7\x85]\xb5\x01\xa6a{c\xcaP_\xc7\xd3VgS\xad\t8\xce+1\xb1\xec\x14\x87Ylz\x95Mg\x84\x8d\xbcQ\xd6\x176\xfa\x14\x1e\xbf\xe2qC\xda|\x04\x02\xe0\xb5\x0e_\xd9\x05\xd4\xf2\xcf|b(\xc9\xe26\x9b\xcf(\x04L\x8fGz\xd4Y,\xaf\xf3(Ǭ\xff\nG\xc0!ֺ\xa4~#\x8bXi*\x9ba\xda\b3\xa2ٴ\xa8͎prȈ\x00/\xf3\x10G\xc3\xfe\x1fvy{\x89Ujq\x97Q\xad'CBj}-=\x8fs8\xa9\xb1\x06\xf9\x19\xaau\xdc_\xad~\xac\x1c\xbbe|\xf8\xbf\xbcZ\x12w\xf5\xcb\xf4M\x8bT濉\x10\x94߯\xc1\x06\xf9\xf9\x8e\x03\xb8Gu\x9fE\x9c\xc8!\x01\xe8\n}\x11G\xe5O\xe9\xa7힟\xf17YG\"\xd4|\xdf\x14\x14{\xc2Q\x7f\x01m\xd1T۽$\x84l\x0fO\xeff#\xf79~֪\xd8N\x19\xfd\x19υ&#\xd5\xc0A6\xb3\x8e\x1d\x16\xab\xb8\"~\xc8\xe0\b-\x7ffz}\r#2*\xf8\t˜\x1cñ#\xa0\x85\x95\xcc5-D\xc2\xe1\x10\x8f\xf3&\r\xf8(\xe0rF3\x9c\\Ri\xa2\x82\x03\xecfO\xc8Q\xf96䱈\xe9\x11\xdb6\x1e\xac̴}a\x024\xf06\x8b\xf9С3g;A\xd2\xf4\xaa\xa3\xe9֤o\x9bjz\xe4W\x8a\xfc\xb6S0O\xa5\x0eN\x9eU\x9b\xa2\x91Cl\xa3\u05cdz_\xb7);\xd2\xc4wf\x03\x10\xba\xce\xd1\xda\xefO&3\xe5\xe4w\xa1\xd6;:x\xb7W\x03\f\xf5\xaeVM\x18\xc9\x1am\xd6\x1d\xea\x17\xdd8\"\x1d\x9b\xfb\x80\xb7\x1d\x04;\xab\xa2La\xf22\x8d\x1f:\x8c*\xec\xe0K\xc3\xf5X\x8b\x03r\x01\x8e\xf0.\xeb\xf9(\x8a\x15E;\"\x11P\xcfO\x98\x15\xf1\v?\x03(\xe0\x8c\xe3>\xf3\xf8k\x1eux\xf4.\xc0{\xa6gy\x1f\x1b(\xa1\xb0\xdds\xac\xe7\b;\xd8\xccv\x8eĶF\x8c|t6\x95\xe7\xf9v\xc4v~\xc4S\x8eҭr\xb8\x81\x87\xb9\xd2\xf1r\xe6 \xd7\xf2\xb9\xa9,\x8d\x81\\\xca\b\xb2\xa8\xe4\x8f\xec\xa3<\xc6\xe5q\x1b\x1c<'\xa3u\xc0\x81\xa9\xfa\x17\r\x0f\x9b\v\x8aP\x8a.҃\xda\x16\xb2\xb1e\x8f\xe5Q\xa7HD\xf9svx\xfa1\xfe-\xe2\xc3\xd2\xcaa>\xe4\x03vq\x94J\x1aiEx\xf0\x92N\x0eC(a*\x93\x19\x14\xe5B\xb6\x9e[\xd9ԅ\xbb\xeb\x00\xce\b\xe8\xc3\n\xbe\xea\xa8=Q\xc5\tʩ\xa0\x9a\x16R\xc9&\x8f\x02. ;\xa6D\xfcm̉¶\xc4\x06\x87S\xa5$\xe68\\\xech\xd1}\xeeN\x7f!\xc7\xfb\xb9;y\xcar\xcb\xc4M|\xca\x7f\xb9߉\xf3\r\xed\xf5,q9\xadֈ\x16^\xa4\xbc;\x11\xd0\xc2\xd2\xe0-%ױ\x9d\xd7\x12\xd1M4)\r5<ɚ\x04\xa9_\xcd\xd2\b\xe9\x18I \x00N\xf2\x1d\xd6%d\x16\xbc\xca[\x89P\x9f\x18\xbe!\x92\xaf\x95\xae\x9f\xe8ݥ\x11\xee\xdb\xff\xb6_,\x95\xfa\xe9?\\\xcb.\x96\xa4J}-Q\xea\xc7F\x00\xca\xd4㮝4j\xd1?ٺ\xd4݂\x00\xe4\xd3\xcd.冮q\xf45\x91\xa4\x13\x80\xd0H\x95v\xf9+Rfl\xb5=_\xd6\xcd\b@\x99\xfa\xa6v\xc7\xed\x83J\xd2'\x1a\x9dX\xf5\xbbJ\x00BC\xf5\xafq\xfa\xbc\xce.MH\xb4\xfa\xf1 \x00\xf9T\xa2e]\xf8\x9e\\\x1b>N\x86\xfaN\xe3\x01\x91᧘\xf9|\x95\x111~\xa6u\v\v\xf8\x9f\x04\xb9>\x06\xc4\xf3\x93\x9a^\n\x99\xc9\xcd\\IaT\x1ef3k\xf9\xbe\xbb_\tH\f\x01mHe\x18W2\x8d\xb1\fup\x98\xad\x96\xbd,gM\xf4\xd1\xdc\xeeK@\x1b|\xe4p!\xa3\xb8\x98\xe1\x14ҟޤ\xe1\xc7\x03\x04h\xa6\x963\x9c\xe0\x10e\xece\x0f\xa7H\xe2'\xee\xdd\xff\xb2\xb4\x8ft\xb2\xc8$\x934\xbc\x88fꩣ\x8eZZ\x92\xa9x\xe2\b\xe8\xe6\xe8>\x1fW\xef!\xa0\x87\x80\x1e\x02z\b\xe8!\xa0\x87\x80\x1e\x02\x12\x8d\xff\x05\x05\xc82\x06R\xea7\xf8\x00\x00\x00%tEXtdate:create\x002023-01-14T01:24:07+00:00\xf8'2\x9e\x00\x00\x00%tEXtdate:modify\x002023-01-14T01:24:07+00:00\x89z\x8a\"\x00\x00\x00\x19tEXtSoftware\x00www.inkscape.org\x9b\xee<\x1a\x00\x00\x00\x00IEND\xaeB`\x82"), } +var ResShuffleSvg = &fyne.StaticResource{ + StaticName: "shuffle.svg", + StaticContent: []byte( + "\r\n\r\n\r\n\r\n\r\n\t\r\n\t\t\r\n\t\t\r\n\t\t\r\n\t\r\n\r\n"), +} +var ResShuffleInvertSvg = &fyne.StaticResource{ + StaticName: "shuffle-invert.svg", + StaticContent: []byte( + "\n\n\n\r\n\r\n\r \n\r"), +} diff --git a/res/bundled_gen.sh b/res/bundled_gen.sh index 4a4644d..34fb455 100755 --- a/res/bundled_gen.sh +++ b/res/bundled_gen.sh @@ -1,25 +1,27 @@ #!/bin/sh -fyne bundle -package res -prefix Res icons/disc.png > bundled.go -fyne bundle -append -prefix Res icons/disc-invert.png >> bundled.go -fyne bundle -append -prefix Res icons/headphones.png >> bundled.go -fyne bundle -append -prefix Res icons/headphones-invert.png >> bundled.go -fyne bundle -append -prefix Res icons/heart-filled.png >> bundled.go -fyne bundle -append -prefix Res icons/heart-filled-invert.png >> bundled.go -fyne bundle -append -prefix Res icons/heart-outline.png >> bundled.go -fyne bundle -append -prefix Res icons/heart-outline-invert.png >> bundled.go -fyne bundle -append -prefix Res icons/musicnotes.png >> bundled.go -fyne bundle -append -prefix Res icons/musicnotes-invert.png >> bundled.go -fyne bundle -append -prefix Res icons/people.png >> bundled.go -fyne bundle -append -prefix Res icons/people-invert.png >> bundled.go -fyne bundle -append -prefix Res icons/playbutton.png >> bundled.go -fyne bundle -append -prefix Res icons/playlist.png >> bundled.go -fyne bundle -append -prefix Res icons/playlist-invert.png >> bundled.go -fyne bundle -append -prefix Res icons/podcast.png >> bundled.go -fyne bundle -append -prefix Res icons/podcast-invert.png >> bundled.go -fyne bundle -append -prefix Res icons/star-filled.png >> bundled.go -fyne bundle -append -prefix Res icons/star-filled-invert.png >> bundled.go -fyne bundle -append -prefix Res icons/star-outline.png >> bundled.go -fyne bundle -append -prefix Res icons/star-outline-invert.png >> bundled.go -fyne bundle -append -prefix Res icons/theatermasks.png >> bundled.go -fyne bundle -append -prefix Res icons/theatermasks-invert.png >> bundled.go +fyne bundle -package res -prefix Res icons/freepik/disc.png > bundled.go +fyne bundle -append -prefix Res icons/freepik/disc-invert.png >> bundled.go +fyne bundle -append -prefix Res icons/freepik/headphones.png >> bundled.go +fyne bundle -append -prefix Res icons/freepik/headphones-invert.png >> bundled.go +fyne bundle -append -prefix Res icons/freepik/heart-filled.png >> bundled.go +fyne bundle -append -prefix Res icons/freepik/heart-filled-invert.png >> bundled.go +fyne bundle -append -prefix Res icons/freepik/heart-outline.png >> bundled.go +fyne bundle -append -prefix Res icons/freepik/heart-outline-invert.png >> bundled.go +fyne bundle -append -prefix Res icons/freepik/musicnotes.png >> bundled.go +fyne bundle -append -prefix Res icons/freepik/musicnotes-invert.png >> bundled.go +fyne bundle -append -prefix Res icons/freepik/people.png >> bundled.go +fyne bundle -append -prefix Res icons/freepik/people-invert.png >> bundled.go +fyne bundle -append -prefix Res icons/freepik/playbutton.png >> bundled.go +fyne bundle -append -prefix Res icons/freepik/playlist.png >> bundled.go +fyne bundle -append -prefix Res icons/freepik/playlist-invert.png >> bundled.go +fyne bundle -append -prefix Res icons/freepik/podcast.png >> bundled.go +fyne bundle -append -prefix Res icons/freepik/podcast-invert.png >> bundled.go +fyne bundle -append -prefix Res icons/freepik/star-filled.png >> bundled.go +fyne bundle -append -prefix Res icons/freepik/star-filled-invert.png >> bundled.go +fyne bundle -append -prefix Res icons/freepik/star-outline.png >> bundled.go +fyne bundle -append -prefix Res icons/freepik/star-outline-invert.png >> bundled.go +fyne bundle -append -prefix Res icons/freepik/theatermasks.png >> bundled.go +fyne bundle -append -prefix Res icons/freepik/theatermasks-invert.png >> bundled.go +fyne bundle -append -prefix Res icons/publicdomain/shuffle.svg >> bundled.go +fyne bundle -append -prefix Res icons/publicdomain/shuffle-invert.svg >> bundled.go diff --git a/res/icons/disc-invert.png b/res/icons/freepik/disc-invert.png similarity index 100% rename from res/icons/disc-invert.png rename to res/icons/freepik/disc-invert.png diff --git a/res/icons/disc.png b/res/icons/freepik/disc.png similarity index 100% rename from res/icons/disc.png rename to res/icons/freepik/disc.png diff --git a/res/icons/headphones-invert.png b/res/icons/freepik/headphones-invert.png similarity index 100% rename from res/icons/headphones-invert.png rename to res/icons/freepik/headphones-invert.png diff --git a/res/icons/headphones.png b/res/icons/freepik/headphones.png similarity index 100% rename from res/icons/headphones.png rename to res/icons/freepik/headphones.png diff --git a/res/icons/heart-filled-invert.png b/res/icons/freepik/heart-filled-invert.png similarity index 100% rename from res/icons/heart-filled-invert.png rename to res/icons/freepik/heart-filled-invert.png diff --git a/res/icons/heart-filled.png b/res/icons/freepik/heart-filled.png similarity index 100% rename from res/icons/heart-filled.png rename to res/icons/freepik/heart-filled.png diff --git a/res/icons/heart-outline-invert.png b/res/icons/freepik/heart-outline-invert.png similarity index 100% rename from res/icons/heart-outline-invert.png rename to res/icons/freepik/heart-outline-invert.png diff --git a/res/icons/heart-outline.png b/res/icons/freepik/heart-outline.png similarity index 100% rename from res/icons/heart-outline.png rename to res/icons/freepik/heart-outline.png diff --git a/res/icons/icons-attrib.txt b/res/icons/freepik/icons-attrib.txt similarity index 100% rename from res/icons/icons-attrib.txt rename to res/icons/freepik/icons-attrib.txt diff --git a/res/icons/musicnotes-invert.png b/res/icons/freepik/musicnotes-invert.png similarity index 100% rename from res/icons/musicnotes-invert.png rename to res/icons/freepik/musicnotes-invert.png diff --git a/res/icons/musicnotes.png b/res/icons/freepik/musicnotes.png similarity index 100% rename from res/icons/musicnotes.png rename to res/icons/freepik/musicnotes.png diff --git a/res/icons/people-invert.png b/res/icons/freepik/people-invert.png similarity index 100% rename from res/icons/people-invert.png rename to res/icons/freepik/people-invert.png diff --git a/res/icons/people.png b/res/icons/freepik/people.png similarity index 100% rename from res/icons/people.png rename to res/icons/freepik/people.png diff --git a/res/icons/playbutton.png b/res/icons/freepik/playbutton.png similarity index 100% rename from res/icons/playbutton.png rename to res/icons/freepik/playbutton.png diff --git a/res/icons/playlist-invert.png b/res/icons/freepik/playlist-invert.png similarity index 100% rename from res/icons/playlist-invert.png rename to res/icons/freepik/playlist-invert.png diff --git a/res/icons/playlist.png b/res/icons/freepik/playlist.png similarity index 100% rename from res/icons/playlist.png rename to res/icons/freepik/playlist.png diff --git a/res/icons/podcast-invert.png b/res/icons/freepik/podcast-invert.png similarity index 100% rename from res/icons/podcast-invert.png rename to res/icons/freepik/podcast-invert.png diff --git a/res/icons/podcast.png b/res/icons/freepik/podcast.png similarity index 100% rename from res/icons/podcast.png rename to res/icons/freepik/podcast.png diff --git a/res/icons/star-filled-invert.png b/res/icons/freepik/star-filled-invert.png similarity index 100% rename from res/icons/star-filled-invert.png rename to res/icons/freepik/star-filled-invert.png diff --git a/res/icons/star-filled.png b/res/icons/freepik/star-filled.png similarity index 100% rename from res/icons/star-filled.png rename to res/icons/freepik/star-filled.png diff --git a/res/icons/star-outline-invert.png b/res/icons/freepik/star-outline-invert.png similarity index 100% rename from res/icons/star-outline-invert.png rename to res/icons/freepik/star-outline-invert.png diff --git a/res/icons/star-outline.png b/res/icons/freepik/star-outline.png similarity index 100% rename from res/icons/star-outline.png rename to res/icons/freepik/star-outline.png diff --git a/res/icons/theatermasks-invert.png b/res/icons/freepik/theatermasks-invert.png similarity index 100% rename from res/icons/theatermasks-invert.png rename to res/icons/freepik/theatermasks-invert.png diff --git a/res/icons/theatermasks.png b/res/icons/freepik/theatermasks.png similarity index 100% rename from res/icons/theatermasks.png rename to res/icons/freepik/theatermasks.png diff --git a/res/icons/publicdomain/shuffle-invert.svg b/res/icons/publicdomain/shuffle-invert.svg new file mode 100644 index 0000000..fddbcb9 --- /dev/null +++ b/res/icons/publicdomain/shuffle-invert.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/res/icons/publicdomain/shuffle.svg b/res/icons/publicdomain/shuffle.svg new file mode 100644 index 0000000..0f474f1 --- /dev/null +++ b/res/icons/publicdomain/shuffle.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/ui/browsing/albumpage.go b/ui/browsing/albumpage.go index 32dac31..639a7e8 100644 --- a/ui/browsing/albumpage.go +++ b/ui/browsing/albumpage.go @@ -4,6 +4,7 @@ import ( "fmt" "log" "supersonic/backend" + "supersonic/res" "supersonic/ui/controller" "supersonic/ui/layouts" "supersonic/ui/util" @@ -65,9 +66,9 @@ func NewAlbumPage( widgets.ColumnArtist, widgets.ColumnTime, widgets.ColumnPlays}) // connect tracklist actions a.tracklist.OnPlayTrackAt = a.onPlayTrackAt - a.tracklist.OnAddToQueue = func(tracks []*subsonic.Child) { a.pm.LoadTracks(tracks, true) } + a.tracklist.OnAddToQueue = func(tracks []*subsonic.Child) { a.pm.LoadTracks(tracks, true, false) } a.tracklist.OnPlaySelection = func(tracks []*subsonic.Child) { - a.pm.LoadTracks(tracks, false) + a.pm.LoadTracks(tracks, false, false) a.pm.PlayFromBeginning() } a.tracklist.OnAddToPlaylist = a.contr.DoAddTracksToPlaylistWorkflow @@ -148,7 +149,6 @@ type AlbumPageHeader struct { miscLabel *widget.Label toggleFavButton *widgets.FavoriteButton - playButton *widget.Button container *fyne.Container } @@ -178,9 +178,13 @@ func NewAlbumPageHeader(page *AlbumPage) *AlbumPageHeader { page.nav(GenreRoute(a.genre)) } a.miscLabel = widget.NewLabel("") - a.playButton = widget.NewButtonWithIcon("Play", theme.MediaPlayIcon(), func() { + playButton := widget.NewButtonWithIcon("Play", theme.MediaPlayIcon(), func() { page.onPlayTrackAt(0) }) + shuffleBtn := widget.NewButtonWithIcon(" Shuffle", res.ResShuffleInvertSvg, func() { + page.pm.LoadAlbum(page.albumID, false, true) + page.pm.PlayFromBeginning() + }) a.toggleFavButton = widgets.NewFavoriteButton(a.toggleFavorited) // Todo: there's got to be a way to make this less convoluted. Custom layout? @@ -190,7 +194,7 @@ func NewAlbumPageHeader(page *AlbumPage) *AlbumPageHeader { container.NewVBox( container.New(&layouts.VboxCustomPadding{ExtraPad: -12}, a.artistLabel, a.genreLabel, a.miscLabel), container.NewVBox( - container.NewHBox(widgets.NewHSpace(2), a.playButton), + container.NewHBox(widgets.NewHSpace(2), playButton, shuffleBtn), container.NewHBox(widgets.NewHSpace(2), a.toggleFavButton), ), ), diff --git a/ui/browsing/playlistpage.go b/ui/browsing/playlistpage.go index 287fa69..0db1cb8 100644 --- a/ui/browsing/playlistpage.go +++ b/ui/browsing/playlistpage.go @@ -57,9 +57,9 @@ func NewPlaylistPage( } // connect tracklist actions a.tracklist.OnPlayTrackAt = a.onPlayTrackAt - a.tracklist.OnAddToQueue = func(tracks []*subsonic.Child) { a.pm.LoadTracks(tracks, true) } + a.tracklist.OnAddToQueue = func(tracks []*subsonic.Child) { a.pm.LoadTracks(tracks, true, false) } a.tracklist.OnPlaySelection = func(tracks []*subsonic.Child) { - a.pm.LoadTracks(tracks, false) + a.pm.LoadTracks(tracks, false, false) a.pm.PlayFromBeginning() } a.tracklist.OnAddToPlaylist = a.contr.DoAddTracksToPlaylistWorkflow @@ -143,8 +143,6 @@ type PlaylistPageHeader struct { ownerLabel *widget.Label trackTimeLabel *widget.Label - playButton *widget.Button - container *fyne.Container } @@ -162,16 +160,21 @@ func NewPlaylistPageHeader(page *PlaylistPage) *PlaylistPageHeader { a.ownerLabel = widget.NewLabel("") a.createdAtLabel = widget.NewLabel("") a.trackTimeLabel = widget.NewLabel("") - a.playButton = widget.NewButtonWithIcon("Play", theme.MediaPlayIcon(), func() { + playButton := widget.NewButtonWithIcon("Play", theme.MediaPlayIcon(), func() { page.onPlayTrackAt(0) }) + // TODO: find way to pad shuffle svg rather than using a space in the label string + shuffleBtn := widget.NewButtonWithIcon(" Shuffle", res.ResShuffleInvertSvg, func() { + page.pm.LoadPlaylist(page.playlistID, false /*append*/, true /*shuffle*/) + page.pm.PlayFromBeginning() + }) a.container = container.NewBorder(nil, nil, a.image, nil, container.NewVBox(a.titleLabel, container.New(&layouts.VboxCustomPadding{ExtraPad: -10}, a.descriptionLabel, a.ownerLabel, a.trackTimeLabel), - container.NewHBox(a.playButton), + container.NewHBox(playButton, shuffleBtn), )) return a }