Merge pull request #732 from dweymouth/feature/queue-sidebar
Add toggleable play queue sidebar
This commit is contained in:
@@ -57,6 +57,8 @@ type AppConfig struct {
|
||||
EnableAutoUpdateChecker bool
|
||||
RequestTimeoutSeconds int
|
||||
EnableOSMediaPlayerAPIs bool
|
||||
ShowSidebar bool
|
||||
SidebarWidthFraction float64
|
||||
|
||||
FontNormalTTF string
|
||||
FontBoldTTF string
|
||||
@@ -207,6 +209,7 @@ func DefaultConfig(appVersionTag string) *Config {
|
||||
EnableAutoUpdateChecker: true,
|
||||
RequestTimeoutSeconds: 15,
|
||||
EnableOSMediaPlayerAPIs: true,
|
||||
SidebarWidthFraction: 0.8,
|
||||
},
|
||||
AlbumPage: AlbumPageConfig{
|
||||
TracklistColumns: []string{"Artist", "Time", "Plays", "Favorite", "Rating"},
|
||||
|
||||
@@ -80,6 +80,11 @@ var ResPlayqueueSvg = &fyne.StaticResource{
|
||||
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>"),
|
||||
}
|
||||
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{
|
||||
StaticName: "star-outline.svg",
|
||||
StaticContent: []byte(
|
||||
|
||||
@@ -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/playlist.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-filled.svg >> bundled.go
|
||||
fyne bundle -append -prefix Res icons/publicdomain/theatermasks.svg >> bundled.go
|
||||
|
||||
@@ -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 |
@@ -236,6 +236,7 @@
|
||||
"Time": "Time",
|
||||
"Title": "Title",
|
||||
"Title (A-Z)": "Title (A-Z)",
|
||||
"Toggle sidebar": "Toggle sidebar",
|
||||
"Top Tracks": "Top Tracks",
|
||||
"Total time": "Total time",
|
||||
"Track": "Track",
|
||||
|
||||
+26
-134
@@ -3,17 +3,12 @@ package browsing
|
||||
import (
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/container"
|
||||
"fyne.io/fyne/v2/lang"
|
||||
"fyne.io/fyne/v2/layout"
|
||||
"fyne.io/fyne/v2/theme"
|
||||
"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/layouts"
|
||||
myTheme "github.com/dweymouth/supersonic/ui/theme"
|
||||
|
||||
ttwidget "github.com/dweymouth/fyne-tooltip/widget"
|
||||
)
|
||||
|
||||
type Page interface {
|
||||
@@ -60,60 +55,25 @@ type CanShowPlayQueue interface {
|
||||
type BrowsingPane struct {
|
||||
widget.BaseWidget
|
||||
|
||||
app *backend.App
|
||||
OnHistoryChanged func()
|
||||
|
||||
playbackManager *backend.PlaybackManager
|
||||
|
||||
curPage Page
|
||||
|
||||
home *ttwidget.Button
|
||||
forward *ttwidget.Button
|
||||
back *ttwidget.Button
|
||||
reload *ttwidget.Button
|
||||
history []SavedPage
|
||||
historyIdx int
|
||||
|
||||
settingsBtn *ttwidget.Button
|
||||
settingsMenu *fyne.Menu
|
||||
navBtnsContainer *fyne.Container
|
||||
pageContainer *fyne.Container
|
||||
container *fyne.Container
|
||||
navBtnsPageMap map[controller.PageName]fyne.Resource
|
||||
}
|
||||
|
||||
func NewBrowsingPane(app *backend.App, contr *controller.Controller, onGoHome func()) *BrowsingPane {
|
||||
b := &BrowsingPane{app: app}
|
||||
func NewBrowsingPane(app *backend.PlaybackManager, contr *controller.Controller, onGoHome func()) *BrowsingPane {
|
||||
b := &BrowsingPane{playbackManager: app}
|
||||
b.ExtendBaseWidget(b)
|
||||
b.home = ttwidget.NewButtonWithIcon("", theme.HomeIcon(), onGoHome)
|
||||
b.home.SetToolTip(lang.L("Home"))
|
||||
b.back = ttwidget.NewButtonWithIcon("", theme.NavigateBackIcon(), b.GoBack)
|
||||
b.back.SetToolTip(lang.L("Back"))
|
||||
b.forward = ttwidget.NewButtonWithIcon("", theme.NavigateNextIcon(), b.GoForward)
|
||||
b.forward.SetToolTip(lang.L("Forward"))
|
||||
b.reload = ttwidget.NewButtonWithIcon("", theme.ViewRefreshIcon(), b.Reload)
|
||||
b.reload.SetToolTip(lang.L("Reload"))
|
||||
b.app.PlaybackManager.OnSongChange(b.onSongChange)
|
||||
b.app.PlaybackManager.OnPlayTimeUpdate(b.onPlayTimeUpdate)
|
||||
b.app.PlaybackManager.OnQueueChange(b.onQueueChange)
|
||||
b.playbackManager.OnSongChange(b.onSongChange)
|
||||
b.playbackManager.OnPlayTimeUpdate(b.onPlayTimeUpdate)
|
||||
b.playbackManager.OnQueueChange(b.onQueueChange)
|
||||
bkgrnd := myTheme.NewThemedRectangle(myTheme.ColorNamePageBackground)
|
||||
b.pageContainer = container.NewStack(bkgrnd, layout.NewSpacer())
|
||||
b.settingsBtn = ttwidget.NewButtonWithIcon("", theme.SettingsIcon(), func() {
|
||||
p := widget.NewPopUpMenu(b.settingsMenu,
|
||||
fyne.CurrentApp().Driver().CanvasForObject(b.settingsBtn))
|
||||
p.ShowAtPosition(fyne.NewPos(b.Size().Width-p.MinSize().Width+4,
|
||||
b.navBtnsContainer.MinSize().Height+theme.Padding()))
|
||||
})
|
||||
b.settingsBtn.SetToolTip(lang.L("Menu"))
|
||||
quickSearchBtn := ttwidget.NewButtonWithIcon("", theme.SearchIcon(), contr.ShowQuickSearch)
|
||||
quickSearchBtn.SetToolTip(lang.L("Search Everywhere"))
|
||||
b.settingsMenu = fyne.NewMenu("")
|
||||
b.navBtnsContainer = container.NewHBox()
|
||||
b.navBtnsPageMap = map[controller.PageName]fyne.Resource{}
|
||||
b.container = container.NewBorder(container.New(
|
||||
&layout.CustomPaddedLayout{LeftPadding: -5, RightPadding: -5},
|
||||
container.New(layouts.NewLeftMiddleRightLayout(0, 0),
|
||||
container.NewHBox(b.home, b.back, b.forward, b.reload), b.navBtnsContainer,
|
||||
container.NewHBox(layout.NewSpacer(), quickSearchBtn, b.settingsBtn))),
|
||||
nil, nil, nil, b.pageContainer)
|
||||
b.updateHistoryButtons()
|
||||
return b
|
||||
}
|
||||
|
||||
@@ -124,77 +84,19 @@ func (b *BrowsingPane) SetPage(p Page) {
|
||||
b.pageContainer.Objects[1] = layout.NewSpacer()
|
||||
b.curPage = nil
|
||||
b.pageContainer.Refresh()
|
||||
b.updateNavBtnsColor(p)
|
||||
} else {
|
||||
oldPage := b.curPage
|
||||
if b.doSetPage(p) && oldPage != nil {
|
||||
b.addPageToHistory(oldPage, true)
|
||||
b.updateHistoryButtons()
|
||||
}
|
||||
}
|
||||
b.onHistoryChanged()
|
||||
}
|
||||
|
||||
func (b *BrowsingPane) ClearHistory() {
|
||||
b.history = nil
|
||||
b.historyIdx = 0
|
||||
b.updateHistoryButtons()
|
||||
}
|
||||
|
||||
func (b *BrowsingPane) AddSettingsMenuItem(label string, icon fyne.Resource, action func()) {
|
||||
item := fyne.NewMenuItem(label, action)
|
||||
item.Icon = icon
|
||||
b.settingsMenu.Items = append(b.settingsMenu.Items, item)
|
||||
}
|
||||
|
||||
func (b *BrowsingPane) AddSettingsSubmenu(label string, icon fyne.Resource, menu *fyne.Menu) {
|
||||
item := fyne.NewMenuItem(label, nil)
|
||||
item.ChildMenu = menu
|
||||
item.Icon = icon
|
||||
b.settingsMenu.Items = append(b.settingsMenu.Items, item)
|
||||
}
|
||||
|
||||
func (b *BrowsingPane) SetSubmenuForMenuItem(label string, submenu *fyne.Menu) {
|
||||
for _, item := range b.settingsMenu.Items {
|
||||
if item.Label == label {
|
||||
item.ChildMenu = submenu
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *BrowsingPane) AddSettingsMenuSeparator() {
|
||||
b.settingsMenu.Items = append(b.settingsMenu.Items,
|
||||
fyne.NewMenuItemSeparator())
|
||||
}
|
||||
|
||||
func (b *BrowsingPane) AddNavigationButton(icon fyne.Resource, pageName controller.PageName, action func()) *ttwidget.Button {
|
||||
// make a copy of the icon, because it can change the color
|
||||
browsingPaneIcon := theme.NewThemedResource(icon)
|
||||
btn := ttwidget.NewButtonWithIcon("", browsingPaneIcon, action)
|
||||
btn.SetToolTip(lang.L(pageName.String()))
|
||||
b.navBtnsContainer.Add(btn)
|
||||
b.navBtnsPageMap[pageName] = browsingPaneIcon
|
||||
return btn
|
||||
}
|
||||
|
||||
func (b *BrowsingPane) DisableNavigationButtons() {
|
||||
for _, obj := range b.navBtnsContainer.Objects {
|
||||
obj.(fyne.Disableable).Disable()
|
||||
}
|
||||
}
|
||||
|
||||
func (b *BrowsingPane) EnableNavigationButtons() {
|
||||
for _, obj := range b.navBtnsContainer.Objects {
|
||||
obj.(fyne.Disableable).Enable()
|
||||
}
|
||||
}
|
||||
|
||||
func (b *BrowsingPane) ActivateNavigationButton(num int) {
|
||||
if num < len(b.navBtnsContainer.Objects) {
|
||||
btn := b.navBtnsContainer.Objects[num].(*widget.Button)
|
||||
if !btn.Disabled() && !btn.Hidden {
|
||||
btn.OnTapped()
|
||||
}
|
||||
}
|
||||
b.onHistoryChanged()
|
||||
}
|
||||
|
||||
func (b *BrowsingPane) GetSearchBarIfAny() fyne.Focusable {
|
||||
@@ -255,15 +157,20 @@ func (b *BrowsingPane) doSetPage(p Page) bool {
|
||||
b.curPage = p
|
||||
if np, ok := p.(CanShowNowPlaying); ok {
|
||||
// inform page of currently playing track
|
||||
np.OnSongChange(b.app.PlaybackManager.NowPlaying(), nil)
|
||||
np.OnSongChange(b.playbackManager.NowPlaying(), nil)
|
||||
}
|
||||
b.pageContainer.Remove(b.curPage)
|
||||
b.pageContainer.Objects[1] = p
|
||||
b.updateNavBtnsColor(p)
|
||||
b.Refresh()
|
||||
return true
|
||||
}
|
||||
|
||||
func (b *BrowsingPane) onHistoryChanged() {
|
||||
if b.OnHistoryChanged != nil {
|
||||
b.OnHistoryChanged()
|
||||
}
|
||||
}
|
||||
|
||||
func (b *BrowsingPane) onSongChange(song mediaprovider.MediaItem, lastScrobbledIfAny *mediaprovider.Track) {
|
||||
fyne.Do(func() {
|
||||
if b.curPage == nil {
|
||||
@@ -313,17 +220,12 @@ func (b *BrowsingPane) addPageToHistory(p Page, truncate bool) {
|
||||
b.historyIdx++
|
||||
}
|
||||
|
||||
func (b *BrowsingPane) updateHistoryButtons() {
|
||||
if b.historyIdx > 0 {
|
||||
b.back.Enable()
|
||||
} else {
|
||||
b.back.Disable()
|
||||
}
|
||||
if b.historyIdx < len(b.history)-1 {
|
||||
b.forward.Enable()
|
||||
} else {
|
||||
b.forward.Disable()
|
||||
}
|
||||
func (b *BrowsingPane) CanGoBack() bool {
|
||||
return b.historyIdx > 0
|
||||
}
|
||||
|
||||
func (b *BrowsingPane) CanGoForward() bool {
|
||||
return b.historyIdx < len(b.history)-1
|
||||
}
|
||||
|
||||
func (b *BrowsingPane) GoBack() {
|
||||
@@ -334,7 +236,7 @@ func (b *BrowsingPane) GoBack() {
|
||||
b.addPageToHistory(b.curPage, false)
|
||||
b.historyIdx -= 2
|
||||
b.doSetPage(p)
|
||||
b.updateHistoryButtons()
|
||||
b.onHistoryChanged()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -343,7 +245,7 @@ func (b *BrowsingPane) GoForward() {
|
||||
p := b.history[b.historyIdx+1].Restore()
|
||||
b.addPageToHistory(b.curPage, false)
|
||||
b.doSetPage(p)
|
||||
b.updateHistoryButtons()
|
||||
b.onHistoryChanged()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -361,15 +263,5 @@ func (b *BrowsingPane) CurrentPage() controller.Route {
|
||||
}
|
||||
|
||||
func (b *BrowsingPane) CreateRenderer() fyne.WidgetRenderer {
|
||||
return widget.NewSimpleRenderer(b.container)
|
||||
}
|
||||
|
||||
func (b *BrowsingPane) updateNavBtnsColor(p Page) {
|
||||
for pageName, icon := range b.navBtnsPageMap {
|
||||
if p != nil && pageName == p.Route().Page {
|
||||
icon.(*theme.ThemedResource).ColorName = theme.ColorNamePrimary
|
||||
} else {
|
||||
icon.(*theme.ThemedResource).ColorName = theme.ColorNameForeground
|
||||
}
|
||||
}
|
||||
return widget.NewSimpleRenderer(b.pageContainer)
|
||||
}
|
||||
|
||||
@@ -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 := ÷r{
|
||||
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 ÷rRenderer{
|
||||
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
|
||||
}
|
||||
+51
-55
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/dweymouth/supersonic/backend/mediaprovider"
|
||||
"github.com/dweymouth/supersonic/res"
|
||||
"github.com/dweymouth/supersonic/ui/browsing"
|
||||
uicontainer "github.com/dweymouth/supersonic/ui/container"
|
||||
"github.com/dweymouth/supersonic/ui/controller"
|
||||
"github.com/dweymouth/supersonic/ui/dialogs"
|
||||
"github.com/dweymouth/supersonic/ui/shortcuts"
|
||||
@@ -24,6 +25,7 @@ import (
|
||||
"fyne.io/fyne/v2/dialog"
|
||||
"fyne.io/fyne/v2/driver/desktop"
|
||||
"fyne.io/fyne/v2/lang"
|
||||
"fyne.io/fyne/v2/layout"
|
||||
"fyne.io/fyne/v2/theme"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
)
|
||||
@@ -35,17 +37,17 @@ type MainWindow struct {
|
||||
Router browsing.Router
|
||||
Controller *controller.Controller
|
||||
BrowsingPane *browsing.BrowsingPane
|
||||
Sidebar *Sidebar
|
||||
Toolbar *Toolbar
|
||||
BottomPanel *BottomPanel
|
||||
ToastOverlay *ToastOverlay
|
||||
|
||||
splitContainer *uicontainer.Split
|
||||
theme *myTheme.MyTheme
|
||||
haveSystemTray bool
|
||||
alreadyConnected bool // tracks if we have already connected to a server before
|
||||
content *mainWindowContent
|
||||
|
||||
// needs to be shown/hidden when switching between servers based on whether they support radio
|
||||
radioBtn fyne.CanvasObject
|
||||
|
||||
// updated when changing servers or libraries
|
||||
librarySubmenu *fyne.Menu
|
||||
}
|
||||
@@ -66,9 +68,13 @@ func NewMainWindow(fyneApp fyne.App, appName, displayAppName, appVersion string,
|
||||
m.SetupSystemTrayMenu(displayAppName, fyneApp)
|
||||
}
|
||||
m.Controller = controller.New(app, appVersion, m.Window)
|
||||
m.BrowsingPane = browsing.NewBrowsingPane(app, 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.Router = browsing.NewRouter(app, m.Controller, m.BrowsingPane)
|
||||
goHomeFn := func() { m.Router.NavigateTo(m.StartupPage()) }
|
||||
m.Toolbar = NewToolbar(m.BrowsingPane, m.Router.NavigateTo, goHomeFn, m.Controller.ShowQuickSearch, m.toggleSidebar)
|
||||
|
||||
// inject controller dependencies
|
||||
m.Controller.NavHandler = m.Router.NavigateTo
|
||||
m.Controller.ReloadFunc = m.BrowsingPane.Reload
|
||||
@@ -94,27 +100,30 @@ func NewMainWindow(fyneApp fyne.App, appName, displayAppName, appVersion string,
|
||||
app.PlaybackManager.OnSongChange(func(item mediaprovider.MediaItem, _ *mediaprovider.Track) {
|
||||
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) {
|
||||
go m.RunOnServerConnectedTasks(conf, app, displayAppName)
|
||||
})
|
||||
app.ServerManager.OnLogout(func() {
|
||||
m.BrowsingPane.DisableNavigationButtons()
|
||||
m.Toolbar.DisableNavigationButtons()
|
||||
m.BrowsingPane.SetPage(nil)
|
||||
m.BrowsingPane.ClearHistory()
|
||||
m.Controller.PromptForLoginAndConnect()
|
||||
})
|
||||
m.BrowsingPane.AddSettingsMenuItem(lang.L("Log Out"), theme.LogoutIcon(), func() { app.ServerManager.Logout(true) })
|
||||
m.BrowsingPane.AddSettingsMenuItem(lang.L("Switch Servers"), theme.LoginIcon(), func() { app.ServerManager.Logout(false) })
|
||||
m.BrowsingPane.AddSettingsSubmenu(lang.L("Select Library"), myTheme.LibraryIcon, fyne.NewMenu("",
|
||||
m.Toolbar.AddSettingsMenuItem(lang.L("Log Out"), theme.LogoutIcon(), func() { app.ServerManager.Logout(true) })
|
||||
m.Toolbar.AddSettingsMenuItem(lang.L("Switch Servers"), theme.LoginIcon(), func() { app.ServerManager.Logout(false) })
|
||||
m.Toolbar.AddSettingsSubmenu(lang.L("Select Library"), myTheme.LibraryIcon, fyne.NewMenu("",
|
||||
fyne.NewMenuItem(lang.L("All Libraries"), func() { /* dummy - will get replaced on server login */ })))
|
||||
m.BrowsingPane.AddSettingsMenuItem(lang.L("Rescan Library"), theme.ViewRefreshIcon(), func() { app.ServerManager.Server.RescanLibrary() })
|
||||
m.BrowsingPane.AddSettingsMenuSeparator()
|
||||
m.BrowsingPane.AddSettingsSubmenu(lang.L("Visualizations"), myTheme.VisualizationIcon,
|
||||
m.Toolbar.AddSettingsMenuItem(lang.L("Rescan Library"), theme.ViewRefreshIcon(), func() { app.ServerManager.Server.RescanLibrary() })
|
||||
m.Toolbar.AddSettingsMenuSeparator()
|
||||
m.Toolbar.AddSettingsSubmenu(lang.L("Visualizations"), myTheme.VisualizationIcon,
|
||||
fyne.NewMenu("", []*fyne.MenuItem{
|
||||
fyne.NewMenuItem(lang.L("Peak Meter"), m.Controller.ShowPeakMeter),
|
||||
}...))
|
||||
m.BrowsingPane.AddSettingsMenuSeparator()
|
||||
m.BrowsingPane.AddSettingsMenuItem(lang.L("Check for Updates"), theme.DownloadIcon(), func() {
|
||||
m.Toolbar.AddSettingsMenuSeparator()
|
||||
m.Toolbar.AddSettingsMenuItem(lang.L("Check for Updates"), theme.DownloadIcon(), func() {
|
||||
go func() {
|
||||
if t := app.UpdateChecker.CheckLatestVersionTag(); t != "" && t != app.VersionTag() {
|
||||
fyne.Do(func() { m.ShowNewVersionDialog(displayAppName, t) })
|
||||
@@ -127,20 +136,24 @@ func NewMainWindow(fyneApp fyne.App, appName, displayAppName, appVersion string,
|
||||
}
|
||||
}()
|
||||
})
|
||||
m.BrowsingPane.AddSettingsMenuItem(lang.L("Settings")+"...", theme.SettingsIcon(), m.showSettingsDialog)
|
||||
m.BrowsingPane.AddSettingsMenuItem(lang.L("About")+"...", theme.InfoIcon(), m.Controller.ShowAboutDialog)
|
||||
m.addNavigationButtons()
|
||||
m.BrowsingPane.DisableNavigationButtons()
|
||||
m.Toolbar.AddSettingsMenuItem(lang.L("Settings")+"...", theme.SettingsIcon(), m.showSettingsDialog)
|
||||
m.Toolbar.AddSettingsMenuItem(lang.L("About")+"...", theme.InfoIcon(), m.Controller.ShowAboutDialog)
|
||||
m.Toolbar.DisableNavigationButtons()
|
||||
m.addShortcuts()
|
||||
|
||||
center := container.NewStack(m.BrowsingPane, m.ToastOverlay)
|
||||
m.content = newMainWindowContent(container.NewBorder(nil, m.BottomPanel, nil, nil, center),
|
||||
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(
|
||||
&layout.CustomPaddedLayout{LeftPadding: -theme.Padding(), RightPadding: -theme.Padding()}, m.Toolbar)
|
||||
m.content = newMainWindowContent(container.NewBorder(toolbarWrapper, m.BottomPanel, nil, nil, center),
|
||||
m.Controller.UnselectAll)
|
||||
m.Window.SetContent(fynetooltip.AddWindowToolTipLayer(m.content, m.Window.Canvas()))
|
||||
m.setInitialSize()
|
||||
|
||||
m.Window.SetCloseIntercept(func() {
|
||||
m.SaveWindowSize()
|
||||
m.SaveWindowSettings()
|
||||
if app.Config.Application.CloseToSystemTray && m.HaveSystemTray() {
|
||||
m.Window.Hide()
|
||||
} else {
|
||||
@@ -154,9 +167,12 @@ func NewMainWindow(fyneApp fyne.App, appName, displayAppName, appVersion string,
|
||||
func (m *MainWindow) UpdateOnTrackChange(item mediaprovider.MediaItem) {
|
||||
if item == nil {
|
||||
m.Window.SetTitle(res.DisplayName)
|
||||
m.Sidebar.SetNowPlaying("")
|
||||
return
|
||||
}
|
||||
|
||||
meta := item.Metadata()
|
||||
m.Sidebar.SetNowPlaying(meta.ID)
|
||||
artistDisp := ""
|
||||
if tr, ok := item.(*mediaprovider.Track); ok {
|
||||
artistDisp = " – " + strings.Join(tr.ArtistNames, ", ")
|
||||
@@ -285,24 +301,20 @@ func (m *MainWindow) RunOnServerConnectedTasks(serverConf *backend.ServerConfig,
|
||||
}
|
||||
m.librarySubmenu = libraryMenu
|
||||
m.librarySubmenu.Items[initialLibraryMenuIdx].Checked = true
|
||||
m.BrowsingPane.SetSubmenuForMenuItem(lang.L("Select Library"), libraryMenu)
|
||||
m.Toolbar.SetSubmenuForMenuItem(lang.L("Select Library"), libraryMenu)
|
||||
|
||||
if initialLibraryID != "" {
|
||||
m.App.ServerManager.Server.SetLibrary(initialLibraryID)
|
||||
}
|
||||
|
||||
fyne.Do(func() {
|
||||
m.BrowsingPane.EnableNavigationButtons()
|
||||
m.Toolbar.EnableNavigationButtons()
|
||||
m.Router.NavigateTo(m.StartupPage())
|
||||
_, canRate := m.App.ServerManager.Server.(mediaprovider.SupportsRating)
|
||||
m.BottomPanel.NowPlaying.DisableRating = !canRate
|
||||
|
||||
_, supportsRadio := m.App.ServerManager.Server.(mediaprovider.RadioProvider)
|
||||
if supportsRadio {
|
||||
m.radioBtn.Show()
|
||||
} else {
|
||||
m.radioBtn.Hide()
|
||||
}
|
||||
m.Toolbar.SetRadioButtonVisible(supportsRadio)
|
||||
})
|
||||
|
||||
m.App.SaveConfigFile()
|
||||
@@ -395,31 +407,13 @@ func (m *MainWindow) ShowWhatsNewDialog() {
|
||||
dialog.ShowCustom("What's new in "+res.AppVersion, lang.L("Close"), dialogs.NewWhatsNewDialog(), m.Window)
|
||||
}
|
||||
|
||||
func (m *MainWindow) addNavigationButtons() {
|
||||
m.BrowsingPane.AddNavigationButton(myTheme.NowPlayingIcon, controller.NowPlaying, func() {
|
||||
m.Router.NavigateTo(controller.NowPlayingRoute())
|
||||
})
|
||||
m.BrowsingPane.AddNavigationButton(myTheme.FavoriteIcon, controller.Favorites, func() {
|
||||
m.Router.NavigateTo(controller.FavoritesRoute())
|
||||
})
|
||||
m.BrowsingPane.AddNavigationButton(myTheme.AlbumIcon, controller.Albums, func() {
|
||||
m.Router.NavigateTo(controller.AlbumsRoute())
|
||||
})
|
||||
m.BrowsingPane.AddNavigationButton(myTheme.ArtistIcon, controller.Artists, func() {
|
||||
m.Router.NavigateTo(controller.ArtistsRoute())
|
||||
})
|
||||
m.BrowsingPane.AddNavigationButton(myTheme.GenreIcon, controller.Genres, func() {
|
||||
m.Router.NavigateTo(controller.GenresRoute())
|
||||
})
|
||||
m.BrowsingPane.AddNavigationButton(myTheme.PlaylistIcon, controller.Playlists, func() {
|
||||
m.Router.NavigateTo(controller.PlaylistsRoute())
|
||||
})
|
||||
m.BrowsingPane.AddNavigationButton(myTheme.TracksIcon, controller.Tracks, func() {
|
||||
m.Router.NavigateTo(controller.TracksRoute())
|
||||
})
|
||||
m.radioBtn = m.BrowsingPane.AddNavigationButton(myTheme.RadioIcon, controller.Radios, func() {
|
||||
m.Router.NavigateTo(controller.RadiosRoute())
|
||||
})
|
||||
func (m *MainWindow) toggleSidebar() {
|
||||
if m.Sidebar.Visible() {
|
||||
m.Sidebar.Hide()
|
||||
} else {
|
||||
m.Sidebar.Show()
|
||||
}
|
||||
m.splitContainer.Refresh()
|
||||
}
|
||||
|
||||
func (m *MainWindow) addShortcuts() {
|
||||
@@ -473,7 +467,7 @@ func (m *MainWindow) addShortcuts() {
|
||||
for i, ns := range shortcuts.NavShortcuts {
|
||||
m.Canvas().AddShortcut(&ns, func(i int) func(fyne.Shortcut) {
|
||||
return func(fyne.Shortcut) {
|
||||
m.BrowsingPane.ActivateNavigationButton(i)
|
||||
m.Toolbar.ActivateNavigationButton(i)
|
||||
}
|
||||
}(i))
|
||||
}
|
||||
@@ -525,14 +519,16 @@ func (m *MainWindow) SetContent(c fyne.CanvasObject) {
|
||||
}
|
||||
|
||||
func (m *MainWindow) Quit() {
|
||||
m.SaveWindowSize()
|
||||
m.SaveWindowSettings()
|
||||
fyne.CurrentApp().Quit()
|
||||
}
|
||||
|
||||
func (m *MainWindow) SaveWindowSize() {
|
||||
func (m *MainWindow) SaveWindowSettings() {
|
||||
util.SaveWindowSize(m.Window,
|
||||
&m.App.Config.Application.WindowWidth,
|
||||
&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
|
||||
|
||||
@@ -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)),
|
||||
),
|
||||
))
|
||||
}
|
||||
@@ -60,6 +60,7 @@ var (
|
||||
FilterIcon fyne.Resource = theme.NewThemedResource(res.ResFilterSvg)
|
||||
RepeatIcon fyne.Resource = theme.NewThemedResource(res.ResRepeatSvg)
|
||||
RepeatOneIcon fyne.Resource = theme.NewThemedResource(res.ResRepeatoneSvg)
|
||||
SidebarIcon fyne.Resource = theme.NewThemedResource(res.ResSidebarSvg)
|
||||
SortIcon fyne.Resource = theme.NewThemedResource(res.ResUpdownarrowSvg)
|
||||
VisualizationIcon fyne.Resource = theme.NewThemedResource(res.ResOscilloscopeSvg)
|
||||
LibraryIcon fyne.Resource = theme.NewThemedResource(res.ResLibrarySvg)
|
||||
|
||||
+206
@@ -0,0 +1,206 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/container"
|
||||
"fyne.io/fyne/v2/lang"
|
||||
"fyne.io/fyne/v2/layout"
|
||||
"fyne.io/fyne/v2/theme"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
"github.com/dweymouth/supersonic/ui/browsing"
|
||||
"github.com/dweymouth/supersonic/ui/controller"
|
||||
"github.com/dweymouth/supersonic/ui/layouts"
|
||||
myTheme "github.com/dweymouth/supersonic/ui/theme"
|
||||
|
||||
ttwidget "github.com/dweymouth/fyne-tooltip/widget"
|
||||
)
|
||||
|
||||
type Toolbar struct {
|
||||
widget.BaseWidget
|
||||
|
||||
browsingPane *browsing.BrowsingPane
|
||||
|
||||
home *ttwidget.Button
|
||||
forward *ttwidget.Button
|
||||
back *ttwidget.Button
|
||||
reload *ttwidget.Button
|
||||
|
||||
navBtnsContainer *fyne.Container
|
||||
navBtnsPageMap map[controller.PageName]fyne.Resource
|
||||
radioBtn fyne.CanvasObject
|
||||
|
||||
quickSearchBtn *ttwidget.Button
|
||||
sidebarBtn *ttwidget.Button
|
||||
settingsBtn *ttwidget.Button
|
||||
settingsMenu *fyne.Menu
|
||||
}
|
||||
|
||||
func NewToolbar(browsingPane *browsing.BrowsingPane, navigateFn func(controller.Route), homeFunc, showSearchFn, toggleSidebarFn func()) *Toolbar {
|
||||
t := &Toolbar{
|
||||
browsingPane: browsingPane,
|
||||
navBtnsContainer: container.NewHBox(),
|
||||
navBtnsPageMap: make(map[controller.PageName]fyne.Resource),
|
||||
settingsMenu: fyne.NewMenu(""),
|
||||
}
|
||||
t.ExtendBaseWidget(t)
|
||||
|
||||
browsingPane.OnHistoryChanged = t.updateNavButtons
|
||||
|
||||
t.home = ttwidget.NewButtonWithIcon("", theme.HomeIcon(), homeFunc)
|
||||
t.home.SetToolTip(lang.L("Home"))
|
||||
t.back = ttwidget.NewButtonWithIcon("", theme.NavigateBackIcon(), browsingPane.GoBack)
|
||||
t.back.SetToolTip(lang.L("Back"))
|
||||
t.forward = ttwidget.NewButtonWithIcon("", theme.NavigateNextIcon(), browsingPane.GoForward)
|
||||
t.forward.SetToolTip(lang.L("Forward"))
|
||||
t.reload = ttwidget.NewButtonWithIcon("", theme.ViewRefreshIcon(), browsingPane.Reload)
|
||||
t.reload.SetToolTip(lang.L("Reload"))
|
||||
|
||||
t.setupNavigationButtons(navigateFn)
|
||||
|
||||
t.quickSearchBtn = ttwidget.NewButtonWithIcon("", theme.SearchIcon(), showSearchFn)
|
||||
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() {
|
||||
p := widget.NewPopUpMenu(t.settingsMenu,
|
||||
fyne.CurrentApp().Driver().CanvasForObject(t.settingsBtn))
|
||||
p.ShowAtPosition(fyne.NewPos(t.Size().Width-p.MinSize().Width+4,
|
||||
t.navBtnsContainer.MinSize().Height+theme.Padding()))
|
||||
})
|
||||
t.settingsBtn.SetToolTip(lang.L("Menu"))
|
||||
|
||||
t.updateNavButtons()
|
||||
return t
|
||||
}
|
||||
|
||||
// SetRadioButtonVisible sets whether the radio button is visible
|
||||
func (t *Toolbar) SetRadioButtonVisible(vis bool) {
|
||||
if vis {
|
||||
t.radioBtn.Show()
|
||||
} else {
|
||||
t.radioBtn.Hide()
|
||||
}
|
||||
}
|
||||
|
||||
// AddSettingsMenuItem adds an item to the Settings menu
|
||||
func (t *Toolbar) AddSettingsMenuItem(label string, icon fyne.Resource, action func()) {
|
||||
item := fyne.NewMenuItem(label, action)
|
||||
item.Icon = icon
|
||||
t.settingsMenu.Items = append(t.settingsMenu.Items, item)
|
||||
}
|
||||
|
||||
// AddSettingsSubmenu adds a submenu to the Settings menu
|
||||
func (t *Toolbar) AddSettingsSubmenu(label string, icon fyne.Resource, menu *fyne.Menu) {
|
||||
item := fyne.NewMenuItem(label, nil)
|
||||
item.ChildMenu = menu
|
||||
item.Icon = icon
|
||||
t.settingsMenu.Items = append(t.settingsMenu.Items, item)
|
||||
}
|
||||
|
||||
// AddSettingsMenuSeparator adds a separator to the settings menu
|
||||
func (t *Toolbar) AddSettingsMenuSeparator() {
|
||||
t.settingsMenu.Items = append(t.settingsMenu.Items,
|
||||
fyne.NewMenuItemSeparator())
|
||||
}
|
||||
|
||||
// SetSubmenuForMenuItem sets (changes) the submenu for the first menu item matching the label
|
||||
func (t *Toolbar) SetSubmenuForMenuItem(label string, submenu *fyne.Menu) {
|
||||
for _, item := range t.settingsMenu.Items {
|
||||
if item.Label == label {
|
||||
item.ChildMenu = submenu
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DisableNavigationButtons disables all navigation buttons
|
||||
func (t *Toolbar) DisableNavigationButtons() {
|
||||
for _, obj := range t.navBtnsContainer.Objects {
|
||||
obj.(fyne.Disableable).Disable()
|
||||
}
|
||||
}
|
||||
|
||||
// EnableNavigationButtons enables all navigation buttons
|
||||
func (t *Toolbar) EnableNavigationButtons() {
|
||||
for _, obj := range t.navBtnsContainer.Objects {
|
||||
obj.(fyne.Disableable).Enable()
|
||||
}
|
||||
}
|
||||
|
||||
// ActivateNavigationButton executes the button action for the nth navigation button
|
||||
func (t *Toolbar) ActivateNavigationButton(n int) {
|
||||
if n < len(t.navBtnsContainer.Objects) {
|
||||
btn := t.navBtnsContainer.Objects[n].(*ttwidget.Button)
|
||||
if !btn.Disabled() && !btn.Hidden {
|
||||
btn.OnTapped()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Toolbar) CreateRenderer() fyne.WidgetRenderer {
|
||||
content := container.New(layouts.NewLeftMiddleRightLayout(0, 0),
|
||||
container.NewHBox(t.home, t.back, t.forward, t.reload),
|
||||
t.navBtnsContainer,
|
||||
container.NewHBox(layout.NewSpacer(), t.quickSearchBtn, t.sidebarBtn, t.settingsBtn))
|
||||
return widget.NewSimpleRenderer(content)
|
||||
}
|
||||
|
||||
func (t *Toolbar) setupNavigationButtons(navigateFn func(controller.Route)) {
|
||||
t.addNavigationButton(myTheme.NowPlayingIcon, controller.NowPlaying, func() {
|
||||
navigateFn(controller.NowPlayingRoute())
|
||||
})
|
||||
t.addNavigationButton(myTheme.FavoriteIcon, controller.Favorites, func() {
|
||||
navigateFn(controller.FavoritesRoute())
|
||||
})
|
||||
t.addNavigationButton(myTheme.AlbumIcon, controller.Albums, func() {
|
||||
navigateFn(controller.AlbumsRoute())
|
||||
})
|
||||
t.addNavigationButton(myTheme.ArtistIcon, controller.Artists, func() {
|
||||
navigateFn(controller.ArtistsRoute())
|
||||
})
|
||||
t.addNavigationButton(myTheme.GenreIcon, controller.Genres, func() {
|
||||
navigateFn(controller.GenresRoute())
|
||||
})
|
||||
t.addNavigationButton(myTheme.PlaylistIcon, controller.Playlists, func() {
|
||||
navigateFn(controller.PlaylistsRoute())
|
||||
})
|
||||
t.addNavigationButton(myTheme.TracksIcon, controller.Tracks, func() {
|
||||
navigateFn(controller.TracksRoute())
|
||||
})
|
||||
t.radioBtn = t.addNavigationButton(myTheme.RadioIcon, controller.Radios, func() {
|
||||
navigateFn(controller.RadiosRoute())
|
||||
})
|
||||
}
|
||||
|
||||
func (t *Toolbar) addNavigationButton(icon fyne.Resource, pageName controller.PageName, action func()) *ttwidget.Button {
|
||||
// make a copy of the icon, because it can change the color
|
||||
browsingPaneIcon := theme.NewThemedResource(icon)
|
||||
btn := ttwidget.NewButtonWithIcon("", browsingPaneIcon, action)
|
||||
btn.SetToolTip(lang.L(pageName.String()))
|
||||
t.navBtnsContainer.Add(btn)
|
||||
t.navBtnsPageMap[pageName] = browsingPaneIcon
|
||||
return btn
|
||||
}
|
||||
|
||||
func (t *Toolbar) updateNavButtons() {
|
||||
if t.browsingPane.CanGoBack() {
|
||||
t.back.Enable()
|
||||
} else {
|
||||
t.back.Disable()
|
||||
}
|
||||
if t.browsingPane.CanGoForward() {
|
||||
t.forward.Enable()
|
||||
} else {
|
||||
t.forward.Disable()
|
||||
}
|
||||
|
||||
p := t.browsingPane.CurrentPage()
|
||||
for pageName, icon := range t.navBtnsPageMap {
|
||||
if pageName == p.Page {
|
||||
icon.(*theme.ThemedResource).ColorName = theme.ColorNamePrimary
|
||||
} else {
|
||||
icon.(*theme.ThemedResource).ColorName = theme.ColorNameForeground
|
||||
}
|
||||
}
|
||||
t.Refresh()
|
||||
}
|
||||
Reference in New Issue
Block a user