343 lines
8.8 KiB
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
|
|
}
|