From 72f3f88369c34f08149b9a67d1d60a829cf499cb Mon Sep 17 00:00:00 2001 From: laki Date: Mon, 16 Feb 2026 12:58:43 +0000 Subject: [PATCH] Added proper plugin system (plugins will be released in a separate repository), added experimental scripting support (Lua, JavaScript) --- .gitignore | 14 + Makefile | 5 +- install.sh | 10 +- src/controls.c | 30 +- src/controls.h | 4 +- src/dialogs.c | 175 ++++++- src/dialogs.h | 10 +- src/main.c | 4 +- src/mpv_loader.c | 2 + src/mpv_loader.h | 2 + src/player.c | 146 +++++- src/player.h | 1 + src/plugin.h | 194 ++++++++ src/plugin_manager.c | 1028 ++++++++++++++++++++++++++++++++++++++++++ src/plugin_manager.h | 145 ++++++ src/ui.c | 411 ++++++++++++++++- src/ui.h | 14 + src/ui_callbacks.h | 19 + 18 files changed, 2145 insertions(+), 69 deletions(-) create mode 100644 src/plugin.h create mode 100644 src/plugin_manager.c create mode 100644 src/plugin_manager.h create mode 100644 src/ui_callbacks.h diff --git a/.gitignore b/.gitignore index 4ef1106..4a6a8c0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,16 @@ src/dialogs.o gtk2-mpv-player +mpv.conf +test_grayscale.conf +test_vibrant.conf +plugins/audio_player.zip +.gitignore +plugins/screenshot_timer.zip +src/plugin_manager.o +src/playlist.o +src/player.o +src/mpv_loader.o +src/mpris.o +src/main.o +src/controls.o +src/ui.o diff --git a/Makefile b/Makefile index 9bf20c3..766a807 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ CFLAGS = $(shell pkg-config --cflags gtk+-2.0 gio-2.0) -Wall -Wextra -g -std=c99 -LDFLAGS = $(shell pkg-config --libs gtk+-2.0 gio-2.0) -ldl -lm -Wl,--as-needed -Wl,-rpath,./lib +LDFLAGS = $(shell pkg-config --libs gtk+-2.0 gio-2.0) -ldl -lm -Wl,--as-needed -Wl,-rpath,./lib -rdynamic SRC_DIR = src SRCS = $(SRC_DIR)/main.c \ @@ -9,7 +9,8 @@ SRCS = $(SRC_DIR)/main.c \ $(SRC_DIR)/playlist.c \ $(SRC_DIR)/dialogs.c \ $(SRC_DIR)/mpv_loader.c \ - $(SRC_DIR)/mpris.c + $(SRC_DIR)/mpris.c \ + $(SRC_DIR)/plugin_manager.c OBJS = $(SRCS:.c=.o) TARGET = gtk2-mpv-player diff --git a/install.sh b/install.sh index 95ae651..346b2f9 100755 --- a/install.sh +++ b/install.sh @@ -16,9 +16,12 @@ echo "Installing to $INSTALL_DIR..." # Create directory mkdir -p "$INSTALL_DIR" -# Copy binary and metadata -cp gtk2-mpv-player "$INSTALL_DIR/" -cp assets/icon.png "$INSTALL_DIR/" +# Copy binary and metadata using 'install' to handle running processes correctly +install -m 755 gtk2-mpv-player "$INSTALL_DIR/" +install -m 644 assets/icon.png "$INSTALL_DIR/" + +# For directory copy, we use cp but remove destination first to ensure clean update +rm -rf "$INSTALL_DIR/lib" cp -r lib "$INSTALL_DIR/" # Create wrapper script @@ -39,4 +42,5 @@ echo "Updating MIME database..." update-desktop-database echo "Installation complete!" +echo "Installed version timestamp: $(date -r "$INSTALL_DIR/gtk2-mpv-player")" echo "You can now run 'gtk2-mpv-player' or find it in your application menu." diff --git a/src/controls.c b/src/controls.c index 08f0963..b3be964 100644 --- a/src/controls.c +++ b/src/controls.c @@ -5,6 +5,7 @@ struct Controls { Player *player; + Preferences *prefs; /* Main container */ GtkWidget *root; /* GtkEventBox for background/windowing */ @@ -69,10 +70,11 @@ static void format_time(double seconds, char *buffer, size_t size) } } -Controls* controls_new(Player *player) +Controls* controls_new(Player *player, Preferences *prefs) { Controls *controls = g_new0(Controls, 1); controls->player = player; + controls->prefs = prefs; controls->seeking = FALSE; controls->duration = 0.0; controls->muted = FALSE; @@ -367,10 +369,32 @@ static gboolean on_seek_change_value(GtkRange *range, GtkScrollType scroll, gdou static gboolean on_seek_button_press(GtkWidget *widget, GdkEventButton *event, gpointer data) { - (void)widget; - (void)event; Controls *controls = (Controls *)data; controls->seeking = TRUE; + + if (event->button == 1) { + if (controls->prefs && controls->prefs->seekbar_direct_jump) { + /* Left click jump-to-position */ + GtkRange *range = GTK_RANGE(widget); + GtkAdjustment *adj = gtk_range_get_adjustment(range); + + /* Calculate relative position (0.0 to 1.0) based on click x coordinate */ + double relative = event->x / (double)widget->allocation.width; + if (relative < 0) relative = 0; + if (relative > 1) relative = 1; + + double new_val = adj->lower + relative * (adj->upper - adj->lower); + gtk_range_set_value(range, new_val); + + /* Perform seek immediately */ + if (controls->duration > 0) { + double position = (new_val / 100.0) * controls->duration; + player_seek_absolute(controls->player, position); + } + } + /* Return FALSE to allow GTK to process the rest of the event (starting the drag/step) */ + } + return FALSE; } diff --git a/src/controls.h b/src/controls.h index 4aac780..970070d 100644 --- a/src/controls.h +++ b/src/controls.h @@ -2,7 +2,7 @@ #define CONTROLS_H #include -#include "player.h" +#include "dialogs.h" typedef struct Controls Controls; @@ -11,7 +11,7 @@ typedef void (*ControlsFullscreenCallback)(gboolean fullscreen, gpointer user_da typedef void (*ControlsPlayPauseCallback)(gpointer user_data); /* Create controls widget */ -Controls* controls_new(Player *player); +Controls* controls_new(Player *player, Preferences *prefs); void controls_destroy(Controls *controls); /* Get main widget */ diff --git a/src/dialogs.c b/src/dialogs.c index 27ff21d..13c3429 100644 --- a/src/dialogs.c +++ b/src/dialogs.c @@ -2,6 +2,7 @@ #include #include #include +#include "plugin_manager.h" /* File filter helpers */ static void add_video_filters(GtkFileChooser *chooser) @@ -321,7 +322,7 @@ void dialogs_show_about(GtkWindow *parent) gtk_label_set_markup(GTK_LABEL(title_label), "GTK2 Media Player"); gtk_box_pack_start(GTK_BOX(about_vbox), title_label, FALSE, FALSE, 0); - GtkWidget *version_label = gtk_label_new("Version 1.0.0"); + GtkWidget *version_label = gtk_label_new("Version 0.7.1"); gtk_box_pack_start(GTK_BOX(about_vbox), version_label, FALSE, FALSE, 0); GtkWidget *comment_label = gtk_label_new("A lightweight GTK2 media player frontend using libmpv."); @@ -410,8 +411,15 @@ Preferences* preferences_new(void) prefs->remember_last_dir = TRUE; /* Default to TRUE */ prefs->use_mpris = TRUE; /* Default to TRUE */ prefs->open_in_new_window = TRUE; /* Default to TRUE */ + prefs->seekbar_direct_jump = TRUE; /* Default to TRUE as requested earlier */ + prefs->hide_focus_rect = FALSE; + prefs->enable_osd = TRUE; prefs->last_dir = NULL; prefs->screenshot_directory = g_strdup(g_get_home_dir()); + prefs->screenshot_format = g_strdup("png"); + + const char *config_dir = g_get_user_config_dir(); + prefs->mpv_config_path = g_build_filename(config_dir, "gtk2-media-player", "mpv.conf", NULL); return prefs; } @@ -419,6 +427,8 @@ void preferences_free(Preferences *prefs) { if (!prefs) return; g_free(prefs->screenshot_directory); + g_free(prefs->screenshot_format); + g_free(prefs->mpv_config_path); g_free(prefs->last_dir); g_free(prefs); } @@ -482,6 +492,36 @@ void preferences_load(Preferences *prefs) g_clear_error(&error); g_free(last_dir); } + + gboolean direct_jump = g_key_file_get_boolean(keyfile, "General", "seekbar_direct_jump", &error); + if (!error) prefs->seekbar_direct_jump = direct_jump; + else { g_clear_error(&error); } + + gboolean hide_focus = g_key_file_get_boolean(keyfile, "General", "hide_focus_rect", &error); + if (!error) prefs->hide_focus_rect = hide_focus; + else { g_clear_error(&error); } + + gboolean osd = g_key_file_get_boolean(keyfile, "General", "enable_osd", &error); + if (!error) prefs->enable_osd = osd; + else { g_clear_error(&error); } + + char *ss_format = g_key_file_get_string(keyfile, "General", "screenshot_format", &error); + if (!error && ss_format) { + g_free(prefs->screenshot_format); + prefs->screenshot_format = ss_format; + } else { + g_clear_error(&error); + g_free(ss_format); + } + + char *mpv_cfg = g_key_file_get_string(keyfile, "General", "mpv_config_path", &error); + if (!error && mpv_cfg) { + g_free(prefs->mpv_config_path); + prefs->mpv_config_path = mpv_cfg; + } else { + g_clear_error(&error); + g_free(mpv_cfg); + } } g_key_file_free(keyfile); @@ -515,6 +555,15 @@ void preferences_save(Preferences *prefs) g_key_file_set_string(keyfile, "General", "screenshot_directory", prefs->screenshot_directory); } + if (prefs->screenshot_format) { + g_key_file_set_string(keyfile, "General", "screenshot_format", prefs->screenshot_format); + } + if (prefs->mpv_config_path) { + g_key_file_set_string(keyfile, "General", "mpv_config_path", prefs->mpv_config_path); + } + g_key_file_set_boolean(keyfile, "General", "seekbar_direct_jump", prefs->seekbar_direct_jump); + g_key_file_set_boolean(keyfile, "General", "hide_focus_rect", prefs->hide_focus_rect); + g_key_file_set_boolean(keyfile, "General", "enable_osd", prefs->enable_osd); gsize length; char *data = g_key_file_to_data(keyfile, &length, NULL); @@ -528,7 +577,41 @@ void preferences_save(Preferences *prefs) g_free(config_path); } -gboolean dialogs_show_preferences(GtkWindow *parent, Preferences *prefs) +static void on_cfg_browse_clicked(GtkButton *btn, gpointer data) +{ + GtkWindow *parent = GTK_WINDOW(data); + GtkWidget *entry = g_object_get_data(G_OBJECT(btn), "entry"); + GtkWidget *file_dialog = gtk_file_chooser_dialog_new("Select mpv.conf", parent, + GTK_FILE_CHOOSER_ACTION_OPEN, + GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL, + GTK_STOCK_OPEN, GTK_RESPONSE_ACCEPT, NULL); + + if (gtk_dialog_run(GTK_DIALOG(file_dialog)) == GTK_RESPONSE_ACCEPT) { + char *filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(file_dialog)); + gtk_entry_set_text(GTK_ENTRY(entry), filename); + g_free(filename); + } + gtk_widget_destroy(file_dialog); +} + +static void on_cfg_reset_clicked(GtkButton *btn, gpointer entry) +{ + (void)btn; + const char *config_dir = g_get_user_config_dir(); + char *path = g_build_filename(config_dir, "gtk2-media-player", "mpv.conf", NULL); + gtk_entry_set_text(GTK_ENTRY(entry), path); + g_free(path); +} + +static void on_speed_preset_clicked(GtkButton *button, gpointer data) +{ + (void)data; + int speed_int = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(button), "speed")); + GtkWidget *spin = g_object_get_data(G_OBJECT(button), "spin"); + gtk_spin_button_set_value(GTK_SPIN_BUTTON(spin), speed_int / 100.0); +} + +gboolean dialogs_show_preferences(GtkWindow *parent, Preferences *prefs, PluginManager *pm) { if (!prefs) return FALSE; @@ -537,8 +620,10 @@ gboolean dialogs_show_preferences(GtkWindow *parent, Preferences *prefs) parent, GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT, GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL, - GTK_STOCK_OK, GTK_RESPONSE_ACCEPT, + GTK_STOCK_OK, GTK_RESPONSE_OK, NULL); + + gtk_window_set_default_size(GTK_WINDOW(dialog), 600, 500); GtkWidget *content_area = gtk_dialog_get_content_area(GTK_DIALOG(dialog)); gtk_container_set_border_width(GTK_CONTAINER(content_area), 5); @@ -565,14 +650,47 @@ gboolean dialogs_show_preferences(GtkWindow *parent, Preferences *prefs) GtkWidget *open_new_check = gtk_check_button_new_with_label("Always open in a new window (Single-Instance off)"); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(open_new_check), prefs->open_in_new_window); gtk_box_pack_start(GTK_BOX(interface_vbox), open_new_check, FALSE, FALSE, 0); + + GtkWidget *jump_check = gtk_check_button_new_with_label("Seek bar: Jump to clicked position immediately"); + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(jump_check), prefs->seekbar_direct_jump); + gtk_box_pack_start(GTK_BOX(interface_vbox), jump_check, FALSE, FALSE, 0); + + GtkWidget *focus_check = gtk_check_button_new_with_label("Hide focus rectangles (dotted lines) around UI elements"); + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(focus_check), prefs->hide_focus_rect); + gtk_box_pack_start(GTK_BOX(interface_vbox), focus_check, FALSE, FALSE, 0); + + GtkWidget *osd_check = gtk_check_button_new_with_label("Enable On-Screen Display (OSD)"); + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(osd_check), prefs->enable_osd); + gtk_box_pack_start(GTK_BOX(interface_vbox), osd_check, FALSE, FALSE, 0); gtk_notebook_append_page(GTK_NOTEBOOK(notebook), interface_vbox, gtk_label_new("Interface")); - /* 2. Config File Tab (Placeholder) */ + /* 2. Config File Tab */ GtkWidget *config_vbox = gtk_vbox_new(FALSE, 10); gtk_container_set_border_width(GTK_CONTAINER(config_vbox), 10); - GtkWidget *config_label = gtk_label_new("Config File settings - Upcoming feature"); - gtk_box_pack_start(GTK_BOX(config_vbox), config_label, TRUE, TRUE, 0); + + GtkWidget *config_info_label = gtk_label_new(NULL); + gtk_label_set_markup(GTK_LABEL(config_info_label), "mpv Configuration File\n" + "Specify an external mpv.conf file to load custom settings, shaders, and advanced options.\n" + "Note: Standard MPV syntax is supported."); + gtk_label_set_line_wrap(GTK_LABEL(config_info_label), TRUE); + gtk_box_pack_start(GTK_BOX(config_vbox), config_info_label, FALSE, FALSE, 5); + + GtkWidget *cfg_hbox = gtk_hbox_new(FALSE, 10); + GtkWidget *cfg_entry = gtk_entry_new(); + gtk_entry_set_text(GTK_ENTRY(cfg_entry), prefs->mpv_config_path ? prefs->mpv_config_path : ""); + gtk_box_pack_start(GTK_BOX(cfg_hbox), cfg_entry, TRUE, TRUE, 0); + + GtkWidget *cfg_browse_btn = gtk_button_new_with_label("Browse..."); + g_object_set_data(G_OBJECT(cfg_browse_btn), "entry", cfg_entry); + g_signal_connect(cfg_browse_btn, "clicked", G_CALLBACK(on_cfg_browse_clicked), dialog); + gtk_box_pack_start(GTK_BOX(cfg_hbox), cfg_browse_btn, FALSE, FALSE, 0); + gtk_box_pack_start(GTK_BOX(config_vbox), cfg_hbox, FALSE, FALSE, 5); + + GtkWidget *cfg_reset_btn = gtk_button_new_with_label("Reset to Default"); + g_signal_connect(cfg_reset_btn, "clicked", G_CALLBACK(on_cfg_reset_clicked), cfg_entry); + gtk_box_pack_start(GTK_BOX(config_vbox), cfg_reset_btn, FALSE, FALSE, 0); + gtk_notebook_append_page(GTK_NOTEBOOK(notebook), config_vbox, gtk_label_new("Config File")); /* 3. Miscellaneous Tab */ @@ -605,20 +723,32 @@ gboolean dialogs_show_preferences(GtkWindow *parent, Preferences *prefs) gtk_entry_set_text(GTK_ENTRY(ss_entry), prefs->screenshot_directory ? prefs->screenshot_directory : ""); gtk_box_pack_start(GTK_BOX(ss_hbox), ss_entry, TRUE, TRUE, 0); gtk_box_pack_start(GTK_BOX(misc_vbox), ss_hbox, FALSE, FALSE, 0); + + GtkWidget *format_hbox = gtk_hbox_new(FALSE, 10); + gtk_box_pack_start(GTK_BOX(format_hbox), gtk_label_new("Screenshot Format:"), FALSE, FALSE, 0); + GtkWidget *format_combo = gtk_combo_box_text_new(); + gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(format_combo), "webp"); + gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(format_combo), "png"); + gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(format_combo), "jpg"); + + if (g_strcmp0(prefs->screenshot_format, "webp") == 0) gtk_combo_box_set_active(GTK_COMBO_BOX(format_combo), 0); + else if (g_strcmp0(prefs->screenshot_format, "png") == 0) gtk_combo_box_set_active(GTK_COMBO_BOX(format_combo), 1); + else if (g_strcmp0(prefs->screenshot_format, "jpg") == 0) gtk_combo_box_set_active(GTK_COMBO_BOX(format_combo), 2); + else gtk_combo_box_set_active(GTK_COMBO_BOX(format_combo), 0); + + gtk_box_pack_start(GTK_BOX(format_hbox), format_combo, FALSE, FALSE, 0); + gtk_box_pack_start(GTK_BOX(misc_vbox), format_hbox, FALSE, FALSE, 0); gtk_notebook_append_page(GTK_NOTEBOOK(notebook), misc_vbox, gtk_label_new("Miscellaneous")); - /* 4. Plugins Tab (Placeholder) */ - GtkWidget *plugins_vbox = gtk_vbox_new(FALSE, 10); - gtk_container_set_border_width(GTK_CONTAINER(plugins_vbox), 10); - GtkWidget *plugins_label = gtk_label_new("Plugins management - Upcoming feature"); - gtk_box_pack_start(GTK_BOX(plugins_vbox), plugins_label, TRUE, TRUE, 0); + /* 4. Plugins Tab */ + GtkWidget *plugins_vbox = plugin_manager_create_config_widget(pm); gtk_notebook_append_page(GTK_NOTEBOOK(notebook), plugins_vbox, gtk_label_new("Plugins")); gtk_widget_show_all(dialog); gboolean result = FALSE; - if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT) { + if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_OK) { prefs->default_volume = gtk_spin_button_get_value(GTK_SPIN_BUTTON(vol_spin)); prefs->remember_position = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(position_check)); prefs->show_playlist = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(playlist_check)); @@ -627,9 +757,18 @@ gboolean dialogs_show_preferences(GtkWindow *parent, Preferences *prefs) prefs->remember_last_dir = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(dir_check)); prefs->use_mpris = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(mpris_check)); prefs->open_in_new_window = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(open_new_check)); + prefs->seekbar_direct_jump = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(jump_check)); + prefs->hide_focus_rect = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(focus_check)); + prefs->enable_osd = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(osd_check)); g_free(prefs->screenshot_directory); prefs->screenshot_directory = g_strdup(gtk_entry_get_text(GTK_ENTRY(ss_entry))); + + g_free(prefs->screenshot_format); + prefs->screenshot_format = gtk_combo_box_text_get_active_text(GTK_COMBO_BOX_TEXT(format_combo)); + + g_free(prefs->mpv_config_path); + prefs->mpv_config_path = g_strdup(gtk_entry_get_text(GTK_ENTRY(cfg_entry))); preferences_save(prefs); result = TRUE; @@ -709,17 +848,7 @@ double dialogs_select_speed(GtkWindow *parent, double current_speed) GtkWidget *btn = gtk_button_new_with_label(label); g_object_set_data(G_OBJECT(btn), "speed", GINT_TO_POINTER((int)(presets[i] * 100))); g_object_set_data(G_OBJECT(btn), "spin", speed_spin); - g_signal_connect(btn, "clicked", G_CALLBACK( - (void (*)(GtkButton*, gpointer)) ({ - void anon(GtkButton *button, gpointer data) { - (void)data; - int speed_int = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(button), "speed")); - GtkWidget *spin = g_object_get_data(G_OBJECT(button), "spin"); - gtk_spin_button_set_value(GTK_SPIN_BUTTON(spin), speed_int / 100.0); - } - anon; - }) - ), NULL); + g_signal_connect(btn, "clicked", G_CALLBACK(on_speed_preset_clicked), NULL); gtk_box_pack_start(GTK_BOX(hbox), btn, TRUE, TRUE, 0); } diff --git a/src/dialogs.h b/src/dialogs.h index 1149eff..75d0608 100644 --- a/src/dialogs.h +++ b/src/dialogs.h @@ -5,6 +5,9 @@ #include "player.h" #include "playlist.h" +/* Forward declaration */ +struct PluginManager; + /* Preferences dialog */ typedef struct { double default_volume; @@ -15,15 +18,20 @@ typedef struct { gboolean remember_last_dir; gboolean use_mpris; gboolean open_in_new_window; + gboolean seekbar_direct_jump; + gboolean hide_focus_rect; + gboolean enable_osd; char *last_dir; char *screenshot_directory; + char *screenshot_format; + char *mpv_config_path; } Preferences; Preferences* preferences_new(void); void preferences_free(Preferences *prefs); void preferences_load(Preferences *prefs); void preferences_save(Preferences *prefs); -gboolean dialogs_show_preferences(GtkWindow *parent, Preferences *prefs); +gboolean dialogs_show_preferences(GtkWindow *parent, Preferences *prefs, struct PluginManager *pm); /* File dialogs */ GSList* dialogs_open_files(GtkWindow *parent, Preferences *prefs); diff --git a/src/main.c b/src/main.c index a09a1b1..d461424 100644 --- a/src/main.c +++ b/src/main.c @@ -83,7 +83,7 @@ int main(int argc, char *argv[]) /* Basic command line parsing */ for (int i = 1; i < argc; i++) { if (strcmp(argv[i], "--help") == 0 || strcmp(argv[i], "-h") == 0) { - printf("Media Player v1.0.0\n"); + printf("Media Player v0.7\n"); printf("Usage: %s [options] [files...]\n\n", argv[0]); printf("Options:\n"); printf(" -h, --help Show this help message\n"); @@ -99,7 +99,7 @@ int main(int argc, char *argv[]) return 0; } if (strcmp(argv[i], "--version") == 0 || strcmp(argv[i], "-v") == 0) { - printf("Media Player v1.0.0\n"); + printf("Media Player v0.7.1\n"); return 0; } } diff --git a/src/mpv_loader.c b/src/mpv_loader.c index fc5eb41..256ea60 100644 --- a/src/mpv_loader.c +++ b/src/mpv_loader.c @@ -11,6 +11,7 @@ p_mpv_terminate_destroy mpv_terminate_destroy = NULL; p_mpv_set_option_string mpv_set_option_string = NULL; p_mpv_set_wakeup_callback mpv_set_wakeup_callback = NULL; p_mpv_command_async mpv_command_async = NULL; +p_mpv_command mpv_command = NULL; p_mpv_set_property_async mpv_set_property_async = NULL; p_mpv_get_property mpv_get_property = NULL; p_mpv_free mpv_free = NULL; @@ -62,6 +63,7 @@ int load_mpv_library(void) LOAD_SYM(set_option_string); LOAD_SYM(set_wakeup_callback); LOAD_SYM(command_async); + LOAD_SYM(command); LOAD_SYM(set_property_async); LOAD_SYM(get_property); LOAD_SYM(free); diff --git a/src/mpv_loader.h b/src/mpv_loader.h index 5a42bd6..454bca1 100644 --- a/src/mpv_loader.h +++ b/src/mpv_loader.h @@ -82,6 +82,7 @@ typedef mpv_event *(*p_mpv_wait_event)(mpv_handle *ctx, double timeout); typedef int (*p_mpv_observe_property)(mpv_handle *mpv, uint64_t reply_userdata, const char *name, mpv_format format); typedef int (*p_mpv_unobserve_property)(mpv_handle *mpv, uint64_t reply_userdata); typedef const char *(*p_mpv_event_name)(mpv_event_id event); +typedef int (*p_mpv_command)(mpv_handle *ctx, const char **args); /* Global function pointers */ extern p_mpv_error_string mpv_error_string; @@ -91,6 +92,7 @@ extern p_mpv_terminate_destroy mpv_terminate_destroy; extern p_mpv_set_option_string mpv_set_option_string; extern p_mpv_set_wakeup_callback mpv_set_wakeup_callback; extern p_mpv_command_async mpv_command_async; +extern p_mpv_command mpv_command; extern p_mpv_set_property_async mpv_set_property_async; extern p_mpv_get_property mpv_get_property; extern p_mpv_free mpv_free; diff --git a/src/player.c b/src/player.c index 8dc759d..f9b67ed 100644 --- a/src/player.c +++ b/src/player.c @@ -2,6 +2,9 @@ #include #include #include +#include +#include +#include struct Player { mpv_handle *mpv; @@ -9,6 +12,7 @@ struct Player { PlayerEventCallback event_callback; gpointer event_callback_data; gboolean initialized; + char *config_path; }; static void check_error(int status) @@ -48,7 +52,7 @@ void player_destroy(Player *player) if (player->mpv) { mpv_terminate_destroy(player->mpv); } - + g_free(player->config_path); g_free(player); } @@ -58,38 +62,104 @@ void player_set_window(Player *player, unsigned long wid) player->wid = wid; } +void player_set_config_path(Player *player, const char *path) +{ + if (!player) return; + g_free(player->config_path); + player->config_path = path ? g_strdup(path) : NULL; +} + static void wakeup_callback(void *ctx) { /* This is called from mpv's thread. We need to wake up the GTK main loop. */ g_idle_add_full(G_PRIORITY_HIGH, (GSourceFunc)player_process_events, ctx, NULL); } + +static void load_scripts(mpv_handle *mpv) +{ + const char *config_dir = g_get_user_config_dir(); + char *scripts_dir = g_build_filename(config_dir, "gtk2-media-player", "scripts", NULL); + + printf("Scanning for scripts in: %s\n", scripts_dir); + + /* Create scripts directory if it doesn't exist */ + g_mkdir_with_parents(scripts_dir, 0755); + + DIR *dir = opendir(scripts_dir); + if (!dir) { + printf("Failed to open scripts directory!\n"); + } else { + struct dirent *entry; + int count = 0; + while ((entry = readdir(dir)) != NULL) { + if (entry->d_name[0] == '.') continue; + + char *path = g_build_filename(scripts_dir, entry->d_name, NULL); + size_t len = strlen(entry->d_name); + + /* Check extension (Lua or JS) */ + if ((len > 4 && strcmp(entry->d_name + len - 4, ".lua") == 0) || + (len > 3 && strcmp(entry->d_name + len - 3, ".js") == 0)) { + + printf("Loading script: %s\n", path); + + /* Use load-script command (works after mpv_initialize) */ + const char *cmd[] = {"load-script", path, NULL}; + int res = mpv_command(mpv, cmd); + if (res < 0) { + printf(" -> Failed! Error: %s\n", mpv_error_string(res)); + } else { + printf(" -> OK\n"); + count++; + } + } + g_free(path); + } + closedir(dir); + printf("Loaded %d script(s)\n", count); + } + g_free(scripts_dir); +} + int player_init(Player *player) + { if (!player || !player->mpv) return -1; - /* Set window ID if provided */ + /* Set window ID if provided - Must be before initialize */ if (player->wid != 0) { char wid_str[64]; snprintf(wid_str, sizeof(wid_str), "%lu", player->wid); check_error(mpv_set_option_string(player->mpv, "wid", wid_str)); } - /* Basic configuration */ + /* Basic configuration - Set BEFORE config loading so config can override defaults */ check_error(mpv_set_option_string(player->mpv, "input-default-bindings", "no")); check_error(mpv_set_option_string(player->mpv, "input-vo-keyboard", "no")); check_error(mpv_set_option_string(player->mpv, "osc", "no")); /* We have our own controls */ + check_error(mpv_set_option_string(player->mpv, "osd-bar", "yes")); + check_error(mpv_set_option_string(player->mpv, "osd-duration", "2000")); check_error(mpv_set_option_string(player->mpv, "input-cursor", "no")); /* Let GTK handle the cursor */ check_error(mpv_set_option_string(player->mpv, "keep-open", "yes")); check_error(mpv_set_option_string(player->mpv, "idle", "yes")); - - /* Initialize mpv */ - int ret = mpv_initialize(player->mpv); - if (ret < 0) { - fprintf(stderr, "Failed to initialize mpv: %s\n", mpv_error_string(ret)); - return ret; + + /* Load external config file if provided */ + if (player->config_path && g_file_test(player->config_path, G_FILE_TEST_EXISTS)) { + check_error(mpv_set_option_string(player->mpv, "config", "yes")); + check_error(mpv_set_option_string(player->mpv, "config-file", player->config_path)); } + /* Initialize MPV */ + int res = mpv_initialize(player->mpv); + if (res < 0) { + check_error(res); + return -1; + } + + /* Load scripts AFTER initialization using load-script command */ + load_scripts(player->mpv); + /* Set wakeup callback for event processing */ mpv_set_wakeup_callback(player->mpv, wakeup_callback, player); @@ -147,6 +217,10 @@ void player_seek(Player *player, double seconds) snprintf(sec_str, sizeof(sec_str), "%f", seconds); const char *cmd[] = {"seek", sec_str, "relative", NULL}; check_error(mpv_command_async(player->mpv, 0, cmd)); + + /* Show progress OSD */ + const char *osd_cmd[] = {"show-progress", NULL}; + mpv_command_async(player->mpv, 0, osd_cmd); } void player_seek_absolute(Player *player, double position) @@ -155,8 +229,12 @@ void player_seek_absolute(Player *player, double position) char pos_str[32]; snprintf(pos_str, sizeof(pos_str), "%f", position); - const char *cmd[] = {"seek", pos_str, "absolute", NULL}; + const char *cmd[] = {"seek", pos_str, "absolute+exact", NULL}; check_error(mpv_command_async(player->mpv, 0, cmd)); + + /* Show progress OSD */ + const char *osd_cmd[] = {"show-progress", NULL}; + mpv_command_async(player->mpv, 0, osd_cmd); } void player_frame_step(Player *player) @@ -178,13 +256,22 @@ void player_frame_back_step(Player *player) void player_set_option_string(Player *player, const char *name, const char *value) { if (!player || !player->mpv) return; - check_error(mpv_set_option_string(player->mpv, name, value)); + if (player->initialized) { + check_error(mpv_set_property_string(player->mpv, name, value)); + } else { + check_error(mpv_set_option_string(player->mpv, name, value)); + } } void player_set_option_flag(Player *player, const char *name, int value) { if (!player || !player->mpv) return; - check_error(mpv_set_property_async(player->mpv, 0, name, MPV_FORMAT_FLAG, &value)); + if (player->initialized) { + const char *val = value ? "yes" : "no"; + check_error(mpv_set_property_string(player->mpv, name, val)); + } else { + check_error(mpv_set_option_string(player->mpv, name, value ? "yes" : "no")); + } } /* Property getters */ @@ -349,22 +436,37 @@ void player_set_volume(Player *player, double volume) { if (!player || !player->mpv) return; - mpv_set_property_async(player->mpv, 0, "volume", MPV_FORMAT_DOUBLE, &volume); + char vol_str[32]; + snprintf(vol_str, sizeof(vol_str), "%f", volume); + + if (player->initialized) { + check_error(mpv_set_property_string(player->mpv, "volume", vol_str)); + /* Show volume bar on OSD */ + const char *cmd[] = {"osd-msg-bar", "set", "volume", NULL}; + mpv_command_async(player->mpv, 0, cmd); + } else { + check_error(mpv_set_option_string(player->mpv, "volume", vol_str)); + } } void player_set_muted(Player *player, gboolean muted) { if (!player || !player->mpv) return; - - int flag = muted ? 1 : 0; - mpv_set_property_async(player->mpv, 0, "mute", MPV_FORMAT_FLAG, &flag); + if (player->initialized) { + const char *val = muted ? "yes" : "no"; + check_error(mpv_set_property_string(player->mpv, "mute", val)); + } else { + check_error(mpv_set_option_string(player->mpv, "mute", muted ? "yes" : "no")); + } } void player_set_speed(Player *player, double speed) { if (!player || !player->mpv) return; - mpv_set_property_async(player->mpv, 0, "speed", MPV_FORMAT_DOUBLE, &speed); + char speed_str[32]; + snprintf(speed_str, sizeof(speed_str), "%f", speed); + check_error(mpv_set_property_string(player->mpv, "speed", speed_str)); } /* Track management */ @@ -435,16 +537,18 @@ void player_set_audio_track(Player *player, int track) { if (!player || !player->mpv) return; - int64_t aid = track; - mpv_set_property_async(player->mpv, 0, "aid", MPV_FORMAT_INT64, &aid); + char aid_str[16]; + snprintf(aid_str, sizeof(aid_str), "%d", track); + check_error(mpv_set_property_string(player->mpv, "aid", aid_str)); } void player_set_subtitle_track(Player *player, int track) { if (!player || !player->mpv) return; - int64_t sid = track; - mpv_set_property_async(player->mpv, 0, "sid", MPV_FORMAT_INT64, &sid); + char sid_str[16]; + snprintf(sid_str, sizeof(sid_str), "%d", track); + check_error(mpv_set_property_string(player->mpv, "sid", sid_str)); } void player_load_subtitle(Player *player, const char *path) diff --git a/src/player.h b/src/player.h index 93b566a..eda3059 100644 --- a/src/player.h +++ b/src/player.h @@ -13,6 +13,7 @@ typedef void (*PlayerEventCallback)(Player *player, mpv_event *event, gpointer u Player* player_new(void); void player_destroy(Player *player); void player_set_window(Player *player, unsigned long wid); +void player_set_config_path(Player *player, const char *path); int player_init(Player *player); /* Playback commands */ diff --git a/src/plugin.h b/src/plugin.h new file mode 100644 index 0000000..84c9d92 --- /dev/null +++ b/src/plugin.h @@ -0,0 +1,194 @@ +/** + * GTK2 Media Player - Plugin API + * + * This header defines the public API that plugins must implement + * and the functions they can call to interact with the application. + */ + +#ifndef PLUGIN_H +#define PLUGIN_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* Plugin API version - plugins must match this */ +#define PLUGIN_API_VERSION 2 + +/** + * Plugin metadata structure + * Returned by plugin_get_info() + */ +typedef struct { + const char *name; /* Display name, e.g. "Screenshot Timer" */ + const char *version; /* Semantic version, e.g. "1.0.0" */ + const char *description; /* Short description of functionality */ + const char *author; /* Author name/email */ + int api_version; /* Must be PLUGIN_API_VERSION */ +} PluginInfo; + +/** + * Application API provided to plugins + * Passed to plugin_init() for plugins to store and use + */ +typedef struct PluginAPI { + /* Player control */ + void (*play)(void); + void (*pause)(void); + void (*stop)(void); + void (*toggle_pause)(void); + void (*seek)(double seconds); + void (*seek_absolute)(double position); + + /* Player state */ + double (*get_position)(void); + double (*get_duration)(void); + double (*get_volume)(void); + gboolean (*is_paused)(void); + gboolean (*is_playing)(void); + + /* Media info */ + char* (*get_current_file)(void); /* Caller must g_free() */ + char* (*get_media_title)(void); /* Caller must g_free() */ + + /* Screenshot */ + void (*take_screenshot)(void); + void (*take_screenshot_to_file)(const char *path); + + /* UI access */ + GtkWindow* (*get_main_window)(void); + + /* Message dialogs */ + void (*show_info)(const char *title, const char *message); + void (*show_warning)(const char *title, const char *message); + void (*show_error)(const char *title, const char *message); + + /* Menu registration - add items to Plugins menu */ + void (*register_menu_item)(GtkWidget *menu_item); + void (*unregister_menu_item)(GtkWidget *menu_item); + + /* UI layouts (Added for Audio Player plugin) */ + void (*set_video_area_visible)(gboolean visible); + void (*set_window_size)(int width, int height); + + /* Advanced MPV access */ + void (*execute_mpv_command)(const char **args); + + /* Reserved for future expansion */ + void *_reserved[5]; +} PluginAPI; + +/** + * Plugin event types for optional hooks + */ +typedef enum { + PLUGIN_EVENT_FILE_LOADED, /* New file loaded, data = filepath (const char*) */ + PLUGIN_EVENT_PLAYBACK_STARTED, /* Playback started, data = NULL */ + PLUGIN_EVENT_PLAYBACK_PAUSED, /* Playback paused, data = NULL */ + PLUGIN_EVENT_PLAYBACK_STOPPED, /* Playback stopped, data = NULL */ + PLUGIN_EVENT_POSITION_CHANGED, /* Position changed, data = position (double*) */ + PLUGIN_EVENT_VOLUME_CHANGED, /* Volume changed, data = volume (double*) */ +} PluginEventType; + +/* + * ============================================================================= + * REQUIRED PLUGIN EXPORTS + * + * Every plugin MUST implement and export these three functions: + * ============================================================================= + */ + +/** + * Get plugin metadata + * Must return a pointer to static PluginInfo structure + * + * @return Pointer to plugin info (must remain valid until shutdown) + */ +#define PLUGIN_EXPORT_GET_INFO "plugin_get_info" +typedef const PluginInfo* (*PluginGetInfoFunc)(void); + +/** + * Initialize plugin + * Called when plugin is loaded. Store the API pointer for later use. + * + * @param api Application API for plugin to use + * @return 0 on success, non-zero on failure + */ +#define PLUGIN_EXPORT_INIT "plugin_init" +typedef int (*PluginInitFunc)(const PluginAPI *api); + +/** + * Shutdown plugin + * Called when plugin is unloaded. Clean up any resources. + */ +#define PLUGIN_EXPORT_SHUTDOWN "plugin_shutdown" +typedef void (*PluginShutdownFunc)(void); + +/* + * ============================================================================= + * OPTIONAL PLUGIN EXPORTS + * + * Plugins MAY implement these for additional functionality: + * ============================================================================= + */ + +/** + * Get plugin's menu item(s) + * If plugin wants to add menu items, return a GtkMenuItem. + * For multiple items, use a submenu. + * + * @return GtkWidget* menu item, or NULL if none + */ +#define PLUGIN_EXPORT_GET_MENU "plugin_get_menu_item" +typedef GtkWidget* (*PluginGetMenuFunc)(void); + +/** + * Handle application events + * Called when various application events occur. + * + * @param event Event type + * @param data Event-specific data (see PluginEventType comments) + */ +#define PLUGIN_EXPORT_ON_EVENT "plugin_on_event" +typedef void (*PluginOnEventFunc)(PluginEventType event, void *data); + +/** + * Configure plugin + * Called when user selects "Configure" in plugin manager. + * Should open a configuration dialog. + */ +#define PLUGIN_EXPORT_CONFIGURE "plugin_configure" +typedef void (*PluginConfigureFunc)(void); + +/** + * Show about dialog + * Called when user selects "About" in plugin manager. + * Should open an about dialog. + */ +#define PLUGIN_EXPORT_ABOUT "plugin_about" +typedef void (*PluginAboutFunc)(void); + +/* + * ============================================================================= + * HELPER MACROS FOR PLUGIN DEVELOPMENT + * ============================================================================= + */ + +/* Declare standard plugin info */ +#define DECLARE_PLUGIN_INFO(p_name, p_version, p_description, p_author) \ + static const PluginInfo _plugin_info = { \ + .name = p_name, \ + .version = p_version, \ + .description = p_description, \ + .author = p_author, \ + .api_version = PLUGIN_API_VERSION \ + }; \ + const PluginInfo* plugin_get_info(void) { return &_plugin_info; } + +#ifdef __cplusplus +} +#endif + +#endif /* PLUGIN_H */ diff --git a/src/plugin_manager.c b/src/plugin_manager.c new file mode 100644 index 0000000..7af4860 --- /dev/null +++ b/src/plugin_manager.c @@ -0,0 +1,1028 @@ +#define _XOPEN_SOURCE 500 +/** + * GTK2 Media Player - Plugin Manager Implementation + */ + +#include "plugin_manager.h" +#include "ui.h" +#include +#include +#include +#include +#include +#include +#include + +#define MAX_PLUGINS 64 +#define PLUGINS_SUBDIR "gtk2-media-player/plugins" + +struct PluginManager { + AppUI *ui; + char *plugins_dir; + char *last_plugin_dir; + LoadedPlugin *plugins[MAX_PLUGINS]; + int plugin_count; + GtkWidget *plugins_menu; + GList *registered_menu_items; + PluginAPI api; +}; + +/* Forward declarations for static API functions */ +static PluginManager *g_pm = NULL; /* Global for API callbacks */ + +static void api_play(void); +static void api_pause(void); +static void api_stop(void); +static void api_toggle_pause(void); +static void api_seek(double seconds); +static void api_seek_absolute(double position); +static double api_get_position(void); +static double api_get_duration(void); +static double api_get_volume(void); +static gboolean api_is_paused(void); +static gboolean api_is_playing(void); +static char* api_get_current_file(void); +static char* api_get_media_title(void); +static void api_take_screenshot(void); +static void api_take_screenshot_to_file(const char *path); +static void api_take_screenshot_to_file(const char *path); +static GtkWindow* api_get_main_window(void); +static void api_show_info(const char *title, const char *message); +static void api_show_warning(const char *title, const char *message); +static void api_show_error(const char *title, const char *message); +static void api_register_menu_item(GtkWidget *item); +static void api_register_menu_item(GtkWidget *item); +static void api_unregister_menu_item(GtkWidget *item); +static void api_set_video_area_visible(gboolean visible); +static void api_set_window_size(int width, int height); +static void api_execute_mpv_command(const char **args); + +/* Player callback storage */ +static struct { + void (*play)(void); + void (*pause)(void); + void (*stop)(void); + void (*toggle_pause)(void); + void (*seek)(double); + void (*seek_absolute)(double); + double (*get_position)(void); + double (*get_duration)(void); + double (*get_volume)(void); + gboolean (*is_paused)(void); + gboolean (*is_playing)(void); + char* (*get_current_file)(void); + char* (*get_media_title)(void); + void (*take_screenshot)(void); + void (*take_screenshot_to_file)(const char*); +} player_cbs = {0}; + +/* ============================================================================ + * Plugin Manager Lifecycle + * ============================================================================ */ + +PluginManager* plugin_manager_new(AppUI *ui) +{ + PluginManager *pm = g_malloc0(sizeof(PluginManager)); + pm->ui = ui; + pm->plugin_count = 0; + pm->registered_menu_items = NULL; + + /* Build plugins directory path */ + const char *config_dir = g_get_user_config_dir(); + pm->plugins_dir = g_build_filename(config_dir, PLUGINS_SUBDIR, NULL); + + /* Create directory if it doesn't exist */ + g_mkdir_with_parents(pm->plugins_dir, 0755); + + /* Initialize API structure */ + pm->api.play = api_play; + pm->api.pause = api_pause; + pm->api.stop = api_stop; + pm->api.toggle_pause = api_toggle_pause; + pm->api.seek = api_seek; + pm->api.seek_absolute = api_seek_absolute; + pm->api.get_position = api_get_position; + pm->api.get_duration = api_get_duration; + pm->api.get_volume = api_get_volume; + pm->api.is_paused = api_is_paused; + pm->api.is_playing = api_is_playing; + pm->api.get_current_file = api_get_current_file; + pm->api.get_media_title = api_get_media_title; + pm->api.take_screenshot = api_take_screenshot; + pm->api.take_screenshot_to_file = api_take_screenshot_to_file; + pm->api.get_main_window = api_get_main_window; + pm->api.show_info = api_show_info; + pm->api.show_warning = api_show_warning; + pm->api.show_error = api_show_error; + pm->api.register_menu_item = api_register_menu_item; + pm->api.unregister_menu_item = api_unregister_menu_item; + pm->api.set_video_area_visible = api_set_video_area_visible; + pm->api.set_window_size = api_set_window_size; + pm->api.execute_mpv_command = api_execute_mpv_command; + + g_pm = pm; + + printf("Plugin manager initialized. Plugins directory: %s\n", pm->plugins_dir); + return pm; +} + +void plugin_manager_destroy(PluginManager *pm) +{ + if (!pm) return; + + /* Shutdown and unload all plugins */ + for (int i = 0; i < pm->plugin_count; i++) { + LoadedPlugin *p = pm->plugins[i]; + if (p) { + if (p->enabled && p->shutdown) { + printf("Shutting down plugin: %s\n", p->info.name); + p->shutdown(); + } + if (p->handle) { + dlclose(p->handle); + } + g_free(p->id); + g_free(p->path); + g_free(p); + } + } + + g_list_free(pm->registered_menu_items); + g_free(pm->plugins_dir); + g_free(pm); + g_pm = NULL; +} + +void plugin_manager_set_player_callbacks(PluginManager *pm, + void (*play)(void), + void (*pause)(void), + void (*stop)(void), + void (*toggle_pause)(void), + void (*seek)(double), + void (*seek_absolute)(double), + double (*get_position)(void), + double (*get_duration)(void), + double (*get_volume)(void), + gboolean (*is_paused)(void), + gboolean (*is_playing)(void), + char* (*get_current_file)(void), + char* (*get_media_title)(void), + void (*take_screenshot)(void), + void (*take_screenshot_to_file)(const char*)) +{ + (void)pm; + player_cbs.play = play; + player_cbs.pause = pause; + player_cbs.stop = stop; + player_cbs.toggle_pause = toggle_pause; + player_cbs.seek = seek; + player_cbs.seek_absolute = seek_absolute; + player_cbs.get_position = get_position; + player_cbs.get_duration = get_duration; + player_cbs.get_volume = get_volume; + player_cbs.is_paused = is_paused; + player_cbs.is_playing = is_playing; + player_cbs.get_current_file = get_current_file; + player_cbs.get_media_title = get_media_title; + player_cbs.take_screenshot = take_screenshot; + player_cbs.take_screenshot_to_file = take_screenshot_to_file; +} + +/* ============================================================================ + * Plugin Loading + * ============================================================================ */ + +static LoadedPlugin* load_plugin(PluginManager *pm, const char *plugin_dir, const char *plugin_id) +{ + /* Find .so file in plugin directory */ + DIR *dir = opendir(plugin_dir); + if (!dir) { + fprintf(stderr, "Cannot open plugin directory: %s\n", plugin_dir); + return NULL; + } + + char *so_path = NULL; + struct dirent *entry; + while ((entry = readdir(dir)) != NULL) { + size_t len = strlen(entry->d_name); + if (len > 3 && strcmp(entry->d_name + len - 3, ".so") == 0) { + so_path = g_build_filename(plugin_dir, entry->d_name, NULL); + break; + } + } + closedir(dir); + + if (!so_path) { + fprintf(stderr, "No .so file found in: %s\n", plugin_dir); + return NULL; + } + + /* Load the shared library */ + char *abs_so = realpath(so_path, NULL); + printf("Attempting to load plugin library: %s\n", abs_so ? abs_so : so_path); + + void *handle = dlopen(so_path, RTLD_NOW); + if (!handle) { + fprintf(stderr, "Failed to load plugin %s: %s\n", so_path, dlerror()); + + /* Debug: attempt to see what dependencies are missing */ + printf("DEBUG: Dependency check for %s:\n", so_path); + char *ldd_cmd = g_strdup_printf("ldd '%s'", so_path); + system(ldd_cmd); + g_free(ldd_cmd); + + g_free(so_path); + if (abs_so) free(abs_so); + return NULL; + } + if (abs_so) free(abs_so); + + /* Load required symbols */ + PluginGetInfoFunc get_info = (PluginGetInfoFunc)dlsym(handle, PLUGIN_EXPORT_GET_INFO); + PluginInitFunc init = (PluginInitFunc)dlsym(handle, PLUGIN_EXPORT_INIT); + PluginShutdownFunc shutdown = (PluginShutdownFunc)dlsym(handle, PLUGIN_EXPORT_SHUTDOWN); + + if (!get_info || !init || !shutdown) { + fprintf(stderr, "Plugin %s missing required exports\n", so_path); + dlclose(handle); + g_free(so_path); + return NULL; + } + + /* Get plugin info */ + const PluginInfo *info = get_info(); + if (!info) { + fprintf(stderr, "Plugin %s returned NULL info\n", so_path); + dlclose(handle); + g_free(so_path); + return NULL; + } + + /* Check API version */ + if (info->api_version != PLUGIN_API_VERSION) { + fprintf(stderr, "Plugin %s API version mismatch (got %d, expected %d)\n", + info->name, info->api_version, PLUGIN_API_VERSION); + dlclose(handle); + g_free(so_path); + return NULL; + } + + /* Create plugin instance */ + LoadedPlugin *p = g_malloc0(sizeof(LoadedPlugin)); + p->id = g_strdup(plugin_id); + p->path = so_path; + p->handle = handle; + p->info = *info; + p->get_info = get_info; + p->init = init; + p->shutdown = shutdown; + + /* Load optional symbols */ + p->get_menu_item = (PluginGetMenuFunc)dlsym(handle, PLUGIN_EXPORT_GET_MENU); + p->on_event = (PluginOnEventFunc)dlsym(handle, PLUGIN_EXPORT_ON_EVENT); + p->configure = (PluginConfigureFunc)dlsym(handle, PLUGIN_EXPORT_CONFIGURE); + p->about = (PluginAboutFunc)dlsym(handle, PLUGIN_EXPORT_ABOUT); + + /* Initialize the plugin */ + if (init(&pm->api) != 0) { + fprintf(stderr, "Plugin %s init failed\n", info->name); + dlclose(handle); + g_free(p->id); + g_free(p->path); + g_free(p); + return NULL; + } + + p->enabled = TRUE; + + /* Get menu item if available */ + if (p->get_menu_item) { + p->menu_item = p->get_menu_item(); + p->has_menu = (p->menu_item != NULL); + } + + printf("Plugin loaded successfully: %s v%s\n", info->name, info->version); + return p; +} + +int plugin_manager_load_all(PluginManager *pm) +{ + if (!pm) return 0; + + DIR *dir = opendir(pm->plugins_dir); + if (!dir) { + printf("Plugins directory does not exist or is empty: %s\n", pm->plugins_dir); + return 0; + } + + int loaded = 0; + struct dirent *entry; + while ((entry = readdir(dir)) != NULL && pm->plugin_count < MAX_PLUGINS) { + if (entry->d_name[0] == '.') continue; + + char *plugin_path = g_build_filename(pm->plugins_dir, entry->d_name, NULL); + + struct stat st; + if (stat(plugin_path, &st) == 0 && S_ISDIR(st.st_mode)) { + LoadedPlugin *p = load_plugin(pm, plugin_path, entry->d_name); + if (p) { + pm->plugins[pm->plugin_count++] = p; + loaded++; + } + } + + g_free(plugin_path); + } + closedir(dir); + + printf("Loaded %d plugin(s)\n", loaded); + return loaded; +} + +/* ============================================================================ + * Plugin Installation + * ============================================================================ */ + +gboolean plugin_manager_install_zip(PluginManager *pm, const char *zip_path) +{ + if (!pm || !zip_path) return FALSE; + + /* Extract zip to temp directory first to check contents */ + char *temp_dir = g_build_filename(g_get_tmp_dir(), "plugin_install_XXXXXX", NULL); + char *actual_temp = g_mkdtemp(temp_dir); + if (!actual_temp) { + fprintf(stderr, "Failed to create temp directory\n"); + g_free(temp_dir); + return FALSE; + } + + /* Run unzip command */ + char *cmd = g_strdup_printf("unzip -q '%s' -d '%s'", zip_path, actual_temp); + int ret = system(cmd); + g_free(cmd); + + if (ret != 0) { + fprintf(stderr, "Failed to extract zip file\n"); + /* Clean up temp dir */ + cmd = g_strdup_printf("rm -rf '%s'", actual_temp); + system(cmd); + g_free(cmd); + g_free(temp_dir); + return FALSE; + } + + /* Find the plugin directory (first subdirectory) */ + DIR *dir = opendir(actual_temp); + if (!dir) { + g_free(temp_dir); + return FALSE; + } + + char *plugin_name = NULL; + struct dirent *entry; + while ((entry = readdir(dir)) != NULL) { + if (entry->d_name[0] == '.') continue; + + char *path = g_build_filename(actual_temp, entry->d_name, NULL); + struct stat st; + if (stat(path, &st) == 0 && S_ISDIR(st.st_mode)) { + plugin_name = g_strdup(entry->d_name); + g_free(path); + break; + } + g_free(path); + } + closedir(dir); + + if (!plugin_name) { + /* Maybe files are at root level, use zip name */ + char *base = g_path_get_basename(zip_path); + /* Remove .zip extension */ + char *dot = strrchr(base, '.'); + if (dot) *dot = '\0'; + plugin_name = base; + } + + /* Move to plugins directory */ + char *dest = g_build_filename(pm->plugins_dir, plugin_name, NULL); + + /* Remove existing if present */ + if (g_file_test(dest, G_FILE_TEST_EXISTS)) { + cmd = g_strdup_printf("rm -rf '%s'", dest); + system(cmd); + g_free(cmd); + } + + /* Check if we need to move subdirectory or temp dir itself */ + char *src = g_build_filename(actual_temp, plugin_name, NULL); + if (!g_file_test(src, G_FILE_TEST_IS_DIR)) { + g_free(src); + src = g_strdup(actual_temp); + } + + cmd = g_strdup_printf("mv '%s' '%s'", src, dest); + ret = system(cmd); + g_free(cmd); + g_free(src); + + /* Clean up temp dir */ + cmd = g_strdup_printf("rm -rf '%s'", actual_temp); + system(cmd); + g_free(cmd); + g_free(temp_dir); + + if (ret != 0) { + fprintf(stderr, "Failed to install plugin to %s\n", dest); + g_free(dest); + g_free(plugin_name); + return FALSE; + } + + /* Check if plugin is already loaded (e.g., from startup) */ + for (int i = 0; i < pm->plugin_count; i++) { + if (pm->plugins[i] && strcmp(pm->plugins[i]->id, plugin_name) == 0) { + printf("Plugin already loaded: %s\n", plugin_name); + g_free(dest); + g_free(plugin_name); + return TRUE; /* Already loaded, consider it a success */ + } + } + + /* Load the newly installed plugin */ + if (pm->plugin_count < MAX_PLUGINS) { + LoadedPlugin *p = load_plugin(pm, dest, plugin_name); + if (p) { + pm->plugins[pm->plugin_count++] = p; + printf("Successfully installed plugin: %s\n", p->info.name); + g_free(dest); + g_free(plugin_name); + return TRUE; + } else { + fprintf(stderr, "Failed to load plugin after installation: %s\n", plugin_name); + } + } + + g_free(dest); + g_free(plugin_name); + return FALSE; +} + +gboolean plugin_manager_uninstall(PluginManager *pm, const char *plugin_id) +{ + if (!pm || !plugin_id) return FALSE; + + /* Find the plugin */ + int idx = -1; + for (int i = 0; i < pm->plugin_count; i++) { + if (pm->plugins[i] && strcmp(pm->plugins[i]->id, plugin_id) == 0) { + idx = i; + break; + } + } + + if (idx < 0) { + fprintf(stderr, "Plugin not found: %s\n", plugin_id); + return FALSE; + } + + LoadedPlugin *p = pm->plugins[idx]; + + /* Shutdown plugin */ + if (p->enabled && p->shutdown) { + p->shutdown(); + } + + /* Close library */ + if (p->handle) { + dlclose(p->handle); + } + + /* Remove directory */ + char *plugin_dir = g_build_filename(pm->plugins_dir, plugin_id, NULL); + char *cmd = g_strdup_printf("rm -rf '%s'", plugin_dir); + system(cmd); + g_free(cmd); + g_free(plugin_dir); + + /* Remove from array */ + g_free(p->id); + g_free(p->path); + g_free(p); + + for (int i = idx; i < pm->plugin_count - 1; i++) { + pm->plugins[i] = pm->plugins[i + 1]; + } + pm->plugin_count--; + + return TRUE; +} + +/* ============================================================================ + * Plugin Menu + * ============================================================================ */ + +LoadedPlugin** plugin_manager_get_list(PluginManager *pm, int *count) +{ + if (!pm) { + if (count) *count = 0; + return NULL; + } + if (count) *count = pm->plugin_count; + return pm->plugins; +} + +static void on_install_plugin(GtkMenuItem *item, gpointer data) +{ + (void)item; + PluginManager *pm = (PluginManager*)data; + + GtkWidget *dialog = gtk_file_chooser_dialog_new( + "Install Plugin", + GTK_WINDOW(ui_get_window(pm->ui)), + GTK_FILE_CHOOSER_ACTION_OPEN, + GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL, + GTK_STOCK_OPEN, GTK_RESPONSE_ACCEPT, + NULL); + + GtkFileFilter *filter = gtk_file_filter_new(); + gtk_file_filter_set_name(filter, "Plugin Archives (*.zip)"); + gtk_file_filter_add_pattern(filter, "*.zip"); + gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(dialog), filter); + + if (pm->last_plugin_dir) { + gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(dialog), pm->last_plugin_dir); + } + + if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT) { + char *filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dialog)); + + /* Save the directory for next time */ + g_free(pm->last_plugin_dir); + pm->last_plugin_dir = g_path_get_dirname(filename); + + if (plugin_manager_install_zip(pm, filename)) { + GtkWidget *msg = gtk_message_dialog_new( + GTK_WINDOW(ui_get_window(pm->ui)), + GTK_DIALOG_MODAL, + GTK_MESSAGE_QUESTION, + GTK_BUTTONS_YES_NO, + "Plugin installed successfully!\n\nRestart the application now to use the new plugin?"); + + gtk_window_set_title(GTK_WINDOW(msg), "Restart Required"); + + if (gtk_dialog_run(GTK_DIALOG(msg)) == GTK_RESPONSE_YES) { + ui_restart(pm->ui); + } + gtk_widget_destroy(msg); + } else { + GtkWidget *msg = gtk_message_dialog_new( + GTK_WINDOW(ui_get_window(pm->ui)), + GTK_DIALOG_MODAL, + GTK_MESSAGE_ERROR, + GTK_BUTTONS_OK, + "Failed to install plugin."); + gtk_dialog_run(GTK_DIALOG(msg)); + gtk_widget_destroy(msg); + } + g_free(filename); + } + + gtk_widget_destroy(dialog); +} + +GtkWidget* plugin_manager_get_menu(PluginManager *pm) +{ + if (!pm) return NULL; + + GtkWidget *menu = gtk_menu_new(); + + /* Add plugin menu items */ + for (int i = 0; i < pm->plugin_count; i++) { + LoadedPlugin *p = pm->plugins[i]; + if (p && p->has_menu && p->menu_item) { + gtk_menu_shell_append(GTK_MENU_SHELL(menu), p->menu_item); + } + } + + /* Add registered menu items */ + for (GList *l = pm->registered_menu_items; l != NULL; l = l->next) { + gtk_menu_shell_append(GTK_MENU_SHELL(menu), GTK_WIDGET(l->data)); + } + + /* Separator if we have plugins */ + if (pm->plugin_count > 0 || pm->registered_menu_items != NULL) { + GtkWidget *sep = gtk_separator_menu_item_new(); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), sep); + } + + GtkWidget *install_item = gtk_menu_item_new_with_label("Install Plugin..."); + g_signal_connect(install_item, "activate", G_CALLBACK(on_install_plugin), pm); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), install_item); + + GtkWidget *scripts_item = gtk_menu_item_new_with_label("Open Scripts Folder..."); + g_signal_connect_swapped(scripts_item, "activate", G_CALLBACK(ui_open_scripts_folder), pm->ui); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), scripts_item); + + /* Manage plugins - Removed as per user request (moved to preferences) */ + /* + GtkWidget *manage_item = gtk_menu_item_new_with_label("Manage Plugins..."); + g_signal_connect_swapped(manage_item, "activate", G_CALLBACK(plugin_manager_show_dialog), pm); + gtk_menu_shell_append(GTK_MENU_SHELL(menu), manage_item); + */ + + gtk_widget_show_all(menu); + pm->plugins_menu = menu; + + return menu; +} + +void plugin_manager_register_menu_item(PluginManager *pm, GtkWidget *item) +{ + if (!pm || !item) return; + pm->registered_menu_items = g_list_append(pm->registered_menu_items, item); +} + +void plugin_manager_unregister_menu_item(PluginManager *pm, GtkWidget *item) +{ + if (!pm || !item) return; + pm->registered_menu_items = g_list_remove(pm->registered_menu_items, item); +} + +/* ============================================================================ + * Plugin Events + * ============================================================================ */ + +void plugin_manager_broadcast_event(PluginManager *pm, PluginEventType event, void *data) +{ + if (!pm) return; + + for (int i = 0; i < pm->plugin_count; i++) { + LoadedPlugin *p = pm->plugins[i]; + if (p && p->enabled && p->on_event) { + p->on_event(event, data); + } + } +} + +/* ============================================================================ + * Manage Plugins Dialog + * ============================================================================ */ + +static void on_configure_clicked(GtkButton *button, gpointer data) +{ + (void)data; + LoadedPlugin *p = g_object_get_data(G_OBJECT(button), "plugin"); + if (p && p->enabled && p->configure) { + p->configure(); + } +} + +static void on_about_clicked(GtkButton *button, gpointer data) +{ + (void)data; + LoadedPlugin *p = g_object_get_data(G_OBJECT(button), "plugin"); + if (p && p->enabled && p->about) { + p->about(); + } +} + +static void on_uninstall_clicked(GtkButton *button, gpointer data) +{ + GtkWidget *frame = GTK_WIDGET(data); + LoadedPlugin *p = g_object_get_data(G_OBJECT(button), "plugin"); + PluginManager *pm = g_object_get_data(G_OBJECT(button), "pm"); // Need to store PM too + + if (!p || !pm) return; + + GtkWidget *dialog = gtk_message_dialog_new( + NULL, + GTK_DIALOG_MODAL, + GTK_MESSAGE_QUESTION, + GTK_BUTTONS_YES_NO, + "Uninstall plugin '%s'?\nThis cannot be undone.", p->info.name); + + if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_YES) { + gtk_widget_destroy(dialog); + + char *name = g_strdup(p->info.name); + if (plugin_manager_uninstall(pm, p->id)) { + /* Destroy the UI row */ + gtk_widget_destroy(frame); + + GtkWidget *msg = gtk_message_dialog_new( + NULL, + GTK_DIALOG_MODAL, + GTK_MESSAGE_INFO, + GTK_BUTTONS_OK, + "Plugin '%s' uninstalled.\nRestart application to fully clear resources.", name); + gtk_dialog_run(GTK_DIALOG(msg)); + gtk_widget_destroy(msg); + } else { + GtkWidget *msg = gtk_message_dialog_new( + NULL, + GTK_DIALOG_MODAL, + GTK_MESSAGE_ERROR, + GTK_BUTTONS_OK, + "Failed to uninstall plugin '%s'.", name); + gtk_dialog_run(GTK_DIALOG(msg)); + gtk_widget_destroy(msg); + } + g_free(name); + } else { + gtk_widget_destroy(dialog); + } +} + +GtkWidget* plugin_manager_create_config_widget(PluginManager *pm) +{ + if (!pm) return gtk_label_new("Plugin manager not initialized"); + + GtkWidget *vbox = gtk_vbox_new(FALSE, 10); + gtk_container_set_border_width(GTK_CONTAINER(vbox), 10); + + /* Top Toolbar Area */ + GtkWidget *toolbar_hbox = gtk_hbox_new(FALSE, 10); + + GtkWidget *install_btn = gtk_button_new_with_mnemonic("_Install Plugin from ZIP..."); + GtkWidget *install_icon = gtk_image_new_from_stock(GTK_STOCK_ADD, GTK_ICON_SIZE_BUTTON); + gtk_button_set_image(GTK_BUTTON(install_btn), install_icon); + g_signal_connect(install_btn, "clicked", G_CALLBACK(on_install_plugin), pm); + gtk_box_pack_start(GTK_BOX(toolbar_hbox), install_btn, FALSE, FALSE, 0); + + /* Search/Info label */ + char *dir_text = g_strdup_printf("Plugins dir: %s", pm->plugins_dir); + GtkWidget *dir_label = gtk_label_new(NULL); + gtk_label_set_markup(GTK_LABEL(dir_label), dir_text); + gtk_label_set_ellipsize(GTK_LABEL(dir_label), PANGO_ELLIPSIZE_END); + gtk_misc_set_alignment(GTK_MISC(dir_label), 1.0, 0.5); + gtk_box_pack_end(GTK_BOX(toolbar_hbox), dir_label, TRUE, TRUE, 0); + g_free(dir_text); + + gtk_box_pack_start(GTK_BOX(vbox), toolbar_hbox, FALSE, FALSE, 0); + gtk_box_pack_start(GTK_BOX(vbox), gtk_hseparator_new(), FALSE, FALSE, 0); + + GtkWidget *scroll = gtk_scrolled_window_new(NULL, NULL); + gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroll), + GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); + gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(scroll), GTK_SHADOW_NONE); + + GtkWidget *list_vbox = gtk_vbox_new(FALSE, 8); + + if (pm->plugin_count == 0) { + GtkWidget *empty_label = gtk_label_new("No plugins installed."); + gtk_widget_modify_fg(empty_label, GTK_STATE_NORMAL, &(GdkColor){0, 0x8888, 0x8888, 0x8888}); + gtk_box_pack_start(GTK_BOX(list_vbox), empty_label, TRUE, TRUE, 30); + } else { + for (int i = 0; i < pm->plugin_count; i++) { + LoadedPlugin *p = pm->plugins[i]; + if (!p) continue; + + GtkWidget *frame = gtk_frame_new(NULL); + gtk_frame_set_shadow_type(GTK_FRAME(frame), GTK_SHADOW_IN); + + GtkWidget *row_hbox = gtk_hbox_new(FALSE, 12); + gtk_container_set_border_width(GTK_CONTAINER(row_hbox), 12); + + /* Left side: Info */ + GtkWidget *info_vbox = gtk_vbox_new(FALSE, 4); + + char *name_markup = g_strdup_printf("%s v%s", + p->info.name, p->info.version); + GtkWidget *name_label = gtk_label_new(NULL); + gtk_label_set_markup(GTK_LABEL(name_label), name_markup); + gtk_misc_set_alignment(GTK_MISC(name_label), 0, 0.5); + g_free(name_markup); + + GtkWidget *desc_label = gtk_label_new(p->info.description); + gtk_misc_set_alignment(GTK_MISC(desc_label), 0, 0.5); + gtk_label_set_line_wrap(GTK_LABEL(desc_label), TRUE); + gtk_label_set_max_width_chars(GTK_LABEL(desc_label), 50); + + char *author_markup = g_strdup_printf("by %s", p->info.author); + GtkWidget *author_label = gtk_label_new(NULL); + gtk_label_set_markup(GTK_LABEL(author_label), author_markup); + gtk_misc_set_alignment(GTK_MISC(author_label), 0, 0.5); + g_free(author_markup); + + gtk_box_pack_start(GTK_BOX(info_vbox), name_label, FALSE, FALSE, 0); + gtk_box_pack_start(GTK_BOX(info_vbox), desc_label, FALSE, FALSE, 2); + gtk_box_pack_start(GTK_BOX(info_vbox), author_label, FALSE, FALSE, 0); + + /* Right side: Actions */ + GtkWidget *actions_vbox = gtk_vbox_new(FALSE, 4); + + /* Put Configure/About in a small grid or hbox? + Actually, a column of buttons on the right works well if they have uniform width. */ + + if (p->configure) { + GtkWidget *config_btn = gtk_button_new_with_label("Configure"); + g_object_set_data(G_OBJECT(config_btn), "plugin", p); + g_signal_connect(config_btn, "clicked", G_CALLBACK(on_configure_clicked), NULL); + gtk_box_pack_start(GTK_BOX(actions_vbox), config_btn, FALSE, FALSE, 0); + } + + if (p->about) { + GtkWidget *about_btn = gtk_button_new_with_label("About"); + g_object_set_data(G_OBJECT(about_btn), "plugin", p); + g_signal_connect(about_btn, "clicked", G_CALLBACK(on_about_clicked), NULL); + gtk_box_pack_start(GTK_BOX(actions_vbox), about_btn, FALSE, FALSE, 0); + } + + /* Separator if we had configure/about */ + if (p->configure || p->about) { + GtkWidget *spacer = gtk_vbox_new(FALSE, 0); + gtk_widget_set_size_request(spacer, -1, 4); + gtk_box_pack_start(GTK_BOX(actions_vbox), spacer, FALSE, FALSE, 0); + } + + GtkWidget *uninstall_btn = gtk_button_new_from_stock(GTK_STOCK_DELETE); + gtk_button_set_label(GTK_BUTTON(uninstall_btn), "Uninstall"); + g_object_set_data(G_OBJECT(uninstall_btn), "plugin", p); + g_object_set_data(G_OBJECT(uninstall_btn), "pm", pm); + g_signal_connect(uninstall_btn, "clicked", G_CALLBACK(on_uninstall_clicked), frame); + gtk_box_pack_start(GTK_BOX(actions_vbox), uninstall_btn, FALSE, FALSE, 0); + + /* Make all buttons in the actions_vbox the same width */ + GtkSizeGroup *sg = gtk_size_group_new(GTK_SIZE_GROUP_HORIZONTAL); + if (p->configure) gtk_size_group_add_widget(sg, g_list_last(gtk_container_get_children(GTK_CONTAINER(actions_vbox)))->data); // Fix this later + /* Actually it's easier to just iterate the vbox children */ + GList *btns = gtk_container_get_children(GTK_CONTAINER(actions_vbox)); + for (GList *l = btns; l != NULL; l = l->next) { + if (GTK_IS_BUTTON(l->data)) gtk_size_group_add_widget(sg, GTK_WIDGET(l->data)); + } + g_list_free(btns); + g_object_unref(sg); + + gtk_box_pack_start(GTK_BOX(row_hbox), info_vbox, TRUE, TRUE, 0); + gtk_box_pack_end(GTK_BOX(row_hbox), actions_vbox, FALSE, FALSE, 0); + + gtk_container_add(GTK_CONTAINER(frame), row_hbox); + gtk_box_pack_start(GTK_BOX(list_vbox), frame, FALSE, FALSE, 0); + } + } + + gtk_scrolled_window_add_with_viewport(GTK_SCROLLED_WINDOW(scroll), list_vbox); + gtk_box_pack_start(GTK_BOX(vbox), scroll, TRUE, TRUE, 0); + + return vbox; +} + +void plugin_manager_show_dialog(PluginManager *pm) +{ + if (!pm) return; + + GtkWidget *dialog = gtk_dialog_new_with_buttons( + "Manage Plugins", + GTK_WINDOW(ui_get_window(pm->ui)), + GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT, + GTK_STOCK_CLOSE, GTK_RESPONSE_CLOSE, + NULL); + + gtk_window_set_default_size(GTK_WINDOW(dialog), 450, 400); + + GtkWidget *content = gtk_dialog_get_content_area(GTK_DIALOG(dialog)); + GtkWidget *config_widget = plugin_manager_create_config_widget(pm); + gtk_box_pack_start(GTK_BOX(content), config_widget, TRUE, TRUE, 0); + + gtk_widget_show_all(dialog); + gtk_dialog_run(GTK_DIALOG(dialog)); + gtk_widget_destroy(dialog); +} + +const char* plugin_manager_get_plugins_dir(PluginManager *pm) +{ + return pm ? pm->plugins_dir : NULL; +} + +/* ============================================================================ + * API Implementation (callbacks to application) + * ============================================================================ */ + +static void api_play(void) { + if (player_cbs.play) player_cbs.play(); +} + +static void api_pause(void) { + if (player_cbs.pause) player_cbs.pause(); +} + +static void api_stop(void) { + if (player_cbs.stop) player_cbs.stop(); +} + +static void api_toggle_pause(void) { + if (player_cbs.toggle_pause) player_cbs.toggle_pause(); +} + +static void api_seek(double seconds) { + if (player_cbs.seek) player_cbs.seek(seconds); +} + +static void api_seek_absolute(double position) { + if (player_cbs.seek_absolute) player_cbs.seek_absolute(position); +} + +static double api_get_position(void) { + return player_cbs.get_position ? player_cbs.get_position() : 0.0; +} + +static double api_get_duration(void) { + return player_cbs.get_duration ? player_cbs.get_duration() : 0.0; +} + +static double api_get_volume(void) { + return player_cbs.get_volume ? player_cbs.get_volume() : 100.0; +} + +static gboolean api_is_paused(void) { + return player_cbs.is_paused ? player_cbs.is_paused() : TRUE; +} + +static gboolean api_is_playing(void) { + return player_cbs.is_playing ? player_cbs.is_playing() : FALSE; +} + +static char* api_get_current_file(void) { + return player_cbs.get_current_file ? player_cbs.get_current_file() : NULL; +} + +static char* api_get_media_title(void) { + return player_cbs.get_media_title ? player_cbs.get_media_title() : NULL; +} + +static void api_take_screenshot(void) { + if (player_cbs.take_screenshot) player_cbs.take_screenshot(); +} + +static void api_take_screenshot_to_file(const char *path) { + if (player_cbs.take_screenshot_to_file) player_cbs.take_screenshot_to_file(path); +} + +static GtkWindow* api_get_main_window(void) { + if (!g_pm || !g_pm->ui) return NULL; + return GTK_WINDOW(ui_get_window(g_pm->ui)); +} + +static void api_show_info(const char *title, const char *message) { + if (!g_pm || !g_pm->ui) return; + GtkWindow *window = GTK_WINDOW(ui_get_window(g_pm->ui)); + GtkWidget *dialog = gtk_message_dialog_new( + window, GTK_DIALOG_MODAL, GTK_MESSAGE_INFO, GTK_BUTTONS_OK, + "%s", message); + gtk_window_set_title(GTK_WINDOW(dialog), title); + gtk_dialog_run(GTK_DIALOG(dialog)); + gtk_widget_destroy(dialog); +} + +static void api_show_warning(const char *title, const char *message) { + if (!g_pm || !g_pm->ui) return; + GtkWindow *window = GTK_WINDOW(ui_get_window(g_pm->ui)); + GtkWidget *dialog = gtk_message_dialog_new( + window, GTK_DIALOG_MODAL, GTK_MESSAGE_WARNING, GTK_BUTTONS_OK, + "%s", message); + gtk_window_set_title(GTK_WINDOW(dialog), title); + gtk_dialog_run(GTK_DIALOG(dialog)); + gtk_widget_destroy(dialog); +} + +static void api_show_error(const char *title, const char *message) { + if (!g_pm || !g_pm->ui) return; + GtkWindow *window = GTK_WINDOW(ui_get_window(g_pm->ui)); + GtkWidget *dialog = gtk_message_dialog_new( + window, GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, + "%s", message); + gtk_window_set_title(GTK_WINDOW(dialog), title); + gtk_dialog_run(GTK_DIALOG(dialog)); + gtk_widget_destroy(dialog); +} + + +static void api_register_menu_item(GtkWidget *item) { + if (g_pm) plugin_manager_register_menu_item(g_pm, item); +} + +static void api_unregister_menu_item(GtkWidget *item) { + if (g_pm) plugin_manager_unregister_menu_item(g_pm, item); +} + +static void api_set_video_area_visible(gboolean visible) { + if (!g_pm || !g_pm->ui) return; + ui_set_video_area_visible(g_pm->ui, visible); +} + +static void api_set_window_size(int width, int height) { + if (!g_pm || !g_pm->ui) return; + ui_set_window_size(g_pm->ui, width, height); +} + +static void api_execute_mpv_command(const char **args) { + if (!g_pm || !g_pm->ui) return; + Player *player = ui_get_player(g_pm->ui); + if (player) { + mpv_handle *mpv = player_get_mpv_handle(player); + if (mpv) { + mpv_command_async(mpv, 0, args); + } + } +} + diff --git a/src/plugin_manager.h b/src/plugin_manager.h new file mode 100644 index 0000000..dcee0b2 --- /dev/null +++ b/src/plugin_manager.h @@ -0,0 +1,145 @@ +/** + * GTK2 Media Player - Plugin Manager + * + * Internal interface for loading, managing, and unloading plugins. + */ + +#ifndef PLUGIN_MANAGER_H +#define PLUGIN_MANAGER_H + +#include +#include "plugin.h" + +typedef struct PluginManager PluginManager; + +/** + * Loaded plugin instance + */ +typedef struct { + char *id; /* Plugin ID (directory name) */ + char *path; /* Path to .so file */ + void *handle; /* dlopen handle */ + PluginInfo info; /* Plugin metadata */ + gboolean enabled; /* Currently active */ + gboolean has_menu; /* Has menu item */ + GtkWidget *menu_item; /* Registered menu item */ + + /* Function pointers */ + PluginGetInfoFunc get_info; + PluginInitFunc init; + PluginShutdownFunc shutdown; + PluginGetMenuFunc get_menu_item; + PluginOnEventFunc on_event; + PluginConfigureFunc configure; + PluginAboutFunc about; +} LoadedPlugin; + +typedef struct AppUI AppUI; + +/** + * Create a new plugin manager + * + * @param ui Main application UI structure + * @return New plugin manager instance + */ +PluginManager* plugin_manager_new(AppUI *ui); + +/** + * Destroy plugin manager and unload all plugins + */ +void plugin_manager_destroy(PluginManager *pm); + +/** + * Set player control callbacks + * Must be called before loading plugins + */ +void plugin_manager_set_player_callbacks(PluginManager *pm, + void (*play)(void), + void (*pause)(void), + void (*stop)(void), + void (*toggle_pause)(void), + void (*seek)(double), + void (*seek_absolute)(double), + double (*get_position)(void), + double (*get_duration)(void), + double (*get_volume)(void), + gboolean (*is_paused)(void), + gboolean (*is_playing)(void), + char* (*get_current_file)(void), + char* (*get_media_title)(void), + void (*take_screenshot)(void), + void (*take_screenshot_to_file)(const char*) +); + +/** + * Load all plugins from the plugins directory + * + * @return Number of plugins loaded + */ +int plugin_manager_load_all(PluginManager *pm); + +/** + * Install a plugin from a .zip file + * + * @param zip_path Path to the .zip file + * @return TRUE on success, FALSE on failure + */ +gboolean plugin_manager_install_zip(PluginManager *pm, const char *zip_path); + +/** + * Uninstall a plugin + * + * @param plugin_id Plugin ID to uninstall + * @return TRUE on success, FALSE on failure + */ +gboolean plugin_manager_uninstall(PluginManager *pm, const char *plugin_id); + +/** + * Get list of loaded plugins + * + * @param count Output: number of plugins + * @return Array of LoadedPlugin pointers (do not free) + */ +LoadedPlugin** plugin_manager_get_list(PluginManager *pm, int *count); + +/** + * Get the plugins menu + * Creates menu items for all loaded plugins + * + * @return GtkMenu widget + */ +GtkWidget* plugin_manager_get_menu(PluginManager *pm); + +/** + * Register a menu item from a plugin + */ +void plugin_manager_register_menu_item(PluginManager *pm, GtkWidget *item); + +/** + * Unregister a menu item from a plugin + */ +void plugin_manager_unregister_menu_item(PluginManager *pm, GtkWidget *item); + +/** + * Broadcast an event to all loaded plugins + */ +void plugin_manager_broadcast_event(PluginManager *pm, PluginEventType event, void *data); + +/** + * Create configuration widget for preferences dialog + * Returns a GtkWidget (VBox) containing the plugin management UI + */ +GtkWidget* plugin_manager_create_config_widget(PluginManager *pm); + +/** + * Show plugin management dialog (Deprecated in favor of Preferences) + */ +void plugin_manager_show_dialog(PluginManager *pm); + +/** + * Get the plugins directory path + * @return Path string (do not free) + */ +const char* plugin_manager_get_plugins_dir(PluginManager *pm); + +#endif /* PLUGIN_MANAGER_H */ diff --git a/src/ui.c b/src/ui.c index 6e51320..38388f4 100644 --- a/src/ui.c +++ b/src/ui.c @@ -6,6 +6,28 @@ #include #include #include "mpris.h" +#include "plugin_manager.h" + +/* Forward declarations */ +static void on_playback_prev(GtkMenuItem *item, gpointer data); +static void on_playback_next(GtkMenuItem *item, gpointer data); +static void on_playback_seek_forward(GtkMenuItem *item, gpointer data); +static void on_playback_seek_backward(GtkMenuItem *item, gpointer data); +static void on_playback_frame_step(GtkMenuItem *item, gpointer data); +static void on_playback_goto_time(GtkMenuItem *item, gpointer data); +static void on_playback_speed(GtkMenuItem *item, gpointer data); +static void on_playback_ab_loop_a(GtkMenuItem *item, gpointer data); +static void on_playback_ab_loop_b(GtkMenuItem *item, gpointer data); +static void on_playback_ab_loop_clear(GtkMenuItem *item, gpointer data); +static void on_audio_track_selected(GtkMenuItem *item, gpointer data); +static void on_subtitle_track_selected(GtkMenuItem *item, gpointer data); +static void on_subtitle_load(GtkMenuItem *item, gpointer data); +static void on_subtitle_disable(GtkMenuItem *item, gpointer data); +static void on_view_fullscreen(GtkMenuItem *item, gpointer data); +static void on_view_playlist(GtkMenuItem *item, gpointer data); +static void on_view_screenshot(GtkMenuItem *item, gpointer data); +static void on_open_scripts_folder(GtkMenuItem *item, gpointer data); +static void on_help_more_software(GtkMenuItem *item, gpointer data); /* Property observation user data IDs */ enum { @@ -74,6 +96,9 @@ struct AppUI { /* MPRIS state tracking */ double last_mpris_pos; + + /* Plugin manager */ + PluginManager *plugin_manager; }; /* Forward declarations */ @@ -122,6 +147,24 @@ static void on_view_preferences(GtkMenuItem *item, gpointer data); static void on_aspect_ratio_selected(GtkMenuItem *item, gpointer data); static void on_help_about(GtkMenuItem *item, gpointer data); +/* Plugin API wrapper forward declarations */ +static AppUI *g_current_ui; +static void plugin_api_play(void); +static void plugin_api_pause(void); +static void plugin_api_stop(void); +static void plugin_api_toggle_pause(void); +static void plugin_api_seek(double seconds); +static void plugin_api_seek_absolute(double position); +static double plugin_api_get_position(void); +static double plugin_api_get_duration(void); +static double plugin_api_get_volume(void); +static gboolean plugin_api_is_paused(void); +static gboolean plugin_api_is_playing(void); +static char* plugin_api_get_current_file(void); +static char* plugin_api_get_media_title(void); +static void plugin_api_take_screenshot(void); +static void plugin_api_take_screenshot_to_file(const char *path); + AppUI* ui_new(void) { AppUI *ui = g_new0(AppUI, 1); @@ -158,6 +201,30 @@ AppUI* ui_new(void) ui->main_vbox = gtk_vbox_new(FALSE, 0); gtk_container_add(GTK_CONTAINER(ui->window), ui->main_vbox); + /* Initialize plugin manager (must be before create_menubar) */ + ui->plugin_manager = plugin_manager_new(ui); + + /* Set up plugin API callbacks and load plugins BEFORE creating menu */ + g_current_ui = ui; + plugin_manager_set_player_callbacks(ui->plugin_manager, + plugin_api_play, + plugin_api_pause, + plugin_api_stop, + plugin_api_toggle_pause, + plugin_api_seek, + plugin_api_seek_absolute, + plugin_api_get_position, + plugin_api_get_duration, + plugin_api_get_volume, + plugin_api_is_paused, + plugin_api_is_playing, + plugin_api_get_current_file, + plugin_api_get_media_title, + plugin_api_take_screenshot, + plugin_api_take_screenshot_to_file + ); + plugin_manager_load_all(ui->plugin_manager); + /* Menu bar */ create_menubar(ui); gtk_box_pack_start(GTK_BOX(ui->main_vbox), ui->menubar, FALSE, FALSE, 0); @@ -205,7 +272,7 @@ AppUI* ui_new(void) } /* Controls */ - ui->controls = controls_new(ui->player); + ui->controls = controls_new(ui->player, ui->prefs); controls_set_fullscreen_callback(ui->controls, on_fullscreen_requested, ui); controls_set_play_pause_callback(ui->controls, on_controls_play_pause, ui); gtk_box_pack_start(GTK_BOX(ui->main_vbox), controls_get_widget(ui->controls), FALSE, FALSE, 0); @@ -213,9 +280,22 @@ AppUI* ui_new(void) /* Set player event callback */ player_set_event_callback(ui->player, on_player_event, ui); + /* Set mpv config path */ + if (ui->prefs->mpv_config_path) { + player_set_config_path(ui->player, ui->prefs->mpv_config_path); + } + /* Set default volume */ player_set_volume(ui->player, ui->prefs->default_volume); + /* Set initial OSD and focus settings */ + player_set_option_string(ui->player, "osd-level", ui->prefs->enable_osd ? "1" : "0"); + player_set_option_string(ui->player, "osd-bar", ui->prefs->enable_osd ? "yes" : "no"); + + if (ui->prefs->hide_focus_rect) { + gtk_rc_parse_string("style \"no-focus-rect\" { GtkWidget::focus-line-width = 0 GtkWidget::focus-padding = 0 } widget \"*\" style \"no-focus-rect\""); + } + ui->is_fullscreen = FALSE; ui->always_on_top = FALSE; ui->auto_hide_timer_id = 0; @@ -250,6 +330,9 @@ void ui_destroy(AppUI *ui) mpris_destroy(ui); + /* Destroy plugin manager */ + plugin_manager_destroy(ui->plugin_manager); + if (ui->fs_window) { gtk_widget_destroy(ui->fs_window); } @@ -274,6 +357,10 @@ void ui_set_preferences(AppUI *ui, Preferences *prefs) /* Apply some immediate settings */ if (ui->player) { player_set_volume(ui->player, ui->prefs->default_volume); + + if (ui->prefs->screenshot_directory) { + player_set_option_string(ui->player, "screenshot-directory", ui->prefs->screenshot_directory); + } } /* Update playlist visibility if UI is already realized */ @@ -305,6 +392,81 @@ Playlist* ui_get_playlist(AppUI *ui) return ui->playlist; } +PluginManager* ui_get_plugin_manager(AppUI *ui) +{ + if (!ui) return NULL; + return ui->plugin_manager; +} + +/* Static wrapper functions for plugin API */ + +static void plugin_api_play(void) { + if (g_current_ui && g_current_ui->player) player_play(g_current_ui->player); +} + +static void plugin_api_pause(void) { + if (g_current_ui && g_current_ui->player) player_pause(g_current_ui->player); +} + +static void plugin_api_stop(void) { + if (g_current_ui && g_current_ui->player) player_stop(g_current_ui->player); +} + +static void plugin_api_toggle_pause(void) { + if (g_current_ui && g_current_ui->player) player_toggle_pause(g_current_ui->player); +} + +static void plugin_api_seek(double seconds) { + if (g_current_ui && g_current_ui->player) player_seek(g_current_ui->player, seconds); +} + +static void plugin_api_seek_absolute(double position) { + if (g_current_ui && g_current_ui->player) player_seek_absolute(g_current_ui->player, position); +} + +static double plugin_api_get_position(void) { + if (g_current_ui && g_current_ui->player) return player_get_position(g_current_ui->player); + return 0.0; +} + +static double plugin_api_get_duration(void) { + if (g_current_ui && g_current_ui->player) return player_get_duration(g_current_ui->player); + return 0.0; +} + +static double plugin_api_get_volume(void) { + if (g_current_ui && g_current_ui->player) return player_get_volume(g_current_ui->player); + return 100.0; +} + +static gboolean plugin_api_is_paused(void) { + if (g_current_ui && g_current_ui->player) return player_get_paused(g_current_ui->player); + return TRUE; +} + +static gboolean plugin_api_is_playing(void) { + if (g_current_ui && g_current_ui->player) return !player_get_paused(g_current_ui->player) && !player_is_idle(g_current_ui->player); + return FALSE; +} + +static char* plugin_api_get_current_file(void) { + if (g_current_ui && g_current_ui->player) return player_get_filename(g_current_ui->player); + return NULL; +} + +static char* plugin_api_get_media_title(void) { + if (g_current_ui && g_current_ui->player) return player_get_media_title(g_current_ui->player); + return NULL; +} + +static void plugin_api_take_screenshot(void) { + if (g_current_ui && g_current_ui->player) player_screenshot(g_current_ui->player); +} + +static void plugin_api_take_screenshot_to_file(const char *path) { + if (g_current_ui && g_current_ui->player) player_screenshot_to_file(g_current_ui->player, path); +} + void ui_run(AppUI *ui) { (void)ui; @@ -1037,6 +1199,9 @@ static void create_menubar(AppUI *ui) { ui->menubar = gtk_menu_bar_new(); + GtkAccelGroup *accel_group = gtk_accel_group_new(); + gtk_window_add_accel_group(GTK_WINDOW(ui->window), accel_group); + /* File Menu */ GtkWidget *file_menu = gtk_menu_new(); GtkWidget *file_item = gtk_menu_item_new_with_mnemonic("_File"); @@ -1044,6 +1209,7 @@ static void create_menubar(AppUI *ui) GtkWidget *open_item = gtk_image_menu_item_new_from_stock(GTK_STOCK_OPEN, NULL); g_signal_connect(open_item, "activate", G_CALLBACK(on_file_open), ui); + gtk_widget_add_accelerator(open_item, "activate", accel_group, GDK_o, GDK_CONTROL_MASK, GTK_ACCEL_VISIBLE); gtk_menu_shell_append(GTK_MENU_SHELL(file_menu), open_item); GtkWidget *open_dir_item = gtk_menu_item_new_with_mnemonic("Open _Folder..."); @@ -1067,10 +1233,10 @@ static void create_menubar(AppUI *ui) GtkWidget *open_url_item = gtk_menu_item_new_with_mnemonic("Open _URL..."); g_signal_connect(open_url_item, "activate", G_CALLBACK(on_file_open_url), ui); + gtk_widget_add_accelerator(open_url_item, "activate", accel_group, GDK_l, GDK_CONTROL_MASK, GTK_ACCEL_VISIBLE); gtk_menu_shell_append(GTK_MENU_SHELL(file_menu), open_url_item); - GtkWidget *save_playlist_item = gtk_image_menu_item_new_from_stock(GTK_STOCK_SAVE, NULL); - gtk_menu_item_set_label(GTK_MENU_ITEM(save_playlist_item), "Save _Playlist..."); + GtkWidget *save_playlist_item = gtk_menu_item_new_with_mnemonic("Save _Playlist..."); g_signal_connect(save_playlist_item, "activate", G_CALLBACK(on_file_save_playlist), ui); gtk_menu_shell_append(GTK_MENU_SHELL(file_menu), save_playlist_item); @@ -1078,6 +1244,9 @@ static void create_menubar(AppUI *ui) GtkWidget *quit_item = gtk_image_menu_item_new_from_stock(GTK_STOCK_QUIT, NULL); g_signal_connect(quit_item, "activate", G_CALLBACK(on_file_quit), ui); + gtk_widget_add_accelerator(quit_item, "activate", accel_group, GDK_q, GDK_CONTROL_MASK, GTK_ACCEL_VISIBLE); + /* Also Q key */ + gtk_widget_add_accelerator(quit_item, "activate", accel_group, GDK_q, 0, 0); gtk_menu_shell_append(GTK_MENU_SHELL(file_menu), quit_item); gtk_menu_shell_append(GTK_MENU_SHELL(ui->menubar), file_item); @@ -1087,30 +1256,49 @@ static void create_menubar(AppUI *ui) GtkWidget *playback_item = gtk_menu_item_new_with_mnemonic("_Playback"); gtk_menu_item_set_submenu(GTK_MENU_ITEM(playback_item), playback_menu); - GtkWidget *play_pause_item = gtk_menu_item_new_with_mnemonic("_Play/Pause"); + GtkWidget *play_pause_item = gtk_image_menu_item_new_from_stock(GTK_STOCK_MEDIA_PLAY, NULL); + gtk_menu_item_set_label(GTK_MENU_ITEM(play_pause_item), "_Play/Pause"); g_signal_connect(play_pause_item, "activate", G_CALLBACK(on_playback_play_pause), ui); + gtk_widget_add_accelerator(play_pause_item, "activate", accel_group, GDK_space, 0, GTK_ACCEL_VISIBLE); gtk_menu_shell_append(GTK_MENU_SHELL(playback_menu), play_pause_item); - GtkWidget *stop_item = gtk_menu_item_new_with_mnemonic("_Stop"); + GtkWidget *stop_item = gtk_image_menu_item_new_from_stock(GTK_STOCK_MEDIA_STOP, NULL); + gtk_menu_item_set_label(GTK_MENU_ITEM(stop_item), "_Stop"); g_signal_connect(stop_item, "activate", G_CALLBACK(on_playback_stop), ui); gtk_menu_shell_append(GTK_MENU_SHELL(playback_menu), stop_item); gtk_menu_shell_append(GTK_MENU_SHELL(playback_menu), gtk_separator_menu_item_new()); + GtkWidget *prev_item = gtk_image_menu_item_new_from_stock(GTK_STOCK_MEDIA_PREVIOUS, NULL); + g_signal_connect(prev_item, "activate", G_CALLBACK(on_playback_prev), ui); + gtk_widget_add_accelerator(prev_item, "activate", accel_group, GDK_Page_Up, 0, GTK_ACCEL_VISIBLE); + gtk_menu_shell_append(GTK_MENU_SHELL(playback_menu), prev_item); + + GtkWidget *next_item = gtk_image_menu_item_new_from_stock(GTK_STOCK_MEDIA_NEXT, NULL); + g_signal_connect(next_item, "activate", G_CALLBACK(on_playback_next), ui); + gtk_widget_add_accelerator(next_item, "activate", accel_group, GDK_Page_Down, 0, GTK_ACCEL_VISIBLE); + gtk_menu_shell_append(GTK_MENU_SHELL(playback_menu), next_item); + + gtk_menu_shell_append(GTK_MENU_SHELL(playback_menu), gtk_separator_menu_item_new()); + GtkWidget *seek_fwd_item = gtk_menu_item_new_with_mnemonic("Seek _Forward 10s"); g_signal_connect(seek_fwd_item, "activate", G_CALLBACK(on_playback_seek_forward), ui); + gtk_widget_add_accelerator(seek_fwd_item, "activate", accel_group, GDK_Right, 0, GTK_ACCEL_VISIBLE); gtk_menu_shell_append(GTK_MENU_SHELL(playback_menu), seek_fwd_item); GtkWidget *seek_back_item = gtk_menu_item_new_with_mnemonic("Seek _Backward 10s"); g_signal_connect(seek_back_item, "activate", G_CALLBACK(on_playback_seek_backward), ui); + gtk_widget_add_accelerator(seek_back_item, "activate", accel_group, GDK_Left, 0, GTK_ACCEL_VISIBLE); gtk_menu_shell_append(GTK_MENU_SHELL(playback_menu), seek_back_item); GtkWidget *frame_step_item = gtk_menu_item_new_with_mnemonic("_Frame Step"); g_signal_connect(frame_step_item, "activate", G_CALLBACK(on_playback_frame_step), ui); + gtk_widget_add_accelerator(frame_step_item, "activate", accel_group, GDK_period, 0, GTK_ACCEL_VISIBLE); gtk_menu_shell_append(GTK_MENU_SHELL(playback_menu), frame_step_item); GtkWidget *goto_time_item = gtk_menu_item_new_with_mnemonic("_Go to Time..."); g_signal_connect(goto_time_item, "activate", G_CALLBACK(on_playback_goto_time), ui); + gtk_widget_add_accelerator(goto_time_item, "activate", accel_group, GDK_g, GDK_CONTROL_MASK, GTK_ACCEL_VISIBLE); gtk_menu_shell_append(GTK_MENU_SHELL(playback_menu), goto_time_item); gtk_menu_shell_append(GTK_MENU_SHELL(playback_menu), gtk_separator_menu_item_new()); @@ -1176,11 +1364,14 @@ static void create_menubar(AppUI *ui) GtkWidget *fullscreen_item = gtk_menu_item_new_with_mnemonic("_Fullscreen"); g_signal_connect(fullscreen_item, "activate", G_CALLBACK(on_view_fullscreen), ui); + gtk_widget_add_accelerator(fullscreen_item, "activate", accel_group, GDK_f, 0, GTK_ACCEL_VISIBLE); + gtk_widget_add_accelerator(fullscreen_item, "activate", accel_group, GDK_F11, 0, GTK_ACCEL_VISIBLE); gtk_menu_shell_append(GTK_MENU_SHELL(view_menu), fullscreen_item); GtkWidget *playlist_toggle_item = gtk_check_menu_item_new_with_mnemonic("_Playlist"); gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(playlist_toggle_item), ui->prefs->show_playlist); g_signal_connect(playlist_toggle_item, "toggled", G_CALLBACK(on_view_playlist), ui); + gtk_widget_add_accelerator(playlist_toggle_item, "activate", accel_group, GDK_p, GDK_CONTROL_MASK, GTK_ACCEL_VISIBLE); gtk_menu_shell_append(GTK_MENU_SHELL(view_menu), playlist_toggle_item); GtkWidget *on_top_item = gtk_check_menu_item_new_with_mnemonic("Always on _Top"); @@ -1210,6 +1401,7 @@ static void create_menubar(AppUI *ui) GtkWidget *screenshot_item = gtk_menu_item_new_with_mnemonic("_Screenshot"); g_signal_connect(screenshot_item, "activate", G_CALLBACK(on_view_screenshot), ui); + gtk_widget_add_accelerator(screenshot_item, "activate", accel_group, GDK_s, GDK_CONTROL_MASK | GDK_SHIFT_MASK, GTK_ACCEL_VISIBLE); gtk_menu_shell_append(GTK_MENU_SHELL(view_menu), screenshot_item); gtk_menu_shell_append(GTK_MENU_SHELL(view_menu), gtk_separator_menu_item_new()); @@ -1220,6 +1412,12 @@ static void create_menubar(AppUI *ui) gtk_menu_shell_append(GTK_MENU_SHELL(ui->menubar), view_item); + /* Plugins Menu */ + GtkWidget *plugins_menu = plugin_manager_get_menu(ui->plugin_manager); + GtkWidget *plugins_item = gtk_menu_item_new_with_mnemonic("P_lugins"); + gtk_menu_item_set_submenu(GTK_MENU_ITEM(plugins_item), plugins_menu); + gtk_menu_shell_append(GTK_MENU_SHELL(ui->menubar), plugins_item); + /* Help Menu */ GtkWidget *help_menu = gtk_menu_new(); GtkWidget *help_item = gtk_menu_item_new_with_mnemonic("_Help"); @@ -1229,6 +1427,13 @@ static void create_menubar(AppUI *ui) g_signal_connect(about_item, "activate", G_CALLBACK(on_help_about), ui); gtk_menu_shell_append(GTK_MENU_SHELL(help_menu), about_item); + GtkWidget *sep = gtk_separator_menu_item_new(); + gtk_menu_shell_append(GTK_MENU_SHELL(help_menu), sep); + + GtkWidget *more_item = gtk_menu_item_new_with_label("More GTK2 Software..."); + g_signal_connect(more_item, "activate", G_CALLBACK(on_help_more_software), ui); + gtk_menu_shell_append(GTK_MENU_SHELL(help_menu), more_item); + gtk_menu_shell_append(GTK_MENU_SHELL(ui->menubar), help_item); } @@ -1319,6 +1524,20 @@ static void on_playback_stop(GtkMenuItem *item, gpointer data) ui_update_title(ui, "GTK2 Media Player"); } +static void on_playback_prev(GtkMenuItem *item, gpointer data) +{ + (void)item; + AppUI *ui = (AppUI *)data; + playlist_play_prev(ui->playlist); +} + +static void on_playback_next(GtkMenuItem *item, gpointer data) +{ + (void)item; + AppUI *ui = (AppUI *)data; + playlist_play_next(ui->playlist); +} + static void on_playback_seek_forward(GtkMenuItem *item, gpointer data) { (void)item; @@ -1440,19 +1659,15 @@ static void on_view_screenshot(GtkMenuItem *item, gpointer data) { (void)item; AppUI *ui = (AppUI *)data; - - char *path = dialogs_save_screenshot(GTK_WINDOW(ui->window)); - if (path) { - player_screenshot_to_file(ui->player, path); - g_free(path); - } + /* Use instant screenshot (saves to configured directory) */ + player_screenshot(ui->player); } static void on_view_preferences(GtkMenuItem *item, gpointer data) { (void)item; AppUI *ui = (AppUI *)data; - if (dialogs_show_preferences(GTK_WINDOW(ui->window), ui->prefs)) { + if (dialogs_show_preferences(GTK_WINDOW(ui->window), ui->prefs, ui->plugin_manager)) { /* Apply changes immediately */ if (!ui->is_fullscreen) { if (ui->prefs->hide_cursor_windowed) { @@ -1470,6 +1685,99 @@ static void on_view_preferences(GtkMenuItem *item, gpointer data) gtk_widget_show(controls_get_widget(ui->controls)); } } + + /* Apply Focus Rect setting (GTK2 specific hack) */ + if (ui->prefs->hide_focus_rect) { + gtk_rc_parse_string("style \"no-focus-rect\" { GtkWidget::focus-line-width = 0 GtkWidget::focus-padding = 0 } widget \"*\" style \"no-focus-rect\""); + } else { + gtk_rc_parse_string("style \"default-focus-rect\" { GtkWidget::focus-line-width = 1 GtkWidget::focus-padding = 1 } widget \"*\" style \"default-focus-rect\""); + } + gtk_widget_reset_rc_styles(ui->window); + gtk_widget_queue_draw(ui->window); + + /* Apply OSD setting */ + player_set_option_string(ui->player, "osd-level", ui->prefs->enable_osd ? "1" : "0"); + player_set_option_string(ui->player, "osd-bar", ui->prefs->enable_osd ? "yes" : "no"); + + /* Apply mpv.conf if changed - parse and apply each option */ + if (ui->prefs->mpv_config_path && g_file_test(ui->prefs->mpv_config_path, G_FILE_TEST_EXISTS)) { + /* First, reset common properties to defaults to avoid "bleeding" from previous configs */ + player_set_option_string(ui->player, "brightness", "0"); + player_set_option_string(ui->player, "contrast", "0"); + player_set_option_string(ui->player, "saturation", "0"); + player_set_option_string(ui->player, "gamma", "0"); + player_set_option_string(ui->player, "hue", "0"); + + player_set_option_string(ui->player, "osd-color", "#FFFFFFFF"); + player_set_option_string(ui->player, "osd-border-color", "#FF000000"); + player_set_option_string(ui->player, "osd-shadow-color", "#80000000"); + player_set_option_string(ui->player, "osd-font-size", "55"); + player_set_option_string(ui->player, "osd-border-size", "3"); + player_set_option_string(ui->player, "osd-shadow-offset", "0"); + + FILE *fp = fopen(ui->prefs->mpv_config_path, "r"); + if (fp) { + char line[512]; + int applied = 0; + while (fgets(line, sizeof(line), fp)) { + /* Strip newline */ + char *nl = strchr(line, '\n'); + if (nl) *nl = '\0'; + + /* Skip comments and empty lines */ + char *p = line; + while (*p == ' ' || *p == '\t') p++; + if (*p == '#' || *p == '\0') continue; + + /* Find '=' separator */ + char *eq = strchr(p, '='); + if (eq) { + *eq = '\0'; + char *key = p; + char *value = eq + 1; + + /* Trim trailing spaces from key */ + char *k_end = key + strlen(key) - 1; + while (k_end > key && (*k_end == ' ' || *k_end == '\t')) *k_end-- = '\0'; + + /* Trim leading spaces from value */ + while (*value == ' ' || *value == '\t') value++; + + /* Remove surrounding quotes if present */ + size_t vlen = strlen(value); + if (vlen >= 2 && ((value[0] == '\'' && value[vlen-1] == '\'') || + (value[0] == '"' && value[vlen-1] == '"'))) { + value[vlen-1] = '\0'; + value++; + } + + player_set_option_string(ui->player, key, value); + applied++; + } + } + fclose(fp); + + /* Show feedback with count */ + char msg[64]; + snprintf(msg, sizeof(msg), "Applied %d config options", applied); + const char *msg_cmd[] = {"show-text", msg, "2000", NULL}; + mpv_command_async(player_get_mpv_handle(ui->player), 0, msg_cmd); + } + } + + /* Apply Screenshot Format */ + if (ui->prefs->screenshot_format) { + player_set_option_string(ui->player, "screenshot-format", ui->prefs->screenshot_format); + } + + /* Apply Screenshot Dir */ + if (ui->prefs->screenshot_directory) { + player_set_option_string(ui->player, "screenshot-directory", ui->prefs->screenshot_directory); + } + + /* Show feedback */ + const char *saved_cmd[] = {"show-text", "Preferences saved", "1500", NULL}; + mpv_command_async(player_get_mpv_handle(ui->player), 0, saved_cmd); } } @@ -1487,6 +1795,17 @@ static void on_help_about(GtkMenuItem *item, gpointer data) dialogs_show_about(GTK_WINDOW(ui->window)); } +static void on_help_more_software(GtkMenuItem *item, gpointer data) +{ + (void)item; + (void)data; + const char *url = "https://lakiweb.net/article/gtk2/software-directory.html"; + char *cmd = g_strdup_printf("xdg-open '%s'", url); + int ret = system(cmd); + (void)ret; + g_free(cmd); +} + static void on_controls_play_pause(gpointer data) { on_playback_play_pause(NULL, data); @@ -1513,3 +1832,71 @@ static void on_recent_item_activated(GtkRecentChooser *chooser, gpointer data) g_free(uri); } } + +/* Plugin UI Helpers */ +void ui_set_video_area_visible(AppUI *ui, gboolean visible) +{ + if (!ui || !ui->video_container) return; + + if (visible) { + gtk_widget_show(ui->video_container); + gtk_widget_set_no_show_all(ui->video_container, FALSE); + } else { + gtk_widget_hide(ui->video_container); + gtk_widget_set_no_show_all(ui->video_container, TRUE); + } +} + +void ui_set_window_size(AppUI *ui, int width, int height) +{ + if (!ui || !ui->window) return; + gtk_window_resize(GTK_WINDOW(ui->window), width, height); +} + + +void ui_restart(AppUI *ui) +{ + (void)ui; + + /* Get the path to the current executable */ + char *bin_path = NULL; + + /* If we are installed, use the installed path */ + if (g_file_test("/usr/local/bin/gtk2-mpv-player", G_FILE_TEST_EXISTS)) { + bin_path = g_strdup("/usr/local/bin/gtk2-mpv-player"); + } else { + /* Otherwise try to find it relative to current dir */ + bin_path = g_build_filename(".", "gtk2-mpv-player", NULL); + } + + printf("Restarting application via %s...\n", bin_path); + + char *argv[] = {bin_path, NULL}; + execvp(bin_path, argv); + + /* If execvp fails */ + perror("execvp"); + g_free(bin_path); +} + + +void ui_open_scripts_folder(AppUI *ui) +{ + (void)ui; + + const char *config_dir = g_get_user_config_dir(); + char *scripts_dir = g_build_filename(config_dir, "gtk2-media-player", "scripts", NULL); + + /* Ensure it exists */ + g_mkdir_with_parents(scripts_dir, 0755); + + printf("Opening scripts directory: %s\n", scripts_dir); + + char *cmd = g_strdup_printf("xdg-open '%s'", scripts_dir); + int ret = system(cmd); + (void)ret; /* Suppress unused result warning */ + + g_free(cmd); + g_free(scripts_dir); +} + diff --git a/src/ui.h b/src/ui.h index 9927295..fd3a5ed 100644 --- a/src/ui.h +++ b/src/ui.h @@ -23,6 +23,11 @@ Player* ui_get_player(AppUI *ui); /* Get playlist */ Playlist* ui_get_playlist(AppUI *ui); +/* Get plugin manager */ +struct PluginManager; +typedef struct PluginManager PluginManager; +PluginManager* ui_get_plugin_manager(AppUI *ui); + /* Run the application */ void ui_run(AppUI *ui); @@ -39,4 +44,13 @@ void ui_toggle_always_on_top(AppUI *ui); /* Update window title */ void ui_update_title(AppUI *ui, const char *media_title); +/* Plugin UI Access */ +void ui_set_video_area_visible(AppUI *ui, gboolean visible); +void ui_set_window_size(AppUI *ui, int width, int height); +Player* ui_get_player(AppUI *ui); /* Helper to get player from UI */ + +/* Application control */ +void ui_restart(AppUI *ui); +void ui_open_scripts_folder(AppUI *ui); + #endif /* UI_H */ diff --git a/src/ui_callbacks.h b/src/ui_callbacks.h new file mode 100644 index 0000000..4d4d666 --- /dev/null +++ b/src/ui_callbacks.h @@ -0,0 +1,19 @@ + +/* Forward declarations for callbacks used in menu */ +static void on_playback_prev(GtkMenuItem *item, gpointer data); +static void on_playback_next(GtkMenuItem *item, gpointer data); +static void on_playback_seek_forward(GtkMenuItem *item, gpointer data); +static void on_playback_seek_backward(GtkMenuItem *item, gpointer data); +static void on_playback_frame_step(GtkMenuItem *item, gpointer data); +static void on_playback_goto_time(GtkMenuItem *item, gpointer data); +static void on_playback_speed(GtkMenuItem *item, gpointer data); +static void on_playback_ab_loop_a(GtkMenuItem *item, gpointer data); +static void on_playback_ab_loop_b(GtkMenuItem *item, gpointer data); +static void on_playback_ab_loop_clear(GtkMenuItem *item, gpointer data); +static void on_audio_track_selected(GtkMenuItem *item, gpointer data); +static void on_subtitle_track_selected(GtkMenuItem *item, gpointer data); +static void on_subtitle_load(GtkMenuItem *item, gpointer data); +static void on_subtitle_disable(GtkMenuItem *item, gpointer data); +static void on_view_fullscreen(GtkMenuItem *item, gpointer data); +static void on_view_playlist(GtkMenuItem *item, gpointer data); +static void on_view_screenshot(GtkMenuItem *item, gpointer data);