The officially official Devuan Forum!

You are not logged in.

#51 2026-01-24 23:43:57

greenjeans
Member
Registered: 2017-04-07
Posts: 1,486  
Website

Re: New Project, a simple music player. And now a video player!!

Got the scraper whupped finally and the gui script modded, looking good! 4 different viewing modes now too. All in under 45 kb. wink

98qrxi.jpg


https://sourceforge.net/projects/vuu-do/ New Vuu-do isos uploaded December 2025!
Vuu-do GNU/Linux, minimal Devuan-based Openbox and Mate systems to build on. Also a max version for OB.
Devuan 5 mate-mini iso, pure Devuan, 100% no-vuu-do. wink Devuan 6 version also available for testing.
Please donate to support Devuan and init freedom! https://devuan.org/os/donate

Online

#52 2026-01-29 03:22:46

greenjeans
Member
Registered: 2017-04-07
Posts: 1,486  
Website

Re: New Project, a simple music player. And now a video player!!

Holy cow, spent all day today and part of yesterday just trying to squash one bug, whatever real devs get paid (if they get paid) is not near enough, this stuff is HARD.

But I did it, squashed the only couple of bugs left, this thing is stable and works great! Just need to work on a couple more features and it will be done.


https://sourceforge.net/projects/vuu-do/ New Vuu-do isos uploaded December 2025!
Vuu-do GNU/Linux, minimal Devuan-based Openbox and Mate systems to build on. Also a max version for OB.
Devuan 5 mate-mini iso, pure Devuan, 100% no-vuu-do. wink Devuan 6 version also available for testing.
Please donate to support Devuan and init freedom! https://devuan.org/os/donate

Online

#53 2026-02-01 18:08:26

greenjeans
Member
Registered: 2017-04-07
Posts: 1,486  
Website

Re: New Project, a simple music player. And now a video player!!

Added some seek functions and created a wrapper script for startup that checks for running instance of vsvp as two at the same time don't play nice, and also moved the DB check in there from the main script. Perfectly useable now, but i'm working on a couple of possible additions. But i'm happy as it is. This was a lot of work, the inherent limitations of ffplay for a backend as compared to something like mpv, made it a real challenge, I had to cobble together features in some odd ways, especially since I was going for minimalism of code. Still under 45k for the whole thing.

idyyg9.jpg


https://sourceforge.net/projects/vuu-do/ New Vuu-do isos uploaded December 2025!
Vuu-do GNU/Linux, minimal Devuan-based Openbox and Mate systems to build on. Also a max version for OB.
Devuan 5 mate-mini iso, pure Devuan, 100% no-vuu-do. wink Devuan 6 version also available for testing.
Please donate to support Devuan and init freedom! https://devuan.org/os/donate

Online

#54 2026-02-02 22:13:30

zapper
Member
Registered: 2017-05-29
Posts: 1,227  

Re: New Project, a simple music player. And now a video player!!

Would it be possible for you to make A PKGBUILD of that?

Also, does this application support sndio and alsa?

Just asking since the size interests me.

or even in source if possible too.

I do use Hyperbola though.

Last edited by zapper (2026-02-02 22:14:09)


Freedom is never more than one generation away from extinction. Feelings are not facts
If you wish to be humbled, try to exalt yourself long term  If you wish to be exalted, try to humble yourself long term
Favourite operating systems: Hyperbola Devuan OpenBSD Gnuinos
Peace Be With us All!

Offline

#55 2026-02-02 23:51:42

greenjeans
Member
Registered: 2017-04-07
Posts: 1,486  
Website

Re: New Project, a simple music player. And now a video player!!

Hi zappy! Are you referring to the video player? If so i'm about to package tomorrow probably, just a plain .deb using dpkg-deb --build like my other ones. This app was 100% built on an Alsa-only system, it plays nice with AlsaTune too. That being said I don't know how it would play if at all with sndio. It also is built on Daedalus libraries like the music player, and won't work on Excalibur until I build it against the newer libav. It is possible to install the 59 packages alongside the 61 packages and have it work, but that's a lot of extra depends. You could try building it yourself, it's just the main script that needs doing, the wrapper and the scraper are just bash/yad and i'm happy to share the source code, build depends and compile command that i'm currently using.

Also just in general, probably not going to work right on Wayland, the script includes an X11-specific hack and headers.

If you're talking about the music player the .deb is already up on my sourceforge.


https://sourceforge.net/projects/vuu-do/ New Vuu-do isos uploaded December 2025!
Vuu-do GNU/Linux, minimal Devuan-based Openbox and Mate systems to build on. Also a max version for OB.
Devuan 5 mate-mini iso, pure Devuan, 100% no-vuu-do. wink Devuan 6 version also available for testing.
Please donate to support Devuan and init freedom! https://devuan.org/os/donate

Online

#56 2026-02-03 22:04:01

greenjeans
Member
Registered: 2017-04-07
Posts: 1,486  
Website

Re: New Project, a simple music player. And now a video player!!

Uploaded the first test version, looks good, test installs all went well and no issues playing.

https://sourceforge.net/projects/vuu-do … apps/VSVP/


https://sourceforge.net/projects/vuu-do/ New Vuu-do isos uploaded December 2025!
Vuu-do GNU/Linux, minimal Devuan-based Openbox and Mate systems to build on. Also a max version for OB.
Devuan 5 mate-mini iso, pure Devuan, 100% no-vuu-do. wink Devuan 6 version also available for testing.
Please donate to support Devuan and init freedom! https://devuan.org/os/donate

