Added proper plugin system (plugins will be released in a separate repository), added experimental scripting support (Lua, JavaScript)

This commit is contained in:
laki 2026-02-16 12:58:43 +00:00
parent 60255275eb
commit 72f3f88369
18 changed files with 2145 additions and 69 deletions

14
.gitignore vendored
View File

@ -1,2 +1,16 @@
src/dialogs.o src/dialogs.o
gtk2-mpv-player gtk2-mpv-player
mpv.conf
test_grayscale.conf
test_vibrant.conf
plugins/audio_player.zip
.gitignore
plugins/screenshot_timer.zip
src/plugin_manager.o
src/playlist.o
src/player.o
src/mpv_loader.o
src/mpris.o
src/main.o
src/controls.o
src/ui.o

View File

@ -1,5 +1,5 @@
CFLAGS = $(shell pkg-config --cflags gtk+-2.0 gio-2.0) -Wall -Wextra -g -std=c99 CFLAGS = $(shell pkg-config --cflags gtk+-2.0 gio-2.0) -Wall -Wextra -g -std=c99
LDFLAGS = $(shell pkg-config --libs gtk+-2.0 gio-2.0) -ldl -lm -Wl,--as-needed -Wl,-rpath,./lib LDFLAGS = $(shell pkg-config --libs gtk+-2.0 gio-2.0) -ldl -lm -Wl,--as-needed -Wl,-rpath,./lib -rdynamic
SRC_DIR = src SRC_DIR = src
SRCS = $(SRC_DIR)/main.c \ SRCS = $(SRC_DIR)/main.c \
@ -9,7 +9,8 @@ SRCS = $(SRC_DIR)/main.c \
$(SRC_DIR)/playlist.c \ $(SRC_DIR)/playlist.c \
$(SRC_DIR)/dialogs.c \ $(SRC_DIR)/dialogs.c \
$(SRC_DIR)/mpv_loader.c \ $(SRC_DIR)/mpv_loader.c \
$(SRC_DIR)/mpris.c $(SRC_DIR)/mpris.c \
$(SRC_DIR)/plugin_manager.c
OBJS = $(SRCS:.c=.o) OBJS = $(SRCS:.c=.o)
TARGET = gtk2-mpv-player TARGET = gtk2-mpv-player

View File

@ -16,9 +16,12 @@ echo "Installing to $INSTALL_DIR..."
# Create directory # Create directory
mkdir -p "$INSTALL_DIR" mkdir -p "$INSTALL_DIR"
# Copy binary and metadata # Copy binary and metadata using 'install' to handle running processes correctly
cp gtk2-mpv-player "$INSTALL_DIR/" install -m 755 gtk2-mpv-player "$INSTALL_DIR/"
cp assets/icon.png "$INSTALL_DIR/" install -m 644 assets/icon.png "$INSTALL_DIR/"
# For directory copy, we use cp but remove destination first to ensure clean update
rm -rf "$INSTALL_DIR/lib"
cp -r lib "$INSTALL_DIR/" cp -r lib "$INSTALL_DIR/"
# Create wrapper script # Create wrapper script
@ -39,4 +42,5 @@ echo "Updating MIME database..."
update-desktop-database update-desktop-database
echo "Installation complete!" echo "Installation complete!"
echo "Installed version timestamp: $(date -r "$INSTALL_DIR/gtk2-mpv-player")"
echo "You can now run 'gtk2-mpv-player' or find it in your application menu." echo "You can now run 'gtk2-mpv-player' or find it in your application menu."

View File

