package gui import ( "fmt" "log" "math" "strings" "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" "suruatoel.xyz/mcg/internal/i18n" ) type coverPanel struct { *gtk.Overlay toolbar *gtk.Box fullscreenButton *gtk.Button coverInfoScroll *gtk.ScrolledWindow coverStack *gtk.Stack coverSpinner *gtk.Spinner coverScroll *gtk.ScrolledWindow coverBox *gtk.Viewport coverDefault *gtk.Image coverImage *gtk.Image infoRevealer *gtk.Revealer albumTitleLabel *gtk.Label albumDateLabel *gtk.Label albumArtistLabel *gtk.Label songsScale *gtk.Scale currentAlbum *common.PlaylistAlbum currentPixbuf *gdkpixbuf.Pixbuf currentWidth int currentHeight int timer *glib.SourceHandle coverBoxController *gtk.GestureClick isFullscreenActive bool } func newCoverPanel() *coverPanel { panel := coverPanel{} panel.loadWidgets() panel.connectSignals() return &panel } func (p *coverPanel) loadWidgets() { builder := gtk.NewBuilderFromString(data.CoverPanelXML) p.toolbar = builder.GetObject("toolbar").Cast().(*gtk.Box) p.fullscreenButton = builder.GetObject("fullscreen_button").Cast().(*gtk.Button) p.Overlay = builder.GetObject("McgCoverPanel").Cast().(*gtk.Overlay) p.coverInfoScroll = builder.GetObject("cover_info_scroll").Cast().(*gtk.ScrolledWindow) p.infoRevealer = builder.GetObject("info_revealer").Cast().(*gtk.Revealer) p.albumTitleLabel = builder.GetObject("album_title_label").Cast().(*gtk.Label) p.albumDateLabel = builder.GetObject("album_date_label").Cast().(*gtk.Label) p.albumArtistLabel = builder.GetObject("album_artist_label").Cast().(*gtk.Label) p.songsScale = builder.GetObject("songs_scale").Cast().(*gtk.Scale) p.coverStack = builder.GetObject("cover_stack").Cast().(*gtk.Stack) p.coverSpinner = builder.GetObject("cover_spinner").Cast().(*gtk.Spinner) p.coverScroll = builder.GetObject("cover_scroll").Cast().(*gtk.ScrolledWindow) p.coverBox = builder.GetObject("cover_box").Cast().(*gtk.Viewport) p.coverDefault = builder.GetObject("cover_default").Cast().(*gtk.Image) p.coverImage = builder.GetObject("cover_image").Cast().(*gtk.Image) } func (p *coverPanel) connectSignals() { p.coverBoxController = gtk.NewGestureClick() p.coverBox.AddController(p.coverBoxController) buttonController := gtk.NewGestureClick() buttonController.ConnectPressed(p.OnSongsScalePressed) buttonController.ConnectUnpairedRelease(p.OnSongsScaleUnpairedReleased) p.songsScale.AddController(buttonController) } func (p *coverPanel) ConnectFullscreen(callback func()) { p.fullscreenButton.ConnectClicked(callback) p.coverBoxController.ConnectPressed(func(nPress int, x float64, y float64) { if nPress == 2 && p.currentAlbum != nil { callback() } }) } func (p *coverPanel) OnSongsScalePressed(nPress int, x float64, y float64) { p.clearTimer() } func (p *coverPanel) OnSongsScaleUnpairedReleased(x float64, y float64, button uint, sequence *gdk.EventSequence) { value := uint64(p.songsScale.Value()) time := p.currentAlbum.Length() songs := p.currentAlbum.Songs() pos := 0 for index := len(songs) - 1; index >= 0; index-- { time = time - songs[index].Length() pos = int(songs[index].Pos()) if time < value { break } } time = max(value-time-1, 0) client.Instance().Seek(pos, time) } func (p *coverPanel) Toolbar() gtk.Widgetter { return p.toolbar } func (p *coverPanel) SetPlay(pos uint64, time uint64) { p.clearTimer() defer p.startTimer() dsongs := p.currentAlbum.Songs() for index := range pos { time += uint64(dsongs[index].Length()) } p.songsScale.SetValue(float64(time + 1)) } func (p *coverPanel) SetPause() { p.clearTimer() } func (p *coverPanel) clearTimer() { if p.timer != nil { glib.SourceRemove(*p.timer) p.timer = nil } } func (p *coverPanel) startTimer() { newTimer := glib.TimeoutAdd(1000, p.playing) p.timer = &newTimer } func (p *coverPanel) playing() bool { p.songsScale.SetValue(p.songsScale.Value() + 1) return true } func (p *coverPanel) SetAlbum(state string, album *common.PlaylistAlbum) { if album != nil { // Set labels p.albumTitleLabel.SetLabel(album.Title()) p.albumDateLabel.SetLabel(strings.Join(album.Dates(), ", ")) p.albumArtistLabel.SetLabel(strings.Join(album.Artists(), ",")) p.infoRevealer.SetRevealChild(true) p.setSongs(album) } else { p.infoRevealer.SetRevealChild(false) } // Load cover if album != nil { p.coverSpinner.Start() p.coverStack.SetVisibleChild(p.coverSpinner) client.Instance().LoadAlbumart(album.Album) } // Set current album p.currentAlbum = album p.fullscreenButton.SetSensitive(album != nil) p.enableSonglist() } func (p *coverPanel) Fullscreen(active bool) { p.isFullscreenActive = active p.enableSonglist() glib.IdleAdd(p.resizeImage) } func (p *coverPanel) enableSonglist() { enable := p.currentAlbum != nil && !p.isFullscreenActive p.infoRevealer.SetRevealChild(enable) } func (p *coverPanel) SetAlbumart(album *common.Album, albumart []byte) { if p.currentAlbum == nil || album != p.currentAlbum.Album { return } defer glib.IdleAdd(func() { p.coverStack.SetVisibleChild(p.coverScroll) p.coverSpinner.Stop() }) p.currentPixbuf = 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.currentPixbuf = loader.Pixbuf() glib.IdleAdd(p.showImage) } func (p *coverPanel) showImage() { if p.currentPixbuf != nil { p.resizeImage() p.coverStack.SetVisibleChild(p.coverScroll) } else { p.coverStack.SetVisibleChild(p.coverDefault) } } func loadPixbuf(data []byte) (*gdkpixbuf.PixbufLoader, error) { loader := gdkpixbuf.NewPixbufLoader() defer loader.Close() return loader, loader.Write(data) } func (p *coverPanel) setSongs(album *common.PlaylistAlbum) { p.songsScale.ClearMarks() p.songsScale.SetRange(0, float64(album.Length())) length := uint64(0) for _, song := range album.Songs() { curLength := length if length > 0 && length < album.Length() { curLength += 1 } songTitle := createSongTitle(song) p.songsScale.AddMark( float64(curLength), gtk.PosRight, glib.MarkupEscapeText(songTitle), ) length += song.Length() } p.songsScale.AddMark(float64(length), gtk.PosRight, createAlbumLengthTitle(length)) p.alignSongsScaleMarks() } func (p *coverPanel) alignSongsScaleMarks() { p.alignScaleMarks(&p.songsScale.Widget) } func (p *coverPanel) alignScaleMarks(widgetter gtk.Widgetter) { widget := widgetter.(*gtk.Widget) child := widget.FirstChild() for child != nil { if label, isLabel := child.(*gtk.Label); isLabel { label.SetHAlign(gtk.AlignStart) child = label.NextSibling() } else { p.alignScaleMarks(child) child = child.(*gtk.Widget).NextSibling() } } } func createSongTitle(song *common.PlaylistSong) string { songArtists := song.SongArtists() if len(songArtists) > 0 { return fmt.Sprintf( i18n.T("%s feat. %s"), song.Title(), strings.Join(songArtists, ", "), ) } return song.Title() } func createAlbumLengthTitle(length uint64) string { minutes := length / 60 seconds := length % 60 return fmt.Sprintf(i18n.T("%d:%d minutes"), minutes, seconds) } func (p *coverPanel) SetWidth(width int) { glib.IdleAdd(p.resizeImage) glib.IdleAdd(func() { p.coverInfoScroll.SetMaxContentWidth(width / 2) }) } func (p *coverPanel) resizeImage() { newWidth, newHeight := p.getSize() if p.hasSizeChanged(newWidth, newHeight) { return } p.setSize(newWidth, newHeight) if p.currentPixbuf == nil { p.coverImage.SetPixelSize(min(newWidth, newHeight)) return } width, height := p.calculateCoverSize() if width <= 0 || height <= 0 { return } scaledPixbuf := p.currentPixbuf.ScaleSimple(width, height, gdkpixbuf.InterpHyper) p.coverImage.SetFromPaintable(gdk.NewTextureForPixbuf(scaledPixbuf)) p.coverImage.SetPixelSize(min(width, height)) } func (p *coverPanel) getSize() (int, int) { return p.coverStack.Size(gtk.OrientationHorizontal), p.coverStack.Size(gtk.OrientationVertical) } func (p *coverPanel) hasSizeChanged(newWidth, newHeight int) bool { return newWidth == p.currentWidth && newHeight == p.currentHeight } func (p *coverPanel) setSize(newWidth, newHeight int) { p.currentWidth = newWidth p.currentHeight = newHeight } func (p *coverPanel) calculateCoverSize() (width, height int) { ratioWidth := float64(p.currentWidth) / float64(p.currentPixbuf.Width()) ratioHeight := float64(p.currentHeight) / float64(p.currentPixbuf.Height()) ratio := min(min(ratioWidth, ratioHeight), 1) width = int(math.Floor(float64(p.currentPixbuf.Width()) * ratio)) height = int(math.Floor(float64(p.currentPixbuf.Height()) * ratio)) return }