Online

#57 Today 03:54:43

zapper
Member
Registered: 2017-05-29
Posts: 1,227  

Re: New Project, a simple music player. And now a video player!!

@greenjeans  I was actually speaking of  the source code at minimum or a PKGBUILD that would work in hyperbola gnu/linux-libre

If you could do one or the other that would help.

When I say source code I do mean like the tar.gz with instructions how to build.


Freedom is never more than one generation away from extinction. Feelings are not facts
If you wish to be humbled, try to exalt yourself long term  If you wish to be exalted, try to humble yourself long term
Favourite operating systems: Hyperbola Devuan OpenBSD Gnuinos
Peace Be With us All!

Offline

#58 Today 18:09:04

greenjeans
Member
Registered: 2017-04-07
Posts: 1,486  
Website

Re: New Project, a simple music player. And now a video player!!

I have no idea what would work in Hyperbola as i've never used it, and keep in mind this is built on Devuan 5 (daedalus) libraries, I have not tried compiling it or running on the newer excalibur based libraries. The app is already compressed with everything in the .deb but the source code for the single small binary, the other two scripts are tiny shellscripts, and a .desktop. If you want to try compiling the binary yourself:

1. You'll need GCC and pkg-config and their deps, common build stuff.
2. You'll need to install libgtk-3-dev, in Devuan that pulls in the other dev packages you'll need (libx11-dev, libxext-dev etc.), in hyperbola I don't know if it does.
3. Compile command:
gcc -Wall -o vsvp vsvp.c `pkg-config --cflags --libs gtk+-3.0 x11 xext`

Source for the binary, the source script is 30.9 kb, compiled 43.5 kb, vsvp.c:

#include <gtk/gtk.h>
#include <gtk/gtkx.h>
#include <gdk/gdkx.h>              // For GDK X11 functions
#include <X11/Xlib.h>              // For XSelectInput
#include <X11/extensions/shape.h>  // Secret shape sauce
#include <X11/keysym.h>            // For keysyms like XK_Up
#include <stdlib.h>
#include <signal.h>
#include <stdio.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>
#include <glib.h>

#define SCRAPER_CMD "/usr/local/bin/vsvps" // For re-scan button

typedef struct {
    pid_t ffplay_pid;
    GtkWidget *play_button, *pause_button, *stop_button, *rebuild_button;
    GtkWidget *socket;            // For embedding ffplay window
    GtkWidget *placeholder_label; // Placeholder when no video is playing
    GtkWidget *socket_container;  // Overlay container
    GtkWidget *paused_overlay;    // Semi-transparent paused cover
    GtkWidget *stopped_overlay;   // Opaque stopped/select screen
    GtkWidget *placeholder_pane;  // Initial right pane
    GtkWidget *paned;             // Paned for swapping
    GtkTreeStore *video_store;
    GtkTreeIter current_video_iter;
    gboolean is_playing;
    gboolean is_paused;
    GtkWidget *video_tree;
    GtkWidget *control_box;
    gboolean socket_active;
    GtkWidget *external_play_button; // Launch external ffplay window
    unsigned long ffplay_wid;        // For sending key events
} AppData;

typedef struct {
    GList *path_components;
    gchar *title;
    gchar *fullpath;
    gboolean is_folder; // set later
} Entry;

// Forward declarations
void on_stop_clicked(GtkButton *button, AppData *app_data);
void populate_treeview(AppData *app_data);

// Send simulated key to ffplay
static void send_key_to_ffplay(AppData *app_data, KeySym keysym) {
    if (app_data->ffplay_wid == 0) return;

    Display *display = gdk_x11_get_default_xdisplay();
    if (!display) return;

    // Optional: Temp focus if needed (uncomment the below if seeks don't work),
	// so far it's working without it, but keeping this here pending further testing.

    // Window original_focus = XGetInputFocus(display, &original_focus, &revert_to);
    // XSetInputFocus(display, app_data->ffplay_wid, RevertToParent, CurrentTime);
    // XFlush(display);

    XEvent event = {0};
    event.xkey.display = display;
    event.xkey.window = app_data->ffplay_wid;
    event.xkey.root = RootWindow(display, DefaultScreen(display));
    event.xkey.subwindow = None;
    event.xkey.time = CurrentTime;
    event.xkey.x = 1; event.xkey.y = 1; // Dummy pos
    event.xkey.x_root = 1; event.xkey.y_root = 1;
    event.xkey.state = 0;
    event.xkey.keycode = XKeysymToKeycode(display, keysym);
    event.xkey.same_screen = True;

    // Press
    event.type = KeyPress;
    XSendEvent(display, app_data->ffplay_wid, True, KeyPressMask, &event);
    XFlush(display);

    // Release
    event.type = KeyRelease;
    XSendEvent(display, app_data->ffplay_wid, True, KeyReleaseMask, &event);
    XFlush(display);

    // Optional: Revert focus, uncomment this too if uncommenting the other focus lines above.

    // XSetInputFocus(display, original_focus, RevertToParent, CurrentTime);
    // XFlush(display);
}