@ -5,6 +5,7 @@
struct Controls { struct Controls {
Player *player; Player *player;
Preferences *prefs;
/* Main container */ /* Main container */
GtkWidget *root; /* GtkEventBox for background/windowing */ GtkWidget *root; /* GtkEventBox for background/windowing */
@ -69,10 +70,11 @@ static void format_time(double seconds, char *buffer, size_t size)
} }
} }
Controls* controls_new(Player *player) Controls* controls_new(Player *player, Preferences *prefs)
{ {
Controls *controls = g_new0(Controls, 1); Controls *controls = g_new0(Controls, 1);
controls->player = player; controls->player = player;
controls->prefs = prefs;
controls->seeking = FALSE; controls->seeking = FALSE;
controls->duration = 0.0; controls->duration = 0.0;
controls->muted = FALSE; controls->muted = FALSE;
@ -367,10 +369,32 @@ static gboolean on_seek_change_value(GtkRange *range, GtkScrollType scroll, gdou
static gboolean on_seek_button_press(GtkWidget *widget, GdkEventButton *event, gpointer data) static gboolean on_seek_button_press(GtkWidget *widget, GdkEventButton *event, gpointer data)
{ {
(void)widget;
(void)event;
Controls *controls = (Controls *)data; Controls *controls = (Controls *)data;
controls->seeking = TRUE; controls->seeking = TRUE;
if (event->button == 1) {
if (controls->prefs && controls->prefs->seekbar_direct_jump) {
/* Left click jump-to-position */
GtkRange *range = GTK_RANGE(widget);
GtkAdjustment *adj = gtk_range_get_adjustment(range);
/* Calculate relative position (0.0 to 1.0) based on click x coordinate */
double relative = event->x / (double)widget->allocation.width;
if (relative < 0) relative = 0;
if (relative > 1) relative = 1;
double new_val = adj->lower + relative * (adj->upper - adj->lower);
gtk_range_set_value(range, new_val);
/* Perform seek immediately */
if (controls->duration > 0) {
double position = (new_val / 100.0) * controls->duration;
player_seek_absolute(controls->player, position);
}
}
/* Return FALSE to allow GTK to process the rest of the event (starting the drag/step) */
}
return FALSE; return FALSE;
} }

View File

@ -2,7 +2,7 @@
#define CONTROLS_H #define CONTROLS_H
#include <gtk/gtk.h> #include <gtk/gtk.h>
#include "player.h" #include "dialogs.h"
typedef struct Controls Controls; typedef struct Controls Controls;
@ -11,7 +11,7 @@ typedef void (*ControlsFullscreenCallback)(gboolean fullscreen, gpointer user_da
typedef void (*ControlsPlayPauseCallback)(gpointer user_data); typedef void (*ControlsPlayPauseCallback)(gpointer user_data);
/* Create controls widget */ /* Create controls widget */
Controls* controls_new(Player *player); Controls* controls_new(Player *player, Preferences *prefs);
void controls_destroy(Controls *controls); void controls_destroy(Controls *controls);
/* Get main widget */ /* Get main widget */

View File

@ -2,6 +2,7 @@
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <stdio.h> #include <stdio.h>
#include "plugin_manager.h"
/* File filter helpers */ /* File filter helpers */
static void add_video_filters(GtkFileChooser *chooser) static void add_video_filters(GtkFileChooser *chooser)
@ -321,7 +322,7 @@ void dialogs_show_about(GtkWindow *parent)
gtk_label_set_markup(GTK_LABEL(title_label), "<span size='xx-large' weight='bold'>GTK2 Media Player</span>"); gtk_label_set_markup(GTK_LABEL(title_label), "<span size='xx-large' weight='bold'>GTK2 Media Player</span>");
gtk_box_pack_start(GTK_BOX(about_vbox), title_label, FALSE, FALSE, 0); gtk_box_pack_start(GTK_BOX(about_vbox), title_label, FALSE, FALSE, 0);
GtkWidget *version_label = gtk_label_new("Version 1.0.0"); GtkWidget *version_label = gtk_label_new("Version 0.7.1");
gtk_box_pack_start(GTK_BOX(about_vbox), version_label, FALSE, FALSE, 0); gtk_box_pack_start(GTK_BOX(about_vbox), version_label, FALSE, FALSE, 0);
GtkWidget *comment_label = gtk_label_new("A lightweight GTK2 media player frontend using libmpv."); GtkWidget *comment_label = gtk_label_new("A lightweight GTK2 media player frontend using libmpv.");
@ -410,8 +411,15 @@ Preferences* preferences_new(void)
prefs->remember_last_dir = TRUE; /* Default to TRUE */ prefs->remember_last_dir = TRUE; /* Default to TRUE */
prefs->use_mpris = TRUE; /* Default to TRUE */ prefs->use_mpris = TRUE; /* Default to TRUE */
prefs->open_in_new_window = TRUE; /* Default to TRUE */ prefs->open_in_new_window = TRUE; /* Default to TRUE */
prefs->seekbar_direct_jump = TRUE; /* Default to TRUE as requested earlier */
prefs->hide_focus_rect = FALSE;
prefs->enable_osd = TRUE;
prefs->last_dir = NULL; prefs->last_dir = NULL;
prefs->screenshot_directory = g_strdup(g_get_home_dir()); prefs->screenshot_directory = g_strdup(g_get_home_dir());
prefs->screenshot_format = g_strdup("png");
const char *config_dir = g_get_user_config_dir();
prefs->mpv_config_path = g_build_filename(config_dir, "gtk2-media-player", "mpv.conf", NULL);
return prefs; return prefs;
} }
@ -419,6 +427,8 @@ void preferences_free(Preferences *prefs)
{ {
if (!prefs) return; if (!prefs) return;
g_free(prefs->screenshot_directory); g_free(prefs->screenshot_directory);
g_free(prefs->screenshot_format);
g_free(prefs->mpv_config_path);
g_free(prefs->last_dir); g_free(prefs->last_dir);
g_free(prefs); g_free(prefs);
} }
@ -482,6 +492,36 @@ void preferences_load(Preferences *prefs)
g_clear_error(&error); g_clear_error(&error);
g_free(last_dir); g_free(last_dir);
} }
gboolean direct_jump = g_key_file_get_boolean(keyfile, "General", "seekbar_direct_jump", &error);
if (!error) prefs->seekbar_direct_jump = direct_jump;
else { g_clear_error(&error); }
gboolean hide_focus = g_key_file_get_boolean(keyfile, "General", "hide_focus_rect", &error);
if (!error) prefs->hide_focus_rect = hide_focus;
else { g_clear_error(&error); }
gboolean osd = g_key_file_get_boolean(keyfile, "General", "enable_osd", &error);
if (!error) prefs->enable_osd = osd;
else { g_clear_error(&error); }
char *ss_format = g_key_file_get_string(keyfile, "General", "screenshot_format", &error);
if (!error && ss_format) {
g_free(prefs->screenshot_format);
prefs->screenshot_format = ss_format;
} else {
g_clear_error(&error);
g_free(ss_format);
}
char *mpv_cfg = g_key_file_get_string(keyfile, "General", "mpv_config_path", &error);
if (!error && mpv_cfg) {
g_free(prefs->mpv_config_path);
prefs->mpv_config_path = mpv_cfg;
} else {
g_clear_error(&error);
g_free(mpv_cfg);
}
} }
g_key_file_free(keyfile); g_key_file_free(keyfile);
@ -515,6 +555,15 @@ void preferences_save(Preferences *prefs)
g_key_file_set_string(keyfile, "General", "screenshot_directory", g_key_file_set_string(keyfile, "General", "screenshot_directory",
prefs->screenshot_directory); prefs->screenshot_directory);
} }
if (prefs->screenshot_format) {
g_key_file_set_string(keyfile, "General", "screenshot_format", prefs->screenshot_format);
}
if (prefs->mpv_config_path) {
g_key_file_set_string(keyfile, "General", "mpv_config_path", prefs->mpv_config_path);
}
g_key_file_set_boolean(keyfile, "General", "seekbar_direct_jump", prefs->seekbar_direct_jump);
g_key_file_set_boolean(keyfile, "General", "hide_focus_rect", prefs->hide_focus_rect);
g_key_file_set_boolean(keyfile, "General", "enable_osd", prefs->enable_osd);
gsize length; gsize length;
char *data = g_key_file_to_data(keyfile, &length, NULL); char *data = g_key_file_to_data(keyfile, &length, NULL);
@ -528,7 +577,41 @@ void preferences_save(Preferences *prefs)
g_free(config_path); g_free(config_path);
} }
gboolean dialogs_show_preferences(GtkWindow *parent, Preferences *prefs) static void on_cfg_browse_clicked(GtkButton *btn, gpointer data)
{
GtkWindow *parent = GTK_WINDOW(data);
GtkWidget *entry = g_object_get_data(G_OBJECT(btn), "entry");
GtkWidget *file_dialog = gtk_file_chooser_dialog_new("Select mpv.conf", parent,
GTK_FILE_CHOOSER_ACTION_OPEN,
GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
GTK_STOCK_OPEN, GTK_RESPONSE_ACCEPT, NULL);
if (gtk_dialog_run(GTK_DIALOG(file_dialog)) == GTK_RESPONSE_ACCEPT) {
char *filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(file_dialog));
gtk_entry_set_text(GTK_ENTRY(entry), filename);
g_free(filename);
}
gtk_widget_destroy(file_dialog);
}
static void on_cfg_reset_clicked(GtkButton *btn, gpointer entry)
{
(void)btn;
const char *config_dir = g_get_user_config_dir();
char *path = g_build_filename(config_dir, "gtk2-media-player", "mpv.conf", NULL);
gtk_entry_set_text(GTK_ENTRY(entry), path);
g_free(path);
}
static void on_speed_preset_clicked(GtkButton *button, gpointer data)
{
(void)data;
int speed_int = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(button), "speed"));
GtkWidget *spin = g_object_get_data(G_OBJECT(button), "spin");
gtk_spin_button_set_value(GTK_SPIN_BUTTON(spin), speed_int / 100.0);
}
gboolean dialogs_show_preferences(GtkWindow *parent, Preferences *prefs, PluginManager *pm)
{ {
if (!prefs) return FALSE; if (!prefs) return FALSE;
@ -537,9 +620,11 @@ gboolean dialogs_show_preferences(GtkWindow *parent, Preferences *prefs)
parent, parent,
GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT, GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT,
GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL, GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
GTK_STOCK_OK, GTK_RESPONSE_ACCEPT, GTK_STOCK_OK, GTK_RESPONSE_OK,
NULL); NULL);
gtk_window_set_default_size(GTK_WINDOW(dialog), 600, 500);
GtkWidget *content_area = gtk_dialog_get_content_area(GTK_DIALOG(dialog)); GtkWidget *content_area = gtk_dialog_get_content_area(GTK_DIALOG(dialog));
gtk_container_set_border_width(GTK_CONTAINER(content_area), 5); gtk_container_set_border_width(GTK_CONTAINER(content_area), 5);
@ -566,13 +651,46 @@ gboolean dialogs_show_preferences(GtkWindow *parent, Preferences *prefs)
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(open_new_check), prefs->open_in_new_window); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(open_new_check), prefs->open_in_new_window);
gtk_box_pack_start(GTK_BOX(interface_vbox), open_new_check, FALSE, FALSE, 0); gtk_box_pack_start(GTK_BOX(interface_vbox), open_new_check, FALSE, FALSE, 0);
GtkWidget *jump_check = gtk_check_button_new_with_label("Seek bar: Jump to clicked position immediately");
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(jump_check), prefs->seekbar_direct_jump);
gtk_box_pack_start(GTK_BOX(interface_vbox), jump_check, FALSE, FALSE, 0);
GtkWidget *focus_check = gtk_check_button_new_with_label("Hide focus rectangles (dotted lines) around UI elements");
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(focus_check), prefs->hide_focus_rect);
gtk_box_pack_start(GTK_BOX(interface_vbox), focus_check, FALSE, FALSE, 0);
GtkWidget *osd_check = gtk_check_button_new_with_label("Enable On-Screen Display (OSD)");
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(osd_check), prefs->enable_osd);
gtk_box_pack_start(GTK_BOX(interface_vbox), osd_check, FALSE, FALSE, 0);
gtk_notebook_append_page(GTK_NOTEBOOK(notebook), interface_vbox, gtk_label_new("Interface")); gtk_notebook_append_page(GTK_NOTEBOOK(notebook), interface_vbox, gtk_label_new("Interface"));
/* 2. Config File Tab (Placeholder) */ /* 2. Config File Tab */
GtkWidget *config_vbox = gtk_vbox_new(FALSE, 10); GtkWidget *config_vbox = gtk_vbox_new(FALSE, 10);
gtk_container_set_border_width(GTK_CONTAINER(config_vbox), 10); gtk_container_set_border_width(GTK_CONTAINER(config_vbox), 10);
GtkWidget *config_label = gtk_label_new("Config File settings - Upcoming feature");
gtk_box_pack_start(GTK_BOX(config_vbox), config_label, TRUE, TRUE, 0); GtkWidget *config_info_label = gtk_label_new(NULL);
gtk_label_set_markup(GTK_LABEL(config_info_label), "<b>mpv Configuration File</b>\n"
"Specify an external mpv.conf file to load custom settings, shaders, and advanced options.\n"
"<i>Note: Standard MPV syntax is supported.</i>");
gtk_label_set_line_wrap(GTK_LABEL(config_info_label), TRUE);
gtk_box_pack_start(GTK_BOX(config_vbox), config_info_label, FALSE, FALSE, 5);
GtkWidget *cfg_hbox = gtk_hbox_new(FALSE, 10);
GtkWidget *cfg_entry = gtk_entry_new();
gtk_entry_set_text(GTK_ENTRY(cfg_entry), prefs->mpv_config_path ? prefs->mpv_config_path : "");
gtk_box_pack_start(GTK_BOX(cfg_hbox), cfg_entry, TRUE, TRUE, 0);
GtkWidget *cfg_browse_btn = gtk_button_new_with_label("Browse...");
g_object_set_data(G_OBJECT(cfg_browse_btn), "entry", cfg_entry);
g_signal_connect(cfg_browse_btn, "clicked", G_CALLBACK(on_cfg_browse_clicked), dialog);
gtk_box_pack_start(GTK_BOX(cfg_hbox), cfg_browse_btn, FALSE, FALSE, 0);
gtk_box_pack_start(GTK_BOX(config_vbox), cfg_hbox, FALSE, FALSE, 5);
GtkWidget *cfg_reset_btn = gtk_button_new_with_label("Reset to Default");
g_signal_connect(cfg_reset_btn, "clicked", G_CALLBACK(on_cfg_reset_clicked), cfg_entry);
gtk_box_pack_start(GTK_BOX(config_vbox), cfg_reset_btn, FALSE, FALSE, 0);
gtk_notebook_append_page(GTK_NOTEBOOK(notebook), config_vbox, gtk_label_new("Config File")); gtk_notebook_append_page(GTK_NOTEBOOK(notebook), config_vbox, gtk_label_new("Config File"));
/* 3. Miscellaneous Tab */ /* 3. Miscellaneous Tab */
@ -606,19 +724,31 @@ gboolean dialogs_show_preferences(GtkWindow *parent, Preferences *prefs)
gtk_box_pack_start(GTK_BOX(ss_hbox), ss_entry, TRUE, TRUE, 0); gtk_box_pack_start(GTK_BOX(ss_hbox), ss_entry, TRUE, TRUE, 0);
gtk_box_pack_start(GTK_BOX(misc_vbox), ss_hbox, FALSE, FALSE, 0); gtk_box_pack_start(GTK_BOX(misc_vbox), ss_hbox, FALSE, FALSE, 0);
GtkWidget *format_hbox = gtk_hbox_new(FALSE, 10);
gtk_box_pack_start(GTK_BOX(format_hbox), gtk_label_new("Screenshot Format:"), FALSE, FALSE, 0);
GtkWidget *format_combo = gtk_combo_box_text_new();
gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(format_combo), "webp");
gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(format_combo), "png");
gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(format_combo), "jpg");
if (g_strcmp0(prefs->screenshot_format, "webp") == 0) gtk_combo_box_set_active(GTK_COMBO_BOX(format_combo), 0);
else if (g_strcmp0(prefs->screenshot_format, "png") == 0) gtk_combo_box_set_active(GTK_COMBO_BOX(format_combo), 1);
else if (g_strcmp0(prefs->screenshot_format, "jpg") == 0) gtk_combo_box_set_active(GTK_COMBO_BOX(format_combo), 2);
else gtk_combo_box_set_active(GTK_COMBO_BOX(format_combo), 0);
gtk_box_pack_start(GTK_BOX(format_hbox), format_combo, FALSE, FALSE, 0);
gtk_box_pack_start(GTK_BOX(misc_vbox), format_hbox, FALSE, FALSE, 0);
gtk_notebook_append_page(GTK_NOTEBOOK(notebook), misc_vbox, gtk_label_new("Miscellaneous")); gtk_notebook_append_page(GTK_NOTEBOOK(notebook), misc_vbox, gtk_label_new("Miscellaneous"));
/* 4. Plugins Tab (Placeholder) */ /* 4. Plugins Tab */
GtkWidget *plugins_vbox = gtk_vbox_new(FALSE, 10); GtkWidget *plugins_vbox = plugin_manager_create_config_widget(pm);
gtk_container_set_border_width(GTK_CONTAINER(plugins_vbox), 10);
GtkWidget *plugins_label = gtk_label_new("Plugins management - Upcoming feature");
gtk_box_pack_start(GTK_BOX(plugins_vbox), plugins_label, TRUE, TRUE, 0);
gtk_notebook_append_page(GTK_NOTEBOOK(notebook), plugins_vbox, gtk_label_new("Plugins")); gtk_notebook_append_page(GTK_NOTEBOOK(notebook), plugins_vbox, gtk_label_new("Plugins"));
gtk_widget_show_all(dialog); gtk_widget_show_all(dialog);
gboolean result = FALSE; gboolean result = FALSE;
if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT) { if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_OK) {
prefs->default_volume = gtk_spin_button_get_value(GTK_SPIN_BUTTON(vol_spin)); prefs->default_volume = gtk_spin_button_get_value(GTK_SPIN_BUTTON(vol_spin));
prefs->remember_position = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(position_check)); prefs->remember_position = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(position_check));
prefs->show_playlist = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(playlist_check)); prefs->show_playlist = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(playlist_check));
@ -627,10 +757,19 @@ gboolean dialogs_show_preferences(GtkWindow *parent, Preferences *prefs)
prefs->remember_last_dir = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(dir_check)); prefs->remember_last_dir = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(dir_check));
prefs->use_mpris = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(mpris_check)); prefs->use_mpris = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(mpris_check));
prefs->open_in_new_window = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(open_new_check)); prefs->open_in_new_window = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(open_new_check));
prefs->seekbar_direct_jump = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(jump_check));
prefs->hide_focus_rect = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(focus_check));
prefs->enable_osd = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(osd_check));
g_free(prefs->screenshot_directory); g_free(prefs->screenshot_directory);
prefs->screenshot_directory = g_strdup(gtk_entry_get_text(GTK_ENTRY(ss_entry))); prefs->screenshot_directory = g_strdup(gtk_entry_get_text(GTK_ENTRY(ss_entry)));
g_free(prefs->screenshot_format);
prefs->screenshot_format = gtk_combo_box_text_get_active_text(GTK_COMBO_BOX_TEXT(format_combo));
g_free(prefs->mpv_config_path);
prefs->mpv_config_path = g_strdup(gtk_entry_get_text(GTK_ENTRY(cfg_entry)));
preferences_save(prefs); preferences_save(prefs);
result = TRUE; result = TRUE;
} }
@ -709,17 +848,7 @@ double dialogs_select_speed(GtkWindow *parent, double current_speed)
GtkWidget *btn = gtk_button_new_with_label(label); GtkWidget *btn = gtk_button_new_with_label(label);
g_object_set_data(G_OBJECT(btn), "speed", GINT_TO_POINTER((int)(presets[i] * 100))); g_object_set_data(G_OBJECT(btn), "speed", GINT_TO_POINTER((int)(presets[i] * 100)));
g_object_set_data(G_OBJECT(btn), "spin", speed_spin); g_object_set_data(G_OBJECT(btn), "spin", speed_spin);
g_signal_connect(btn, "clicked", G_CALLBACK( g_signal_connect(btn, "clicked", G_CALLBACK(on_speed_preset_clicked), NULL);
(void (*)(GtkButton*, gpointer)) ({
void anon(GtkButton *button, gpointer data) {
(void)data;
int speed_int = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(button), "speed"));
GtkWidget *spin = g_object_get_data(G_OBJECT(button), "spin");
gtk_spin_button_set_value(GTK_SPIN_BUTTON(spin), speed_int / 100.0);
}
anon;
})
), NULL);
gtk_box_pack_start(GTK_BOX(hbox), btn, TRUE, TRUE, 0); gtk_box_pack_start(GTK_BOX(hbox), btn, TRUE, TRUE, 0);
} }

