diff --git a/.gitignore b/.gitignore index fa8a283..c66b82a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ videotools.log .gocache/ .gomodcache/ +.cache/ VideoTools VTPlayer vt_player +cmd/gtkplayer/gtkplayer +test_*.sh diff --git a/cmd/gtkplayer/main.go b/cmd/gtkplayer/main.go index f1161df..b250590 100644 --- a/cmd/gtkplayer/main.go +++ b/cmd/gtkplayer/main.go @@ -4,8 +4,8 @@ import ( "fmt" "log" "path/filepath" - "time" "strings" + "time" "git.leaktechnologies.dev/stu/VT_Player/player/mpvembed" @@ -40,10 +40,21 @@ type pane struct { area *gtk.DrawingArea mpv *mpvembed.Client path string + id int } func (p *pane) hasVideo() bool { return p.path != "" } +type videoEntry struct { + id int + path string +} + +var ( + playlist []videoEntry + nextVideoID = 1 +) + func main() { gtk.Init(nil) @@ -70,7 +81,8 @@ func main() { applyCSS() preferDark() - setupDragDest(left, right, win) + setupDragDest(left, left, right) + setupDragDest(right, left, right) win.Connect("destroy", func() { if left.mpv != nil { @@ -92,24 +104,31 @@ func newPane() *pane { 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 - + var xid uint64 if w, err := da.GetWindow(); err == nil && w != nil { - if xid := getWindowID(w); xid != 0 { - _ = mpv.SetWID(xid) - } + xid = getWindowID(w) } - _ = mpv.SetOptionString("pause", "yes") - if err := mpv.Initialize(); err != nil { - log.Printf("mpv init: %v", err) + + if p.mpv == nil { + mpv, err := mpvembed.New() + if err != nil { + log.Printf("mpv create: %v", err) + return + } + p.mpv = mpv + _ = p.mpv.SetOptionString("pause", "yes") + if xid != 0 { + _ = p.mpv.SetWID(xid) + } + if err := p.mpv.Initialize(); err != nil { + log.Printf("mpv init: %v", err) + } + return + } + + // mpv already exists (created before realize); make sure WID is bound now + if xid != 0 { + _ = p.mpv.SetWID(xid) } }) return p @@ -218,6 +237,7 @@ func loadIntoPane(p *pane, filename string) { return } p.path = filename + p.id = getOrAddVideoID(filename) if err := p.mpv.Command("loadfile", filename, "replace"); err != nil { log.Printf("loadfile %s: %v", filename, err) } @@ -233,7 +253,11 @@ func metaSummary(a, b *pane) string { 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) + tag := "" + if p.id > 0 { + tag = fmt.Sprintf("#%d ", p.id) + } + return fmt.Sprintf("%s%s | %dx%d | %.1f/%.1fs", tag, filepath.Base(p.path), w, h, pos, dur) } return fmt.Sprintf("L: %s | R: %s", parts(a), parts(b)) } @@ -273,15 +297,15 @@ func preferDark() { } } -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) +func setupDragDest(targetPane *pane, left, right *pane) { + uriTarget, 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) { + targets := []gtk.TargetEntry{*uriTarget} + targetPane.area.DragDestSet(gtk.DEST_DEFAULT_ALL, targets, gdk.ACTION_COPY) + + targetPane.area.Connect("drag-data-received", func(_ *gtk.DrawingArea, _ *gdk.DragContext, x, y int, data *gtk.SelectionData, _ uint, _ uint32) { defer func() { if r := recover(); r != nil { log.Printf("drag handler panic: %v", r) @@ -290,24 +314,13 @@ func setupDragDest(p *pane, win *gtk.Window) { 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 == "" { + + uris := parseURIs(data) + for _, u := range uris { + if u == "" { continue } - assignPathToPane(ln, p, nil) + assignPathToPane(u, left, right) break } }) @@ -370,3 +383,56 @@ func uriToPath(u string) string { } return u } + +func getOrAddVideoID(path string) int { + if path == "" { + return 0 + } + for _, e := range playlist { + if e.path == path { + return e.id + } + } + id := nextVideoID + nextVideoID++ + playlist = append(playlist, videoEntry{id: id, path: path}) + return id +} + +// parseURIs tries to extract URIs from SelectionData while avoiding crashes on bad payloads. +func parseURIs(data *gtk.SelectionData) []string { + if data == nil { + return nil + } + + // try safe path using raw bytes first + raw := data.GetData() + if len(raw) == 0 { + if txt := data.GetText(); txt != "" { + raw = []byte(txt) + } + } + if len(raw) > 0 { + var out []string + for _, ln := range strings.Split(string(raw), "\n") { + ln = strings.TrimSpace(ln) + if ln != "" { + out = append(out, ln) + } + } + if len(out) > 0 { + return out + } + } + + // fallback to GetURIs; guard with recover because upstream may panic on nil C arrays + defer func() { + if r := recover(); r != nil { + log.Printf("GetURIs panic: %v", r) + } + }() + if uris := data.GetURIs(); len(uris) > 0 { + return uris + } + return nil +} diff --git a/player/mpvembed/render.go b/player/mpvembed/render.go index 245c1d4..bf161a9 100644 --- a/player/mpvembed/render.go +++ b/player/mpvembed/render.go @@ -32,10 +32,10 @@ func NewRenderContext(c *Client, params []RenderParam) (*RenderContext, error) { } cparams := make([]C.mpv_render_param, len(params)+1) for i, p := range params { - cparams[i]._type = C.int(p.Type) + cparams[i]._type = uint32(p.Type) cparams[i].data = p.Data } - cparams[len(params)]._type = C.MPV_RENDER_PARAM_INVALID + cparams[len(params)]._type = uint32(C.MPV_RENDER_PARAM_INVALID) var rctx *C.mpv_render_context if res := C.mpv_render_context_create(&rctx, c.handle, &cparams[0]); res < 0 { @@ -70,10 +70,10 @@ func (r *RenderContext) Render(params []RenderParam) error { } cparams := make([]C.mpv_render_param, len(params)+1) for i, p := range params { - cparams[i]._type = C.int(p.Type) + cparams[i]._type = uint32(p.Type) cparams[i].data = p.Data } - cparams[len(params)]._type = C.MPV_RENDER_PARAM_INVALID + cparams[len(params)]._type = uint32(C.MPV_RENDER_PARAM_INVALID) if res := C.mpv_render_context_render(r.ctx, &cparams[0]); res < 0 { return fmt.Errorf("mpv_render_context_render failed: %s", C.GoString(C.mpv_errstr_render(res))) diff --git a/scripts/run.sh b/scripts/run.sh index 610546c..c0da5df 100755 --- a/scripts/run.sh +++ b/scripts/run.sh @@ -16,7 +16,10 @@ if [ -d "$GTK_ENTRY" ]; then echo "🚀 Starting VT Player (GTK/mpv)..." echo "════════════════════════════════════════════════════════════════" echo "" - export GDK_BACKEND=x11 + # Prefer an explicit backend only if the user hasn’t set one; default to X11 (works under XWayland). + if [ -z "$GDK_BACKEND" ]; then + export GDK_BACKEND=x11 + fi export GOCACHE="$PROJECT_ROOT/.cache/go-build" export GOMODCACHE="$PROJECT_ROOT/.cache/go-mod" GOCACHE="$GOCACHE" GOMODCACHE="$GOMODCACHE" \