// Seek button clicked
static void on_seek_clicked(GtkButton *button, AppData *app_data) {
    const char *label = gtk_button_get_label(button);
    KeySym keysym = 0;

    if (g_strcmp0(label, "Previous chapter") == 0) keysym = XK_Page_Down;
    else if (g_strcmp0(label, "<<1m") == 0) keysym = XK_Down;
    else if (g_strcmp0(label, "1m>>") == 0) keysym = XK_Up;
    else if (g_strcmp0(label, "Next chapter") == 0) keysym = XK_Page_Up;

    if (keysym != 0) {
        send_key_to_ffplay(app_data, keysym);
    }
}

// Helper to free Entry
static void free_entry(Entry *e) {
    g_list_free_full(e->path_components, g_free);
    g_free(e->title);
    g_free(e->fullpath);
    g_free(e);
}

// Sort function (folders before files, alpha within level)
static gint compare_entries(gconstpointer a_ptr, gconstpointer b_ptr) {
    const Entry *a = a_ptr;
    const Entry *b = b_ptr;
    GString *key_a = g_string_new("");
    GList *p;
    for (p = a->path_components; p; p = p->next) {
        if (key_a->len) g_string_append_c(key_a, '/');
        g_string_append(key_a, p->data);
    }
    if (key_a->len) g_string_append_c(key_a, '/');
    g_string_append(key_a, a->title);
    GString *key_b = g_string_new("");
    for (p = b->path_components; p; p = p->next) {
        if (key_b->len) g_string_append_c(key_b, '/');
        g_string_append(key_b, p->data);
    }
    if (key_b->len) g_string_append_c(key_b, '/');
    g_string_append(key_b, b->title);
    gint result = g_strcmp0(key_a->str, key_b->str);
    g_string_free(key_a, TRUE);
    g_string_free(key_b, TRUE);
    return result;
}

void recreate_socket(AppData *app_data) {
    if (app_data->socket && GTK_IS_WIDGET(app_data->socket)) {
        gtk_widget_destroy(app_data->socket);
    }
    app_data->socket = gtk_socket_new();
    gtk_widget_set_size_request(app_data->socket, 650, 400);
    gtk_container_add(GTK_CONTAINER(app_data->socket_container), app_data->socket);
    if (GTK_IS_WIDGET(app_data->socket)) gtk_widget_hide(app_data->socket);
}

void play_video(AppData *app_data, const char *file_path, GtkTreeIter *iter) {
    // First, ensure overlays are hidden BEFORE anything else
    if (GTK_IS_WIDGET(app_data->paused_overlay)) gtk_widget_hide(app_data->paused_overlay);
    if (GTK_IS_WIDGET(app_data->stopped_overlay)) gtk_widget_hide(app_data->stopped_overlay);
    if (!app_data->socket_active) {
        gtk_container_remove(GTK_CONTAINER(app_data->paned), app_data->placeholder_pane);
        gtk_paned_pack2(GTK_PANED(app_data->paned), app_data->socket_container, TRUE, FALSE);
        gtk_widget_show_all(app_data->socket_container);
        // Force overlays hidden AGAIN after container is shown
        if (GTK_IS_WIDGET(app_data->paused_overlay)) gtk_widget_hide(app_data->paused_overlay);
        if (GTK_IS_WIDGET(app_data->stopped_overlay)) gtk_widget_hide(app_data->stopped_overlay);
        app_data->socket_active = TRUE;
        gtk_widget_queue_draw(app_data->paned);
        gtk_widget_queue_resize(app_data->paned);
    }
    if (app_data->ffplay_pid > 0) {
        if (app_data->is_paused) {
            kill(app_data->ffplay_pid, SIGCONT);
            app_data->is_paused = FALSE;
        }
        kill(app_data->ffplay_pid, SIGTERM);
        waitpid(app_data->ffplay_pid, NULL, 0);
        app_data->ffplay_pid = 0;
        app_data->is_playing = FALSE;
        if (GTK_IS_WIDGET(app_data->socket)) gtk_widget_hide(app_data->socket);
        recreate_socket(app_data);
    }
    pid_t pid = fork();
    if (pid == 0) {
        execlp("ffplay", "ffplay", "-autoexit", "-x", "650", "-y", "400", "-noborder",
               "-loglevel", "quiet", "-hide_banner",  // Prevent log spam, ffplay is extremely verbose
               "-bufsize", "128k", "-af", "atempo=1.0",
               file_path, NULL);
        exit(1);
    } else if (pid > 0) {
        app_data->ffplay_pid = pid;
        app_data->is_playing = TRUE;
        app_data->is_paused = FALSE;
        if (iter) {
            app_data->current_video_iter = *iter;
        }
        unsigned long wid = 0;
        int attempts = 80;
        for (int i = 0; i < attempts; i++) {
            usleep(100000);
            char command[512];
            FILE *fp;
            snprintf(command, sizeof(command),
                     "xwininfo -name 'ffplay' -int 2>/dev/null | grep 'Window id' | awk '{print $4}'");
            fp = popen(command, "r");
            if (fp) {
                char buf[32];
                if (fgets(buf, sizeof(buf), fp)) wid = strtoul(buf, NULL, 10);
                pclose(fp);
            }
            if (wid == 0) {
                snprintf(command, sizeof(command),
                         "xwininfo -root -tree 2>/dev/null | grep -i ffplay | head -1 | awk '{print $1}'");
                fp = popen(command, "r");
                if (fp) {
                    char buf[32];
                    if (fgets(buf, sizeof(buf), fp)) wid = strtoul(buf, NULL, 16);
                    pclose(fp);
                }
            }
            if (wid > 0) {
                usleep(50000);
                gtk_socket_add_id(GTK_SOCKET(app_data->socket), wid);
                if (GTK_IS_WIDGET(app_data->socket)) gtk_widget_show(app_data->socket);
                // Secret sauce X11 hack, set empty input shape to block mouse clicks in embedded window
                Display *display = gdk_x11_get_default_xdisplay();
                if (display) {
                    Pixmap empty_shape = XCreatePixmap(display, wid, 1, 1, 1);
                    XShapeCombineMask(display, wid, ShapeInput, 0, 0, empty_shape, ShapeSet);
                    XFreePixmap(display, empty_shape);
                    XSync(display, False);
                } else {
                    g_print("Failed to get X display for shape hack\n");
                }
                app_data->ffplay_wid = wid;  // Store for key sends
                break;
            }
        }
        if (wid == 0) g_print("Error: Could not find ffplay window ID after %d attempts\n", attempts);
    }
}