View File

@ -5,6 +5,9 @@
#include "player.h" #include "player.h"
#include "playlist.h" #include "playlist.h"
/* Forward declaration */
struct PluginManager;
/* Preferences dialog */ /* Preferences dialog */
typedef struct { typedef struct {
double default_volume; double default_volume;
@ -15,15 +18,20 @@ typedef struct {
gboolean remember_last_dir; gboolean remember_last_dir;
gboolean use_mpris; gboolean use_mpris;
gboolean open_in_new_window; gboolean open_in_new_window;
gboolean seekbar_direct_jump;
gboolean hide_focus_rect;
gboolean enable_osd;
char *last_dir; char *last_dir;
char *screenshot_directory; char *screenshot_directory;
char *screenshot_format;
char *mpv_config_path;
} Preferences; } Preferences;
Preferences* preferences_new(void); Preferences* preferences_new(void);
void preferences_free(Preferences *prefs); void preferences_free(Preferences *prefs);
void preferences_load(Preferences *prefs); void preferences_load(Preferences *prefs);
void preferences_save(Preferences *prefs); void preferences_save(Preferences *prefs);
gboolean dialogs_show_preferences(GtkWindow *parent, Preferences *prefs); gboolean dialogs_show_preferences(GtkWindow *parent, Preferences *prefs, struct PluginManager *pm);
/* File dialogs */ /* File dialogs */
GSList* dialogs_open_files(GtkWindow *parent, Preferences *prefs); GSList* dialogs_open_files(GtkWindow *parent, Preferences *prefs);

View File

@ -83,7 +83,7 @@ int main(int argc, char *argv[])
/* Basic command line parsing */ /* Basic command line parsing */
for (int i = 1; i < argc; i++) { for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "--help") == 0 || strcmp(argv[i], "-h") == 0) { if (strcmp(argv[i], "--help") == 0 || strcmp(argv[i], "-h") == 0) {
printf("Media Player v1.0.0\n"); printf("Media Player v0.7\n");
printf("Usage: %s [options] [files...]\n\n", argv[0]); printf("Usage: %s [options] [files...]\n\n", argv[0]);
printf("Options:\n"); printf("Options:\n");
printf(" -h, --help Show this help message\n"); printf(" -h, --help Show this help message\n");
@ -99,7 +99,7 @@ int main(int argc, char *argv[])
return 0; return 0;
} }
if (strcmp(argv[i], "--version") == 0 || strcmp(argv[i], "-v") == 0) { if (strcmp(argv[i], "--version") == 0 || strcmp(argv[i], "-v") == 0) {
printf("Media Player v1.0.0\n"); printf("Media Player v0.7.1\n");
return 0; return 0;
} }
} }

View File

@ -11,6 +11,7 @@ p_mpv_terminate_destroy mpv_terminate_destroy = NULL;
p_mpv_set_option_string mpv_set_option_string = NULL; p_mpv_set_option_string mpv_set_option_string = NULL;
p_mpv_set_wakeup_callback mpv_set_wakeup_callback = NULL; p_mpv_set_wakeup_callback mpv_set_wakeup_callback = NULL;
p_mpv_command_async mpv_command_async = NULL; p_mpv_command_async mpv_command_async = NULL;
p_mpv_command mpv_command = NULL;
p_mpv_set_property_async mpv_set_property_async = NULL; p_mpv_set_property_async mpv_set_property_async = NULL;
p_mpv_get_property mpv_get_property = NULL; p_mpv_get_property mpv_get_property = NULL;
p_mpv_free mpv_free = NULL; p_mpv_free mpv_free = NULL;
@ -62,6 +63,7 @@ int load_mpv_library(void)
LOAD_SYM(set_option_string); LOAD_SYM(set_option_string);
LOAD_SYM(set_wakeup_callback); LOAD_SYM(set_wakeup_callback);
LOAD_SYM(command_async); LOAD_SYM(command_async);
LOAD_SYM(command);
LOAD_SYM(set_property_async); LOAD_SYM(set_property_async);
LOAD_SYM(get_property); LOAD_SYM(get_property);
LOAD_SYM(free); LOAD_SYM(free);

View File

@ -82,6 +82,7 @@ typedef mpv_event *(*p_mpv_wait_event)(mpv_handle *ctx, double timeout);
typedef int (*p_mpv_observe_property)(mpv_handle *mpv, uint64_t reply_userdata, const char *name, mpv_format format); typedef int (*p_mpv_observe_property)(mpv_handle *mpv, uint64_t reply_userdata, const char *name, mpv_format format);
typedef int (*p_mpv_unobserve_property)(mpv_handle *mpv, uint64_t reply_userdata); typedef int (*p_mpv_unobserve_property)(mpv_handle *mpv, uint64_t reply_userdata);
typedef const char *(*p_mpv_event_name)(mpv_event_id event); typedef const char *(*p_mpv_event_name)(mpv_event_id event);
typedef int (*p_mpv_command)(mpv_handle *ctx, const char **args);
/* Global function pointers */ /* Global function pointers */
extern p_mpv_error_string mpv_error_string; extern p_mpv_error_string mpv_error_string;
@ -91,6 +92,7 @@ extern p_mpv_terminate_destroy mpv_terminate_destroy;
extern p_mpv_set_option_string mpv_set_option_string; extern p_mpv_set_option_string mpv_set_option_string;
extern p_mpv_set_wakeup_callback mpv_set_wakeup_callback; extern p_mpv_set_wakeup_callback mpv_set_wakeup_callback;
extern p_mpv_command_async mpv_command_async; extern p_mpv_command_async mpv_command_async;
extern p_mpv_command mpv_command;
extern p_mpv_set_property_async mpv_set_property_async; extern p_mpv_set_property_async mpv_set_property_async;
extern p_mpv_get_property mpv_get_property; extern p_mpv_get_property mpv_get_property;
extern p_mpv_free mpv_free; extern p_mpv_free mpv_free;

View File

