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) }