void play_video_external(AppData *app_data, const char *file_path) {
    pid_t pid = fork();
    if (pid == 0) {
        // Launch ffplay standalone, no size constraints, autoexit when done, keyboard hints in title, no log spam
        execlp("ffplay", "ffplay", "-autoexit", "-loglevel", "quiet", "-hide_banner",
	           "-window_title", "Keyboard controls:  f=fullscreen,  p=pause/play,  q=quit",
               file_path, NULL);
        exit(1);
    } else if (pid > 0) {
    } else {
        g_print("Failed to fork for external ffplay\n");
    }
}

gboolean check_video_end(gpointer user_data) {
    AppData *app_data = (AppData *)user_data;
    if (app_data->ffplay_pid <= 0 || !app_data->is_playing || app_data->is_paused) {
        return TRUE;
    }
    int status;
    pid_t result = waitpid(app_data->ffplay_pid, &status, WNOHANG);
    if (result == app_data->ffplay_pid) {
        app_data->ffplay_pid = 0;
        app_data->is_playing = FALSE;
        app_data->is_paused = FALSE;
        if (GTK_IS_WIDGET(app_data->socket)) gtk_widget_hide(app_data->socket);
        if (GTK_IS_WIDGET(app_data->paused_overlay)) gtk_widget_hide(app_data->paused_overlay);
        if (GTK_IS_WIDGET(app_data->stopped_overlay)) gtk_widget_show(app_data->stopped_overlay);
        recreate_socket(app_data);
    }
    return TRUE;
}

void on_play_clicked(GtkButton *button, AppData *app_data) {
    GtkTreeView *video_tree = g_object_get_data(G_OBJECT(button), "video_tree");
    GtkTreeSelection *selection = gtk_tree_view_get_selection(video_tree);
    GtkTreeIter iter;
    GtkTreeModel *model;
    if (!gtk_tree_selection_get_selected(selection, &model, &iter)) {
        return; // nothing selected
    }
    gchar *file_path = NULL;
    gtk_tree_model_get(model, &iter, 1, &file_path, -1);
    if (!file_path || file_path[0] == '\0') {
        g_free(file_path);
        return; // clicked a folder - do nothing
    }
    // Visually indicate selection
    gtk_tree_selection_select_iter(selection, &iter);
    play_video(app_data, file_path, &iter);
    g_free(file_path);
}

void on_external_play_clicked(GtkButton *button, AppData *app_data) {
    GtkTreeView *video_tree = g_object_get_data(G_OBJECT(button), "video_tree");
    GtkTreeSelection *selection = gtk_tree_view_get_selection(video_tree);
    GtkTreeIter iter;
    GtkTreeModel *model;
    if (!gtk_tree_selection_get_selected(selection, &model, &iter)) {
        return; // nothing selected
    }
    gchar *file_path = NULL;
    gtk_tree_model_get(model, &iter, 1, &file_path, -1);
    // Guard: ignore if it's a folder (no path)
    if (!file_path || file_path[0] == '\0') {
        g_free(file_path);
        return; // clicked a folder - do nothing, no need to stop anything
    }
    // If something is currently playing in the embedded player - stop it first
    if (app_data->ffplay_pid > 0) {
        on_stop_clicked(NULL, app_data); // reuse existing stop logic
    }
    // Now it's safe to launch external
    play_video_external(app_data, file_path);
    g_free(file_path);
}

void on_pause_clicked(GtkButton *button, AppData *app_data) {
    if (app_data->ffplay_pid > 0) {
        if (app_data->is_playing && !app_data->is_paused) {
            kill(app_data->ffplay_pid, SIGSTOP);
            app_data->is_playing = FALSE;
            app_data->is_paused = TRUE;
            if (GTK_IS_WIDGET(app_data->paused_overlay)) {
                gtk_widget_show(app_data->paused_overlay);
            }
            if (GTK_IS_WIDGET(app_data->stopped_overlay)) {
                gtk_widget_hide(app_data->stopped_overlay);
            }
        } else if (app_data->is_paused) {
            kill(app_data->ffplay_pid, SIGCONT);
            app_data->is_playing = TRUE;
            app_data->is_paused = FALSE;
            if (GTK_IS_WIDGET(app_data->paused_overlay)) gtk_widget_hide(app_data->paused_overlay);
        }
    }
}