@ -2,6 +2,9 @@
#include <stdlib.h> #include <stdlib.h>
#include <stdio.h> #include <stdio.h>
#include <string.h> #include <string.h>
#include <dirent.h>
#include <sys/stat.h>
#include <glib.h>
struct Player { struct Player {
mpv_handle *mpv; mpv_handle *mpv;
@ -9,6 +12,7 @@ struct Player {
PlayerEventCallback event_callback; PlayerEventCallback event_callback;
gpointer event_callback_data; gpointer event_callback_data;
gboolean initialized; gboolean initialized;
char *config_path;
}; };
static void check_error(int status) static void check_error(int status)
@ -48,7 +52,7 @@ void player_destroy(Player *player)
if (player->mpv) { if (player->mpv) {
mpv_terminate_destroy(player->mpv); mpv_terminate_destroy(player->mpv);
} }
g_free(player->config_path);
g_free(player); g_free(player);
} }
@ -58,38 +62,104 @@ void player_set_window(Player *player, unsigned long wid)
player->wid = wid; player->wid = wid;
} }
void player_set_config_path(Player *player, const char *path)
{
if (!player) return;
g_free(player->config_path);
player->config_path = path ? g_strdup(path) : NULL;
}
static void wakeup_callback(void *ctx) static void wakeup_callback(void *ctx)
{ {
/* This is called from mpv's thread. We need to wake up the GTK main loop. */ /* This is called from mpv's thread. We need to wake up the GTK main loop. */
g_idle_add_full(G_PRIORITY_HIGH, (GSourceFunc)player_process_events, ctx, NULL); g_idle_add_full(G_PRIORITY_HIGH, (GSourceFunc)player_process_events, ctx, NULL);
} }
static void load_scripts(mpv_handle *mpv)
{
const char *config_dir = g_get_user_config_dir();
char *scripts_dir = g_build_filename(config_dir, "gtk2-media-player", "scripts", NULL);
printf("Scanning for scripts in: %s\n", scripts_dir);
/* Create scripts directory if it doesn't exist */
g_mkdir_with_parents(scripts_dir, 0755);
DIR *dir = opendir(scripts_dir);
if (!dir) {
printf("Failed to open scripts directory!\n");
} else {
struct dirent *entry;
int count = 0;
while ((entry = readdir(dir)) != NULL) {
if (entry->d_name[0] == '.') continue;
char *path = g_build_filename(scripts_dir, entry->d_name, NULL);
size_t len = strlen(entry->d_name);
/* Check extension (Lua or JS) */
if ((len > 4 && strcmp(entry->d_name + len - 4, ".lua") == 0) ||
(len > 3 && strcmp(entry->d_name + len - 3, ".js") == 0)) {
printf("Loading script: %s\n", path);
/* Use load-script command (works after mpv_initialize) */
const char *cmd[] = {"load-script", path, NULL};
int res = mpv_command(mpv, cmd);
if (res < 0) {
printf(" -> Failed! Error: %s\n", mpv_error_string(res));
} else {
printf(" -> OK\n");
count++;
}
}
g_free(path);
}
closedir(dir);
printf("Loaded %d script(s)\n", count);
}
g_free(scripts_dir);
}
int player_init(Player *player) int player_init(Player *player)
{ {
if (!player || !player->mpv) return -1; if (!player || !player->mpv) return -1;
/* Set window ID if provided */ /* Set window ID if provided - Must be before initialize */
if (player->wid != 0) { if (player->wid != 0) {
char wid_str[64]; char wid_str[64];
snprintf(wid_str, sizeof(wid_str), "%lu", player->wid); snprintf(wid_str, sizeof(wid_str), "%lu", player->wid);
check_error(mpv_set_option_string(player->mpv, "wid", wid_str)); check_error(mpv_set_option_string(player->mpv, "wid", wid_str));
} }
/* Basic configuration */ /* Basic configuration - Set BEFORE config loading so config can override defaults */
check_error(mpv_set_option_string(player->mpv, "input-default-bindings", "no")); check_error(mpv_set_option_string(player->mpv, "input-default-bindings", "no"));
check_error(mpv_set_option_string(player->mpv, "input-vo-keyboard", "no")); check_error(mpv_set_option_string(player->mpv, "input-vo-keyboard", "no"));
check_error(mpv_set_option_string(player->mpv, "osc", "no")); /* We have our own controls */ check_error(mpv_set_option_string(player->mpv, "osc", "no")); /* We have our own controls */
check_error(mpv_set_option_string(player->mpv, "osd-bar", "yes"));
check_error(mpv_set_option_string(player->mpv, "osd-duration", "2000"));
check_error(mpv_set_option_string(player->mpv, "input-cursor", "no")); /* Let GTK handle the cursor */ check_error(mpv_set_option_string(player->mpv, "input-cursor", "no")); /* Let GTK handle the cursor */
check_error(mpv_set_option_string(player->mpv, "keep-open", "yes")); check_error(mpv_set_option_string(player->mpv, "keep-open", "yes"));
check_error(mpv_set_option_string(player->mpv, "idle", "yes")); check_error(mpv_set_option_string(player->mpv, "idle", "yes"));
/* Initialize mpv */ /* Load external config file if provided */
int ret = mpv_initialize(player->mpv); if (player->config_path && g_file_test(player->config_path, G_FILE_TEST_EXISTS)) {
if (ret < 0) { check_error(mpv_set_option_string(player->mpv, "config", "yes"));
fprintf(stderr, "Failed to initialize mpv: %s\n", mpv_error_string(ret)); check_error(mpv_set_option_string(player->mpv, "config-file", player->config_path));
return ret;
} }
/* Initialize MPV */
int res = mpv_initialize(player->mpv);
if (res < 0) {
check_error(res);
return -1;
}
/* Load scripts AFTER initialization using load-script command */
load_scripts(player->mpv);
/* Set wakeup callback for event processing */ /* Set wakeup callback for event processing */
mpv_set_wakeup_callback(player->mpv, wakeup_callback, player); mpv_set_wakeup_callback(player->mpv, wakeup_callback, player);
@ -147,6 +217,10 @@ void player_seek(Player *player, double seconds)
snprintf(sec_str, sizeof(sec_str), "%f", seconds); snprintf(sec_str, sizeof(sec_str), "%f", seconds);
const char *cmd[] = {"seek", sec_str, "relative", NULL}; const char *cmd[] = {"seek", sec_str, "relative", NULL};
check_error(mpv_command_async(player->mpv, 0, cmd)); check_error(mpv_command_async(player->mpv, 0, cmd));
/* Show progress OSD */
const char *osd_cmd[] = {"show-progress", NULL};
mpv_command_async(player->mpv, 0, osd_cmd);
} }
void player_seek_absolute(Player *player, double position) void player_seek_absolute(Player *player, double position)
@ -155,8 +229,12 @@ void player_seek_absolute(Player *player, double position)
char pos_str[32]; char pos_str[32];
snprintf(pos_str, sizeof(pos_str), "%f", position); snprintf(pos_str, sizeof(pos_str), "%f", position);
const char *cmd[] = {"seek", pos_str, "absolute", NULL}; const char *cmd[] = {"seek", pos_str, "absolute+exact", NULL};
check_error(mpv_command_async(player->mpv, 0, cmd)); check_error(mpv_command_async(player->mpv, 0, cmd));
/* Show progress OSD */
const char *osd_cmd[] = {"show-progress", NULL};
mpv_command_async(player->mpv, 0, osd_cmd);
} }
void player_frame_step(Player *player) void player_frame_step(Player *player)
@ -178,13 +256,22 @@ void player_frame_back_step(Player *player)
void player_set_option_string(Player *player, const char *name, const char *value) void player_set_option_string(Player *player, const char *name, const char *value)
{ {
if (!player || !player->mpv) return; if (!player || !player->mpv) return;
if (player->initialized) {
check_error(mpv_set_property_string(player->mpv, name, value));
} else {
check_error(mpv_set_option_string(player->mpv, name, value)); check_error(mpv_set_option_string(player->mpv, name, value));
}
} }
void player_set_option_flag(Player *player, const char *name, int value) void player_set_option_flag(Player *player, const char *name, int value)
{ {
if (!player || !player->mpv) return; if (!player || !player->mpv) return;
check_error(mpv_set_property_async(player->mpv, 0, name, MPV_FORMAT_FLAG, &value)); if (player->initialized) {
const char *val = value ? "yes" : "no";
check_error(mpv_set_property_string(player->mpv, name, val));
} else {
check_error(mpv_set_option_string(player->mpv, name, value ? "yes" : "no"));
}
} }
/* Property getters */ /* Property getters */
@ -349,22 +436,37 @@ void player_set_volume(Player *player, double volume)
{ {
if (!player || !player->mpv) return; if (!player || !player->mpv) return;
mpv_set_property_async(player->mpv, 0, "volume", MPV_FORMAT_DOUBLE, &volume); char vol_str[32];
snprintf(vol_str, sizeof(vol_str), "%f", volume);
if (player->initialized) {
check_error(mpv_set_property_string(player->mpv, "volume", vol_str));
/* Show volume bar on OSD */
const char *cmd[] = {"osd-msg-bar", "set", "volume", NULL};
mpv_command_async(player->mpv, 0, cmd);
} else {
check_error(mpv_set_option_string(player->mpv, "volume", vol_str));
}
} }
void player_set_muted(Player *player, gboolean muted) void player_set_muted(Player *player, gboolean muted)
{ {
if (!player || !player->mpv) return; if (!player || !player->mpv) return;
if (player->initialized) {
int flag = muted ? 1 : 0; const char *val = muted ? "yes" : "no";
mpv_set_property_async(player->mpv, 0, "mute", MPV_FORMAT_FLAG, &flag); check_error(mpv_set_property_string(player->mpv, "mute", val));
} else {
check_error(mpv_set_option_string(player->mpv, "mute", muted ? "yes" : "no"));
}
} }
void player_set_speed(Player *player, double speed) void player_set_speed(Player *player, double speed)
{ {
if (!player || !player->mpv) return; if (!player || !player->mpv) return;
mpv_set_property_async(player->mpv, 0, "speed", MPV_FORMAT_DOUBLE, &speed); char speed_str[32];
snprintf(speed_str, sizeof(speed_str), "%f", speed);
check_error(mpv_set_property_string(player->mpv, "speed", speed_str));
} }
/* Track management */ /* Track management */
@ -435,16 +537,18 @@ void player_set_audio_track(Player *player, int track)
{ {
if (!player || !player->mpv) return; if (!player || !player->mpv) return;
int64_t aid = track; char aid_str[16];
mpv_set_property_async(player->mpv, 0, "aid", MPV_FORMAT_INT64, &aid); snprintf(aid_str, sizeof(aid_str), "%d", track);
check_error(mpv_set_property_string(player->mpv, "aid", aid_str));
} }
void player_set_subtitle_track(Player *player, int track) void player_set_subtitle_track(Player *player, int track)
{ {
if (!player || !player->mpv) return; if (!player || !player->mpv) return;
int64_t sid = track; char sid_str[16];
mpv_set_property_async(player->mpv, 0, "sid", MPV_FORMAT_INT64, &sid); snprintf(sid_str, sizeof(sid_str), "%d", track);
check_error(mpv_set_property_string(player->mpv, "sid", sid_str));
} }
void player_load_subtitle(Player *player, const char *path) void player_load_subtitle(Player *player, const char *path)

View File

