Add GTK application implementation
This commit is contained in:
parent
d1bb505e0a
commit
b7c0307f6b
29 changed files with 2564 additions and 266 deletions
454
internal/gui/window.go
Normal file
454
internal/gui/window.go
Normal file
|
|
@ -0,0 +1,454 @@
|
|||
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)
|
||||
}
|
||||
Loading…
Reference in a new issue