Add GTK application implementation
This commit is contained in:
parent
d1bb505e0a
commit
b7c0307f6b
29 changed files with 2564 additions and 266 deletions
343
internal/gui/coverpanel.go
Normal file
343
internal/gui/coverpanel.go
Normal file
|
|
@ -0,0 +1,343 @@
|
|||
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
|
||||
}
|
||||
Loading…
Reference in a new issue