@ -13,6 +13,7 @@ typedef void (*PlayerEventCallback)(Player *player, mpv_event *event, gpointer u
Player* player_new(void); Player* player_new(void);
void player_destroy(Player *player); void player_destroy(Player *player);
void player_set_window(Player *player, unsigned long wid); void player_set_window(Player *player, unsigned long wid);
void player_set_config_path(Player *player, const char *path);
int player_init(Player *player); int player_init(Player *player);
/* Playback commands */ /* Playback commands */

194
src/plugin.h Normal file
View File

@ -0,0 +1,194 @@
/**
* GTK2 Media Player - Plugin API
*
* This header defines the public API that plugins must implement
* and the functions they can call to interact with the application.
*/
#ifndef PLUGIN_H
#define PLUGIN_H
#include <gtk/gtk.h>
#ifdef __cplusplus
extern "C" {
#endif
/* Plugin API version - plugins must match this */
#define PLUGIN_API_VERSION 2
/**
* Plugin metadata structure
* Returned by plugin_get_info()
*/
typedef struct {
const char *name; /* Display name, e.g. "Screenshot Timer" */
const char *version; /* Semantic version, e.g. "1.0.0" */
const char *description; /* Short description of functionality */
const char *author; /* Author name/email */
int api_version; /* Must be PLUGIN_API_VERSION */
} PluginInfo;
/**
* Application API provided to plugins
* Passed to plugin_init() for plugins to store and use
*/
typedef struct PluginAPI {
/* Player control */
void (*play)(void);
void (*pause)(void);
void (*stop)(void);
void (*toggle_pause)(void);
void (*seek)(double seconds);
void (*seek_absolute)(double position);
/* Player state */
double (*get_position)(void);
double (*get_duration)(void);
double (*get_volume)(void);
gboolean (*is_paused)(void);
gboolean (*is_playing)(void);
/* Media info */
char* (*get_current_file)(void); /* Caller must g_free() */
char* (*get_media_title)(void); /* Caller must g_free() */
/* Screenshot */
void (*take_screenshot)(void);
void (*take_screenshot_to_file)(const char *path);
/* UI access */
GtkWindow* (*get_main_window)(void);
/* Message dialogs */
void (*show_info)(const char *title, const char *message);
void (*show_warning)(const char *title, const char *message);
void (*show_error)(const char *title, const char *message);
/* Menu registration - add items to Plugins menu */
void (*register_menu_item)(GtkWidget *menu_item);
void (*unregister_menu_item)(GtkWidget *menu_item);
/* UI layouts (Added for Audio Player plugin) */
void (*set_video_area_visible)(gboolean visible);
void (*set_window_size)(int width, int height);
/* Advanced MPV access */
void (*execute_mpv_command)(const char **args);
/* Reserved for future expansion */
void *_reserved[5];
} PluginAPI;
/**
* Plugin event types for optional hooks
*/
typedef enum {
PLUGIN_EVENT_FILE_LOADED, /* New file loaded, data = filepath (const char*) */
PLUGIN_EVENT_PLAYBACK_STARTED, /* Playback started, data = NULL */
PLUGIN_EVENT_PLAYBACK_PAUSED, /* Playback paused, data = NULL */
PLUGIN_EVENT_PLAYBACK_STOPPED, /* Playback stopped, data = NULL */
PLUGIN_EVENT_POSITION_CHANGED, /* Position changed, data = position (double*) */
PLUGIN_EVENT_VOLUME_CHANGED, /* Volume changed, data = volume (double*) */
} PluginEventType;
/*
* =============================================================================
* REQUIRED PLUGIN EXPORTS
*
* Every plugin MUST implement and export these three functions:
* =============================================================================
*/
/**
* Get plugin metadata
* Must return a pointer to static PluginInfo structure
*
* @return Pointer to plugin info (must remain valid until shutdown)
*/
#define PLUGIN_EXPORT_GET_INFO "plugin_get_info"
typedef const PluginInfo* (*PluginGetInfoFunc)(void);
/**
* Initialize plugin
* Called when plugin is loaded. Store the API pointer for later use.
*
* @param api Application API for plugin to use
* @return 0 on success, non-zero on failure
*/
#define PLUGIN_EXPORT_INIT "plugin_init"
typedef int (*PluginInitFunc)(const PluginAPI *api);
/**
* Shutdown plugin
* Called when plugin is unloaded. Clean up any resources.
*/
#define PLUGIN_EXPORT_SHUTDOWN "plugin_shutdown"
typedef void (*PluginShutdownFunc)(void);
/*
* =============================================================================
* OPTIONAL PLUGIN EXPORTS
*
* Plugins MAY implement these for additional functionality:
* =============================================================================
*/
/**
* Get plugin's menu item(s)
* If plugin wants to add menu items, return a GtkMenuItem.
* For multiple items, use a submenu.
*
* @return GtkWidget* menu item, or NULL if none
*/
#define PLUGIN_EXPORT_GET_MENU "plugin_get_menu_item"
typedef GtkWidget* (*PluginGetMenuFunc)(void);
/**
* Handle application events
* Called when various application events occur.
*
* @param event Event type
* @param data Event-specific data (see PluginEventType comments)
*/
#define PLUGIN_EXPORT_ON_EVENT "plugin_on_event"
typedef void (*PluginOnEventFunc)(PluginEventType event, void *data);
/**
* Configure plugin
* Called when user selects "Configure" in plugin manager.
* Should open a configuration dialog.
*/
#define PLUGIN_EXPORT_CONFIGURE "plugin_configure"
typedef void (*PluginConfigureFunc)(void);
/**
* Show about dialog
* Called when user selects "About" in plugin manager.
* Should open an about dialog.
*/
#define PLUGIN_EXPORT_ABOUT "plugin_about"
typedef void (*PluginAboutFunc)(void);
/*
* =============================================================================
* HELPER MACROS FOR PLUGIN DEVELOPMENT
* =============================================================================
*/
/* Declare standard plugin info */
#define DECLARE_PLUGIN_INFO(p_name, p_version, p_description, p_author) \
static const PluginInfo _plugin_info = { \
.name = p_name, \
.version = p_version, \
.description = p_description, \
.author = p_author, \
.api_version = PLUGIN_API_VERSION \
}; \
const PluginInfo* plugin_get_info(void) { return &_plugin_info; }
#ifdef __cplusplus
}
#endif
#endif /* PLUGIN_H */

1028
src/plugin_manager.c Normal file

File diff suppressed because it is too large Load Diff

145
src/plugin_manager.h Normal file
View File

@ -0,0 +1,145 @@
/**
* GTK2 Media Player - Plugin Manager
*
* Internal interface for loading, managing, and unloading plugins.
*/
#ifndef PLUGIN_MANAGER_H
#define PLUGIN_MANAGER_H
#include <gtk/gtk.h>
#include "plugin.h"
typedef struct PluginManager PluginManager;
/**
* Loaded plugin instance
*/
typedef struct {
char *id; /* Plugin ID (directory name) */
char *path; /* Path to .so file */
void *handle; /* dlopen handle */
PluginInfo info; /* Plugin metadata */
gboolean enabled; /* Currently active */
gboolean has_menu; /* Has menu item */
GtkWidget *menu_item; /* Registered menu item */
/* Function pointers */
PluginGetInfoFunc get_info;
PluginInitFunc init;
PluginShutdownFunc shutdown;
PluginGetMenuFunc get_menu_item;
PluginOnEventFunc on_event;
PluginConfigureFunc configure;
PluginAboutFunc about;
} LoadedPlugin;
typedef struct AppUI AppUI;
/**
* Create a new plugin manager
*
* @param ui Main application UI structure
* @return New plugin manager instance
*/
PluginManager* plugin_manager_new(AppUI *ui);
/**
* Destroy plugin manager and unload all plugins
*/
void plugin_manager_destroy(PluginManager *pm);
/**
* Set player control callbacks
* Must be called before loading plugins
*/
void plugin_manager_set_player_callbacks(PluginManager *pm,
void (*play)(void),
void (*pause)(void),
void (*stop)(void),
void (*toggle_pause)(void),
void (*seek)(double),
void (*seek_absolute)(double),
double (*get_position)(void),
double (*get_duration)(void),
double (*get_volume)(void),
gboolean (*is_paused)(void),
gboolean (*is_playing)(void),
char* (*get_current_file)(void),
char* (*get_media_title)(void),
void (*take_screenshot)(void),
void (*take_screenshot_to_file)(const char*)
);
/**
* Load all plugins from the plugins directory
*
* @return Number of plugins loaded
*/
int plugin_manager_load_all(PluginManager *pm);
/**
* Install a plugin from a .zip file
*
* @param zip_path Path to the .zip file
* @return TRUE on success, FALSE on failure
*/
gboolean plugin_manager_install_zip(PluginManager *pm, const char *zip_path);
/**
* Uninstall a plugin
*
* @param plugin_id Plugin ID to uninstall
* @return TRUE on success, FALSE on failure
*/
gboolean plugin_manager_uninstall(PluginManager *pm, const char *plugin_id);
/**
* Get list of loaded plugins
*
* @param count Output: number of plugins
* @return Array of LoadedPlugin pointers (do not free)
*/
LoadedPlugin** plugin_manager_get_list(PluginManager *pm, int *count);
/**
* Get the plugins menu
* Creates menu items for all loaded plugins
*
* @return GtkMenu widget
*/
GtkWidget* plugin_manager_get_menu(PluginManager *pm);
/**
* Register a menu item from a plugin
*/
void plugin_manager_register_menu_item(PluginManager *pm, GtkWidget *item);
/**
* Unregister a menu item from a plugin
*/
void plugin_manager_unregister_menu_item(PluginManager *pm, GtkWidget *item);
/**
* Broadcast an event to all loaded plugins
*/
void plugin_manager_broadcast_event(PluginManager *pm, PluginEventType event, void *data);
/**
* Create configuration widget for preferences dialog
* Returns a GtkWidget (VBox) containing the plugin management UI
*/
GtkWidget* plugin_manager_create_config_widget(PluginManager *pm);
/**
* Show plugin management dialog (Deprecated in favor of Preferences)
*/
void plugin_manager_show_dialog(PluginManager *pm);
/**
* Get the plugins directory path
* @return Path string (do not free)
*/
const char* plugin_manager_get_plugins_dir(PluginManager *pm);
#endif /* PLUGIN_MANAGER_H */

411
src/ui.c
View File

@ -6,6 +6,28 @@
#include <stdio.h> #include <stdio.h>
#include <math.h> #include <math.h>
#include "mpris.h" #include "mpris.h"
#include "plugin_manager.h"
/* Forward declarations */
static void on_playback_prev(GtkMenuItem *item, gpointer data);
static void on_playback_next(GtkMenuItem *item, gpointer data);
static void on_playback_seek_forward(GtkMenuItem *item, gpointer data);
static void on_playback_seek_backward(GtkMenuItem *item, gpointer data);
static void on_playback_frame_step(GtkMenuItem *item, gpointer data);
static void on_playback_goto_time(GtkMenuItem *item, gpointer data);
static void on_playback_speed(GtkMenuItem *item, gpointer data);
static void on_playback_ab_loop_a(GtkMenuItem *item, gpointer data);
static void on_playback_ab_loop_b(GtkMenuItem *item, gpointer data);
static void on_playback_ab_loop_clear(GtkMenuItem *item, gpointer data);
static void on_audio_track_selected(GtkMenuItem *item, gpointer data);
static void on_subtitle_track_selected(GtkMenuItem *item, gpointer data);
static void on_subtitle_load(GtkMenuItem *item, gpointer data);
static void on_subtitle_disable(GtkMenuItem *item, gpointer data);
static void on_view_fullscreen(GtkMenuItem *item, gpointer data);
static void on_view_playlist(GtkMenuItem *item, gpointer data);
static void on_view_screenshot(GtkMenuItem *item, gpointer data);
static void on_open_scripts_folder(GtkMenuItem *item, gpointer data);
static void on_help_more_software(GtkMenuItem *item, gpointer data);
/* Property observation user data IDs */ /* Property observation user data IDs */
enum { enum {
@ -74,6 +96,9 @@ struct AppUI {
/* MPRIS state tracking */ /* MPRIS state tracking */
double last_mpris_pos; double last_mpris_pos;
/* Plugin manager */
PluginManager *plugin_manager;
}; };
/* Forward declarations */ /* Forward declarations */
@ -122,6 +147,24 @@ static void on_view_preferences(GtkMenuItem *item, gpointer data);
static void on_aspect_ratio_selected(GtkMenuItem *item, gpointer data); static void on_aspect_ratio_selected(GtkMenuItem *item, gpointer data);
static void on_help_about(GtkMenuItem *item, gpointer data); static void on_help_about(GtkMenuItem *item, gpointer data);
/* Plugin API wrapper forward declarations */
static AppUI *g_current_ui;
static void plugin_api_play(void);
static void plugin_api_pause(void);
static void plugin_api_stop(void);
static void plugin_api_toggle_pause(void);
static void plugin_api_seek(double seconds);
static void plugin_api_seek_absolute(double position);
static double plugin_api_get_position(void);
static double plugin_api_get_duration(void);
static double plugin_api_get_volume(void);
static gboolean plugin_api_is_paused(void);
static gboolean plugin_api_is_playing(void);
static char* plugin_api_get_current_file(void);
static char* plugin_api_get_media_title(void);
static void plugin_api_take_screenshot(void);
static void plugin_api_take_screenshot_to_file(const char *path);
AppUI* ui_new(void) AppUI* ui_new(void)
{ {
AppUI *ui = g_new0(AppUI, 1); AppUI *ui = g_new0(AppUI, 1);
@ -158,6 +201,30 @@ AppUI* ui_new(void)
ui->main_vbox = gtk_vbox_new(FALSE, 0); ui->main_vbox = gtk_vbox_new(FALSE, 0);
gtk_container_add(GTK_CONTAINER(ui->window), ui->main_vbox); gtk_container_add(GTK_CONTAINER(ui->window), ui->main_vbox);
/* Initialize plugin manager (must be before create_menubar) */
ui->plugin_manager = plugin_manager_new(ui);
/* Set up plugin API callbacks and load plugins BEFORE creating menu */
g_current_ui = ui;
plugin_manager_set_player_callbacks(ui->plugin_manager,
plugin_api_play,
plugin_api_pause,
plugin_api_stop,
plugin_api_toggle_pause,
plugin_api_seek,
plugin_api_seek_absolute,
plugin_api_get_position,
plugin_api_get_duration,
plugin_api_get_volume,
plugin_api_is_paused,
plugin_api_is_playing,
plugin_api_get_current_file,
plugin_api_get_media_title,
plugin_api_take_screenshot,
plugin_api_take_screenshot_to_file
);
plugin_manager_load_all(ui->plugin_manager);
/* Menu bar */ /* Menu bar */
create_menubar(ui); create_menubar(ui);
gtk_box_pack_start(GTK_BOX(ui->main_vbox), ui->menubar, FALSE, FALSE, 0); gtk_box_pack_start(GTK_BOX(ui->main_vbox), ui->menubar, FALSE, FALSE, 0);
@ -205,7 +272,7 @@ AppUI* ui_new(void)
} }
/* Controls */ /* Controls */
ui->controls = controls_new(ui->player); ui->controls = controls_new(ui->player, ui->prefs);
controls_set_fullscreen_callback(ui->controls, on_fullscreen_requested, ui); controls_set_fullscreen_callback(ui->controls, on_fullscreen_requested, ui);
controls_set_play_pause_callback(ui->controls, on_controls_play_pause, ui); controls_set_play_pause_callback(ui->controls, on_controls_play_pause, ui);
gtk_box_pack_start(GTK_BOX(ui->main_vbox), controls_get_widget(ui->controls), FALSE, FALSE, 0); gtk_box_pack_start(GTK_BOX(ui->main_vbox), controls_get_widget(ui->controls), FALSE, FALSE, 0);
@ -213,9 +280,22 @@ AppUI* ui_new(void)
/* Set player event callback */ /* Set player event callback */
player_set_event_callback(ui->player, on_player_event, ui); player_set_event_callback(ui->player, on_player_event, ui);
/* Set mpv config path */
if (ui->prefs->mpv_config_path) {
player_set_config_path(ui->player, ui->prefs->mpv_config_path);
}
/* Set default volume */ /* Set default volume */
player_set_volume(ui->player, ui->prefs->default_volume); player_set_volume(ui->player, ui->prefs->default_volume);
/* Set initial OSD and focus settings */
player_set_option_string(ui->player, "osd-level", ui->prefs->enable_osd ? "1" : "0");
player_set_option_string(ui->player, "osd-bar", ui->prefs->enable_osd ? "yes" : "no");
if (ui->prefs->hide_focus_rect) {
gtk_rc_parse_string("style \"no-focus-rect\" { GtkWidget::focus-line-width = 0 GtkWidget::focus-padding = 0 } widget \"*\" style \"no-focus-rect\"");
}
ui->is_fullscreen = FALSE; ui->is_fullscreen = FALSE;
ui->always_on_top = FALSE; ui->always_on_top = FALSE;
ui->auto_hide_timer_id = 0; ui->auto_hide_timer_id = 0;
@ -250,6 +330,9 @@ void ui_destroy(AppUI *ui)
mpris_destroy(ui); mpris_destroy(ui);
/* Destroy plugin manager */
plugin_manager_destroy(ui->plugin_manager);
if (ui->fs_window) { if (ui->fs_window) {
gtk_widget_destroy(ui->fs_window); gtk_widget_destroy(ui->fs_window);
} }
@ -274,6 +357,10 @@ void ui_set_preferences(AppUI *ui, Preferences *prefs)
/* Apply some immediate settings */ /* Apply some immediate settings */
if (ui->player) { if (ui->player) {
player_set_volume(ui->player, ui->prefs->default_volume); player_set_volume(ui->player, ui->prefs->default_volume);
if (ui->prefs->screenshot_directory) {
player_set_option_string(ui->player, "screenshot-directory", ui->prefs->screenshot_directory);
}
} }
/* Update playlist visibility if UI is already realized */ /* Update playlist visibility if UI is already realized */
@ -305,6 +392,81 @@ Playlist* ui_get_playlist(AppUI *ui)
return ui->playlist; return ui->playlist;
} }
PluginManager* ui_get_plugin_manager(AppUI *ui)
{
if (!ui) return NULL;
return ui->plugin_manager;
}
/* Static wrapper functions for plugin API */
static void plugin_api_play(void) {
if (g_current_ui && g_current_ui->player) player_play(g_current_ui->player);
}
static void plugin_api_pause(void) {
if (g_current_ui && g_current_ui->player) player_pause(g_current_ui->player);
}
static void plugin_api_stop(void) {
if (g_current_ui && g_current_ui->player) player_stop(g_current_ui->player);
}
static void plugin_api_toggle_pause(void) {
if (g_current_ui && g_current_ui->player) player_toggle_pause(g_current_ui->player);
}
static void plugin_api_seek(double seconds) {
if (g_current_ui && g_current_ui->player) player_seek(g_current_ui->player, seconds);
}
static void plugin_api_seek_absolute(double position) {
if (g_current_ui && g_current_ui->player) player_seek_absolute(g_current_ui->player, position);
}
static double plugin_api_get_position(void) {
if (g_current_ui && g_current_ui->player) return player_get_position(g_current_ui->player);
return 0.0;
}
static double plugin_api_get_duration(void) {
if (g_current_ui && g_current_ui->player) return player_get_duration(g_current_ui->player);
return 0.0;
}
static double plugin_api_get_volume(void) {
if (g_current_ui && g_current_ui->player) return player_get_volume(g_current_ui->player);
return 100.0;
}
static gboolean plugin_api_is_paused(void) {
if (g_current_ui && g_current_ui->player) return player_get_paused(g_current_ui->player);
return TRUE;
}
static gboolean plugin_api_is_playing(void) {
if (g_current_ui && g_current_ui->player) return !player_get_paused(g_current_ui->player) && !player_is_idle(g_current_ui->player);
return FALSE;
}
static char* plugin_api_get_current_file(void) {
if (g_current_ui && g_current_ui->player) return player_get_filename(g_current_ui->player);
return NULL;
}
static char* plugin_api_get_media_title(void) {
if (g_current_ui && g_current_ui->player) return player_get_media_title(g_current_ui->player);
return NULL;
}
static void plugin_api_take_screenshot(void) {
if (g_current_ui && g_current_ui->player) player_screenshot(g_current_ui->player);
}
static void plugin_api_take_screenshot_to_file(const char *path) {
if (g_current_ui && g_current_ui->player) player_screenshot_to_file(g_current_ui->player, path);
}
void ui_run(AppUI *ui) void ui_run(AppUI *ui)
{ {
(void)ui; (void)ui;
@ -1037,6 +1199,9 @@ static void create_menubar(AppUI *ui)
{ {
ui->menubar = gtk_menu_bar_new(); ui->menubar = gtk_menu_bar_new();
GtkAccelGroup *accel_group = gtk_accel_group_new();
gtk_window_add_accel_group(GTK_WINDOW(ui->window), accel_group);
/* File Menu */ /* File Menu */
GtkWidget *file_menu = gtk_menu_new(); GtkWidget *file_menu = gtk_menu_new();
GtkWidget *file_item = gtk_menu_item_new_with_mnemonic("_File"); GtkWidget *file_item = gtk_menu_item_new_with_mnemonic("_File");
@ -1044,6 +1209,7 @@ static void create_menubar(AppUI *ui)
GtkWidget *open_item = gtk_image_menu_item_new_from_stock(GTK_STOCK_OPEN, NULL); GtkWidget *open_item = gtk_image_menu_item_new_from_stock(GTK_STOCK_OPEN, NULL);
g_signal_connect(open_item, "activate", G_CALLBACK(on_file_open), ui); g_signal_connect(open_item, "activate", G_CALLBACK(on_file_open), ui);
gtk_widget_add_accelerator(open_item, "activate", accel_group, GDK_o, GDK_CONTROL_MASK, GTK_ACCEL_VISIBLE);
gtk_menu_shell_append(GTK_MENU_SHELL(file_menu), open_item); gtk_menu_shell_append(GTK_MENU_SHELL(file_menu), open_item);
GtkWidget *open_dir_item = gtk_menu_item_new_with_mnemonic("Open _Folder..."); GtkWidget *open_dir_item = gtk_menu_item_new_with_mnemonic("Open _Folder...");
@ -1067,10 +1233,10 @@ static void create_menubar(AppUI *ui)
GtkWidget *open_url_item = gtk_menu_item_new_with_mnemonic("Open _URL..."); GtkWidget *open_url_item = gtk_menu_item_new_with_mnemonic("Open _URL...");
g_signal_connect(open_url_item, "activate", G_CALLBACK(on_file_open_url), ui); g_signal_connect(open_url_item, "activate", G_CALLBACK(on_file_open_url), ui);
gtk_widget_add_accelerator(open_url_item, "activate", accel_group, GDK_l, GDK_CONTROL_MASK, GTK_ACCEL_VISIBLE);
gtk_menu_shell_append(GTK_MENU_SHELL(file_menu), open_url_item); gtk_menu_shell_append(GTK_MENU_SHELL(file_menu), open_url_item);
GtkWidget *save_playlist_item = gtk_image_menu_item_new_from_stock(GTK_STOCK_SAVE, NULL); GtkWidget *save_playlist_item = gtk_menu_item_new_with_mnemonic("Save _Playlist...");
gtk_menu_item_set_label(GTK_MENU_ITEM(save_playlist_item), "Save _Playlist...");
g_signal_connect(save_playlist_item, "activate", G_CALLBACK(on_file_save_playlist), ui); g_signal_connect(save_playlist_item, "activate", G_CALLBACK(on_file_save_playlist), ui);
gtk_menu_shell_append(GTK_MENU_SHELL(file_menu), save_playlist_item); gtk_menu_shell_append(GTK_MENU_SHELL(file_menu), save_playlist_item);
@ -1078,6 +1244,9 @@ static void create_menubar(AppUI *ui)
GtkWidget *quit_item = gtk_image_menu_item_new_from_stock(GTK_STOCK_QUIT, NULL); GtkWidget *quit_item = gtk_image_menu_item_new_from_stock(GTK_STOCK_QUIT, NULL);
g_signal_connect(quit_item, "activate", G_CALLBACK(on_file_quit), ui); g_signal_connect(quit_item, "activate", G_CALLBACK(on_file_quit), ui);
gtk_widget_add_accelerator(quit_item, "activate", accel_group, GDK_q, GDK_CONTROL_MASK, GTK_ACCEL_VISIBLE);
/* Also Q key */
gtk_widget_add_accelerator(quit_item, "activate", accel_group, GDK_q, 0, 0);
gtk_menu_shell_append(GTK_MENU_SHELL(file_menu), quit_item); gtk_menu_shell_append(GTK_MENU_SHELL(file_menu), quit_item);
gtk_menu_shell_append(GTK_MENU_SHELL(ui->menubar), file_item); gtk_menu_shell_append(GTK_MENU_SHELL(ui->menubar), file_item);
@ -1087,30 +1256,49 @@ static void create_menubar(AppUI *ui)
GtkWidget *playback_item = gtk_menu_item_new_with_mnemonic("_Playback"); GtkWidget *playback_item = gtk_menu_item_new_with_mnemonic("_Playback");
gtk_menu_item_set_submenu(GTK_MENU_ITEM(playback_item), playback_menu); gtk_menu_item_set_submenu(GTK_MENU_ITEM(playback_item), playback_menu);
GtkWidget *play_pause_item = gtk_menu_item_new_with_mnemonic("_Play/Pause"); GtkWidget *play_pause_item = gtk_image_menu_item_new_from_stock(GTK_STOCK_MEDIA_PLAY, NULL);
gtk_menu_item_set_label(GTK_MENU_ITEM(play_pause_item), "_Play/Pause");
g_signal_connect(play_pause_item, "activate", G_CALLBACK(on_playback_play_pause), ui); g_signal_connect(play_pause_item, "activate", G_CALLBACK(on_playback_play_pause), ui);
gtk_widget_add_accelerator(play_pause_item, "activate", accel_group, GDK_space, 0, GTK_ACCEL_VISIBLE);
gtk_menu_shell_append(GTK_MENU_SHELL(playback_menu), play_pause_item); gtk_menu_shell_append(GTK_MENU_SHELL(playback_menu), play_pause_item);
GtkWidget *stop_item = gtk_menu_item_new_with_mnemonic("_Stop"); GtkWidget *stop_item = gtk_image_menu_item_new_from_stock(GTK_STOCK_MEDIA_STOP, NULL);
gtk_menu_item_set_label(GTK_MENU_ITEM(stop_item), "_Stop");
g_signal_connect(stop_item, "activate", G_CALLBACK(on_playback_stop), ui); g_signal_connect(stop_item, "activate", G_CALLBACK(on_playback_stop), ui);
gtk_menu_shell_append(GTK_MENU_SHELL(playback_menu), stop_item); gtk_menu_shell_append(GTK_MENU_SHELL(playback_menu), stop_item);
gtk_menu_shell_append(GTK_MENU_SHELL(playback_menu), gtk_separator_menu_item_new()); gtk_menu_shell_append(GTK_MENU_SHELL(playback_menu), gtk_separator_menu_item_new());
GtkWidget *prev_item = gtk_image_menu_item_new_from_stock(GTK_STOCK_MEDIA_PREVIOUS, NULL);
g_signal_connect(prev_item, "activate", G_CALLBACK(on_playback_prev), ui);
gtk_widget_add_accelerator(prev_item, "activate", accel_group, GDK_Page_Up, 0, GTK_ACCEL_VISIBLE);
gtk_menu_shell_append(GTK_MENU_SHELL(playback_menu), prev_item);
GtkWidget *next_item = gtk_image_menu_item_new_from_stock(GTK_STOCK_MEDIA_NEXT, NULL);
g_signal_connect(next_item, "activate", G_CALLBACK(on_playback_next), ui);
gtk_widget_add_accelerator(next_item, "activate", accel_group, GDK_Page_Down, 0, GTK_ACCEL_VISIBLE);
gtk_menu_shell_append(GTK_MENU_SHELL(playback_menu), next_item);
gtk_menu_shell_append(GTK_MENU_SHELL(playback_menu), gtk_separator_menu_item_new());
GtkWidget *seek_fwd_item = gtk_menu_item_new_with_mnemonic("Seek _Forward 10s"); GtkWidget *seek_fwd_item = gtk_menu_item_new_with_mnemonic("Seek _Forward 10s");
g_signal_connect(seek_fwd_item, "activate", G_CALLBACK(on_playback_seek_forward), ui); g_signal_connect(seek_fwd_item, "activate", G_CALLBACK(on_playback_seek_forward), ui);
gtk_widget_add_accelerator(seek_fwd_item, "activate", accel_group, GDK_Right, 0, GTK_ACCEL_VISIBLE);
gtk_menu_shell_append(GTK_MENU_SHELL(playback_menu), seek_fwd_item); gtk_menu_shell_append(GTK_MENU_SHELL(playback_menu), seek_fwd_item);
GtkWidget *seek_back_item = gtk_menu_item_new_with_mnemonic("Seek _Backward 10s"); GtkWidget *seek_back_item = gtk_menu_item_new_with_mnemonic("Seek _Backward 10s");
g_signal_connect(seek_back_item, "activate", G_CALLBACK(on_playback_seek_backward), ui); g_signal_connect(seek_back_item, "activate", G_CALLBACK(on_playback_seek_backward), ui);
gtk_widget_add_accelerator(seek_back_item, "activate", accel_group, GDK_Left, 0, GTK_ACCEL_VISIBLE);
gtk_menu_shell_append(GTK_MENU_SHELL(playback_menu), seek_back_item); gtk_menu_shell_append(GTK_MENU_SHELL(playback_menu), seek_back_item);
GtkWidget *frame_step_item = gtk_menu_item_new_with_mnemonic("_Frame Step"); GtkWidget *frame_step_item = gtk_menu_item_new_with_mnemonic("_Frame Step");
g_signal_connect(frame_step_item, "activate", G_CALLBACK(on_playback_frame_step), ui); g_signal_connect(frame_step_item, "activate", G_CALLBACK(on_playback_frame_step), ui);
gtk_widget_add_accelerator(frame_step_item, "activate", accel_group, GDK_period, 0, GTK_ACCEL_VISIBLE);
gtk_menu_shell_append(GTK_MENU_SHELL(playback_menu), frame_step_item); gtk_menu_shell_append(GTK_MENU_SHELL(playback_menu), frame_step_item);
GtkWidget *goto_time_item = gtk_menu_item_new_with_mnemonic("_Go to Time..."); GtkWidget *goto_time_item = gtk_menu_item_new_with_mnemonic("_Go to Time...");
g_signal_connect(goto_time_item, "activate", G_CALLBACK(on_playback_goto_time), ui); g_signal_connect(goto_time_item, "activate", G_CALLBACK(on_playback_goto_time), ui);
gtk_widget_add_accelerator(goto_time_item, "activate", accel_group, GDK_g, GDK_CONTROL_MASK, GTK_ACCEL_VISIBLE);
gtk_menu_shell_append(GTK_MENU_SHELL(playback_menu), goto_time_item); gtk_menu_shell_append(GTK_MENU_SHELL(playback_menu), goto_time_item);
gtk_menu_shell_append(GTK_MENU_SHELL(playback_menu), gtk_separator_menu_item_new()); gtk_menu_shell_append(GTK_MENU_SHELL(playback_menu), gtk_separator_menu_item_new());
@ -1176,11 +1364,14 @@ static void create_menubar(AppUI *ui)
GtkWidget *fullscreen_item = gtk_menu_item_new_with_mnemonic("_Fullscreen"); GtkWidget *fullscreen_item = gtk_menu_item_new_with_mnemonic("_Fullscreen");
g_signal_connect(fullscreen_item, "activate", G_CALLBACK(on_view_fullscreen), ui); g_signal_connect(fullscreen_item, "activate", G_CALLBACK(on_view_fullscreen), ui);
gtk_widget_add_accelerator(fullscreen_item, "activate", accel_group, GDK_f, 0, GTK_ACCEL_VISIBLE);
gtk_widget_add_accelerator(fullscreen_item, "activate", accel_group, GDK_F11, 0, GTK_ACCEL_VISIBLE);
gtk_menu_shell_append(GTK_MENU_SHELL(view_menu), fullscreen_item); gtk_menu_shell_append(GTK_MENU_SHELL(view_menu), fullscreen_item);
GtkWidget *playlist_toggle_item = gtk_check_menu_item_new_with_mnemonic("_Playlist"); GtkWidget *playlist_toggle_item = gtk_check_menu_item_new_with_mnemonic("_Playlist");
gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(playlist_toggle_item), ui->prefs->show_playlist); gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(playlist_toggle_item), ui->prefs->show_playlist);
g_signal_connect(playlist_toggle_item, "toggled", G_CALLBACK(on_view_playlist), ui); g_signal_connect(playlist_toggle_item, "toggled", G_CALLBACK(on_view_playlist), ui);
gtk_widget_add_accelerator(playlist_toggle_item, "activate", accel_group, GDK_p, GDK_CONTROL_MASK, GTK_ACCEL_VISIBLE);
gtk_menu_shell_append(GTK_MENU_SHELL(view_menu), playlist_toggle_item); gtk_menu_shell_append(GTK_MENU_SHELL(view_menu), playlist_toggle_item);
GtkWidget *on_top_item = gtk_check_menu_item_new_with_mnemonic("Always on _Top"); GtkWidget *on_top_item = gtk_check_menu_item_new_with_mnemonic("Always on _Top");
@ -1210,6 +1401,7 @@ static void create_menubar(AppUI *ui)
GtkWidget *screenshot_item = gtk_menu_item_new_with_mnemonic("_Screenshot"); GtkWidget *screenshot_item = gtk_menu_item_new_with_mnemonic("_Screenshot");
g_signal_connect(screenshot_item, "activate", G_CALLBACK(on_view_screenshot), ui); g_signal_connect(screenshot_item, "activate", G_CALLBACK(on_view_screenshot), ui);
gtk_widget_add_accelerator(screenshot_item, "activate", accel_group, GDK_s, GDK_CONTROL_MASK | GDK_SHIFT_MASK, GTK_ACCEL_VISIBLE);
gtk_menu_shell_append(GTK_MENU_SHELL(view_menu), screenshot_item); gtk_menu_shell_append(GTK_MENU_SHELL(view_menu), screenshot_item);
gtk_menu_shell_append(GTK_MENU_SHELL(view_menu), gtk_separator_menu_item_new()); gtk_menu_shell_append(GTK_MENU_SHELL(view_menu), gtk_separator_menu_item_new());
@ -1220,6 +1412,12 @@ static void create_menubar(AppUI *ui)
gtk_menu_shell_append(GTK_MENU_SHELL(ui->menubar), view_item); gtk_menu_shell_append(GTK_MENU_SHELL(ui->menubar), view_item);
/* Plugins Menu */
GtkWidget *plugins_menu = plugin_manager_get_menu(ui->plugin_manager);
GtkWidget *plugins_item = gtk_menu_item_new_with_mnemonic("P_lugins");
gtk_menu_item_set_submenu(GTK_MENU_ITEM(plugins_item), plugins_menu);
gtk_menu_shell_append(GTK_MENU_SHELL(ui->menubar), plugins_item);
/* Help Menu */ /* Help Menu */
GtkWidget *help_menu = gtk_menu_new(); GtkWidget *help_menu = gtk_menu_new();
GtkWidget *help_item = gtk_menu_item_new_with_mnemonic("_Help"); GtkWidget *help_item = gtk_menu_item_new_with_mnemonic("_Help");
@ -1229,6 +1427,13 @@ static void create_menubar(AppUI *ui)
g_signal_connect(about_item, "activate", G_CALLBACK(on_help_about), ui); g_signal_connect(about_item, "activate", G_CALLBACK(on_help_about), ui);
gtk_menu_shell_append(GTK_MENU_SHELL(help_menu), about_item); gtk_menu_shell_append(GTK_MENU_SHELL(help_menu), about_item);
GtkWidget *sep = gtk_separator_menu_item_new();
gtk_menu_shell_append(GTK_MENU_SHELL(help_menu), sep);
GtkWidget *more_item = gtk_menu_item_new_with_label("More GTK2 Software...");
g_signal_connect(more_item, "activate", G_CALLBACK(on_help_more_software), ui);
gtk_menu_shell_append(GTK_MENU_SHELL(help_menu), more_item);
gtk_menu_shell_append(GTK_MENU_SHELL(ui->menubar), help_item); gtk_menu_shell_append(GTK_MENU_SHELL(ui->menubar), help_item);
} }
@ -1319,6 +1524,20 @@ static void on_playback_stop(GtkMenuItem *item, gpointer data)
ui_update_title(ui, "GTK2 Media Player"); ui_update_title(ui, "GTK2 Media Player");
} }
static void on_playback_prev(GtkMenuItem *item, gpointer data)
{
(void)item;
AppUI *ui = (AppUI *)data;
playlist_play_prev(ui->playlist);
}
static void on_playback_next(GtkMenuItem *item, gpointer data)
{
(void)item;
AppUI *ui = (AppUI *)data;
playlist_play_next(ui->playlist);
}
static void on_playback_seek_forward(GtkMenuItem *item, gpointer data) static void on_playback_seek_forward(GtkMenuItem *item, gpointer data)
{ {
(void)item; (void)item;
@ -1440,19 +1659,15 @@ static void on_view_screenshot(GtkMenuItem *item, gpointer data)
{ {
(void)item; (void)item;
AppUI *ui = (AppUI *)data; AppUI *ui = (AppUI *)data;
/* Use instant screenshot (saves to configured directory) */
char *path = dialogs_save_screenshot(GTK_WINDOW(ui->window)); player_screenshot(ui->player);
if (path) {
player_screenshot_to_file(ui->player, path);
g_free(path);
}
} }
static void on_view_preferences(GtkMenuItem *item, gpointer data) static void on_view_preferences(GtkMenuItem *item, gpointer data)
{ {
(void)item; (void)item;
AppUI *ui = (AppUI *)data; AppUI *ui = (AppUI *)data;
if (dialogs_show_preferences(GTK_WINDOW(ui->window), ui->prefs)) { if (dialogs_show_preferences(GTK_WINDOW(ui->window), ui->prefs, ui->plugin_manager)) {
/* Apply changes immediately */ /* Apply changes immediately */
if (!ui->is_fullscreen) { if (!ui->is_fullscreen) {
if (ui->prefs->hide_cursor_windowed) { if (ui->prefs->hide_cursor_windowed) {
@ -1470,6 +1685,99 @@ static void on_view_preferences(GtkMenuItem *item, gpointer data)
gtk_widget_show(controls_get_widget(ui->controls)); gtk_widget_show(controls_get_widget(ui->controls));
} }
} }
/* Apply Focus Rect setting (GTK2 specific hack) */
if (ui->prefs->hide_focus_rect) {
gtk_rc_parse_string("style \"no-focus-rect\" { GtkWidget::focus-line-width = 0 GtkWidget::focus-padding = 0 } widget \"*\" style \"no-focus-rect\"");
} else {
gtk_rc_parse_string("style \"default-focus-rect\" { GtkWidget::focus-line-width = 1 GtkWidget::focus-padding = 1 } widget \"*\" style \"default-focus-rect\"");
}
gtk_widget_reset_rc_styles(ui->window);
gtk_widget_queue_draw(ui->window);
/* Apply OSD setting */
player_set_option_string(ui->player, "osd-level", ui->prefs->enable_osd ? "1" : "0");
player_set_option_string(ui->player, "osd-bar", ui->prefs->enable_osd ? "yes" : "no");
/* Apply mpv.conf if changed - parse and apply each option */
if (ui->prefs->mpv_config_path && g_file_test(ui->prefs->mpv_config_path, G_FILE_TEST_EXISTS)) {
/* First, reset common properties to defaults to avoid "bleeding" from previous configs */
player_set_option_string(ui->player, "brightness", "0");
player_set_option_string(ui->player, "contrast", "0");
player_set_option_string(ui->player, "saturation", "0");
player_set_option_string(ui->player, "gamma", "0");
player_set_option_string(ui->player, "hue", "0");
player_set_option_string(ui->player, "osd-color", "#FFFFFFFF");
player_set_option_string(ui->player, "osd-border-color", "#FF000000");
player_set_option_string(ui->player, "osd-shadow-color", "#80000000");
player_set_option_string(ui->player, "osd-font-size", "55");
player_set_option_string(ui->player, "osd-border-size", "3");
player_set_option_string(ui->player, "osd-shadow-offset", "0");
FILE *fp = fopen(ui->prefs->mpv_config_path, "r");
if (fp) {
char line[512];
int applied = 0;
while (fgets(line, sizeof(line), fp)) {
/* Strip newline */
char *nl = strchr(line, '\n');
if (nl) *nl = '\0';
/* Skip comments and empty lines */
char *p = line;
while (*p == ' ' || *p == '\t') p++;
if (*p == '#' || *p == '\0') continue;
/* Find '=' separator */
char *eq = strchr(p, '=');
if (eq) {
*eq = '\0';
char *key = p;
char *value = eq + 1;
/* Trim trailing spaces from key */
char *k_end = key + strlen(key) - 1;
while (k_end > key && (*k_end == ' ' || *k_end == '\t')) *k_end-- = '\0';
/* Trim leading spaces from value */
while (*value == ' ' || *value == '\t') value++;
/* Remove surrounding quotes if present */
size_t vlen = strlen(value);
if (vlen >= 2 && ((value[0] == '\'' && value[vlen-1] == '\'') ||
(value[0] == '"' && value[vlen-1] == '"'))) {
value[vlen-1] = '\0';
value++;
}
player_set_option_string(ui->player, key, value);
applied++;
}
}
fclose(fp);
/* Show feedback with count */
char msg[64];
snprintf(msg, sizeof(msg), "Applied %d config options", applied);
const char *msg_cmd[] = {"show-text", msg, "2000", NULL};
mpv_command_async(player_get_mpv_handle(ui->player), 0, msg_cmd);
}
}
/* Apply Screenshot Format */
if (ui->prefs->screenshot_format) {
player_set_option_string(ui->player, "screenshot-format", ui->prefs->screenshot_format);
}
/* Apply Screenshot Dir */
if (ui->prefs->screenshot_directory) {
player_set_option_string(ui->player, "screenshot-directory", ui->prefs->screenshot_directory);
}
/* Show feedback */
const char *saved_cmd[] = {"show-text", "Preferences saved", "1500", NULL};
mpv_command_async(player_get_mpv_handle(ui->player), 0, saved_cmd);
} }
} }
@ -1487,6 +1795,17 @@ static void on_help_about(GtkMenuItem *item, gpointer data)
dialogs_show_about(GTK_WINDOW(ui->window)); dialogs_show_about(GTK_WINDOW(ui->window));
} }
static void on_help_more_software(GtkMenuItem *item, gpointer data)
{
(void)item;
(void)data;
const char *url = "https://lakiweb.net/article/gtk2/software-directory.html";
char *cmd = g_strdup_printf("xdg-open '%s'", url);
int ret = system(cmd);
(void)ret;
g_free(cmd);
}
static void on_controls_play_pause(gpointer data) static void on_controls_play_pause(gpointer data)
{ {
on_playback_play_pause(NULL, data); on_playback_play_pause(NULL, data);
@ -1513,3 +1832,71 @@ static void on_recent_item_activated(GtkRecentChooser *chooser, gpointer data)
g_free(uri); g_free(uri);
} }
} }
/* Plugin UI Helpers */
void ui_set_video_area_visible(AppUI *ui, gboolean visible)
{
if (!ui || !ui->video_container) return;
if (visible) {
gtk_widget_show(ui->video_container);
gtk_widget_set_no_show_all(ui->video_container, FALSE);
} else {
gtk_widget_hide(ui->video_container);
gtk_widget_set_no_show_all(ui->video_container, TRUE);
}
}
void ui_set_window_size(AppUI *ui, int width, int height)
{
if (!ui || !ui->window) return;
gtk_window_resize(GTK_WINDOW(ui->window), width, height);
}
void ui_restart(AppUI *ui)
{
(void)ui;
/* Get the path to the current executable */
char *bin_path = NULL;
/* If we are installed, use the installed path */
if (g_file_test("/usr/local/bin/gtk2-mpv-player", G_FILE_TEST_EXISTS)) {
bin_path = g_strdup("/usr/local/bin/gtk2-mpv-player");
} else {
/* Otherwise try to find it relative to current dir */
bin_path = g_build_filename(".", "gtk2-mpv-player", NULL);
}
printf("Restarting application via %s...\n", bin_path);
char *argv[] = {bin_path, NULL};
execvp(bin_path, argv);
/* If execvp fails */
perror("execvp");
g_free(bin_path);
}
void ui_open_scripts_folder(AppUI *ui)
{
(void)ui;
const char *config_dir = g_get_user_config_dir();
char *scripts_dir = g_build_filename(config_dir, "gtk2-media-player", "scripts", NULL);
/* Ensure it exists */
g_mkdir_with_parents(scripts_dir, 0755);
printf("Opening scripts directory: %s\n", scripts_dir);
char *cmd = g_strdup_printf("xdg-open '%s'", scripts_dir);
int ret = system(cmd);
(void)ret; /* Suppress unused result warning */
g_free(cmd);
g_free(scripts_dir);
}