void on_stop_clicked(GtkButton *button, AppData *app_data) {
    if (app_data->ffplay_pid > 0) {
        if (app_data->is_paused) {
            kill(app_data->ffplay_pid, SIGCONT);
            app_data->is_paused = FALSE;
        }
        kill(app_data->ffplay_pid, SIGTERM);
        waitpid(app_data->ffplay_pid, NULL, 0);
        app_data->ffplay_pid = 0;
        app_data->is_playing = FALSE;
        if (GTK_IS_WIDGET(app_data->socket)) gtk_widget_hide(app_data->socket);
        if (GTK_IS_WIDGET(app_data->paused_overlay)) gtk_widget_hide(app_data->paused_overlay);
        if (GTK_IS_WIDGET(app_data->stopped_overlay)) gtk_widget_show(app_data->stopped_overlay);
        recreate_socket(app_data);
    }
}

void on_rebuild_clicked(GtkButton *button, AppData *app_data) {
    if (system(SCRAPER_CMD) != 0) {
        g_print("Warning: Failed to run scraper %s\n", SCRAPER_CMD);
    }
    gtk_tree_store_clear(app_data->video_store);
    populate_treeview(app_data);
    app_data->is_playing = FALSE;
    app_data->is_paused = FALSE;
    if (GTK_IS_WIDGET(app_data->socket)) gtk_widget_hide(app_data->socket);
    if (GTK_IS_WIDGET(app_data->paused_overlay)) gtk_widget_hide(app_data->paused_overlay);
    if (GTK_IS_WIDGET(app_data->stopped_overlay)) gtk_widget_show(app_data->stopped_overlay);
    recreate_socket(app_data);
}

void populate_treeview(AppData *app_data) {
    const char *home = getenv("HOME");
    if (!home) {
        g_print("Error: Could not determine home directory\n");
        return;
    }
    gchar *db_path = g_strdup_printf("%s/.local/share/vsvp/vsvp_db", home);

    FILE *db = fopen(db_path, "r");
    if (!db) {
        g_print("Error: Could not open database %s\n", db_path);
        g_free(db_path);
        return;
    }

    gtk_tree_store_clear(app_data->video_store);
    char line[1024];
    if (!fgets(line, sizeof(line), db)) { // skip header
        fclose(db);
        g_free(db_path);
        return;
    }
    GList *entries = NULL;
    while (fgets(line, sizeof(line), db)) {
        char path_str[512] = {0};
        char title[256] = {0};
        char fullpath[768] = {0};
        if (sscanf(line, "%511[^|]|%255[^|]|%767[^\n]", path_str, title, fullpath) != 3)
            continue;
        GList *components = NULL;
        if (strlen(path_str) > 0) {
            char *saveptr;
            char *token = strtok_r(path_str, "/", &saveptr);
            while (token) {
                components = g_list_append(components, g_strdup(token));
                token = strtok_r(NULL, "/", &saveptr);
            }
        }
        Entry *e = g_new0(Entry, 1);
        e->path_components = components;
        e->title = g_strdup(title);
        e->fullpath = g_strdup(fullpath);
        entries = g_list_prepend(entries, e);
    }
    fclose(db);
    // Sort once - folders before files + alpha within same level
    entries = g_list_sort(entries, compare_entries);
    // Build the actual tree
    GHashTable *folder_iters = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
    GList *l;
    for (l = entries; l != NULL; l = l->next) {
        Entry *e = l->data;
        GtkTreeIter *parent = NULL;
        GtkTreeIter iter;
        GString *current_path = g_string_new("");
        GList *comp;
        for (comp = e->path_components; comp != NULL; comp = comp->next) {
            gchar *folder_name = comp->data;
            if (current_path->len > 0)
                g_string_append_c(current_path, '/');
            g_string_append(current_path, folder_name);
            GtkTreeIter *existing = g_hash_table_lookup(folder_iters, current_path->str);
            if (existing) {
                parent = existing;
                continue;
            }
            // New folder node
            if (parent) {
                gtk_tree_store_append(app_data->video_store, &iter, parent);
            } else {
                gtk_tree_store_append(app_data->video_store, &iter, NULL);
            }
            gtk_tree_store_set(app_data->video_store, &iter,
                               0, folder_name,
                               1, NULL,
                               -1);
            // Store a copy of the iter
            GtkTreeIter *stored = g_new(GtkTreeIter, 1);
            *stored = iter;
            g_hash_table_insert(folder_iters, g_strdup(current_path->str), stored);
            parent = stored;
        }
        // Finally the file leaf
        gtk_tree_store_append(app_data->video_store, &iter, parent);
        gtk_tree_store_set(app_data->video_store, &iter,
                           0, e->title,
                           1, e->fullpath,
                           -1);
    }
    // Cleanup
    g_list_free_full(entries, (GDestroyNotify)free_entry);
    g_hash_table_destroy(folder_iters);
    g_free(db_path);
}

