#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; } Player* plugin_manager_get_player(PluginManager *pm) { if (!pm || !pm->ui) return NULL; return ui_get_player(pm->ui); } 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); /* Tools / Plugins menu doesn't need script management anymore as it's in Preferences */ /* 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); } } }