View File

@ -23,6 +23,11 @@ Player* ui_get_player(AppUI *ui);
/* Get playlist */ /* Get playlist */
Playlist* ui_get_playlist(AppUI *ui); Playlist* ui_get_playlist(AppUI *ui);
/* Get plugin manager */
struct PluginManager;
typedef struct PluginManager PluginManager;
PluginManager* ui_get_plugin_manager(AppUI *ui);
/* Run the application */ /* Run the application */
void ui_run(AppUI *ui); void ui_run(AppUI *ui);
@ -39,4 +44,13 @@ void ui_toggle_always_on_top(AppUI *ui);
/* Update window title */ /* Update window title */
void ui_update_title(AppUI *ui, const char *media_title); void ui_update_title(AppUI *ui, const char *media_title);
/* Plugin UI Access */
void ui_set_video_area_visible(AppUI *ui, gboolean visible);
void ui_set_window_size(AppUI *ui, int width, int height);
Player* ui_get_player(AppUI *ui); /* Helper to get player from UI */
/* Application control */
void ui_restart(AppUI *ui);
void ui_open_scripts_folder(AppUI *ui);
#endif /* UI_H */ #endif /* UI_H */

19
src/ui_callbacks.h Normal file
View File

@ -0,0 +1,19 @@
/* Forward declarations for callbacks used in menu */
static void on_playback_prev(GtkMenuItem *item, gpointer data);
static void on_playback_next(GtkMenuItem *item, gpointer data);
static void on_playback_seek_forward(GtkMenuItem *item, gpointer data);
static void on_playback_seek_backward(GtkMenuItem *item, gpointer data);
static void on_playback_frame_step(GtkMenuItem *item, gpointer data);
static void on_playback_goto_time(GtkMenuItem *item, gpointer data);
static void on_playback_speed(GtkMenuItem *item, gpointer data);
static void on_playback_ab_loop_a(GtkMenuItem *item, gpointer data);
static void on_playback_ab_loop_b(GtkMenuItem *item, gpointer data);
static void on_playback_ab_loop_clear(GtkMenuItem *item, gpointer data);
static void on_audio_track_selected(GtkMenuItem *item, gpointer data);
static void on_subtitle_track_selected(GtkMenuItem *item, gpointer data);
static void on_subtitle_load(GtkMenuItem *item, gpointer data);
static void on_subtitle_disable(GtkMenuItem *item, gpointer data);
static void on_view_fullscreen(GtkMenuItem *item, gpointer data);
static void on_view_playlist(GtkMenuItem *item, gpointer data);
static void on_view_screenshot(GtkMenuItem *item, gpointer data);