// Hide treeview and borders when maximized for quasi-theater-mode
gboolean on_window_state_changed(GtkWidget *widget, GdkEventWindowState *event, gpointer user_data) {
    AppData *app_data = (AppData *)user_data;
    if (event->changed_mask & GDK_WINDOW_STATE_MAXIMIZED) {
        gboolean is_maximized = (event->new_window_state & GDK_WINDOW_STATE_MAXIMIZED);
        // Playlist hide/show
        if (is_maximized) {
            gtk_widget_hide(app_data->video_tree);
        } else {
            gtk_widget_show(app_data->video_tree);
        }
        // Window border padding
        gtk_container_set_border_width(GTK_CONTAINER(widget), is_maximized ? 0 : 10);
        // Get vbox from window
        GtkWidget *vbox = gtk_bin_get_child(GTK_BIN(widget));
        if (GTK_IS_BOX(vbox)) {
            // Zero spacing (covers the tiny 1px if any)
            gtk_box_set_spacing(GTK_BOX(vbox), is_maximized ? 0 : 1);
            // KEY: Dynamically change paned's packing padding
            gtk_box_set_child_packing(GTK_BOX(vbox),
                                      app_data->paned,
                                      TRUE, // expand (keep as is)
                                      TRUE, // fill (keep as is)
                                      is_maximized ? 0 : 10, // padding
                                      GTK_PACK_START); // pack_type (keep as is)
        } else {
            g_warning("Main vbox not found or not a GtkBox!");
        }
    }
    return FALSE;
}

void on_window_destroy(GtkWidget *widget, AppData *app_data) {
    if (app_data->ffplay_pid > 0) {
        if (app_data->is_paused) {
            kill(app_data->ffplay_pid, SIGCONT);
            app_data->is_paused = FALSE;
        }
        kill(app_data->ffplay_pid, SIGTERM);
        waitpid(app_data->ffplay_pid, NULL, 0);
        app_data->ffplay_pid = 0;
        app_data->is_playing = FALSE;
    }
    gtk_main_quit();
}

