kino/src/plugin_manager.c

1031 lines
35 KiB
C

#define _XOPEN_SOURCE 500
/**
* GTK2 Media Player - Plugin Manager Implementation
*/
#include "plugin_manager.h"
#include "ui.h"
#include <dlfcn.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <dirent.h>
#include <sys/stat.h>
#include <unistd.h>
#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("<small>Plugins dir: %s</small>", 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("<span size='large' weight='bold'>%s</span> <span color='#888'>v%s</span>",
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("<span size='small' color='#666'>by %s</span>", 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);
}
}
}