package main import ( "fmt" "log" "path/filepath" "time" "strings" "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() { defer func() { _ = recover() }() 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 using a target list target, err := gtk.TargetEntryNew("text/uri-list", gtk.TARGET_OTHER_APP, 0) if err != nil { return } // DragDestSet requires at least one target; use the URI target. 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) { defer func() { if r := recover(); r != nil { log.Printf("drag handler panic: %v", r) } }() if data == nil { return } raw := data.GetData() if len(raw) == 0 { // try text fallback if txt := data.GetText(); txt != "" { raw = []byte(txt) } } if len(raw) == 0 { return } // text/uri-list: newline or CRLF separated lines := strings.Split(string(raw), "\n") for _, ln := range lines { ln = strings.TrimSpace(ln) if ln == "" { continue } if path := uriToPath(ln); path != "" { loadIntoPane(p, path) break } } }) } 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 }