diff --git a/backend/config.go b/backend/config.go index f6e5ae1..b1facae 100644 --- a/backend/config.go +++ b/backend/config.go @@ -111,7 +111,8 @@ type TracksPageConfig struct { } type NowPlayingPageConfig struct { - InitialView string + InitialView string + UseBackgroundImage bool } type PlaybackConfig struct { @@ -242,7 +243,8 @@ func DefaultConfig(appVersionTag string) *Config { InitialView: "List", }, NowPlayingConfig: NowPlayingPageConfig{ - InitialView: "Play Queue", + InitialView: "Play Queue", + UseBackgroundImage: true, }, TracksPage: TracksPageConfig{ TracklistColumns: []string{"Album", "Time", "Plays"}, diff --git a/go.mod b/go.mod index 51856e7..6e64dbb 100644 --- a/go.mod +++ b/go.mod @@ -1,11 +1,12 @@ module github.com/dweymouth/supersonic -go 1.23.0 +go 1.24.0 require ( fyne.io/fyne/v2 v2.6.1 github.com/20after4/configdir v0.1.1 github.com/Microsoft/go-winio v0.6.2 + github.com/boxes-ltd/imaging v1.7.1 github.com/cenkalti/dominantcolor v1.0.3 github.com/charlievieth/strcase v0.0.5 github.com/deluan/sanitize v0.0.0-20230310221930-6e18967d9fc1 @@ -27,7 +28,7 @@ require ( golang.org/x/net v0.38.0 golang.org/x/sys v0.31.0 golang.org/x/term v0.30.0 - golang.org/x/text v0.23.0 + golang.org/x/text v0.30.0 ) require ( @@ -64,7 +65,7 @@ require ( github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect github.com/stretchr/testify v1.10.0 // indirect github.com/yuin/goldmark v1.7.8 // indirect - golang.org/x/image v0.24.0 // indirect + golang.org/x/image v0.32.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 37d11ea..55c1945 100644 --- a/go.sum +++ b/go.sum @@ -8,6 +8,8 @@ github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0 github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/boxes-ltd/imaging v1.7.1 h1:JlfPQkr9sowLuPY24OLNw1Wa0UjriPVo2BN0YyxdCf0= +github.com/boxes-ltd/imaging v1.7.1/go.mod h1:m89K2cMB8PXek+mUi/9SAha2A86+ZgfAOKjGypjMuDk= github.com/cenkalti/dominantcolor v1.0.3 h1:Pt0vfRZ8enkZh1n22RvoboA53SMM/v2aEwNQTZKSqww= github.com/cenkalti/dominantcolor v1.0.3/go.mod h1:mGpFMbWUnyXaGN48Zbf9bU9HJP1eCCD7dnsscb4lyR4= github.com/charlievieth/strcase v0.0.5 h1:gV4iXVyD6eI5KdfOV+/vIVCKXZwtCWOmDMcu7Uy00Rs= @@ -142,16 +144,16 @@ github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic= github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= github.com/zalando/go-keyring v0.2.6 h1:r7Yc3+H+Ux0+M72zacZoItR3UDxeWfKTcabvkI8ua9s= github.com/zalando/go-keyring v0.2.6/go.mod h1:2TCrxYrbUNYfNS/Kgy/LSrkSQzZ5UPVH85RwfczwvcI= -golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ= -golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8= +golang.org/x/image v0.32.0 h1:6lZQWq75h7L5IWNk0r+SCpUJ6tUVd3v4ZHnbRKLkUDQ= +golang.org/x/image v0.32.0/go.mod h1:/R37rrQmKXtO6tYXAjtDLwQgFLHmhW+V6ayXlxzP2Pc= golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= -golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/res/translations/en.json b/res/translations/en.json index a0c3299..a67d69b 100644 --- a/res/translations/en.json +++ b/res/translations/en.json @@ -254,7 +254,9 @@ "Unable to play artist radio": "Unable to play artist radio", "Unable to play random tracks": "Unable to play random tracks", "URL": "URL", + "Use blurred album cover for Now Playing page background": "Use blurred album cover for Now Playing page background", "Use legacy authentication": "Use legacy authentication", + "Use waveform seekbar": "Use waveform seekbar", "Username": "Username", "version": "version", "Visualizations": "Visualizations", diff --git a/ui/browsing/nowplayingpage.go b/ui/browsing/nowplayingpage.go index 82514b6..48fe285 100644 --- a/ui/browsing/nowplayingpage.go +++ b/ui/browsing/nowplayingpage.go @@ -10,6 +10,7 @@ import ( "slices" "strings" + "github.com/boxes-ltd/imaging" "github.com/cenkalti/dominantcolor" "github.com/dweymouth/supersonic/backend" "github.com/dweymouth/supersonic/backend/mediaprovider" @@ -49,16 +50,18 @@ type NowPlayingPage struct { alreadyLoaded bool // widgets for render - background *canvas.LinearGradient - queueList *widgets.PlayQueueList - relatedList *widgets.PlayQueueList - lyricsViewer *widgets.LyricsViewer - card *widgets.LargeNowPlayingCard - statusLabel *widget.Label - tabs *container.AppTabs - lyricsLoading *widgets.LoadingDots - relatedLoading *widgets.LoadingDots - container *fyne.Container + backgroundImgA *canvas.Image + backgroundImgB *canvas.Image + backgroundGradient *canvas.LinearGradient + queueList *widgets.PlayQueueList + relatedList *widgets.PlayQueueList + lyricsViewer *widgets.LyricsViewer + card *widgets.LargeNowPlayingCard + statusLabel *widget.Label + tabs *container.AppTabs + lyricsLoading *widgets.LoadingDots + relatedLoading *widgets.LoadingDots + container *fyne.Container // cancel funcs for background fetch tasks imageLoadCancel context.CancelFunc @@ -232,7 +235,9 @@ func (a *NowPlayingPage) CreateRenderer() fyne.WidgetRenderer { a.updateRelatedList() } c := theme.Color(myTheme.ColorNamePageBackground) - a.background = canvas.NewLinearGradient(c, c, 0) + a.backgroundGradient = canvas.NewLinearGradient(c, c, 0) + a.backgroundImgA = canvas.NewImageFromImage(nil) + a.backgroundImgB = canvas.NewImageFromImage(nil) mainContent := container.NewGridWithColumns(2, container.New(paddedLayout, a.card), @@ -240,7 +245,9 @@ func (a *NowPlayingPage) CreateRenderer() fyne.WidgetRenderer { util.AddHeaderBackgroundWithColorName( a.tabs, myTheme.ColorNameNowPlayingPanel))) a.container = container.NewStack( - a.background, + a.backgroundImgA, + a.backgroundImgB, + a.backgroundGradient, mainContent, container.NewVBox( layout.NewSpacer(), @@ -318,20 +325,46 @@ func (a *NowPlayingPage) onImageLoaded(img image.Image, err error) { return } - c := dominantcolor.Find(img) - if c == a.background.StartColor { - return - } - - // Fyne animation starting is currently thread-safe, - // despite not being marked as such - // TODO: if this changes, use fyne.Do - anim := canvas.NewColorRGBAAnimation( - a.background.StartColor, c, myTheme.AnimationDurationMedium, func(c color.Color) { - a.background.StartColor = c - a.background.Refresh() + if a.conf.UseBackgroundImage { + resized := imaging.Resize(img, 300, 0, imaging.NearestNeighbor) + blurred := imaging.Blur(resized, 10.0) + fyne.Do(func() { + if a.backgroundGradient.StartColor != color.Transparent { + a.backgroundGradient.StartColor = color.Transparent + a.backgroundGradient.Refresh() + } + a.backgroundImgA.Hidden = false + a.backgroundImgB.Hidden = false + a.backgroundImgA.Image = a.backgroundImgB.Image + a.backgroundImgB.Image = blurred + fyne.NewAnimation(myTheme.AnimationDurationMedium, func(f float32) { + a.backgroundImgA.Translucency = float64(f) + a.backgroundImgB.Translucency = float64(1 - f) + a.backgroundImgA.Refresh() + a.backgroundImgB.Refresh() + }).Start() }) - anim.Start() + } else { + c := dominantcolor.Find(img) + if c == a.backgroundGradient.StartColor { + return + } + if !a.backgroundImgA.Hidden { + a.backgroundImgA.Hide() + } + if !a.backgroundImgB.Hidden { + a.backgroundImgB.Hide() + } + // Fyne animation starting is currently thread-safe, + // despite not being marked as such + // TODO: if this changes, use fyne.Do + anim := canvas.NewColorRGBAAnimation( + a.backgroundGradient.StartColor, c, myTheme.AnimationDurationMedium, func(c color.Color) { + a.backgroundGradient.StartColor = c + a.backgroundGradient.Refresh() + }) + anim.Start() + } } func (a *NowPlayingPage) updateLyrics() { @@ -486,11 +519,11 @@ func (a *NowPlayingPage) UnselectAll() { } func (a *NowPlayingPage) Refresh() { - if a.background != nil { + if a.backgroundGradient != nil { c := theme.Color(myTheme.ColorNamePageBackground) - if c != a.background.EndColor { - a.background.EndColor = c - a.background.Refresh() + if c != a.backgroundGradient.EndColor { + a.backgroundGradient.EndColor = c + a.backgroundGradient.Refresh() } } a.BaseWidget.Refresh() diff --git a/ui/dialogs/settingsdialog.go b/ui/dialogs/settingsdialog.go index 2e81666..aba1b4e 100644 --- a/ui/dialogs/settingsdialog.go +++ b/ui/dialogs/settingsdialog.go @@ -571,6 +571,8 @@ func (s *SettingsDialog) createAppearanceTab(window fyne.Window) *container.TabI }) useWaveformSeekbar.Checked = s.config.Playback.UseWaveformSeekbar + nowPlayingBackground := widget.NewCheckWithData(lang.L("Use blurred album cover for Now Playing page background"), binding.BindBool(&s.config.NowPlayingConfig.UseBackgroundImage)) + return container.NewTabItem(lang.L("Appearance"), container.NewVBox( util.NewHSpace(0), // insert a theme.Padding amount of space at top container.NewBorder(nil, nil, widget.NewLabel(lang.L("Theme")), /*left*/ @@ -583,6 +585,7 @@ func (s *SettingsDialog) createAppearanceTab(window fyne.Window) *container.TabI disableDPI, s.newSectionSeparator(), useWaveformSeekbar, + nowPlayingBackground, s.newSectionSeparator(), widget.NewRichText(&widget.TextSegment{Text: lang.L("Application font"), Style: util.BoldRichTextStyle}), container.New(layout.NewFormLayout(),