From 1ad45a33db96042c6e9bc6e2480a8831b25ac07d Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Fri, 14 Mar 2025 18:51:14 -0700 Subject: [PATCH] more progress - player switching working --- backend/playbackengine.go | 1 + res/bundled.go | 5 +++++ res/bundled_gen.sh | 1 + res/icons/publicdomain/cast.svg | 4 ++++ ui/bottompanel.go | 6 ++++++ ui/controller/controller.go | 20 ++++++++++++++++++++ ui/theme/theme.go | 1 + ui/widgets/auxcontrols.go | 22 ++++++++++++++++++++-- 8 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 res/icons/publicdomain/cast.svg diff --git a/backend/playbackengine.go b/backend/playbackengine.go index dd7f6a1..397515b 100644 --- a/backend/playbackengine.go +++ b/backend/playbackengine.go @@ -157,6 +157,7 @@ func (p *playbackEngine) SetPlayer(pl player.BasePlayer) { p.SeekSeconds(p.pendingPlayerChangeTimePos) p.pendingPlayerChange = false } + p.invokeNoArgCallbacks(p.onPlayerChange) } func (p *playbackEngine) PlayTrackAt(idx int) error { diff --git a/res/bundled.go b/res/bundled.go index dfb3f95..590f9a0 100644 --- a/res/bundled.go +++ b/res/bundled.go @@ -20,6 +20,11 @@ var ResPlaybuttonPng = &fyne.StaticResource{ StaticContent: []byte( "\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x80\x00\x00\x00\x80\b\x03\x00\x00\x00\xf4\xe0\x91\xf9\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\x01\xcePLTE\x00\x00\x00\"\xd7)!\xd3(#\xe2+!\xd6( \xce'\"\xd9)!\xd5(\"\xd6)\x1b\xae!!\xd2(\"\xd8)\x1b\xb0!\"\xd8)!\xd5(!\xd5(\"\xd7)!\xd6(!\xd6(!\xd6(!\xd6(\"\xd7)!\xd6(\"\xdb)!\xd6(!\xd6(!\xd6(!\xd6(!\xd6(\"\xd7)\"\xd6)\"\xd6)\"\xd7)!\xd6(!\xd6(!\xd6(!\xd6(!\xd6(\"\xd7)!\xd6(!\xd5(!\xd6(\"\xd7)!\xd6(#\xde*!\xd5(!\xd6(!\xd6(!\xd6( \xcd'!\xd3(!\xd6(!\xd6(!\xd2(!\xd5(!\xd6(!\xd6(!\xd6(!\xd6(\"\xd7)!\xd6(!\xd6(\"\xd7)!\xd6(!\xd5(!\xd6(\"\xd6)\"\xd7)\"\xd6)!\xd6(!\xd6(\"\xd6)!\xd6(!\xd5(!\xd6(\"\xd6)!\xd6(!\xd6(!\xd6(\"\xd6)!\xd5(!\xd6(\"\xd6)\"\xd7)\"\xd7)!\xd6(\"\xd7)!\xd6(!\xd5(\"\xd6)!\xd6(!\xd5(\"\xd7)!\xd6(!\xd6(!\xd6(!\xd6(!\xd5(!\xd6(!\xd4(!\xd6(!\xd5(!\xd6(!\xd6(!\xd5(\"\xd7)\"\xd7)!\xd6(!\xd6(\"\xd7)\"\xd7)\"\xd6)!\xd6(!\xd6(\"\xd7)\"\xd6) \xd6'(\xd8/)\xd80\x1f\xd6&>\xdcD\x97\xec\x9b\xc3\xf4\xc5\xc6\xf4Ǐ\xea\x93?\xdcE\xc5\xf4\xc7\xff\xff\xff\xfe\xfe\xfe\xdc\xf8\xddz\xe6~-\xd94\x97\xec\x9a\xfa\xfe\xfa\xba\xf2\xbcQ\xdfW\xec\xfb\xec\x90\xeb\x944\xda:9\xdb?\xe0\xf9\xe1\xfc\xfe\xfc\xcf\xf6\xd0f\xe3k$\xd7+<\xdbB\xe4\xfa\xe5\xf2\xfc\xf2\xa7\xef\xa9D\xddI\xbf\xf3\xc1&\xd7-\x98원\xea\x93(2|%\x00\x00\x00qtRNS\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x04\x141S\x7f\x93\xbe\xcf\xdd\x01\x1cEq\xa6\xcb\xe7\xf8\xfe\v3f\xa9\xd9\xfe\x05'e\xb4\xee\xfd\x01\x11L\xa0\xe4\x01\x16\xc7\xf6\x15o\xd0\xfb\x14\xca\nM\xbf\xfc\x01)\xa1\xf5\ro(\xa4\xfb\x04Q\xd7\x0f\x80\xf0\"\xfc*\xbdCC\xdd\xd6\"\x0e\x03\xfd)\rp\tN\x15\xc9p\xfc\x17\xce\x12Ɵ\n3\xed\xa8\xd8\x03\xa60\x92\x9c\x8e\xa7 \x00\x00\x00\x01bKGD\x7fH\xbfq\xe5\x00\x00\x00\atIME\a\xe7\x01\x13\x01%2\x99\xbf\xbb\v\x00\x00\x04[IDATx\xda\xe5\x9bgC\x13A\x10\x86wÑP\x83\xb4H\rx\x82tA\x9a\"\x1d%H\x93Б\xa2\"Ho\xb7\x89\x82\xf5,\xd8{\x03\xfd\xb7\xe6\x12\x90\x00Iؽ\xdd\xcb|\xf0\xfd\xbe\xf7>\x99\x9d\xdcm\x99A\x88Q8ޚp*1)9%զ\x90=)\xb6Ԕ\xe4\xa4\xc4S\t\xd6x\xcc\xfa<6\xf3\xd3ִ\xf4\x8c̬l{\x0e9\xa6\x1c{vVfFz\x9a\xf5\xb4A\x10\x18瞑\xcf\xe6\xd9\xf3I\b\xe5\xdb\xf3\xce\xcagr\xb1x\x06|\xae\xa0\xb0\xa8\xb8\x84P\xa8\xa4\xb8\xa8\xb0\xe0\x9cP\x04\\Zv\xbe\xbc\xa2\xc4Ec\xaf\xc9URQ~\xbe\xacT\x14\x83\xe9B\xa5\\UMk\xbe\xaf\xea*\xb9\xf2\x82I\x84}DM\xed\xc5K\xac\xf6\x9a.]\xac\xad\x89\xe0E\xc0R\x9d|Y\x97\xbd\x17\xe1\xb2\\'qM\x04\xaeohl\xd2k\xaf\xa9\xa9\xb1\xa1^?\x01nnim\xa3μ\xc0r\xb5\xb5\xb64\xebD\xc0W\xae\x1e\xbc\xec\xf4K\xb1]\xbd\xa2\x87\x00G\xb6;:\xf8\xed5u8\xda#\x99\x11\xf0\xb5\xce.\xce\xe8\xfb\xcdCW\xe75F\x02\xdc\xdd\xd3+\xcc\xdfC\xd0\xdb\xd3\xcdB\x80\xa5\xeb\x8e>q\xf6\x9a\xfa\x1c\xd7\xe9\xff\x90\xd8\xec\xec\x17k\xaf\xa9\xdfi\xa6$\xc0x\xa0K@\xf6\x1f\x95\xd25@\xf7\x91\xc4xpH\xbc\xbd\xa6\xa1A\x1a\x02\xe3\xfc\xe9\b\x8c\xf4\xa7!0\xd6\xffd\x02lq\x0e\x1b\xe9OȰ\xd3\x12\x8a\x00\x8f\xf4\x1b\x90\xff\xfeR\xfaGB\x00H\xa3\x0ec\xed59F\xa5\xa0\xfec=7\x8c\a\xb8\xd13\x16\x84\x00\xe3\xce^\xe3\xfd\t\xe9\xed\f\x92\x88\xd2\xf8\xb0\xc0\xefOp\xb9\x86\xc7\x03\x86@\x9ap\x84\xc5\xdfC\xe0\x98\b@\x80\xa3&\x05\xad?NV\xc7d\xd4\xf1I\xb08m\xe1\xf2'\xc4\xe6\xb4\x1c\x9b\x80\xa9V\x83\xdf\x00\xfeRZ\xa7\x8eLB\xb4\xf9f[\xf8\xfc\ti\xbbi\x8e>\x1c\x80[\x8da\xca@\x9f\\\x8d\xb7\x0e\x85 &V\xe6\xda\x7f\xb0\xabI\x8e\x8d\xf1\x030U\xde\x0e\xaf?!\xb7+\xfd\xb6\x8dX\x9a\xbe\x13n\x80;\xd3~\x8bT\xd3\xccl\xb8\xfd\t\x99\x9d1\x1d\x04@\x0e{\x00ݢ\x8d\x81A\x00\xea6\xed\xc2\xdd(\x80\x9d\xdd\xff\x1d\x00z\n\x80\x93\x90\xe9o\b\xfe\"\x02\x7f\x15\x83\x7f\x8c\xc0?\xc7\xe0\v\x12\xf0%\x19\xf8\xa2\x14|Y\x0e\xbe1\x01ߚ\x81oN\xc1\xb7\xe7\xe0\a\x14\xe0G4\xe0\x87T\xe0\xc7t\xf0\a\x95\xe0G\xb5\xe0\x87\xd5\xcc\xc7\xf5\xbfwv\xfe\b=\xaeg\xbe\xb0\xd8\xdd\x15{a\x01\x7fe\x03~i\x05~m\a\x7fq\t~u\v\x7fy\r~}\x0f_\xc0\x00^\xc2\x01_\xc4\x02_\xc6\x03^\xc8\x04_\xca\x05_\xcc\x06^\xce\a_\xd0\b_\xd2\t_\xd4\n^\xd6\v_\xd8\f_\xda\r_\xdc\x0e_\xde\x0f\xdf\xe0\x00\xdf\xe2\x81\xc0\x9b\\\x10|\x9b\x0f|\xa3\x13\x02o\xf5B\xf0\xcdn\b\xbc\xdd\x0f\xc17<\"\xf0\x96O/\x02lӫw\"`\xdb~}\f\xa0\x8d\xcf>\x04\xd8\xd6o\x1f\x03h\xf3\xfb>\x84\xe0\xf6\xff\xbfN\xd2\xc9+\xf1{\x02!\x00\x00\x00%tEXtdate:create\x002023-01-19T01:37:03+00:00/\x9b0\x17\x00\x00\x00%tEXtdate:modify\x002023-01-19T01:36:39+00:00\x9b\xfb\xbb8\x00\x00\x00\x00IEND\xaeB`\x82"), } +var ResCastSvg = &fyne.StaticResource{ + StaticName: "cast.svg", + StaticContent: []byte( + "\n\n \n"), +} var ResDiscSvg = &fyne.StaticResource{ StaticName: "disc.svg", StaticContent: []byte( diff --git a/res/bundled_gen.sh b/res/bundled_gen.sh index a963c50..67c52fa 100755 --- a/res/bundled_gen.sh +++ b/res/bundled_gen.sh @@ -3,6 +3,7 @@ fyne bundle -package res -prefix Res appicon-256.png > bundled.go fyne bundle -append -prefix Res icons/coreui/playlist-add-next.svg >> bundled.go fyne bundle -append -prefix Res icons/freepik/playbutton.png >> bundled.go +fyne bundle -append -prefix Res icons/publicdomain/cast.svg >> bundled.go fyne bundle -append -prefix Res icons/publicdomain/disc.svg >> bundled.go fyne bundle -append -prefix Res icons/publicdomain/headphones.svg >> bundled.go fyne bundle -append -prefix Res icons/publicdomain/heart-filled.svg >> bundled.go diff --git a/res/icons/publicdomain/cast.svg b/res/icons/publicdomain/cast.svg new file mode 100644 index 0000000..0a32d9c --- /dev/null +++ b/res/icons/publicdomain/cast.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/ui/bottompanel.go b/ui/bottompanel.go index 80da01b..cdccac4 100644 --- a/ui/bottompanel.go +++ b/ui/bottompanel.go @@ -3,6 +3,7 @@ package ui import ( "github.com/dweymouth/supersonic/backend" "github.com/dweymouth/supersonic/backend/mediaprovider" + "github.com/dweymouth/supersonic/backend/player/mpv" "github.com/dweymouth/supersonic/ui/controller" "github.com/dweymouth/supersonic/ui/layouts" "github.com/dweymouth/supersonic/ui/util" @@ -111,6 +112,10 @@ func NewBottomPanel(pm *backend.PlaybackManager, im *backend.ImageManager, contr pm.OnVolumeChange(func(vol int) { fyne.Do(func() { bp.AuxControls.VolumeControl.SetVolume(vol) }) }) + pm.OnPlayerChange(func() { + _, local := pm.CurrentPlayer().(*mpv.Player) + fyne.Do(func() { bp.AuxControls.SetIsRemotePlayer(!local) }) + }) bp.AuxControls.VolumeControl.OnSetVolume = func(v int) { pm.SetVolume(v) } @@ -121,6 +126,7 @@ func NewBottomPanel(pm *backend.PlaybackManager, im *backend.ImageManager, contr pm.SetAutoplay(autoplay) } bp.AuxControls.OnShowPlayQueue(contr.ShowPopUpPlayQueue) + bp.AuxControls.OnShowCastMenu(contr.ShowCastMenu) bp.imageLoader = util.NewThumbnailLoader(im, bp.NowPlaying.SetImage) diff --git a/ui/controller/controller.go b/ui/controller/controller.go index d08dfde..c9fff4b 100644 --- a/ui/controller/controller.go +++ b/ui/controller/controller.go @@ -151,6 +151,26 @@ func (m *Controller) HaveModal() bool { return m.haveModal } +func (m *Controller) ShowCastMenu() { + devices := m.App.PlaybackManager.RemotePlayers() + menu := fyne.NewMenu("") + menu.Items = append(menu.Items, fyne.NewMenuItem(lang.L("Local player"), func() { + m.App.PlaybackManager.SetRemotePlayer(nil) + })) + for _, d := range devices { + _d := d + menu.Items = append(menu.Items, fyne.NewMenuItem(d.Name, func() { + m.App.PlaybackManager.SetRemotePlayer(&_d) + })) + } + pop := widget.NewPopUpMenu(menu, m.MainWindow.Canvas()) + canvasSize := m.MainWindow.Canvas().Size() + pop.ShowAtPosition(fyne.NewPos( + canvasSize.Width-pop.MinSize().Width-10, + canvasSize.Height-pop.MinSize().Height-100, + )) +} + func (m *Controller) ShowPopUpPlayQueue() { m.popUpQueueMutex.Lock() if m.popUpQueue == nil { diff --git a/ui/theme/theme.go b/ui/theme/theme.go index a9a1bcc..5df2935 100644 --- a/ui/theme/theme.go +++ b/ui/theme/theme.go @@ -39,6 +39,7 @@ var ( AlbumIcon fyne.Resource = theme.NewThemedResource(res.ResDiscSvg) ArtistIcon fyne.Resource = theme.NewThemedResource(res.ResPeopleSvg) AutoplayIcon fyne.Resource = theme.NewThemedResource(res.ResInfinitySvg) + CastIcon fyne.Resource = theme.NewThemedResource(res.ResCastSvg) RadioIcon fyne.Resource = theme.NewThemedResource(res.ResBroadcastSvg) FavoriteIcon fyne.Resource = theme.NewThemedResource(res.ResHeartFilledSvg) NotFavoriteIcon fyne.Resource = theme.NewThemedResource(res.ResHeartOutlineSvg) diff --git a/ui/widgets/auxcontrols.go b/ui/widgets/auxcontrols.go index ce17cd5..94b4ff4 100644 --- a/ui/widgets/auxcontrols.go +++ b/ui/widgets/auxcontrols.go @@ -25,6 +25,7 @@ type AuxControls struct { VolumeControl *VolumeControl autoplay *IconButton loop *IconButton + cast *IconButton showQueue *IconButton container *fyne.Container @@ -35,10 +36,17 @@ func NewAuxControls(initialVolume int, initialLoopMode backend.LoopMode, initial VolumeControl: NewVolumeControl(initialVolume), autoplay: NewIconButton(myTheme.AutoplayIcon, nil), loop: NewIconButton(myTheme.RepeatIcon, nil), + cast: NewIconButton(myTheme.CastIcon, nil), showQueue: NewIconButton(myTheme.PlayQueueIcon, nil), } + a.loop.IconSize = IconButtonSizeSmaller a.loop.SetToolTip(lang.L("Repeat")) + a.SetLoopMode(initialLoopMode) + + a.cast.IconSize = IconButtonSizeSmaller + a.cast.SetToolTip(lang.L("Cast to device")) + a.autoplay.Highlighted = initialAutoplay //a.autoplay.IconSize = IconButtonSizeSmaller a.autoplay.SetToolTip(lang.L("Autoplay")) @@ -48,9 +56,10 @@ func NewAuxControls(initialVolume int, initialLoopMode backend.LoopMode, initial a.OnChangeAutoplay(a.autoplay.Highlighted) } } - a.SetLoopMode(initialLoopMode) + a.showQueue.IconSize = IconButtonSizeSmaller a.showQueue.SetToolTip(lang.L("Show play queue")) + a.container = container.NewHBox( layout.NewSpacer(), container.NewVBox( @@ -58,7 +67,7 @@ func NewAuxControls(initialVolume int, initialLoopMode backend.LoopMode, initial a.VolumeControl, container.New( layout.NewCustomPaddedHBoxLayout(theme.Padding()*1.5), - layout.NewSpacer(), a.autoplay, a.loop, a.showQueue, util.NewHSpace(5)), + layout.NewSpacer(), a.autoplay, a.loop, a.cast, a.showQueue, util.NewHSpace(5)), layout.NewSpacer(), ), ) @@ -88,6 +97,11 @@ func (a *AuxControls) SetLoopMode(mode backend.LoopMode) { } } +func (a *AuxControls) SetIsRemotePlayer(isRemote bool) { + a.cast.Highlighted = isRemote + a.cast.Refresh() +} + func (a *AuxControls) SetAutoplay(autoplay bool) { if autoplay == a.autoplay.Highlighted { return @@ -100,6 +114,10 @@ func (a *AuxControls) OnShowPlayQueue(f func()) { a.showQueue.OnTapped = f } +func (a *AuxControls) OnShowCastMenu(f func()) { + a.cast.OnTapped = f +} + type volumeSlider struct { widget.Slider