forked from Leak_Technologies/VideoTools
295 lines
6.8 KiB
Go
295 lines
6.8 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"git.leaktechnologies.dev/stu/VT_Player/player/mpvembed"
|
|
|
|
"github.com/gotk3/gotk3/gdk"
|
|
"github.com/gotk3/gotk3/glib"
|
|
"github.com/gotk3/gotk3/gtk"
|
|
)
|
|
|
|
const appCSS = `
|
|
* {
|
|
font-family: "Noto Sans", "Cantarell", "Sans";
|
|
color: #E1EEFF;
|
|
}
|
|
window, GtkDrawingArea, box {
|
|
background-color: #0B0F1A;
|
|
}
|
|
button {
|
|
background: #171C2A;
|
|
color: #E1EEFF;
|
|
border-radius: 6px;
|
|
padding: 4px 8px;
|
|
}
|
|
button:hover {
|
|
background: #24314A;
|
|
}
|
|
label {
|
|
color: #E1EEFF;
|
|
}
|
|
`
|
|
|
|
type pane struct {
|
|
area *gtk.DrawingArea
|
|
mpv *mpvembed.Client
|
|
path string
|
|
}
|
|
|
|
func main() {
|
|
gtk.Init(nil)
|
|
|
|
win, err := gtk.WindowNew(gtk.WINDOW_TOPLEVEL)
|
|
if err != nil {
|
|
log.Fatalf("window: %v", err)
|
|
}
|
|
win.SetTitle("VT Player (GTK/mpv)")
|
|
win.SetDefaultSize(1400, 800)
|
|
|
|
grid, _ := gtk.GridNew()
|
|
grid.SetColumnHomogeneous(true)
|
|
grid.SetRowHomogeneous(false)
|
|
win.Add(grid)
|
|
|
|
left := newPane()
|
|
right := newPane()
|
|
|
|
controls := buildControls(win, left, right)
|
|
grid.Attach(controls, 0, 0, 2, 1)
|
|
grid.Attach(left.area, 0, 1, 1, 1)
|
|
grid.Attach(right.area, 1, 1, 1, 1)
|
|
|
|
applyCSS()
|
|
preferDark()
|
|
|
|
setupDragDest(left, win)
|
|
setupDragDest(right, win)
|
|
|
|
win.Connect("destroy", func() {
|
|
if left.mpv != nil {
|
|
left.mpv.Destroy()
|
|
}
|
|
if right.mpv != nil {
|
|
right.mpv.Destroy()
|
|
}
|
|
gtk.MainQuit()
|
|
})
|
|
|
|
win.ShowAll()
|
|
gtk.Main()
|
|
}
|
|
|
|
func newPane() *pane {
|
|
da, _ := gtk.DrawingAreaNew()
|
|
da.SetHExpand(true)
|
|
da.SetVExpand(true)
|
|
p := &pane{area: da}
|
|
da.Connect("realize", func() {
|
|
if p.mpv != nil {
|
|
return
|
|
}
|
|
mpv, err := mpvembed.New()
|
|
if err != nil {
|
|
log.Printf("mpv create: %v", err)
|
|
return
|
|
}
|
|
p.mpv = mpv
|
|
|
|
if w, err := da.GetWindow(); err == nil && w != nil {
|
|
if xid := getWindowID(w); xid != 0 {
|
|
_ = mpv.SetWID(xid)
|
|
}
|
|
}
|
|
_ = mpv.SetOptionString("pause", "yes")
|
|
if err := mpv.Initialize(); err != nil {
|
|
log.Printf("mpv init: %v", err)
|
|
}
|
|
})
|
|
return p
|
|
}
|
|
|
|
func buildControls(win *gtk.Window, left, right *pane) *gtk.Box {
|
|
box, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 6)
|
|
row1, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 4)
|
|
row2, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 4)
|
|
|
|
openL, _ := gtk.ButtonNewWithLabel("Open Left")
|
|
openR, _ := gtk.ButtonNewWithLabel("Open Right")
|
|
play, _ := gtk.ButtonNewWithLabel("Play Both")
|
|
pause, _ := gtk.ButtonNewWithLabel("Pause Both")
|
|
seek0, _ := gtk.ButtonNewWithLabel("Seek 0")
|
|
stepF, _ := gtk.ButtonNewWithLabel("Step +1f")
|
|
stepB, _ := gtk.ButtonNewWithLabel("Step -1f")
|
|
info, _ := gtk.LabelNew("Meta: -")
|
|
|
|
openL.Connect("clicked", func() { chooseAndLoad(win, left) })
|
|
openR.Connect("clicked", func() { chooseAndLoad(win, right) })
|
|
|
|
play.Connect("clicked", func() {
|
|
if left.mpv != nil {
|
|
_ = left.mpv.SetPropertyBool("pause", false)
|
|
}
|
|
if right.mpv != nil {
|
|
_ = right.mpv.SetPropertyBool("pause", false)
|
|
}
|
|
})
|
|
pause.Connect("clicked", func() {
|
|
if left.mpv != nil {
|
|
_ = left.mpv.SetPropertyBool("pause", true)
|
|
}
|
|
if right.mpv != nil {
|
|
_ = right.mpv.SetPropertyBool("pause", true)
|
|
}
|
|
})
|
|
seek0.Connect("clicked", func() {
|
|
if left.mpv != nil {
|
|
_ = left.mpv.Command("seek", "0", "absolute", "exact")
|
|
}
|
|
if right.mpv != nil {
|
|
_ = right.mpv.Command("seek", "0", "absolute", "exact")
|
|
}
|
|
})
|
|
stepF.Connect("clicked", func() {
|
|
if left.mpv != nil {
|
|
_ = left.mpv.Command("frame-step")
|
|
}
|
|
if right.mpv != nil {
|
|
_ = right.mpv.Command("frame-step")
|
|
}
|
|
})
|
|
stepB.Connect("clicked", func() {
|
|
if left.mpv != nil {
|
|
_ = left.mpv.Command("frame-back-step")
|
|
}
|
|
if right.mpv != nil {
|
|
_ = right.mpv.Command("frame-back-step")
|
|
}
|
|
})
|
|
|
|
go func() {
|
|
t := time.NewTicker(500 * time.Millisecond)
|
|
defer t.Stop()
|
|
for range t.C {
|
|
text := metaSummary(left, right)
|
|
glib.IdleAdd(func() { info.SetText(text) })
|
|
}
|
|
}()
|
|
|
|
row1.PackStart(openL, false, false, 0)
|
|
row1.PackStart(openR, false, false, 0)
|
|
row1.PackStart(play, false, false, 0)
|
|
row1.PackStart(pause, false, false, 0)
|
|
row1.PackStart(seek0, false, false, 0)
|
|
row1.PackStart(stepB, false, false, 0)
|
|
row1.PackStart(stepF, false, false, 0)
|
|
|
|
row2.PackStart(info, false, false, 0)
|
|
|
|
box.PackStart(row1, false, false, 0)
|
|
box.PackStart(row2, false, false, 0)
|
|
return box
|
|
}
|
|
|
|
func chooseAndLoad(win *gtk.Window, p *pane) {
|
|
dlg, _ := gtk.FileChooserDialogNewWith1Button("Open Video", win, gtk.FILE_CHOOSER_ACTION_OPEN, "Open", gtk.RESPONSE_ACCEPT)
|
|
if resp := dlg.Run(); resp == gtk.RESPONSE_ACCEPT {
|
|
filename := dlg.GetFilename()
|
|
if filename != "" {
|
|
loadIntoPane(p, filename)
|
|
}
|
|
}
|
|
dlg.Destroy()
|
|
}
|
|
|
|
func loadIntoPane(p *pane, filename string) {
|
|
if p.mpv == nil {
|
|
return
|
|
}
|
|
p.path = filename
|
|
if err := p.mpv.Command("loadfile", filename, "replace"); err != nil {
|
|
log.Printf("loadfile %s: %v", filename, err)
|
|
}
|
|
_ = p.mpv.SetPropertyBool("pause", false)
|
|
}
|
|
|
|
func metaSummary(a, b *pane) string {
|
|
parts := func(p *pane) string {
|
|
if p == nil || p.mpv == nil || p.path == "" {
|
|
return "[empty]"
|
|
}
|
|
dur, _ := p.mpv.GetPropertyDouble("duration")
|
|
pos, _ := p.mpv.GetPropertyDouble("time-pos")
|
|
w, _ := p.mpv.GetPropertyInt64("width")
|
|
h, _ := p.mpv.GetPropertyInt64("height")
|
|
return fmt.Sprintf("%s | %dx%d | %.1f/%.1fs", filepath.Base(p.path), w, h, pos, dur)
|
|
}
|
|
return fmt.Sprintf("L: %s | R: %s", parts(a), parts(b))
|
|
}
|
|
|
|
// getWindowID returns the native window handle (XID on X11, HWND on Windows).
|
|
func getWindowID(w *gdk.Window) uint64 {
|
|
if w == nil {
|
|
return 0
|
|
}
|
|
// gdk_x11_window_get_xid only works on X11; return 0 on other backends.
|
|
return uint64(gdkWindowGetXID(w))
|
|
}
|
|
|
|
// gdkWindowGetXID extracts the XID from a GDK window when running on X11.
|
|
func gdkWindowGetXID(w *gdk.Window) uint {
|
|
return uint(w.GetXID())
|
|
}
|
|
|
|
func applyCSS() {
|
|
provider, err := gtk.CssProviderNew()
|
|
if err != nil {
|
|
return
|
|
}
|
|
if err := provider.LoadFromData(appCSS); err != nil {
|
|
return
|
|
}
|
|
screen, err := gdk.ScreenGetDefault()
|
|
if err != nil {
|
|
return
|
|
}
|
|
gtk.AddProviderForScreen(screen, provider, gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
|
|
}
|
|
|
|
func preferDark() {
|
|
if settings, err := gtk.SettingsGetDefault(); err == nil && settings != nil {
|
|
_ = settings.SetProperty("gtk-application-prefer-dark-theme", true)
|
|
}
|
|
}
|
|
|
|
func setupDragDest(p *pane, win *gtk.Window) {
|
|
// Accept URI drops
|
|
target := gtk.TargetEntryNew("text/uri-list", gtk.TARGET_OTHER_APP, 0)
|
|
p.area.DragDestSet(gtk.DEST_DEFAULT_ALL, []gtk.TargetEntry{*target}, gdk.ACTION_COPY)
|
|
p.area.Connect("drag-data-received", func(_ *gtk.DrawingArea, ctx *gdk.DragContext, x, y int, data *gtk.SelectionData, info uint, t uint32) {
|
|
uris := data.GetURIs()
|
|
if len(uris) == 0 {
|
|
return
|
|
}
|
|
// Take first URI
|
|
if path := uriToPath(uris[0]); path != "" {
|
|
loadIntoPane(p, path)
|
|
}
|
|
})
|
|
}
|
|
|
|
func uriToPath(u string) string {
|
|
if u == "" {
|
|
return ""
|
|
}
|
|
// text/uri-list format: file:///path or path\n
|
|
if len(u) >= 7 && u[:7] == "file://" {
|
|
u = u[7:]
|
|
}
|
|
return u
|
|
}
|