1031 lines
35 KiB
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);
|
|
}
|
|
}
|
|
}
|
|
|