mcg/internal/gui/window.go

454 lines
13 KiB
Go

package gui
import (
"fmt"
"log"
"github.com/diamondburned/gotk4-adwaita/pkg/adw"
"github.com/diamondburned/gotk4/pkg/gdk/v4"
"github.com/diamondburned/gotk4/pkg/gio/v2"
"github.com/diamondburned/gotk4/pkg/glib/v2"
"github.com/diamondburned/gotk4/pkg/gtk/v4"
"suruatoel.xyz/mcg/data"
"suruatoel.xyz/mcg/internal/client"
"suruatoel.xyz/mcg/internal/common"
"suruatoel.xyz/mcg/internal/i18n"
)
type Panel interface {
Toolbar() gtk.Widgetter
}
type GridPanel interface {
HeaderbarStandalone() *albumHeaderbar
}
type mainWindow struct {
*adw.ApplicationWindow
toggleFullscreenAction *gio.SimpleAction
playAaction *gio.SimpleAction
panelAction *gio.SimpleAction
clearPlaylistAction *gio.SimpleAction
searchLibraryAction *gio.SimpleAction
toolbarView *adw.ToolbarView
headerBar *adw.HeaderBar
connectButton *gtk.Switch
connectButtonActive bool
playButton *gtk.ToggleButton
playButtonActive bool
volumeButton *gtk.VolumeButton
settingVolume bool
toolbarStack *gtk.Stack
infoToast *adw.ToastOverlay
contentStack *gtk.Stack
connectionPanel *connectionPanel
panelStack *adw.ViewStack
serverPanel *serverPanel
coverPanel *coverPanel
playlistPanel *playlistPanel
libraryPanel *libraryPanel
resizeHelper *gtk.DrawingArea
panels map[string]Panel
currentWidth int
coverLoader *CoverLoader
}
func newMainWindow(app *gtk.Application, config Configuration) *mainWindow {
window := mainWindow{
panels: make(map[string]Panel),
}
window.loadWidgets(app, config)
window.connectSignals()
window.registerActions()
window.SetHelpOverlay(newShortcutsDialog().ShortcutsWindow)
return &window
}
func (w *mainWindow) loadWidgets(app *gtk.Application, config Configuration) {
builder := gtk.NewBuilderFromString(data.WindowXML)
w.ApplicationWindow = builder.GetObject("McgAppWindow").Cast().(*adw.ApplicationWindow)
w.ApplicationWindow.SetApplication(app)
w.toolbarView = builder.GetObject("toolbar_view").Cast().(*adw.ToolbarView)
w.headerBar = builder.GetObject("headerbar").Cast().(*adw.HeaderBar)
w.connectButton = builder.GetObject("headerbar_button_connect").Cast().(*gtk.Switch)
w.connectButtonActive = true
w.playButton = builder.GetObject("headerbar_button_playpause").Cast().(*gtk.ToggleButton)
w.playButtonActive = true
w.volumeButton = builder.GetObject("headerbar_button_volume").Cast().(*gtk.VolumeButton)
w.volumeButton.ConnectValueChanged(w.OnVolumeChanged)
w.toolbarStack = builder.GetObject("toolbar_stack").Cast().(*gtk.Stack)
w.infoToast = builder.GetObject("info_toast").Cast().(*adw.ToastOverlay)
w.contentStack = builder.GetObject("content_stack").Cast().(*gtk.Stack)
w.connectionPanel = newConnectionPanel(config)
w.contentStack.AddChild(w.connectionPanel)
w.contentStack.SetVisibleChild(w.connectionPanel)
// Server panel
w.serverPanel = newServerPanel()
w.panels["server"] = w.serverPanel
w.serverPanel.ReceiveOutputDeviceChange(w.OnServerPanelOutputDeviceChange)
// Cover panel
w.coverPanel = newCoverPanel()
w.panels["cover"] = w.coverPanel
// Playlist panel
w.playlistPanel = newPlaylistPanel()
w.panels["playlist"] = w.playlistPanel
// Library panel
w.libraryPanel = newLibraryPanel(config)
w.panels["library"] = w.serverPanel
// Panel stack
w.panelStack = builder.GetObject("panel_stack").Cast().(*adw.ViewStack)
w.panelStack.AddTitledWithIcon(w.serverPanel, "server", i18n.T("Server"), "network-wired-symbolic")
w.panelStack.AddTitledWithIcon(w.coverPanel, "cover", i18n.T("Cover"), "image-x-generic-symbolic")
w.panelStack.AddTitledWithIcon(w.playlistPanel, "playlist", i18n.T("Playlist"), "view-list-symbolic")
w.panelStack.AddTitledWithIcon(w.libraryPanel, "library", i18n.T("Library"), "emblem-music-symbolic")
if _, found := w.panels[config.Panel]; found {
w.panelStack.SetVisibleChildName(config.Panel)
} else {
log.Println("configured initial panel name is unknown:", config.Panel)
}
// Toolbar stack
w.toolbarStack.AddNamed(w.serverPanel.Toolbar(), "server")
w.toolbarStack.AddNamed(w.coverPanel.Toolbar(), "cover")
w.toolbarStack.AddNamed(w.playlistPanel.Toolbar(), "playlist")
w.toolbarStack.AddNamed(w.libraryPanel.Toolbar(), "library")
w.toolbarStack.SetVisibleChildName(config.Panel)
w.resizeHelper = builder.GetObject("resize_helper").Cast().(*gtk.DrawingArea)
}
func (w *mainWindow) connectSignals() {
w.connectButton.Connect("notify::active", func() {
w.connect()
})
w.connectButton.ConnectStateSet(w.OnConnectStateSet)
w.playButton.ConnectToggled(w.OnPlayPauseToggled)
w.panelStack.Connect("notify::visible-child", w.OnPanelStackSwitched)
w.resizeHelper.ConnectResize(w.OnWindowResize)
w.coverPanel.ConnectFullscreen(w.OnCoverPanelFullscreen)
w.Connect("notify::fullscreened", w.OnWindowFullscreen)
w.playlistPanel.ConnectStandalone(w.OnPlaylistPanelStandalone)
w.libraryPanel.ConnectItemSizeChanged(w.onLibraryPanelItemSizeChanged)
w.libraryPanel.ConnectStandalone(w.OnLibraryPanelStandalone)
clientInstance := client.Instance()
clientInstance.ReceiveError(w.OnClientError)
clientInstance.RegisterConnection(w.OnClientConnection)
clientInstance.ReceiveStatus(w.OnClientStatus)
clientInstance.ReceiveStatistics(w.OnClientStatistics)
clientInstance.ReceiveOutputDevices(w.OnClientOutputDevces)
clientInstance.ReceivePlaylist(w.OnClientPlaylist)
clientInstance.ReceiveAlbums(w.OnClientAlbums)
clientInstance.ReceiveAlbumart(w.OnClientAlbumart)
}
func (w *mainWindow) registerActions() {
connectAction := gio.NewSimpleAction("connect", nil)
connectAction.ConnectActivate(func(_ *glib.Variant) {
w.connect()
})
w.AddAction(connectAction)
w.playAaction = gio.NewSimpleAction("play", nil)
w.playAaction.SetEnabled(false)
w.playAaction.ConnectActivate(func(_ *glib.Variant) {
w.OnPlayPauseToggled()
})
w.AddAction(w.playAaction)
w.clearPlaylistAction = gio.NewSimpleAction("clear-playlist", nil)
w.clearPlaylistAction.SetEnabled(false)
w.clearPlaylistAction.ConnectActivate(func(_ *glib.Variant) {
w.playlistPanel.ClearPlaylist()
})
w.AddAction(w.clearPlaylistAction)
panelVariant := glib.NewVariantString("server")
w.panelAction = gio.NewSimpleActionStateful("panel", panelVariant.Type(), panelVariant)
w.panelAction.SetEnabled(false)
w.panelAction.ConnectActivate(func(value *glib.Variant) {
panelName := value.String()
w.panelStack.SetVisibleChildName(panelName)
})
w.AddAction(w.panelAction)
w.toggleFullscreenAction = gio.NewSimpleAction("toggle-fullscreen", nil)
w.toggleFullscreenAction.SetEnabled(false)
w.toggleFullscreenAction.ConnectActivate(func(_ *glib.Variant) {
w.Fullscreen()
})
w.AddAction(w.toggleFullscreenAction)
w.searchLibraryAction = gio.NewSimpleAction("search-library", nil)
w.searchLibraryAction.SetEnabled(false)
w.searchLibraryAction.ConnectActivate(func(_ *glib.Variant) {
w.panelStack.SetVisibleChild(w.libraryPanel)
w.libraryPanel.ShowSearch()
})
w.AddAction(w.searchLibraryAction)
}
func (w *mainWindow) OnPanelStackSwitched() {
w.setVisibleToolbar()
}
func (w *mainWindow) setVisibleToolbar() {
name := w.panelStack.VisibleChildName()
w.toolbarStack.SetVisibleChildName(name)
}
func (w *mainWindow) OnWindowFullscreen() {
w.reactToFullscreen()
}
func (w *mainWindow) reactToFullscreen() {
var cursor *gdk.Cursor
if w.IsFullscreen() {
w.panelStack.SetVisibleChild(w.coverPanel)
w.headerBar.SetVisible(false)
w.coverPanel.Fullscreen(true)
cursor = gdk.NewCursorFromName("none", nil)
} else {
w.headerBar.SetVisible(true)
w.coverPanel.Fullscreen(false)
cursor = gdk.NewCursorFromName("default", nil)
}
w.SetCursor(cursor)
}
func (w *mainWindow) OnCoverPanelFullscreen() {
if w.IsFullscreen() {
w.Unfullscreen()
} else {
w.Fullscreen()
}
}
func (w *mainWindow) OnPlaylistPanelStandalone(open bool) {
w.onPanelStandalone(w.playlistPanel, open)
}
func (w *mainWindow) onLibraryPanelItemSizeChanged(itemSize int) {
w.coverLoader.SetSize(itemSize)
w.playlistPanel.ItemSizeChanged()
}
func (w *mainWindow) OnLibraryPanelStandalone(open bool) {
w.onPanelStandalone(w.libraryPanel, open)
}
func (w *mainWindow) onPanelStandalone(panel GridPanel, open bool) {
if open {
w.toolbarView.AddTopBar(panel.HeaderbarStandalone())
w.toolbarView.Remove(w.headerBar)
} else {
w.toolbarView.AddTopBar(w.headerBar)
w.toolbarView.Remove(panel.HeaderbarStandalone())
}
}
func (w *mainWindow) connect() {
if !w.connectButtonActive {
return
}
if client.Instance().IsConnected() {
w.libraryPanel.Wait()
client.Instance().Disconnect()
} else {
client.Instance().Connect(w.connectionPanel.ConnectionDetails())
}
}
func (w *mainWindow) OnClientConnection(connected bool) {
glib.IdleAdd(func() {
w.connectButtonActive = false
w.connectButton.SetActive(connected)
w.connectButton.SetState(connected)
w.connectButtonActive = true
w.playButton.SetSensitive(connected)
w.volumeButton.SetSensitive(connected)
w.playAaction.SetEnabled(connected)
w.panelAction.SetEnabled(connected)
if connected {
w.contentStack.SetVisibleChild(w.panelStack)
} else {
w.contentStack.SetVisibleChild(w.connectionPanel)
}
w.clearPlaylistAction.SetEnabled(connected)
w.searchLibraryAction.SetEnabled(connected)
})
host, _, _ := w.connectionPanel.ConnectionDetails()
w.coverLoader = NewCoverLoader(host)
w.playlistPanel.setCoverLoader(w.coverLoader)
w.libraryPanel.setCoverLoader(w.coverLoader)
}
func (w *mainWindow) SetConfigError(err error) {
glib.IdleAdd(func() {
w.infoToast.AddToast(adw.NewToast(fmt.Sprintf("Loading the configuration has failed: %s", err.Error())))
})
}
func (w *mainWindow) UpdateConfig(config *Configuration) {
config.Panel = w.panelStack.VisibleChildName()
w.connectionPanel.UpdateConfig(config)
w.libraryPanel.UpdateConfig(config)
}
func (w *mainWindow) OnClientError(message string) {
glib.IdleAdd(func() {
w.infoToast.AddToast(adw.NewToast(message))
})
}
func (w *mainWindow) OnClientStatus(state string, album *common.PlaylistAlbum, pos uint64, time uint64, volume int64, file string, audio string, bitrate string, errorMessage string) {
// Album
glib.IdleAdd(func() {
w.coverPanel.SetAlbum(state, album)
})
w.toggleFullscreenAction.SetEnabled(album != nil)
// Fullscreen
if w.IsFullscreen() && album == nil {
w.Unfullscreen()
}
// State
if state == "play" {
glib.IdleAdd(func() {
w.setPlayPause(true)
})
glib.IdleAdd(func() {
w.coverPanel.SetPlay(pos, time)
})
} else if state == "pause" || state == "stop" {
glib.IdleAdd(func() {
w.setPlayPause(false)
})
w.coverPanel.SetPause()
}
// Volume
glib.IdleAdd(func() {
w.setVolume(volume)
})
glib.IdleAdd(func() {
w.serverPanel.Status(file, audio, bitrate, errorMessage)
})
}
func (w *mainWindow) OnClientStatistics(artists, albums, songs, dbplaytime, playtime, uptime string) {
glib.IdleAdd(func() {
w.serverPanel.Statistics(artists, albums, songs, dbplaytime, playtime, uptime)
})
}
func (w *mainWindow) OnClientOutputDevces(devices []*common.OutputDevice) {
glib.IdleAdd(func() {
w.serverPanel.OutputDevices(devices)
})
}
func (w *mainWindow) OnClientPlaylist(playlist []*common.PlaylistAlbum) {
w.playlistPanel.SetPlaylist(playlist)
}
func (w *mainWindow) OnClientAlbums(albums map[string]*common.Album) {
w.libraryPanel.SetAlbums(albums)
}
func (w *mainWindow) OnClientAlbumart(album *common.Album, albumart []byte) {
w.coverPanel.SetAlbumart(album, albumart)
w.playlistPanel.SetAlbumart(album, albumart)
w.libraryPanel.SetAlbumart(album, albumart)
}
func (w *mainWindow) OnConnectStateSet(state bool) bool {
return true
}
func (w *mainWindow) OnPlayPauseToggled() {
if w.playButtonActive {
client.Instance().PlayPause()
}
}
func (w *mainWindow) OnVolumeChanged(value float64) {
if !w.settingVolume {
client.Instance().SetVolume(int(value * 100))
}
}
func (w *mainWindow) setPlayPause(play bool) {
w.playButtonActive = false
w.playButton.SetActive(play)
w.playButtonActive = true
}
func (w *mainWindow) setVolume(volume int64) {
if volume >= 0 {
w.volumeButton.SetVisible(true)
w.settingVolume = true
w.volumeButton.SetValue(float64(volume) / 100.0)
w.settingVolume = false
} else {
w.volumeButton.SetVisible(false)
}
}
func (w *mainWindow) OnServerPanelOutputDeviceChange(device *common.OutputDevice, enable bool) {
client.Instance().SetOutputDeviceState(device, enable)
}
func (w *mainWindow) OnWindowResize(width int, _ int) {
if !w.hasWidthChanged(width) {
return
}
w.setWidth(width)
if width > 0 {
w.updateUIWidth()
}
}
func (w *mainWindow) hasWidthChanged(width int) bool {
return width != w.currentWidth
}
func (w *mainWindow) setWidth(width int) {
w.currentWidth = width
}
func (w *mainWindow) updateUIWidth() {
w.coverPanel.SetWidth(w.currentWidth)
w.playlistPanel.SetWidth(w.currentWidth)
w.libraryPanel.SetWidth(w.currentWidth)
}