add play queue sidebar

This commit is contained in:
Drew Weymouth
2025-10-05 11:20:27 -07:00
parent 7dc4640269
commit 27ae29a88c
10 changed files with 518 additions and 7 deletions
+3
View File
@@ -57,6 +57,8 @@ type AppConfig struct {
EnableAutoUpdateChecker bool EnableAutoUpdateChecker bool
RequestTimeoutSeconds int RequestTimeoutSeconds int
EnableOSMediaPlayerAPIs bool EnableOSMediaPlayerAPIs bool
ShowSidebar bool
SidebarWidthFraction float64
FontNormalTTF string FontNormalTTF string
FontBoldTTF string FontBoldTTF string
@@ -207,6 +209,7 @@ func DefaultConfig(appVersionTag string) *Config {
EnableAutoUpdateChecker: true, EnableAutoUpdateChecker: true,
RequestTimeoutSeconds: 15, RequestTimeoutSeconds: 15,
EnableOSMediaPlayerAPIs: true, EnableOSMediaPlayerAPIs: true,
SidebarWidthFraction: 0.8,
}, },
AlbumPage: AlbumPageConfig{ AlbumPage: AlbumPageConfig{
TracklistColumns: []string{"Artist", "Time", "Plays", "Favorite", "Rating"}, TracklistColumns: []string{"Artist", "Time", "Plays", "Favorite", "Rating"},
+5
View File
@@ -80,6 +80,11 @@ var ResPlayqueueSvg = &fyne.StaticResource{
StaticContent: []byte( StaticContent: []byte(
"<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->\n<svg fill=\"#000000\" width=\"800px\" height=\"800px\" viewBox=\"0 0 32 32\" style=\"fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;\" version=\"1.1\" xml:space=\"preserve\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:serif=\"http://www.serif.com/\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" stroke=\"#000000\" stroke-width=\"0.48\">\n\r<g id=\"SVGRepo_bgCarrier\" stroke-width=\"0\"/>\n\r<g id=\"SVGRepo_tracerCarrier\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n\r<g id=\"SVGRepo_iconCarrier\">\n\r<path d=\"M5.011,19l21.989,-0c0.552,0 1,-0.448 1,-1c0,-0.552 -0.448,-1 -1,-1l-21.989,0c-0.552,0 -1,0.448 -1,1c-0,0.552 0.448,1 1,1Z\"/>\n\r<path d=\"M5.019,28l21.981,-0c0.552,0 1,-0.448 1,-1c0,-0.552 -0.448,-1 -1,-1l-21.981,0c-0.552,0 -1,0.448 -1,1c0,0.552 0.448,1 1,1Z\"/>\n\r<path d=\"M15.994,10l11.006,0c0.552,-0 1,-0.448 1,-1c-0,-0.552 -0.448,-1 -1,-1l-11.006,0c-0.552,-0 -1,0.448 -1,1c-0,0.552 0.448,1 1,1Z\"/>\n\r<path d=\"M12.496,9.868c0.312,-0.178 0.504,-0.509 0.504,-0.868c-0,-0.359 -0.192,-0.69 -0.504,-0.868l-7,-4c-0.309,-0.177 -0.69,-0.176 -0.998,0.003c-0.308,0.179 -0.498,0.509 -0.498,0.865l-0,8c-0,0.356 0.19,0.686 0.498,0.865c0.308,0.179 0.689,0.18 0.998,0.003l7,-4Z\"/>\n\r</g>\n\r</svg>"), "<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->\n<svg fill=\"#000000\" width=\"800px\" height=\"800px\" viewBox=\"0 0 32 32\" style=\"fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;\" version=\"1.1\" xml:space=\"preserve\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:serif=\"http://www.serif.com/\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" stroke=\"#000000\" stroke-width=\"0.48\">\n\r<g id=\"SVGRepo_bgCarrier\" stroke-width=\"0\"/>\n\r<g id=\"SVGRepo_tracerCarrier\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n\r<g id=\"SVGRepo_iconCarrier\">\n\r<path d=\"M5.011,19l21.989,-0c0.552,0 1,-0.448 1,-1c0,-0.552 -0.448,-1 -1,-1l-21.989,0c-0.552,0 -1,0.448 -1,1c-0,0.552 0.448,1 1,1Z\"/>\n\r<path d=\"M5.019,28l21.981,-0c0.552,0 1,-0.448 1,-1c0,-0.552 -0.448,-1 -1,-1l-21.981,0c-0.552,0 -1,0.448 -1,1c0,0.552 0.448,1 1,1Z\"/>\n\r<path d=\"M15.994,10l11.006,0c0.552,-0 1,-0.448 1,-1c-0,-0.552 -0.448,-1 -1,-1l-11.006,0c-0.552,-0 -1,0.448 -1,1c-0,0.552 0.448,1 1,1Z\"/>\n\r<path d=\"M12.496,9.868c0.312,-0.178 0.504,-0.509 0.504,-0.868c-0,-0.359 -0.192,-0.69 -0.504,-0.868l-7,-4c-0.309,-0.177 -0.69,-0.176 -0.998,0.003c-0.308,0.179 -0.498,0.509 -0.498,0.865l-0,8c-0,0.356 0.19,0.686 0.498,0.865c0.308,0.179 0.689,0.18 0.998,0.003l7,-4Z\"/>\n\r</g>\n\r</svg>"),
} }
var ResSidebarSvg = &fyne.StaticResource{
StaticName: "sidebar.svg",
StaticContent: []byte(
"<?xml version=\"1.0\" encoding=\"utf-8\"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->\r\n<svg width=\"800px\" height=\"800px\" viewBox=\"0 0 16 16\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\r\n<path d=\"M0 2H10V14H0V2Z\" fill=\"#000000\"/>\r\n<path d=\"M12 2H16V14H12V2Z\" fill=\"#000000\"/>\r\n</svg>"),
}
var ResStarOutlineSvg = &fyne.StaticResource{ var ResStarOutlineSvg = &fyne.StaticResource{
StaticName: "star-outline.svg", StaticName: "star-outline.svg",
StaticContent: []byte( StaticContent: []byte(
+1
View File
@@ -15,6 +15,7 @@ fyne bundle -append -prefix Res icons/publicdomain/oscilloscope.svg >> bundled.g
fyne bundle -append -prefix Res icons/publicdomain/people.svg >> bundled.go fyne bundle -append -prefix Res icons/publicdomain/people.svg >> bundled.go
fyne bundle -append -prefix Res icons/publicdomain/playlist.svg >> bundled.go fyne bundle -append -prefix Res icons/publicdomain/playlist.svg >> bundled.go
fyne bundle -append -prefix Res icons/publicdomain/playqueue.svg >> bundled.go fyne bundle -append -prefix Res icons/publicdomain/playqueue.svg >> bundled.go
fyne bundle -append -prefix Res icons/publicdomain/sidebar.svg >> bundled.go
fyne bundle -append -prefix Res icons/publicdomain/star-outline.svg >> bundled.go fyne bundle -append -prefix Res icons/publicdomain/star-outline.svg >> bundled.go
fyne bundle -append -prefix Res icons/publicdomain/star-filled.svg >> bundled.go fyne bundle -append -prefix Res icons/publicdomain/star-filled.svg >> bundled.go
fyne bundle -append -prefix Res icons/publicdomain/theatermasks.svg >> bundled.go fyne bundle -append -prefix Res icons/publicdomain/theatermasks.svg >> bundled.go
+5
View File
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 2H10V14H0V2Z" fill="#000000"/>
<path d="M12 2H16V14H12V2Z" fill="#000000"/>
</svg>

After

Width:  |  Height:  |  Size: 319 B

+1
View File
@@ -236,6 +236,7 @@
"Time": "Time", "Time": "Time",
"Title": "Title", "Title": "Title",
"Title (A-Z)": "Title (A-Z)", "Title (A-Z)": "Title (A-Z)",
"Toggle sidebar": "Toggle sidebar",
"Top Tracks": "Top Tracks", "Top Tracks": "Top Tracks",
"Total time": "Total time", "Total time": "Total time",
"Track": "Track", "Track": "Track",
+422
View File
@@ -0,0 +1,422 @@
// Copied from Fyne develop codebase, to get improvements
// to split container ahead of the Fyne 2.7 release
package container
import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/driver/desktop"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
)
// Declare conformity with CanvasObject interface
var _ fyne.CanvasObject = (*Split)(nil)
// Split defines a container whose size is split between two children.
//
// Since: 1.4
type Split struct {
widget.BaseWidget
Offset float64
Horizontal bool
Leading fyne.CanvasObject
Trailing fyne.CanvasObject
// to communicate to the renderer that the next refresh
// is just an offset update (ie a resize and move only)
// cleared by renderer in Refresh()
offsetUpdated bool
}
// NewHSplit creates a horizontally arranged container with the specified leading and trailing elements.
// A vertical split bar that can be dragged will be added between the elements.
//
// Since: 1.4
func NewHSplit(leading, trailing fyne.CanvasObject) *Split {
return newSplitContainer(true, leading, trailing)
}
// NewVSplit creates a vertically arranged container with the specified top and bottom elements.
// A horizontal split bar that can be dragged will be added between the elements.
//
// Since: 1.4
func NewVSplit(top, bottom fyne.CanvasObject) *Split {
return newSplitContainer(false, top, bottom)
}
func newSplitContainer(horizontal bool, leading, trailing fyne.CanvasObject) *Split {
s := &Split{
Offset: 0.5, // Sensible default, can be overridden with SetOffset
Horizontal: horizontal,
Leading: leading,
Trailing: trailing,
}
s.BaseWidget.ExtendBaseWidget(s)
return s
}
// CreateRenderer is a private method to Fyne which links this widget to its renderer
func (s *Split) CreateRenderer() fyne.WidgetRenderer {
s.BaseWidget.ExtendBaseWidget(s)
d := newDivider(s)
return &splitContainerRenderer{
split: s,
divider: d,
objects: []fyne.CanvasObject{s.Leading, d, s.Trailing},
}
}
// ExtendBaseWidget is used by an extending widget to make use of BaseWidget functionality.
//
// Deprecated: Support for extending containers is being removed
func (s *Split) ExtendBaseWidget(wid fyne.Widget) {
s.BaseWidget.ExtendBaseWidget(wid)
}
// SetOffset sets the offset (0.0 to 1.0) of the Split divider.
// 0.0 - Leading is min size, Trailing uses all remaining space.
// 0.5 - Leading & Trailing equally share the available space.
// 1.0 - Trailing is min size, Leading uses all remaining space.
func (s *Split) SetOffset(offset float64) {
if s.Offset == offset {
return
}
s.Offset = offset
s.offsetUpdated = true
s.Refresh()
}
var _ fyne.WidgetRenderer = (*splitContainerRenderer)(nil)
type splitContainerRenderer struct {
split *Split
divider *divider
objects []fyne.CanvasObject
}
func (r *splitContainerRenderer) Destroy() {
}
func (r *splitContainerRenderer) Layout(size fyne.Size) {
var dividerPos, leadingPos, trailingPos fyne.Position
var dividerSize, leadingSize, trailingSize fyne.Size
dividerVisible := r.split.Leading.Visible() && r.split.Trailing.Visible()
if !r.split.Leading.Visible() {
trailingPos = fyne.NewPos(0, 0)
trailingSize = size
} else if !r.split.Trailing.Visible() {
leadingPos = fyne.NewPos(0, 0)
leadingSize = size
} else if dividerVisible {
if r.split.Horizontal {
lw, tw := r.computeSplitLengths(size.Width, r.minLeadingWidth(), r.minTrailingWidth())
leadingPos.X = 0
leadingSize.Width = lw
leadingSize.Height = size.Height
dividerPos.X = lw
dividerSize.Width = dividerThickness(r.divider)
dividerSize.Height = size.Height
trailingPos.X = lw + dividerSize.Width
trailingSize.Width = tw
trailingSize.Height = size.Height
} else {
lh, th := r.computeSplitLengths(size.Height, r.minLeadingHeight(), r.minTrailingHeight())
leadingPos.Y = 0
leadingSize.Width = size.Width
leadingSize.Height = lh
dividerPos.Y = lh
dividerSize.Width = size.Width
dividerSize.Height = dividerThickness(r.divider)
trailingPos.Y = lh + dividerSize.Height
trailingSize.Width = size.Width
trailingSize.Height = th
}
}
r.divider.Move(dividerPos)
r.divider.Resize(dividerSize)
r.divider.Hidden = !dividerVisible
r.split.Leading.Move(leadingPos)
r.split.Leading.Resize(leadingSize)
r.split.Trailing.Move(trailingPos)
r.split.Trailing.Resize(trailingSize)
canvas.Refresh(r.divider)
}
func (r *splitContainerRenderer) MinSize() fyne.Size {
s := fyne.NewSize(0, 0)
dividerVisible := r.split.Leading.Visible() && r.split.Trailing.Visible()
for i, o := range r.objects {
if (i == 1 /*divider*/ && !dividerVisible) || (i != 1 && !o.Visible()) {
continue
}
min := o.MinSize()
if r.split.Horizontal {
s.Width += min.Width
s.Height = fyne.Max(s.Height, min.Height)
} else {
s.Width = fyne.Max(s.Width, min.Width)
s.Height += min.Height
}
}
return s
}
func (r *splitContainerRenderer) Objects() []fyne.CanvasObject {
return r.objects
}
func (r *splitContainerRenderer) Refresh() {
if r.split.offsetUpdated {
r.Layout(r.split.Size())
r.split.offsetUpdated = false
return
}
r.objects[0] = r.split.Leading
// [1] is divider which doesn't change
r.objects[2] = r.split.Trailing
r.Layout(r.split.Size())
r.split.Leading.Refresh()
r.divider.Refresh()
r.split.Trailing.Refresh()
canvas.Refresh(r.split)
}
func (r *splitContainerRenderer) computeSplitLengths(total, lMin, tMin float32) (float32, float32) {
available := float64(total - dividerThickness(r.divider))
if available <= 0 {
return 0, 0
}
ld := float64(lMin)
tr := float64(tMin)
offset := r.split.Offset
min := ld / available
max := 1 - tr/available
if min <= max {
if offset < min {
offset = min
}
if offset > max {
offset = max
}
} else {
offset = ld / (ld + tr)
}
ld = offset * available
tr = available - ld
return float32(ld), float32(tr)
}
func (r *splitContainerRenderer) minLeadingWidth() float32 {
if r.split.Leading.Visible() {
return r.split.Leading.MinSize().Width
}
return 0
}
func (r *splitContainerRenderer) minLeadingHeight() float32 {
if r.split.Leading.Visible() {
return r.split.Leading.MinSize().Height
}
return 0
}
func (r *splitContainerRenderer) minTrailingWidth() float32 {
if r.split.Trailing.Visible() {
return r.split.Trailing.MinSize().Width
}
return 0
}
func (r *splitContainerRenderer) minTrailingHeight() float32 {
if r.split.Trailing.Visible() {
return r.split.Trailing.MinSize().Height
}
return 0
}
// Declare conformity with interfaces
var (
_ fyne.CanvasObject = (*divider)(nil)
_ fyne.Draggable = (*divider)(nil)
_ desktop.Cursorable = (*divider)(nil)
_ desktop.Hoverable = (*divider)(nil)
)
type divider struct {
widget.BaseWidget
split *Split
hovered bool
startDragOff *fyne.Position
currentDragPos fyne.Position
}
func newDivider(split *Split) *divider {
d := &divider{
split: split,
}
d.ExtendBaseWidget(d)
return d
}
// CreateRenderer is a private method to Fyne which links this widget to its renderer
func (d *divider) CreateRenderer() fyne.WidgetRenderer {
d.ExtendBaseWidget(d)
th := d.Theme()
v := fyne.CurrentApp().Settings().ThemeVariant()
background := canvas.NewRectangle(th.Color(theme.ColorNameShadow, v))
foreground := canvas.NewRectangle(th.Color(theme.ColorNameForeground, v))
return &dividerRenderer{
divider: d,
background: background,
foreground: foreground,
objects: []fyne.CanvasObject{background, foreground},
}
}
func (d *divider) Cursor() desktop.Cursor {
if d.split.Horizontal {
return desktop.HResizeCursor
}
return desktop.VResizeCursor
}
func (d *divider) DragEnd() {
d.startDragOff = nil
}
func (d *divider) Dragged(e *fyne.DragEvent) {
if d.startDragOff == nil {
d.currentDragPos = d.Position().Add(e.Position)
start := e.Position.Subtract(e.Dragged)
d.startDragOff = &start
} else {
d.currentDragPos = d.currentDragPos.Add(e.Dragged)
}
x, y := d.currentDragPos.Components()
var offset, leadingRatio, trailingRatio float64
if d.split.Horizontal {
widthFree := float64(d.split.Size().Width - dividerThickness(d))
leadingRatio = float64(d.split.Leading.MinSize().Width) / widthFree
trailingRatio = 1. - (float64(d.split.Trailing.MinSize().Width) / widthFree)
offset = float64(x-d.startDragOff.X) / widthFree
} else {
heightFree := float64(d.split.Size().Height - dividerThickness(d))
leadingRatio = float64(d.split.Leading.MinSize().Height) / heightFree
trailingRatio = 1. - (float64(d.split.Trailing.MinSize().Height) / heightFree)
offset = float64(y-d.startDragOff.Y) / heightFree
}
if offset < leadingRatio {
offset = leadingRatio
}
if offset > trailingRatio {
offset = trailingRatio
}
d.split.SetOffset(offset)
}
func (d *divider) MouseIn(event *desktop.MouseEvent) {
d.hovered = true
d.Refresh()
}
func (d *divider) MouseMoved(event *desktop.MouseEvent) {}
func (d *divider) MouseOut() {
d.hovered = false
d.Refresh()
}
var _ fyne.WidgetRenderer = (*dividerRenderer)(nil)
type dividerRenderer struct {
divider *divider
background *canvas.Rectangle
foreground *canvas.Rectangle
objects []fyne.CanvasObject
}
func (r *dividerRenderer) Destroy() {
}
func (r *dividerRenderer) Layout(size fyne.Size) {
r.background.Resize(size)
var x, y, w, h float32
if r.divider.split.Horizontal {
x = (dividerThickness(r.divider) - handleThickness(r.divider)) / 2
y = (size.Height - handleLength(r.divider)) / 2
w = handleThickness(r.divider)
h = handleLength(r.divider)
} else {
x = (size.Width - handleLength(r.divider)) / 2
y = (dividerThickness(r.divider) - handleThickness(r.divider)) / 2
w = handleLength(r.divider)
h = handleThickness(r.divider)
}
r.foreground.Move(fyne.NewPos(x, y))
r.foreground.Resize(fyne.NewSize(w, h))
}
func (r *dividerRenderer) MinSize() fyne.Size {
if r.divider.split.Horizontal {
return fyne.NewSize(dividerThickness(r.divider), dividerLength(r.divider))
}
return fyne.NewSize(dividerLength(r.divider), dividerThickness(r.divider))
}
func (r *dividerRenderer) Objects() []fyne.CanvasObject {
return r.objects
}
func (r *dividerRenderer) Refresh() {
th := r.divider.Theme()
v := fyne.CurrentApp().Settings().ThemeVariant()
if r.divider.hovered {
r.background.FillColor = th.Color(theme.ColorNameHover, v)
} else {
r.background.FillColor = th.Color(theme.ColorNameShadow, v)
}
r.background.Refresh()
r.foreground.FillColor = th.Color(theme.ColorNameForeground, v)
r.foreground.Refresh()
r.Layout(r.divider.Size())
}
func dividerTheme(d *divider) fyne.Theme {
if d == nil {
return theme.Current()
}
return d.Theme()
}
func dividerThickness(d *divider) float32 {
th := dividerTheme(d)
return th.Size(theme.SizeNamePadding) * 2
}
func dividerLength(d *divider) float32 {
th := dividerTheme(d)
return th.Size(theme.SizeNamePadding) * 6
}
func handleThickness(d *divider) float32 {
th := dividerTheme(d)
return th.Size(theme.SizeNamePadding) / 2
}
func handleLength(d *divider) float32 {
th := dividerTheme(d)
return th.Size(theme.SizeNamePadding) * 4
}
+29 -5
View File
@@ -13,6 +13,7 @@ import (
"github.com/dweymouth/supersonic/backend/mediaprovider" "github.com/dweymouth/supersonic/backend/mediaprovider"
"github.com/dweymouth/supersonic/res" "github.com/dweymouth/supersonic/res"
"github.com/dweymouth/supersonic/ui/browsing" "github.com/dweymouth/supersonic/ui/browsing"
uicontainer "github.com/dweymouth/supersonic/ui/container"
"github.com/dweymouth/supersonic/ui/controller" "github.com/dweymouth/supersonic/ui/controller"
"github.com/dweymouth/supersonic/ui/dialogs" "github.com/dweymouth/supersonic/ui/dialogs"
"github.com/dweymouth/supersonic/ui/shortcuts" "github.com/dweymouth/supersonic/ui/shortcuts"
@@ -36,10 +37,12 @@ type MainWindow struct {
Router browsing.Router Router browsing.Router
Controller *controller.Controller Controller *controller.Controller
BrowsingPane *browsing.BrowsingPane BrowsingPane *browsing.BrowsingPane
Sidebar *Sidebar
Toolbar *Toolbar Toolbar *Toolbar
BottomPanel *BottomPanel BottomPanel *BottomPanel
ToastOverlay *ToastOverlay ToastOverlay *ToastOverlay
splitContainer *uicontainer.Split
theme *myTheme.MyTheme theme *myTheme.MyTheme
haveSystemTray bool haveSystemTray bool
alreadyConnected bool // tracks if we have already connected to a server before alreadyConnected bool // tracks if we have already connected to a server before
@@ -66,10 +69,11 @@ func NewMainWindow(fyneApp fyne.App, appName, displayAppName, appVersion string,
} }
m.Controller = controller.New(app, appVersion, m.Window) m.Controller = controller.New(app, appVersion, m.Window)
m.BrowsingPane = browsing.NewBrowsingPane(app.PlaybackManager, m.Controller, func() { m.Router.NavigateTo(m.StartupPage()) }) m.BrowsingPane = browsing.NewBrowsingPane(app.PlaybackManager, m.Controller, func() { m.Router.NavigateTo(m.StartupPage()) })
m.Sidebar = NewSidebar(m.Controller, m.App.ImageManager)
m.ToastOverlay = NewToastOverlay() m.ToastOverlay = NewToastOverlay()
m.Router = browsing.NewRouter(app, m.Controller, m.BrowsingPane) m.Router = browsing.NewRouter(app, m.Controller, m.BrowsingPane)
goHomeFn := func() { m.Router.NavigateTo(m.StartupPage()) } goHomeFn := func() { m.Router.NavigateTo(m.StartupPage()) }
m.Toolbar = NewToolbar(m.BrowsingPane, m.Router.NavigateTo, goHomeFn, m.Controller.ShowQuickSearch) m.Toolbar = NewToolbar(m.BrowsingPane, m.Router.NavigateTo, goHomeFn, m.Controller.ShowQuickSearch, m.toggleSidebar)
// inject controller dependencies // inject controller dependencies
m.Controller.NavHandler = m.Router.NavigateTo m.Controller.NavHandler = m.Router.NavigateTo
@@ -96,6 +100,9 @@ func NewMainWindow(fyneApp fyne.App, appName, displayAppName, appVersion string,
app.PlaybackManager.OnSongChange(func(item mediaprovider.MediaItem, _ *mediaprovider.Track) { app.PlaybackManager.OnSongChange(func(item mediaprovider.MediaItem, _ *mediaprovider.Track) {
fyne.Do(func() { m.UpdateOnTrackChange(item) }) fyne.Do(func() { m.UpdateOnTrackChange(item) })
}) })
app.PlaybackManager.OnQueueChange(func() {
fyne.Do(func() { m.Sidebar.SetQueueTracks(app.PlaybackManager.GetPlayQueue()) })
})
app.ServerManager.OnServerConnected(func(conf *backend.ServerConfig) { app.ServerManager.OnServerConnected(func(conf *backend.ServerConfig) {
go m.RunOnServerConnectedTasks(conf, app, displayAppName) go m.RunOnServerConnectedTasks(conf, app, displayAppName)
}) })
@@ -134,7 +141,10 @@ func NewMainWindow(fyneApp fyne.App, appName, displayAppName, appVersion string,
m.Toolbar.DisableNavigationButtons() m.Toolbar.DisableNavigationButtons()
m.addShortcuts() m.addShortcuts()
center := container.NewStack(m.BrowsingPane, m.ToastOverlay) m.splitContainer = uicontainer.NewHSplit(m.BrowsingPane, m.Sidebar)
m.splitContainer.Offset = min(1.0, max(0.0, m.App.Config.Application.SidebarWidthFraction))
m.Sidebar.Hidden = !m.App.Config.Application.ShowSidebar
center := container.NewStack(m.splitContainer, m.ToastOverlay)
toolbarWrapper := container.New( toolbarWrapper := container.New(
&layout.CustomPaddedLayout{LeftPadding: -theme.Padding(), RightPadding: -theme.Padding()}, m.Toolbar) &layout.CustomPaddedLayout{LeftPadding: -theme.Padding(), RightPadding: -theme.Padding()}, m.Toolbar)
m.content = newMainWindowContent(container.NewBorder(toolbarWrapper, m.BottomPanel, nil, nil, center), m.content = newMainWindowContent(container.NewBorder(toolbarWrapper, m.BottomPanel, nil, nil, center),
@@ -143,7 +153,7 @@ func NewMainWindow(fyneApp fyne.App, appName, displayAppName, appVersion string,
m.setInitialSize() m.setInitialSize()
m.Window.SetCloseIntercept(func() { m.Window.SetCloseIntercept(func() {
m.SaveWindowSize() m.SaveWindowSettings()
if app.Config.Application.CloseToSystemTray && m.HaveSystemTray() { if app.Config.Application.CloseToSystemTray && m.HaveSystemTray() {
m.Window.Hide() m.Window.Hide()
} else { } else {
@@ -157,9 +167,12 @@ func NewMainWindow(fyneApp fyne.App, appName, displayAppName, appVersion string,
func (m *MainWindow) UpdateOnTrackChange(item mediaprovider.MediaItem) { func (m *MainWindow) UpdateOnTrackChange(item mediaprovider.MediaItem) {
if item == nil { if item == nil {
m.Window.SetTitle(res.DisplayName) m.Window.SetTitle(res.DisplayName)
m.Sidebar.SetNowPlaying("")
return return
} }
meta := item.Metadata() meta := item.Metadata()
m.Sidebar.SetNowPlaying(meta.ID)
artistDisp := "" artistDisp := ""
if tr, ok := item.(*mediaprovider.Track); ok { if tr, ok := item.(*mediaprovider.Track); ok {
artistDisp = " " + strings.Join(tr.ArtistNames, ", ") artistDisp = " " + strings.Join(tr.ArtistNames, ", ")
@@ -394,6 +407,15 @@ func (m *MainWindow) ShowWhatsNewDialog() {
dialog.ShowCustom("What's new in "+res.AppVersion, lang.L("Close"), dialogs.NewWhatsNewDialog(), m.Window) dialog.ShowCustom("What's new in "+res.AppVersion, lang.L("Close"), dialogs.NewWhatsNewDialog(), m.Window)
} }
func (m *MainWindow) toggleSidebar() {
if m.Sidebar.Visible() {
m.Sidebar.Hide()
} else {
m.Sidebar.Show()
}
m.splitContainer.Refresh()
}
func (m *MainWindow) addShortcuts() { func (m *MainWindow) addShortcuts() {
for _, sh := range shortcuts.BackShortcuts { for _, sh := range shortcuts.BackShortcuts {
m.Canvas().AddShortcut(&sh, func(_ fyne.Shortcut) { m.Canvas().AddShortcut(&sh, func(_ fyne.Shortcut) {
@@ -497,14 +519,16 @@ func (m *MainWindow) SetContent(c fyne.CanvasObject) {
} }
func (m *MainWindow) Quit() { func (m *MainWindow) Quit() {
m.SaveWindowSize() m.SaveWindowSettings()
fyne.CurrentApp().Quit() fyne.CurrentApp().Quit()
} }
func (m *MainWindow) SaveWindowSize() { func (m *MainWindow) SaveWindowSettings() {
util.SaveWindowSize(m.Window, util.SaveWindowSize(m.Window,
&m.App.Config.Application.WindowWidth, &m.App.Config.Application.WindowWidth,
&m.App.Config.Application.WindowHeight) &m.App.Config.Application.WindowHeight)
m.App.Config.Application.ShowSidebar = !m.Sidebar.Hidden
m.App.Config.Application.SidebarWidthFraction = m.splitContainer.Offset
} }
// widget just so we can catch a tap event that doesn't land anywhere else // widget just so we can catch a tap event that doesn't land anywhere else
+46
View File
@@ -0,0 +1,46 @@
package ui
import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/lang"
"fyne.io/fyne/v2/widget"
"github.com/dweymouth/supersonic/backend"
"github.com/dweymouth/supersonic/backend/mediaprovider"
"github.com/dweymouth/supersonic/ui/controller"
"github.com/dweymouth/supersonic/ui/theme"
"github.com/dweymouth/supersonic/ui/widgets"
)
type Sidebar struct {
widget.BaseWidget
queueList *widgets.PlayQueueList
}
func NewSidebar(contr *controller.Controller, im *backend.ImageManager) *Sidebar {
s := &Sidebar{
queueList: widgets.NewPlayQueueList(im, false),
}
s.queueList.Reorderable = true
contr.ConnectPlayQueuelistActions(s.queueList)
s.ExtendBaseWidget(s)
return s
}
func (s *Sidebar) SetQueueTracks(items []mediaprovider.MediaItem) {
s.queueList.SetItems(items)
}
func (s *Sidebar) SetNowPlaying(itemID string) {
s.queueList.SetNowPlaying(itemID)
}
func (s *Sidebar) CreateRenderer() fyne.WidgetRenderer {
return widget.NewSimpleRenderer(container.NewStack(
theme.NewThemedRectangle(theme.ColorNamePageBackground),
container.NewAppTabs(
container.NewTabItem(lang.L("Play Queue"), container.NewPadded(s.queueList)),
),
))
}
+1
View File
@@ -60,6 +60,7 @@ var (
FilterIcon fyne.Resource = theme.NewThemedResource(res.ResFilterSvg) FilterIcon fyne.Resource = theme.NewThemedResource(res.ResFilterSvg)
RepeatIcon fyne.Resource = theme.NewThemedResource(res.ResRepeatSvg) RepeatIcon fyne.Resource = theme.NewThemedResource(res.ResRepeatSvg)
RepeatOneIcon fyne.Resource = theme.NewThemedResource(res.ResRepeatoneSvg) RepeatOneIcon fyne.Resource = theme.NewThemedResource(res.ResRepeatoneSvg)
SidebarIcon fyne.Resource = theme.NewThemedResource(res.ResSidebarSvg)
SortIcon fyne.Resource = theme.NewThemedResource(res.ResUpdownarrowSvg) SortIcon fyne.Resource = theme.NewThemedResource(res.ResUpdownarrowSvg)
VisualizationIcon fyne.Resource = theme.NewThemedResource(res.ResOscilloscopeSvg) VisualizationIcon fyne.Resource = theme.NewThemedResource(res.ResOscilloscopeSvg)
LibraryIcon fyne.Resource = theme.NewThemedResource(res.ResLibrarySvg) LibraryIcon fyne.Resource = theme.NewThemedResource(res.ResLibrarySvg)
+5 -2
View File
@@ -30,11 +30,12 @@ type Toolbar struct {
radioBtn fyne.CanvasObject radioBtn fyne.CanvasObject
quickSearchBtn *ttwidget.Button quickSearchBtn *ttwidget.Button
sidebarBtn *ttwidget.Button
settingsBtn *ttwidget.Button settingsBtn *ttwidget.Button
settingsMenu *fyne.Menu settingsMenu *fyne.Menu
} }
func NewToolbar(browsingPane *browsing.BrowsingPane, navigateFn func(controller.Route), homeFunc, showSearchFn func()) *Toolbar { func NewToolbar(browsingPane *browsing.BrowsingPane, navigateFn func(controller.Route), homeFunc, showSearchFn, toggleSidebarFn func()) *Toolbar {
t := &Toolbar{ t := &Toolbar{
browsingPane: browsingPane, browsingPane: browsingPane,
navBtnsContainer: container.NewHBox(), navBtnsContainer: container.NewHBox(),
@@ -58,6 +59,8 @@ func NewToolbar(browsingPane *browsing.BrowsingPane, navigateFn func(controller.
t.quickSearchBtn = ttwidget.NewButtonWithIcon("", theme.SearchIcon(), showSearchFn) t.quickSearchBtn = ttwidget.NewButtonWithIcon("", theme.SearchIcon(), showSearchFn)
t.quickSearchBtn.SetToolTip(lang.L("Search Everywhere")) t.quickSearchBtn.SetToolTip(lang.L("Search Everywhere"))
t.sidebarBtn = ttwidget.NewButtonWithIcon("", myTheme.SidebarIcon, toggleSidebarFn)
t.sidebarBtn.SetToolTip(lang.L("Toggle sidebar"))
t.settingsBtn = ttwidget.NewButtonWithIcon("", theme.SettingsIcon(), func() { t.settingsBtn = ttwidget.NewButtonWithIcon("", theme.SettingsIcon(), func() {
p := widget.NewPopUpMenu(t.settingsMenu, p := widget.NewPopUpMenu(t.settingsMenu,
fyne.CurrentApp().Driver().CanvasForObject(t.settingsBtn)) fyne.CurrentApp().Driver().CanvasForObject(t.settingsBtn))
@@ -138,7 +141,7 @@ func (t *Toolbar) CreateRenderer() fyne.WidgetRenderer {
content := container.New(layouts.NewLeftMiddleRightLayout(0, 0), content := container.New(layouts.NewLeftMiddleRightLayout(0, 0),
container.NewHBox(t.home, t.back, t.forward, t.reload), container.NewHBox(t.home, t.back, t.forward, t.reload),
t.navBtnsContainer, t.navBtnsContainer,
container.NewHBox(layout.NewSpacer(), t.quickSearchBtn, t.settingsBtn)) container.NewHBox(layout.NewSpacer(), t.quickSearchBtn, t.sidebarBtn, t.settingsBtn))
return widget.NewSimpleRenderer(content) return widget.NewSimpleRenderer(content)
} }