mcg/internal/gui/coverpanel.go

343 lines
8.8 KiB
Go

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
}