int main(int argc, char *argv[]) {
    gtk_init(&argc, &argv);
    if (!getenv("HOME")) {
        g_print("Error: Could not determine home directory\n");
        return 1;
    }
    AppData app_data = {0};
    app_data.video_store = gtk_tree_store_new(2, G_TYPE_STRING, G_TYPE_STRING); // 0 = Title, 1 = Fullpath (NULL for folders)
    app_data.is_playing = FALSE;
    app_data.is_paused = FALSE;
    app_data.socket_active = FALSE;

    g_timeout_add(500, check_video_end, &app_data);
    
    GtkWidget *window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
    gtk_window_set_title(GTK_WINDOW(window), "Vuu-do Simple Video Player");
    gtk_window_set_default_size(GTK_WINDOW(window), 800, 600);
    gtk_window_set_hide_titlebar_when_maximized(GTK_WINDOW(window), FALSE);
    gtk_window_set_icon_name(GTK_WINDOW(window), "applications-multimedia");
    g_signal_connect(window, "destroy", G_CALLBACK(on_window_destroy), &app_data);
    g_signal_connect(window, "window-state-event", G_CALLBACK(on_window_state_changed), &app_data);
    gtk_container_set_border_width(GTK_CONTAINER(window), 10);
    
    GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 1);
    gtk_container_add(GTK_CONTAINER(window), vbox);
    
    app_data.paned = gtk_paned_new(GTK_ORIENTATION_HORIZONTAL);
    gtk_box_pack_start(GTK_BOX(vbox), app_data.paned, TRUE, TRUE, 10);
    
    app_data.video_tree = gtk_tree_view_new_with_model(GTK_TREE_MODEL(app_data.video_store));
    gtk_tree_view_set_model(GTK_TREE_VIEW(app_data.video_tree), GTK_TREE_MODEL(app_data.video_store));
    GtkCellRenderer *renderer = gtk_cell_renderer_text_new();
    gtk_tree_view_insert_column_with_attributes(GTK_TREE_VIEW(app_data.video_tree), -1, "Videos", renderer, "text", 0, NULL);
    gtk_paned_pack1(GTK_PANED(app_data.paned), app_data.video_tree, FALSE, FALSE);
    gtk_widget_set_size_request(app_data.video_tree, 200, -1);
    
    // Placeholder pane (initial state)
    app_data.placeholder_pane = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
    gtk_widget_set_name(app_data.placeholder_pane, "placeholder-pane");
    app_data.placeholder_label = gtk_label_new("Select a video");
    gtk_widget_set_margin_start(app_data.placeholder_label, 10);
    gtk_widget_set_margin_end(app_data.placeholder_label, 10);
    gtk_widget_set_margin_top(app_data.placeholder_label, 10);
    gtk_widget_set_margin_bottom(app_data.placeholder_label, 10);
    gtk_box_pack_start(GTK_BOX(app_data.placeholder_pane), app_data.placeholder_label, TRUE, TRUE, 0);    
    GtkCssProvider *provider = gtk_css_provider_new();
    gtk_css_provider_load_from_data(provider,
        "#placeholder-pane {"
        " background-color: @theme_base_color;"
        " background-image: none;"
        "}"
        "#placeholder-pane label {"
        " color: @theme_text_color;"
        "}",
        -1, NULL);
    GtkStyleContext *pane_ctx = gtk_widget_get_style_context(app_data.placeholder_pane);
    gtk_style_context_add_provider(pane_ctx, GTK_STYLE_PROVIDER(provider),
                                   GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
    GtkStyleContext *label_ctx = gtk_widget_get_style_context(app_data.placeholder_label);
    gtk_style_context_add_provider(label_ctx, GTK_STYLE_PROVIDER(provider),
                                   GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
    gtk_paned_pack2(GTK_PANED(app_data.paned), app_data.placeholder_pane, TRUE, FALSE);
    gtk_widget_show_all(app_data.placeholder_pane);
    
    // Socket container as overlay host
    app_data.socket_container = gtk_overlay_new();
    gtk_widget_set_name(app_data.socket_container, "socket-container");
    
    // Main child: socket
    app_data.socket = gtk_socket_new();
    gtk_widget_set_size_request(app_data.socket, 650, 400);
    gtk_container_add(GTK_CONTAINER(app_data.socket_container), app_data.socket);
    gtk_widget_hide(app_data.socket);
    
    // Paused overlay (semi-transparent "Paused")
    app_data.paused_overlay = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
    gtk_widget_set_halign(app_data.paused_overlay, GTK_ALIGN_FILL);
    gtk_widget_set_valign(app_data.paused_overlay, GTK_ALIGN_FILL);
    GtkWidget *paused_label = gtk_label_new("Paused");
    gtk_box_pack_start(GTK_BOX(app_data.paused_overlay), paused_label, TRUE, TRUE, 0);
    gtk_widget_set_name(app_data.paused_overlay, "paused-overlay");
    gtk_overlay_add_overlay(GTK_OVERLAY(app_data.socket_container), app_data.paused_overlay);
    gtk_widget_hide(app_data.paused_overlay);
    
    // Stopped overlay (opaque, "Select a video")
    app_data.stopped_overlay = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
    gtk_widget_set_halign(app_data.stopped_overlay, GTK_ALIGN_FILL);
    gtk_widget_set_valign(app_data.stopped_overlay, GTK_ALIGN_FILL);
    GtkWidget *stopped_label = gtk_label_new("Select a video");
    gtk_box_pack_start(GTK_BOX(app_data.stopped_overlay), stopped_label, TRUE, TRUE, 0);
    gtk_widget_set_name(app_data.stopped_overlay, "stopped-overlay");
    gtk_overlay_add_overlay(GTK_OVERLAY(app_data.socket_container), app_data.stopped_overlay);
    gtk_widget_hide(app_data.stopped_overlay); // Start hidden
    
    // CSS for overlays and container
    GtkCssProvider *overlay_provider = gtk_css_provider_new();
    gtk_css_provider_load_from_data(overlay_provider,
        "#paused-overlay {"
        " background-color: alpha(@theme_base_color, 0.50);"
        " background-image: none;"
        "}"
        "#paused-overlay label {"
        " color: @theme_fg_color;"
        " font-weight: bold;"
        "}"
        "#stopped-overlay {"
        " background-color: @theme_base_color;"
        " background-image: none;"
        "}"
        "#stopped-overlay label {"
        " color: @theme_text_color;"
        " font-weight: normal;"
        "}"
        "#socket-container {"
        " background-color: @theme_base_color;"
        " background-image: none;"
        "}",
        -1, NULL);
    gtk_style_context_add_provider(gtk_widget_get_style_context(app_data.paused_overlay),
                                   GTK_STYLE_PROVIDER(overlay_provider),
                                   GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
    gtk_style_context_add_provider(gtk_widget_get_style_context(app_data.stopped_overlay),
                                   GTK_STYLE_PROVIDER(overlay_provider),
                                   GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
    gtk_style_context_add_provider(gtk_widget_get_style_context(app_data.socket_container),
                                   GTK_STYLE_PROVIDER(overlay_provider),
                                   GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
    g_object_unref(overlay_provider);
    g_object_unref(provider);

	//CSS for bottom buttons
	GtkCssProvider *button_provider = gtk_css_provider_new();
	gtk_css_provider_load_from_data(button_provider,
    "#control-bar button {"
    "  border-width: 1px;"
    "  border-style: solid;"
    "  border-color: #333333;"
    "  box-shadow: none;"
    "  border-radius: 4px;"
    "}",
    -1, NULL);

	gtk_style_context_add_provider_for_screen(gdk_screen_get_default(),
                                         GTK_STYLE_PROVIDER(button_provider),
                                         GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);    

    // Bottom controls 
    app_data.control_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
    gtk_box_pack_start(GTK_BOX(vbox), app_data.control_box, FALSE, FALSE, 0);
    gtk_widget_set_margin_top(app_data.control_box, 5);
    gtk_widget_set_margin_bottom(app_data.control_box, 5);
    gtk_widget_set_name(app_data.control_box, "control-bar");
    app_data.play_button = gtk_button_new_with_label("Play");
    app_data.pause_button = gtk_button_new_with_label("Pause");
    app_data.stop_button = gtk_button_new_with_label("Stop");
    GtkWidget *seek_back10 = gtk_button_new_with_label("Previous chapter");
    GtkWidget *seek_back1 = gtk_button_new_with_label("<<1m");
    GtkWidget *seek_fwd1 = gtk_button_new_with_label("1m>>");
    GtkWidget *seek_fwd10 = gtk_button_new_with_label("Next chapter");
    gtk_widget_set_tooltip_text(seek_back10, "Go back 1 chapter");
    gtk_widget_set_tooltip_text(seek_back1, "Go back 1 minute");
    gtk_widget_set_tooltip_text(seek_fwd1, "Go forward 1 minute");
    gtk_widget_set_tooltip_text(seek_fwd10, "Go forward 1 chapter");
    app_data.rebuild_button = gtk_button_new_with_label("Re-scan Videos");
    gtk_widget_set_tooltip_text(app_data.rebuild_button, "Re-scan the ~/Videos folder after adding new media");
    app_data.external_play_button = gtk_button_new_with_label("Fullscreen window");
    gtk_widget_set_tooltip_text(app_data.external_play_button,
        "Play video in a separate fullscreen-capable window.\n"
        "Keyboard shortcuts only for that window :\n\n"
        " f  -  toggle fullscreen\n"
        " p  -  toggle pause/play\n"
        " m  -  toggle mute\n\n"
        " left/right  -  seek ±10s\n"
        " up/down  -  seek ±1min\n"
        " pg up/pg dn  -  seek ±1 chapter\n\n"
        " q or ESC  -  quit and close window");
       
    gtk_box_pack_start(GTK_BOX(app_data.control_box), app_data.play_button, FALSE, FALSE, 5);
    gtk_box_pack_start(GTK_BOX(app_data.control_box), app_data.pause_button, FALSE, FALSE, 5);
    gtk_box_pack_start(GTK_BOX(app_data.control_box), app_data.stop_button, FALSE, FALSE, 5);
    gtk_box_pack_start(GTK_BOX(app_data.control_box), seek_back10, FALSE, FALSE, 5);
    gtk_box_pack_start(GTK_BOX(app_data.control_box), seek_back1, FALSE, FALSE, 5);
    gtk_box_pack_start(GTK_BOX(app_data.control_box), seek_fwd1, FALSE, FALSE, 5);
    gtk_box_pack_start(GTK_BOX(app_data.control_box), seek_fwd10, FALSE, FALSE, 5);
    gtk_box_pack_start(GTK_BOX(app_data.control_box), app_data.rebuild_button, FALSE, FALSE, 5);
    gtk_box_pack_start(GTK_BOX(app_data.control_box), app_data.external_play_button, FALSE, FALSE, 5);
   
    g_object_set_data(G_OBJECT(app_data.play_button), "video_tree", app_data.video_tree);
    g_object_set_data(G_OBJECT(app_data.external_play_button), "video_tree", app_data.video_tree);
   
    g_signal_connect(app_data.play_button, "clicked", G_CALLBACK(on_play_clicked), &app_data);
    g_signal_connect(app_data.pause_button, "clicked", G_CALLBACK(on_pause_clicked), &app_data);
    g_signal_connect(app_data.stop_button, "clicked", G_CALLBACK(on_stop_clicked), &app_data);
    g_signal_connect(seek_back10, "clicked", G_CALLBACK(on_seek_clicked), &app_data);
    g_signal_connect(seek_back1, "clicked", G_CALLBACK(on_seek_clicked), &app_data);
    g_signal_connect(seek_fwd1, "clicked", G_CALLBACK(on_seek_clicked), &app_data);
    g_signal_connect(seek_fwd10, "clicked", G_CALLBACK(on_seek_clicked), &app_data);
    g_signal_connect(app_data.rebuild_button, "clicked", G_CALLBACK(on_rebuild_clicked), &app_data);
    g_signal_connect(app_data.external_play_button, "clicked", G_CALLBACK(on_external_play_clicked), &app_data);
   
    populate_treeview(&app_data);
    gtk_widget_show_all(window);
    gtk_main();
    return 0;
}

https://sourceforge.net/projects/vuu-do/ New Vuu-do isos uploaded December 2025!
Vuu-do GNU/Linux, minimal Devuan-based Openbox and Mate systems to build on. Also a max version for OB.
Devuan 5 mate-mini iso, pure Devuan, 100% no-vuu-do. wink Devuan 6 version also available for testing.
Please donate to support Devuan and init freedom! https://devuan.org/os/donate

Online

#59 Today 21:04:38

zapper
Member
Registered: 2017-05-29
Posts: 1,227  

Re: New Project, a simple music player. And now a video player!!

It compiled once I tried it but:

Error: Could not open database /home/user/.local/share/vsvp/vsvp_db

I got that error and whenever I load vsvp  and something peculiar  also happens

https://upload.disroot.org/r/teeZfv04#A … 7ZAznZawU=

This is very peculiar

Last edited by zapper (Today 21:05:03)


Freedom is never more than one generation away from extinction. Feelings are not facts
If you wish to be humbled, try to exalt yourself long term  If you wish to be exalted, try to humble yourself long term
Favourite operating systems: Hyperbola Devuan OpenBSD Gnuinos
Peace Be With us All!

Offline

#60 Today 22:51:04

greenjeans
Member
Registered: 2017-04-07
Posts: 1,486  
Website

Re: New Project, a simple music player. And now a video player!!

Hold on, did you download the .deb and extract it and install the other scripts? I doubt it or you'd have that file since it's the wrapper script that creates it.

Srsly bro, just install the .deb, it will put everything where it needs to be and set proper permissions.


https://sourceforge.net/projects/vuu-do/ New Vuu-do isos uploaded December 2025!
Vuu-do GNU/Linux, minimal Devuan-based Openbox and Mate systems to build on. Also a max version for OB.
Devuan 5 mate-mini iso, pure Devuan, 100% no-vuu-do. wink Devuan 6 version also available for testing.
Please donate to support Devuan and init freedom! https://devuan.org/os/donate

Online

Board footer