package gui import ( "log" "math" "github.com/diamondburned/gotk4-adwaita/pkg/adw" "github.com/diamondburned/gotk4/pkg/gdk/v4" "github.com/diamondburned/gotk4/pkg/gdkpixbuf/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" ) type playlistPanel struct { *adw.Bin toolbar *gtk.Box selectButton *gtk.ToggleButton actionbarRevealer *gtk.Revealer clearButton *gtk.Button playlistGrid *gtk.GridView gridFactory *pictureItemFactory[string] playlistModel *gtk.StringList selectionMulti *gtk.MultiSelection selectionSingle *gtk.SingleSelection headerbarStandalone *albumHeaderbar playlistStack *gtk.Stack panelNormal *gtk.Box panelStandalone *gtk.Box standaloneStack *gtk.Stack standaloneSpinner *gtk.Spinner standaloneScroll *gtk.ScrolledWindow standaloneImage *gtk.Image standalonePlayButton *gtk.Button standaloneRemoveButton *gtk.Button selectionRemoveButton *gtk.Button selectionCancelButton *gtk.Button coverLoader *CoverLoader playlist []*common.PlaylistAlbum albums map[string]*common.PlaylistAlbum pixbufs map[string]*gdkpixbuf.Pixbuf standaloneAlbum *common.PlaylistAlbum standalonePixbuf *gdkpixbuf.Pixbuf standaloneCallback func(bool) } func newPlaylistPanel() *playlistPanel { panel := playlistPanel{ albums: make(map[string]*common.PlaylistAlbum), pixbufs: make(map[string]*gdkpixbuf.Pixbuf), } panel.loadWidgets() panel.connectSignals() return &panel } func (p *playlistPanel) loadWidgets() { builder := gtk.NewBuilderFromString(data.PlaylistPanelXML) p.toolbar = builder.GetObject("toolbar").Cast().(*gtk.Box) p.selectButton = builder.GetObject("select_button").Cast().(*gtk.ToggleButton) p.actionbarRevealer = builder.GetObject("actionbar_revealer").Cast().(*gtk.Revealer) p.clearButton = builder.GetObject("playlist_clear_button").Cast().(*gtk.Button) p.Bin = builder.GetObject("McgPlaylistPanel").Cast().(*adw.Bin) p.playlistGrid = builder.GetObject("playlist_grid").Cast().(*gtk.GridView) p.playlistModel = gtk.NewStringList(nil) p.selectionMulti = gtk.NewMultiSelection(p.playlistModel) p.selectionSingle = gtk.NewSingleSelection(p.playlistModel) p.playlistGrid.SetModel(p.selectionSingle) p.gridFactory = newPictureItemFactory[string](p.itemToModelID, p.modelIDToValue) p.playlistGrid.SetFactory(p.gridFactory.ListItemFactory()) // Headerbar p.headerbarStandalone = newAlbumHeaderbar() p.playlistStack = builder.GetObject("playlist_stack").Cast().(*gtk.Stack) p.panelNormal = builder.GetObject("panel_normal").Cast().(*gtk.Box) p.panelStandalone = builder.GetObject("panel_standalone").Cast().(*gtk.Box) p.standaloneStack = builder.GetObject("standalone_stack").Cast().(*gtk.Stack) p.standaloneSpinner = builder.GetObject("standalone_spinner").Cast().(*gtk.Spinner) p.standaloneScroll = builder.GetObject("standalone_scroll").Cast().(*gtk.ScrolledWindow) p.standaloneImage = builder.GetObject("standalone_image").Cast().(*gtk.Image) p.standalonePlayButton = builder.GetObject("standalone_play_button").Cast().(*gtk.Button) p.standaloneRemoveButton = builder.GetObject("standalone_remove_button").Cast().(*gtk.Button) // Selection p.selectionRemoveButton = builder.GetObject("selection_remove_button").Cast().(*gtk.Button) p.selectionCancelButton = builder.GetObject("selection_cancel_button").Cast().(*gtk.Button) } func (p *playlistPanel) itemToModelID(item *glib.Object) string { return item.Cast().(*gtk.StringObject).String() } func (p *playlistPanel) modelIDToValue(itemID string) (tooltip string, pixbuf *gdkpixbuf.Pixbuf) { if album, ok := p.albums[itemID]; ok { tooltip = album.Tooltip() } pixbuf = p.pixbufs[itemID] return } func (p *playlistPanel) connectSignals() { p.selectButton.ConnectToggled(p.onSelectToggled) p.clearButton.ConnectClicked(p.onClearClicked) p.playlistGrid.ConnectActivate(p.onGridClicked) p.headerbarStandalone.ConnectClose(p.onHeaderbarStandaloneClose) p.standalonePlayButton.ConnectClicked(p.onStandalonePlayClicked) p.standaloneRemoveButton.ConnectClicked(p.onStandaloneRemoveClicked) p.selectionRemoveButton.ConnectClicked(p.onSelectionRemove) p.selectionCancelButton.ConnectClicked(p.onSelectionCancel) } func (p *playlistPanel) ConnectStandalone(callback func(bool)) { p.standaloneCallback = callback } func (p *playlistPanel) onSelectToggled() { if p.selectButton.Active() { p.actionbarRevealer.SetRevealChild(true) p.playlistGrid.SetModel(p.selectionMulti) p.playlistGrid.SetSingleClickActivate(false) p.playlistGrid.StyleContext().AddClass("selection") } else { p.actionbarRevealer.SetRevealChild(false) p.playlistGrid.SetModel(p.selectionSingle) p.playlistGrid.SetSingleClickActivate(true) p.playlistGrid.StyleContext().AddClass("selection") } } func (p *playlistPanel) onClearClicked() { client.Instance().ClearPlaylist() } func (p *playlistPanel) onGridClicked(position uint) { // Get selected album itemObj := p.playlistModel.Item(position) if itemObj == nil { return } strObj := itemObj.Cast().(*gtk.StringObject) id := strObj.String() album, found := p.albums[id] if !found { return } // Show standalone album if p.playlistGrid.Model().Native() == p.selectionSingle.Native() { p.standaloneAlbum = album p.headerbarStandalone.SetAlbum(album.Album) p.standaloneStack.SetVisibleChild(p.standaloneSpinner) p.standaloneSpinner.Start() p.openStandalone() client.Instance().LoadAlbumart(album.Album) } } func (p *playlistPanel) onHeaderbarStandaloneClose() { p.closeStandalone() } func (p *playlistPanel) openStandalone() { p.playlistStack.SetVisibleChild(p.panelStandalone) if p.standaloneCallback != nil { p.standaloneCallback(true) } } func (p *playlistPanel) closeStandalone() { p.playlistStack.SetVisibleChild(p.panelNormal) if p.standaloneCallback != nil { p.standaloneCallback(false) } } func (p *playlistPanel) setCoverLoader(loader *CoverLoader) { p.coverLoader = loader } func (p *playlistPanel) SetAlbumart(album *common.Album, albumart []byte) { if p.standaloneAlbum == nil || album != p.standaloneAlbum.Album { return } p.standalonePixbuf = nil if albumart == nil || len(albumart) == 0 { return } loader, loaderErr := loadPixbuf(albumart) if loaderErr != nil { log.Println("failed to load albumart for ", album.Title(), ":", loaderErr) return } p.standalonePixbuf = loader.Pixbuf() glib.IdleAdd(p.showStandaloneImage) } func (p *playlistPanel) showStandaloneImage() { p.resizeStandaloneImage() p.standaloneStack.SetVisibleChild(p.standaloneScroll) p.standaloneSpinner.Stop() } func (p *playlistPanel) resizeStandaloneImage() { newWidth, newHeight := p.getStandaloneSize() if p.standalonePixbuf == nil { p.standaloneImage.SetPixelSize(min(newWidth, newHeight)) return } width, height := p.calculateCoverSize(newWidth, newHeight) if width <= 0 || height <= 0 { return } scaledPixbuf := p.standalonePixbuf.ScaleSimple(width, height, gdkpixbuf.InterpHyper) p.standaloneImage.SetFromPaintable(gdk.NewTextureForPixbuf(scaledPixbuf)) p.standaloneImage.SetPixelSize(min(width, height)) } func (p *playlistPanel) getStandaloneSize() (int, int) { return p.standaloneStack.Size(gtk.OrientationHorizontal), p.standaloneStack.Size(gtk.OrientationVertical) } func (p *playlistPanel) calculateCoverSize(currentWidth, currentHeight int) (width, height int) { ratioWidth := float64(currentWidth) / float64(p.standalonePixbuf.Width()) ratioHeight := float64(currentHeight) / float64(p.standalonePixbuf.Height()) ratio := min(min(ratioWidth, ratioHeight), 1) width = int(math.Floor(float64(p.standalonePixbuf.Width()) * ratio)) height = int(math.Floor(float64(p.standalonePixbuf.Height()) * ratio)) return } func (p *playlistPanel) onStandalonePlayClicked() { client.Instance().PlayQueuedAlbum(p.standaloneAlbum) p.closeStandalone() } func (p *playlistPanel) onStandaloneRemoveClicked() { client.Instance().UnqueueAlbum(p.standaloneAlbum) p.closeStandalone() } func (p *playlistPanel) onSelectionRemove() { albums := p.getSelectedAlbums() if len(albums) > 0 { client.Instance().UnqueueAlbums(albums) } p.selectButton.SetActive(false) } func (p *playlistPanel) getSelectedAlbums() []*common.PlaylistAlbum { albums := []*common.PlaylistAlbum{} for i := range p.selectionMulti.NItems() { if p.selectionMulti.IsSelected(i) { itemObj := p.selectionMulti.Item(i) if itemObj != nil { itemStr := itemObj.Cast().(*gtk.StringObject) albumID := itemStr.String() album, found := p.albums[albumID] if found { albums = append(albums, album) } } } } return albums } func (p *playlistPanel) onSelectionCancel() { p.selectButton.SetActive(false) } func (p *playlistPanel) Toolbar() gtk.Widgetter { return p.toolbar } func (p *playlistPanel) ItemSizeChanged() { p.redraw() } func (p *playlistPanel) redraw() { if len(p.playlist) > 0 { p.SetPlaylist(p.playlist) } } func (p *playlistPanel) SetPlaylist(playlist []*common.PlaylistAlbum) { go p.setPlaylist(playlist) } func (p *playlistPanel) setPlaylist(playlist []*common.PlaylistAlbum) { p.playlist = playlist for _, album := range playlist { p.albums[album.ID()] = album if p.coverLoader != nil { p.pixbufs[album.ID()] = p.coverLoader.LoadThumbnail(album.Album) } } p.updateGridModel() } func (p *playlistPanel) updateGridModel() { p.updateGrid(p.playlist) } func (p *playlistPanel) updateGrid(albums []*common.PlaylistAlbum) { glib.IdleAdd(func() { n := p.playlistModel.NItems() if n > 0 { p.playlistModel.Splice(0, n, nil) } ids := make([]string, 0, len(albums)) for _, item := range albums { ids = append(ids, item.ID()) } if len(ids) > 0 { p.playlistModel.Splice(0, 0, ids) } }) } func (p *playlistPanel) SetWidth(width int) { p.resizeStandaloneImage() } func (p *playlistPanel) ClearPlaylist() { client.Instance().ClearPlaylist() } func (p *playlistPanel) HeaderbarStandalone() *albumHeaderbar { return p.headerbarStandalone }