kino/src/ui.c

2292 lines
84 KiB
C

#include "ui.h"
#include <gdk/gdkx.h>
#include <gdk/gdkkeysyms.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <math.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 update_chapter_menu(AppUI *ui);
static void on_chapter_selected(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);
/* Property observation user data IDs */
enum {
PROP_TIME_POS = 1,
PROP_DURATION,
PROP_PAUSE,
PROP_VOLUME,
PROP_MUTE,
PROP_MEDIA_TITLE,
PROP_PLAYLIST_POS,
PROP_AB_LOOP_A,
PROP_AB_LOOP_B,
PROP_IDLE_ACTIVE,
PROP_SHUFFLE,
PROP_LOOP,
PROP_CHAPTER,
};
struct AppUI {
/* Main window */
GtkWidget *window;
/* Main vertical box */
GtkWidget *main_vbox;
/* Menu bar */
GtkWidget *menubar;
/* Paned container */
GtkWidget *hpaned;
/* Video container */
GtkWidget *video_container;
/* Video area */
GtkWidget *video_area;
/* Controls and playlist */
Controls *controls;
Playlist *playlist;
/* Side Panel */
GtkWidget *side_notebook;
GtkWidget *chapters_tree;
GtkListStore *chapters_store;
/* Player */
Player *player;
/* Preferences */
Preferences *prefs;
/* State */
gboolean is_fullscreen;
gboolean playlist_visible;
gboolean always_on_top;
/* Fullscreen controls overlay */
GtkWidget *fs_window;
/* Menus for dynamic updates */
GtkWidget *audio_menu;
GtkWidget *subtitle_menu;
GtkWidget *speed_menu;
GtkWidget *aspect_menu;
GtkWidget *chapter_menu;
GtkWidget *recent_menu;
GtkWidget *mute_item;
GtkRecentManager *recent_manager;
/* Auto-hide controls in fullscreen */
guint auto_hide_timer_id;
/* MPRIS state tracking */
double last_mpris_pos;
/* Plugin manager */
PluginManager *plugin_manager;
/* Subtitle dragging */
gboolean sub_dragging;
/* Keybind manager */
KeybindManager *keybinds;
};
/* Forward declarations */
static void create_menubar(AppUI *ui);
static void on_video_area_realize(GtkWidget *widget, gpointer data);
static void on_player_event(Player *player, mpv_event *event, gpointer user_data);
static void on_fullscreen_requested(gboolean fullscreen, gpointer user_data);
static gboolean on_window_configure(GtkWidget *widget, GdkEventConfigure *event, gpointer data);
static gboolean on_window_delete(GtkWidget *widget, GdkEvent *event, gpointer data);
static gboolean on_key_press(GtkWidget *widget, GdkEventKey *event, gpointer data);
static void on_drag_data_received(GtkWidget *widget, GdkDragContext *context, gint x, gint y,
GtkSelectionData *selection, guint info, guint time, gpointer data);
static gboolean on_video_area_scroll(GtkWidget *widget, GdkEventScroll *event, gpointer data);
static gboolean on_video_area_button_press(GtkWidget *widget, GdkEventButton *event, gpointer data);
static gboolean on_video_area_motion(GtkWidget *widget, GdkEventMotion *event, gpointer data);
static gboolean on_video_area_button_release(GtkWidget *widget, GdkEventButton *event, gpointer data);
static gboolean auto_hide_timer_cb(gpointer data);
static void add_path_to_playlist(AppUI *ui, const char *path, gboolean clear);
static void on_recent_item_activated(GtkRecentChooser *chooser, gpointer data);
static void on_controls_play_pause(gpointer data);
/* Menu callbacks */
static void on_file_open(GtkMenuItem *item, gpointer data);
static void on_file_open_directory(GtkMenuItem *item, gpointer data);
static void on_file_open_url(GtkMenuItem *item, gpointer data);
static void on_file_save_playlist(GtkMenuItem *item, gpointer data);
static void on_file_quit(GtkMenuItem *item, gpointer data);
static void on_playback_play_pause(GtkMenuItem *item, gpointer data);
static void on_playback_stop(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_always_on_top(GtkMenuItem *item, gpointer data);
static void on_always_on_top_toggled(GtkCheckMenuItem *item, gpointer data);
static void on_view_screenshot(GtkMenuItem *item, gpointer data);
static void on_view_preferences(GtkMenuItem *item, gpointer data);
static void on_subtitle_settings_clicked(GtkMenuItem *item, gpointer data);
static void on_aspect_ratio_selected(GtkMenuItem *item, gpointer data);
static void ui_apply_ytdl_resolution(AppUI *ui);
static void on_ytdl_resolution_selected(GtkMenuItem *item, gpointer data);
static void on_audio_mute(GtkMenuItem *item, gpointer data);
static void on_help_keybinds(GtkMenuItem *item, gpointer data);
static void on_help_about(GtkMenuItem *item, gpointer data);
static void on_view_chapters(GtkMenuItem *item, gpointer data);
static void on_chapter_row_activated(GtkTreeView *tree_view, GtkTreePath *path, GtkTreeViewColumn *column, gpointer data);
static void on_side_panel_close_clicked(GtkButton *button, gpointer data);
/* Plugin API wrapper forward declarations */
static AppUI *g_current_ui;
static void plugin_api_play(void);
static void plugin_api_pause(void);
static void plugin_api_stop(void);
static void plugin_api_toggle_pause(void);
static void plugin_api_seek(double seconds);
static void plugin_api_seek_absolute(double position);
static double plugin_api_get_position(void);
static double plugin_api_get_duration(void);
static double plugin_api_get_volume(void);
static gboolean plugin_api_is_paused(void);
static gboolean plugin_api_is_playing(void);
static char* plugin_api_get_current_file(void);
static char* plugin_api_get_media_title(void);
static void plugin_api_take_screenshot(void);
static void plugin_api_take_screenshot_to_file(const char *path);
AppUI* ui_new(void)
{
AppUI *ui = g_new0(AppUI, 1);
/* Create player */
ui->player = player_new();
if (!ui->player) {
g_free(ui);
return NULL;
}
/* Load preferences */
ui->prefs = preferences_new();
preferences_load(ui->prefs);
/* Load keybinds from config file */
ui->keybinds = keybind_manager_new();
{
char *config_dir = g_build_filename(g_get_user_config_dir(), "gtk2-media-player", NULL);
char *config_path = g_build_filename(config_dir, "settings.conf", NULL);
GKeyFile *kf = g_key_file_new();
if (g_key_file_load_from_file(kf, config_path, G_KEY_FILE_NONE, NULL)) {
keybind_manager_load(ui->keybinds, kf);
}
g_key_file_free(kf);
g_free(config_path);
g_free(config_dir);
}
/* Main window */
ui->window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
gtk_window_set_title(GTK_WINDOW(ui->window), "Kino");
gtk_window_set_default_size(GTK_WINDOW(ui->window), 900, 600);
gtk_window_set_position(GTK_WINDOW(ui->window), GTK_WIN_POS_CENTER);
/* Set default application icon for all windows (appears in title bar and taskbar) */
if (!gtk_window_set_default_icon_from_file("icon.png", NULL)) {
if (!gtk_window_set_default_icon_from_file("assets/icon.png", NULL)) {
gtk_window_set_default_icon_from_file("/usr/local/lib/gtk2-mpv-player/icon.png", NULL);
}
}
g_signal_connect(ui->window, "delete-event", G_CALLBACK(on_window_delete), ui);
g_signal_connect(ui->window, "configure-event", G_CALLBACK(on_window_configure), ui);
g_signal_connect(ui->window, "key-press-event", G_CALLBACK(on_key_press), ui);
/* Main vertical box */
ui->main_vbox = gtk_vbox_new(FALSE, 0);
gtk_container_add(GTK_CONTAINER(ui->window), ui->main_vbox);
/* Initialize plugin manager (must be before create_menubar) */
ui->plugin_manager = plugin_manager_new(ui);
/* Set up plugin API callbacks and load plugins BEFORE creating menu */
g_current_ui = ui;
plugin_manager_set_player_callbacks(ui->plugin_manager,
plugin_api_play,
plugin_api_pause,
plugin_api_stop,
plugin_api_toggle_pause,
plugin_api_seek,
plugin_api_seek_absolute,
plugin_api_get_position,
plugin_api_get_duration,
plugin_api_get_volume,
plugin_api_is_paused,
plugin_api_is_playing,
plugin_api_get_current_file,
plugin_api_get_media_title,
plugin_api_take_screenshot,
plugin_api_take_screenshot_to_file
);
plugin_manager_load_all(ui->plugin_manager);
/* Menu bar */
create_menubar(ui);
gtk_box_pack_start(GTK_BOX(ui->main_vbox), ui->menubar, FALSE, FALSE, 0);
/* Horizontal paned for video + playlist */
ui->hpaned = gtk_hpaned_new();
gtk_box_pack_start(GTK_BOX(ui->main_vbox), ui->hpaned, TRUE, TRUE, 0);
/* Container for video to help with black bars/background */
ui->video_container = gtk_event_box_new();
GdkColor black = {0, 0, 0, 0};
gtk_widget_modify_bg(ui->video_container, GTK_STATE_NORMAL, &black);
gtk_paned_pack1(GTK_PANED(ui->hpaned), ui->video_container, TRUE, TRUE);
ui->video_area = gtk_drawing_area_new();
gtk_widget_set_size_request(ui->video_area, 640, 360);
gtk_container_add(GTK_CONTAINER(ui->video_container), ui->video_area);
gtk_widget_add_events(ui->video_container, GDK_BUTTON_PRESS_MASK | GDK_SCROLL_MASK | GDK_POINTER_MOTION_MASK);
g_signal_connect(ui->video_area, "realize", G_CALLBACK(on_video_area_realize), ui);
g_signal_connect(ui->video_container, "scroll-event", G_CALLBACK(on_video_area_scroll), ui);
g_signal_connect(ui->video_container, "button-press-event", G_CALLBACK(on_video_area_button_press), ui);
g_signal_connect(ui->video_container, "button-release-event", G_CALLBACK(on_video_area_button_release), ui);
g_signal_connect(ui->video_container, "motion-notify-event", G_CALLBACK(on_video_area_motion), ui);
/* Enable drag and drop on container as well */
static GtkTargetEntry target_entries[] = {
{"text/uri-list", 0, 0}
};
gtk_drag_dest_set(ui->video_container, GTK_DEST_DEFAULT_ALL,
target_entries, 1, GDK_ACTION_COPY);
g_signal_connect(ui->video_container, "drag-data-received", G_CALLBACK(on_drag_data_received), ui);
/* Side Panel Notebook */
ui->side_notebook = gtk_notebook_new();
gtk_notebook_set_tab_pos(GTK_NOTEBOOK(ui->side_notebook), GTK_POS_TOP);
GtkWidget *close_btn = gtk_button_new();
gtk_button_set_relief(GTK_BUTTON(close_btn), GTK_RELIEF_NONE);
GtkWidget *close_img = gtk_image_new_from_stock(GTK_STOCK_CLOSE, GTK_ICON_SIZE_MENU);
gtk_container_add(GTK_CONTAINER(close_btn), close_img);
g_signal_connect(close_btn, "clicked", G_CALLBACK(on_side_panel_close_clicked), ui);
gtk_widget_show_all(close_btn);
gtk_notebook_set_action_widget(GTK_NOTEBOOK(ui->side_notebook), close_btn, GTK_PACK_END);
gtk_widget_set_size_request(ui->side_notebook, 250, -1);
gtk_paned_pack2(GTK_PANED(ui->hpaned), ui->side_notebook, FALSE, TRUE);
/* Handle visibility manually */
gtk_widget_set_no_show_all(ui->side_notebook, TRUE);
/* Playlist */
ui->playlist = playlist_new(ui->player);
GtkWidget *playlist_widget = playlist_get_widget(ui->playlist);
GtkWidget *pl_label = gtk_label_new("Playlist");
gtk_widget_show(pl_label);
gtk_notebook_append_page(GTK_NOTEBOOK(ui->side_notebook), playlist_widget, pl_label);
gtk_widget_show_all(playlist_widget);
/* Chapters Tree */
ui->chapters_store = gtk_list_store_new(2, G_TYPE_INT, G_TYPE_STRING);
ui->chapters_tree = gtk_tree_view_new_with_model(GTK_TREE_MODEL(ui->chapters_store));
g_object_unref(ui->chapters_store);
GtkCellRenderer *renderer = gtk_cell_renderer_text_new();
GtkTreeViewColumn *column = gtk_tree_view_column_new_with_attributes("Chapter", renderer, "text", 1, NULL);
gtk_tree_view_append_column(GTK_TREE_VIEW(ui->chapters_tree), column);
gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(ui->chapters_tree), FALSE);
GtkWidget *chapters_scroll = gtk_scrolled_window_new(NULL, NULL);
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(chapters_scroll), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
gtk_container_add(GTK_CONTAINER(chapters_scroll), ui->chapters_tree);
GtkWidget *ch_label = gtk_label_new("Chapters");
gtk_widget_show(ch_label);
gtk_notebook_append_page(GTK_NOTEBOOK(ui->side_notebook), chapters_scroll, ch_label);
gtk_widget_show_all(chapters_scroll);
g_signal_connect(ui->chapters_tree, "row-activated", G_CALLBACK(on_chapter_row_activated), ui);
ui->playlist_visible = ui->prefs->show_playlist;
if (ui->playlist_visible) {
gtk_widget_show(ui->side_notebook);
}
/* Controls */
ui->controls = controls_new(ui->player, ui->prefs);
controls_set_fullscreen_callback(ui->controls, on_fullscreen_requested, ui);
controls_set_play_pause_callback(ui->controls, on_controls_play_pause, ui);
gtk_box_pack_start(GTK_BOX(ui->main_vbox), controls_get_widget(ui->controls), FALSE, FALSE, 0);
/* Set player event callback */
player_set_event_callback(ui->player, on_player_event, ui);
/* Set mpv config path */
if (ui->prefs->mpv_config_path) {
player_set_config_path(ui->player, ui->prefs->mpv_config_path);
}
/* Set default volume */
player_set_volume(ui->player, ui->prefs->default_volume);
/* Set initial OSD and focus settings */
player_set_option_string(ui->player, "osd-level", ui->prefs->enable_osd ? "1" : "0");
player_set_option_string(ui->player, "osd-bar", ui->prefs->enable_osd ? "yes" : "no");
ui_apply_ytdl_resolution(ui);
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\"");
}
/* Observe chapter changes */
player_observe_property(ui->player, "chapter", MPV_FORMAT_INT64, PROP_CHAPTER);
ui->is_fullscreen = FALSE;
ui->always_on_top = FALSE;
ui->auto_hide_timer_id = 0;
/* Show window */
gtk_widget_show_all(ui->window);
/* Since we used set_no_show_all on playlist, it remains hidden if it was hidden.
If prefs say show, we already called show() above. */
if (ui->always_on_top) {
gtk_window_set_keep_above(GTK_WINDOW(ui->window), TRUE);
}
/* Recent files */
ui->recent_manager = gtk_recent_manager_get_default();
if (ui->prefs->use_mpris) {
mpris_init(ui);
}
/* Apply initial subtitle preferences */
player_set_subtitle_delay(ui->player, ui->prefs->sub_delay);
player_set_subtitle_pos(ui->player, ui->prefs->sub_pos);
player_set_subtitle_scale(ui->player, ui->prefs->sub_scale);
player_set_subtitle_font(ui->player, ui->prefs->sub_font);
return ui;
}
void ui_destroy(AppUI *ui)
{
if (!ui) return;
/* Save keybinds */
if (ui->keybinds) {
char *config_dir = g_build_filename(g_get_user_config_dir(), "gtk2-media-player", NULL);
char *config_path = g_build_filename(config_dir, "settings.conf", NULL);
GKeyFile *kf = g_key_file_new();
g_key_file_load_from_file(kf, config_path, G_KEY_FILE_NONE, NULL);
keybind_manager_save(ui->keybinds, kf);
gchar *data = g_key_file_to_data(kf, NULL, NULL);
if (data) {
g_file_set_contents(config_path, data, -1, NULL);
g_free(data);
}
g_key_file_free(kf);
g_free(config_path);
g_free(config_dir);
keybind_manager_free(ui->keybinds);
}
if (ui->auto_hide_timer_id > 0) {
g_source_remove(ui->auto_hide_timer_id);
}
mpris_destroy(ui);
/* Destroy plugin manager */
plugin_manager_destroy(ui->plugin_manager);
if (ui->fs_window) {
gtk_widget_destroy(ui->fs_window);
}
preferences_free(ui->prefs);
controls_destroy(ui->controls);
playlist_destroy(ui->playlist);
player_destroy(ui->player);
g_free(ui);
}
void ui_apply_subtitle_preferences(AppUI *ui)
{
if (!ui || !ui->player || !ui->prefs) return;
player_set_subtitle_delay(ui->player, ui->prefs->sub_delay);
player_set_subtitle_pos(ui->player, ui->prefs->sub_pos);
player_set_subtitle_scale(ui->player, ui->prefs->sub_scale);
player_set_subtitle_font(ui->player, ui->prefs->sub_font);
player_set_subtitle_font_size(ui->player, ui->prefs->sub_font_size);
player_set_subtitle_color(ui->player, ui->prefs->sub_color, ui->prefs->sub_color_opacity);
player_set_subtitle_border_color(ui->player, ui->prefs->sub_border_color, ui->prefs->sub_border_opacity);
player_set_subtitle_border_size(ui->player, ui->prefs->sub_border_size);
player_set_subtitle_border_enabled(ui->player, ui->prefs->sub_border_enabled);
player_set_subtitle_shadow_color(ui->player, ui->prefs->sub_shadow_color, ui->prefs->sub_shadow_opacity);
player_set_subtitle_shadow_offset(ui->player, ui->prefs->sub_shadow_offset);
player_set_subtitle_shadow_enabled(ui->player, ui->prefs->sub_shadow_enabled);
player_set_subtitle_bold(ui->player, ui->prefs->sub_bold);
player_set_subtitle_italic(ui->player, ui->prefs->sub_italic);
}
static void ui_apply_ytdl_resolution(AppUI *ui)
{
if (!ui || !ui->player || !ui->prefs) return;
if (ui->prefs->ytdl_max_resolution > 0) {
char ytdl_format[128];
snprintf(ytdl_format, sizeof(ytdl_format), "bestvideo[height<=%d]+bestaudio/best[height<=%d]",
ui->prefs->ytdl_max_resolution, ui->prefs->ytdl_max_resolution);
player_set_option_string(ui->player, "ytdl-format", ytdl_format);
} else {
player_set_option_string(ui->player, "ytdl-format", "bestvideo+bestaudio/best");
}
}
static void ui_apply_preferences_internal(gpointer data)
{
AppUI *ui = (AppUI *)data;
if (!ui || !ui->player || !ui->prefs) return;
/* Apply subtitle settings */
ui_apply_subtitle_preferences(ui);
/* Volume settings */
player_set_volume(ui->player, ui->prefs->default_volume);
player_set_volume_boost(ui->player, ui->prefs->enable_volume_boost);
/* Mouse cursor hiding in windowed mode */
if (!ui->is_fullscreen) {
if (ui->prefs->hide_cursor_windowed) {
if (ui->auto_hide_timer_id == 0) {
ui->auto_hide_timer_id = g_timeout_add(3000, auto_hide_timer_cb, ui);
}
} else {
if (ui->auto_hide_timer_id > 0) {
g_source_remove(ui->auto_hide_timer_id);
ui->auto_hide_timer_id = 0;
}
/* Restore cursor */
GdkWindow *window = gtk_widget_get_window(ui->video_area);
if (window) gdk_window_set_cursor(window, NULL);
}
}
ui_apply_ytdl_resolution(ui);
}
void ui_set_preferences(AppUI *ui, Preferences *prefs)
{
if (!ui || !prefs) return;
if (ui->prefs) {
preferences_free(ui->prefs);
}
ui->prefs = prefs;
/* Apply some immediate settings */
if (ui->player) {
player_set_volume_boost(ui->player, ui->prefs->enable_volume_boost);
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);
}
ui_apply_subtitle_preferences(ui);
}
if (ui->controls) {
controls_update_volume_boost(ui->controls, ui->prefs->enable_volume_boost);
}
/* Update playlist visibility if UI is already realized */
if (ui->side_notebook) {
ui->playlist_visible = ui->prefs->show_playlist;
if (ui->playlist_visible) {
gtk_notebook_set_current_page(GTK_NOTEBOOK(ui->side_notebook), 0);
gtk_widget_show(ui->side_notebook);
} else {
gtk_widget_hide(ui->side_notebook);
}
}
}
GtkWidget* ui_get_window(AppUI *ui)
{
if (!ui) return NULL;
return ui->window;
}
Player* ui_get_player(AppUI *ui)
{
if (!ui) return NULL;
return ui->player;
}
Playlist* ui_get_playlist(AppUI *ui)
{
if (!ui) return NULL;
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;
gtk_main();
}
void ui_open_file(AppUI *ui)
{
if (!ui) return;
GSList *files = dialogs_open_files(GTK_WINDOW(ui->window), ui->prefs);
if (files) {
playlist_clear(ui->playlist);
gboolean first = TRUE;
for (GSList *iter = files; iter != NULL; iter = iter->next) {
playlist_add_file(ui->playlist, (const char *)iter->data, !first);
first = FALSE;
}
if (playlist_get_count(ui->playlist) > 0) {
playlist_play_index(ui->playlist, 0);
}
g_slist_free_full(files, g_free);
}
}
static gboolean is_media_file(const char *filename)
{
const char *exts[] = {
".mkv", ".mp4", ".avi", ".wmv", ".mov", ".webm", ".flv", ".m4v", ".ts", ".m2ts",
".vob", ".ogv", ".3gp", ".mp3", ".flac", ".ogg", ".wav", ".m4a", ".aac", ".opus",
NULL
};
char *lower = g_utf8_strdown(filename, -1);
gboolean found = FALSE;
for (int i = 0; exts[i] != NULL; i++) {
if (g_str_has_suffix(lower, exts[i])) {
found = TRUE;
break;
}
}
g_free(lower);
return found;
}
void ui_open_directory(AppUI *ui)
{
if (!ui) return;
char *dir_path = dialogs_open_directory(GTK_WINDOW(ui->window), ui->prefs);
if (dir_path) {
add_path_to_playlist(ui, dir_path, TRUE);
if (playlist_get_count(ui->playlist) > 0) {
playlist_play_index(ui->playlist, 0);
}
g_free(dir_path);
}
}
void ui_open_url(AppUI *ui)
{
if (!ui) return;
char *url = dialogs_open_url(GTK_WINDOW(ui->window), ui->prefs);
if (url) {
playlist_add_file(ui->playlist, url, TRUE);
g_free(url);
}
}
void ui_load_subtitle(AppUI *ui)
{
if (!ui) return;
char *subtitle = dialogs_open_subtitle(GTK_WINDOW(ui->window), ui->prefs);
if (subtitle) {
player_load_subtitle(ui->player, subtitle);
g_free(subtitle);
}
}
void ui_toggle_fullscreen(AppUI *ui)
{
if (!ui) return;
ui->is_fullscreen = !ui->is_fullscreen;
GtkWidget *controls_widget = controls_get_widget(ui->controls);
if (ui->is_fullscreen) {
gtk_widget_hide(ui->menubar);
if (ui->playlist_visible) {
gtk_widget_hide(ui->side_notebook);
}
/* Create overlay window for controls */
ui->fs_window = gtk_window_new(GTK_WINDOW_POPUP);
gtk_window_set_type_hint(GTK_WINDOW(ui->fs_window), GDK_WINDOW_TYPE_HINT_DOCK);
/* Move controls to overlay */
g_object_ref(controls_widget);
gtk_container_remove(GTK_CONTAINER(ui->main_vbox), controls_widget);
gtk_container_add(GTK_CONTAINER(ui->fs_window), controls_widget);
g_object_unref(controls_widget);
/* Position at bottom of screen */
GtkRequisition req;
gtk_widget_size_request(controls_widget, &req);
int ctrl_h = req.height;
/* For fullscreen, we should use the specific monitor size */
GdkScreen *screen = gtk_window_get_screen(GTK_WINDOW(ui->window));
gint monitor_num = gdk_screen_get_monitor_at_window(screen, ui->window->window);
GdkRectangle geom;
gdk_screen_get_monitor_geometry(screen, monitor_num, &geom);
gtk_window_set_default_size(GTK_WINDOW(ui->fs_window), geom.width, ctrl_h);
gtk_window_move(GTK_WINDOW(ui->fs_window), geom.x, geom.y + geom.height - ctrl_h);
gtk_widget_show_all(ui->fs_window);
gtk_window_fullscreen(GTK_WINDOW(ui->window));
/* Start auto-hide timer */
if (ui->auto_hide_timer_id > 0) {
g_source_remove(ui->auto_hide_timer_id);
}
ui->auto_hide_timer_id = g_timeout_add(3000, auto_hide_timer_cb, ui);
} else {
gtk_window_unfullscreen(GTK_WINDOW(ui->window));
/* Restore controls to main window */
if (ui->fs_window) {
gtk_widget_hide(ui->fs_window);
g_object_ref(controls_widget);
gtk_container_remove(GTK_CONTAINER(ui->fs_window), controls_widget);
gtk_box_pack_start(GTK_BOX(ui->main_vbox), controls_widget, FALSE, FALSE, 0);
g_object_unref(controls_widget);
gtk_widget_destroy(ui->fs_window);
ui->fs_window = NULL;
}
gtk_widget_show(ui->menubar);
gtk_widget_show(controls_widget);
if (ui->playlist_visible) {
gtk_widget_show(ui->side_notebook);
}
/* Stop auto-hide timer unless windowed hiding is on */
if (!ui->prefs->hide_cursor_windowed && ui->auto_hide_timer_id > 0) {
g_source_remove(ui->auto_hide_timer_id);
ui->auto_hide_timer_id = 0;
/* Restore cursor */
GdkWindow *window = gtk_widget_get_window(ui->video_area);
if (window) gdk_window_set_cursor(window, NULL);
}
}
}
void ui_toggle_playlist(AppUI *ui)
{
if (!ui) return;
int current_page = gtk_notebook_get_current_page(GTK_NOTEBOOK(ui->side_notebook));
if (ui->playlist_visible && current_page == 0) {
ui->playlist_visible = FALSE;
gtk_widget_hide(ui->side_notebook);
} else {
ui->playlist_visible = TRUE;
gtk_notebook_set_current_page(GTK_NOTEBOOK(ui->side_notebook), 0);
gtk_widget_show(ui->side_notebook);
}
}
void ui_toggle_chapters(AppUI *ui)
{
if (!ui) return;
int current_page = gtk_notebook_get_current_page(GTK_NOTEBOOK(ui->side_notebook));
if (ui->playlist_visible && current_page == 1) {
ui->playlist_visible = FALSE;
gtk_widget_hide(ui->side_notebook);
} else {
ui->playlist_visible = TRUE;
gtk_notebook_set_current_page(GTK_NOTEBOOK(ui->side_notebook), 1);
gtk_widget_show(ui->side_notebook);
}
}
void ui_toggle_always_on_top(AppUI *ui)
{
if (!ui) return;
ui->always_on_top = !ui->always_on_top;
gtk_window_set_keep_above(GTK_WINDOW(ui->window), ui->always_on_top);
}
void ui_update_title(AppUI *ui, const char *media_title)
{
if (!ui) return;
char title[1024];
if (media_title && strlen(media_title) > 0) {
snprintf(title, sizeof(title), "%s - Kino", media_title);
} else {
snprintf(title, sizeof(title), "Kino");
}
gtk_window_set_title(GTK_WINDOW(ui->window), title);
}
/* Private functions */
static void on_video_area_realize(GtkWidget *widget, gpointer data)
{
AppUI *ui = (AppUI *)data;
/* Get XID of the video area */
GdkWindow *gdk_window = gtk_widget_get_window(widget);
unsigned long xid = GDK_WINDOW_XID(gdk_window);
/* Set window and initialize player */
player_set_window(ui->player, xid);
if (player_init(ui->player) < 0) {
fprintf(stderr, "Failed to initialize player\n");
return;
}
/* Set options */
player_set_option_string(ui->player, "osd-level", "1");
if (ui->prefs->remember_position) {
player_set_option_string(ui->player, "save-position-on-quit", "yes");
}
/* Observe properties for UI updates */
player_observe_property(ui->player, "time-pos", MPV_FORMAT_DOUBLE, PROP_TIME_POS);
player_observe_property(ui->player, "duration", MPV_FORMAT_DOUBLE, PROP_DURATION);
player_observe_property(ui->player, "pause", MPV_FORMAT_FLAG, PROP_PAUSE);
player_observe_property(ui->player, "volume", MPV_FORMAT_DOUBLE, PROP_VOLUME);
player_observe_property(ui->player, "mute", MPV_FORMAT_FLAG, PROP_MUTE);
player_observe_property(ui->player, "media-title", MPV_FORMAT_STRING, PROP_MEDIA_TITLE);
player_observe_property(ui->player, "playlist-pos", MPV_FORMAT_INT64, PROP_PLAYLIST_POS);
player_observe_property(ui->player, "idle-active", MPV_FORMAT_FLAG, PROP_IDLE_ACTIVE);
player_observe_property(ui->player, "shuffle", MPV_FORMAT_FLAG, PROP_SHUFFLE);
player_observe_property(ui->player, "loop-playlist", MPV_FORMAT_STRING, PROP_LOOP);
}
static void on_player_event(Player *player, mpv_event *event, gpointer user_data)
{
(void)player;
AppUI *ui = (AppUI *)user_data;
switch (event->event_id) {
case MPV_EVENT_PROPERTY_CHANGE: {
mpv_event_property *prop = (mpv_event_property *)event->data;
if (prop->data == NULL) break;
switch (event->reply_userdata) {
case PROP_TIME_POS:
if (prop->format == MPV_FORMAT_DOUBLE) {
double pos = *(double *)prop->data;
controls_update_position(ui->controls, pos);
/* Detect seeking and notify MPRIS */
if (fabs(pos - ui->last_mpris_pos) > 2.0) {
mpris_emit_seeked(ui, (gint64)(pos * 1000000.0));
}
ui->last_mpris_pos = pos;
}
break;
case PROP_DURATION:
if (prop->format == MPV_FORMAT_DOUBLE) {
double dur = *(double *)prop->data;
controls_update_duration(ui->controls, dur);
}
break;
case PROP_PAUSE:
if (prop->format == MPV_FORMAT_FLAG) {
int paused = *(int *)prop->data;
controls_update_pause_state(ui->controls, paused);
mpris_update_playback_status(ui);
}
break;
case PROP_VOLUME:
if (prop->format == MPV_FORMAT_DOUBLE) {
double vol = *(double *)prop->data;
controls_update_volume(ui->controls, vol);
mpris_update_volume(ui);
}
break;
case PROP_MUTE:
if (prop->format == MPV_FORMAT_FLAG) {
int muted = *(int *)prop->data;
controls_update_mute_state(ui->controls, muted);
if (ui->mute_item) {
g_signal_handlers_block_by_func(ui->mute_item, G_CALLBACK(on_audio_mute), ui);
gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(ui->mute_item), muted);
g_signal_handlers_unblock_by_func(ui->mute_item, G_CALLBACK(on_audio_mute), ui);
}
}
break;
case PROP_MEDIA_TITLE:
if (prop->format == MPV_FORMAT_STRING) {
char *title = *(char **)prop->data;
ui_update_title(ui, title);
mpris_update_metadata(ui);
}
break;
case PROP_PLAYLIST_POS:
if (prop->format == MPV_FORMAT_INT64) {
int64_t pos = *(int64_t *)prop->data;
playlist_set_current_index(ui->playlist, (int)pos);
}
break;
case PROP_IDLE_ACTIVE:
if (prop->format == MPV_FORMAT_FLAG) {
int idle = *(int *)prop->data;
if (idle) {
controls_reset(ui->controls);
ui_update_title(ui, "Kino");
}
mpris_update_playback_status(ui);
}
break;
case PROP_SHUFFLE:
mpris_update_loop_shuffle(ui);
break;
case PROP_LOOP:
mpris_update_loop_shuffle(ui);
break;
case PROP_CHAPTER:
/* Just refresh the menu to update radio selection */
update_chapter_menu(ui);
break;
}
break;
}
case MPV_EVENT_START_FILE:
ui->last_mpris_pos = 0;
mpris_update_playback_status(ui);
/* Re-apply subtitle preferences for each new file */
ui_apply_subtitle_preferences(ui);
break;
case MPV_EVENT_FILE_LOADED: {
/* Sync playlist position */
playlist_sync_from_player(ui->playlist);
/* Update Play/Pause icon (loading starts playing) */
controls_update_pause_state(ui->controls, FALSE);
/* Update audio/subtitle menus */
void update_track_menus(AppUI *ui);
update_track_menus(ui);
update_chapter_menu(ui);
mpris_update_metadata(ui);
mpris_update_playback_status(ui);
/* Auto-resize window to fit video */
if (ui->prefs->auto_resize && !ui->is_fullscreen) {
int vw, vh;
player_get_video_resolution(ui->player, &vw, &vh);
if (vw > 0 && vh > 0) {
GtkAllocation menu_alloc = {0}, ctrl_alloc = {0}, playlist_alloc = {0};
gtk_widget_get_allocation(ui->menubar, &menu_alloc);
gtk_widget_get_allocation(controls_get_widget(ui->controls), &ctrl_alloc);
int win_w = vw;
int win_h = vh + menu_alloc.height + ctrl_alloc.height;
if (ui->playlist_visible) {
gtk_widget_get_allocation(playlist_get_widget(ui->playlist), &playlist_alloc);
win_w += playlist_alloc.width;
}
/* Don't resize if it would be larger than the screen (simplified check) */
GdkScreen *screen = gtk_window_get_screen(GTK_WINDOW(ui->window));
if (win_w < gdk_screen_get_width(screen) && win_h < gdk_screen_get_height(screen)) {
gtk_window_resize(GTK_WINDOW(ui->window), win_w, win_h);
}
}
}
/* Add to recent files */
char *filename = player_get_filename(ui->player);
if (filename) {
char *uri = NULL;
if (g_str_has_prefix(filename, "/")) {
uri = g_filename_to_uri(filename, NULL, NULL);
} else if (strstr(filename, "://")) {
uri = g_strdup(filename);
}
if (uri) {
GtkRecentData data;
memset(&data, 0, sizeof(data));
data.display_name = player_get_media_title(ui->player);
data.mime_type = "video/mp4"; /* Generic */
data.app_name = "gtk2-mpv-player";
data.app_exec = "gtk2-mpv-player %u";
gtk_recent_manager_add_full(ui->recent_manager, uri, &data);
g_free(data.display_name);
g_free(uri);
}
g_free(filename);
}
/* Start auto-hide timer if needed */
if (ui->is_fullscreen || ui->prefs->hide_cursor_windowed) {
if (ui->auto_hide_timer_id > 0) g_source_remove(ui->auto_hide_timer_id);
ui->auto_hide_timer_id = g_timeout_add(3000, auto_hide_timer_cb, ui);
}
break;
}
case MPV_EVENT_END_FILE:
/* mpv now handles looping natively via loop-playlist property */
break;
case MPV_EVENT_SHUTDOWN:
gtk_main_quit();
break;
default:
break;
}
}
static void on_fullscreen_requested(gboolean fullscreen, gpointer user_data)
{
(void)fullscreen;
AppUI *ui = (AppUI *)user_data;
ui_toggle_fullscreen(ui);
}
static gboolean on_window_configure(GtkWidget *widget, GdkEventConfigure *event, gpointer data)
{
(void)event;
AppUI *ui = (AppUI *)data;
/* Force full redraw of main areas on resize to prevent artifacts */
gtk_widget_queue_draw(ui->window);
gtk_widget_queue_draw(ui->video_area);
return FALSE; /* Propagate event */
}
static gboolean on_window_delete(GtkWidget *widget, GdkEvent *event, gpointer data)
{
(void)widget;
(void)event;
(void)data;
gtk_main_quit();
return FALSE;
}
static gboolean on_key_press(GtkWidget *widget, GdkEventKey *event, gpointer data)
{
(void)widget;
AppUI *ui = (AppUI *)data;
GdkModifierType relevant = GDK_CONTROL_MASK | GDK_SHIFT_MASK | GDK_MOD1_MASK;
GdkModifierType mods = event->state & relevant;
KeybindAction action = keybind_manager_find_action(ui->keybinds, event->keyval, mods);
switch (action) {
case KEYBIND_PLAY_PAUSE:
on_playback_play_pause(NULL, ui);
return TRUE;
case KEYBIND_STOP:
player_stop(ui->player);
return TRUE;
case KEYBIND_FULLSCREEN:
ui_toggle_fullscreen(ui);
return TRUE;
case KEYBIND_EXIT_FULLSCREEN:
if (ui->is_fullscreen) ui_toggle_fullscreen(ui);
return TRUE;
case KEYBIND_SEEK_FORWARD:
player_seek(ui->player, 5);
return TRUE;
case KEYBIND_SEEK_BACKWARD:
player_seek(ui->player, -5);
return TRUE;
case KEYBIND_VOLUME_UP:
player_set_volume(ui->player, player_get_volume(ui->player) + 5);
return TRUE;
case KEYBIND_VOLUME_DOWN:
player_set_volume(ui->player, player_get_volume(ui->player) - 5);
return TRUE;
case KEYBIND_MUTE:
player_set_muted(ui->player, !player_get_muted(ui->player));
return TRUE;
case KEYBIND_PREV_CHAPTER:
player_prev_chapter(ui->player);
return TRUE;
case KEYBIND_NEXT_CHAPTER:
player_next_chapter(ui->player);
return TRUE;
case KEYBIND_FRAME_STEP:
on_playback_frame_step(NULL, ui);
return TRUE;
case KEYBIND_OPEN_FILE:
ui_open_file(ui);
return TRUE;
case KEYBIND_OPEN_URL: {
char *url = dialogs_open_url(GTK_WINDOW(ui->window), ui->prefs);
if (url) {
playlist_clear(ui->playlist);
playlist_add_file(ui->playlist, url, FALSE);
playlist_play_index(ui->playlist, 0);
g_free(url);
}
return TRUE;
}
case KEYBIND_GOTO_TIME:
on_playback_goto_time(NULL, ui);
return TRUE;
case KEYBIND_SCREENSHOT:
on_view_screenshot(NULL, ui);
return TRUE;
case KEYBIND_TOGGLE_PLAYLIST:
on_view_playlist(NULL, ui);
return TRUE;
case KEYBIND_QUIT:
gtk_main_quit();
return TRUE;
default:
break;
}
return FALSE;
}
static gboolean on_drag_drop(GtkWidget *widget, GdkDragContext *context, gint x, gint y,
guint time, gpointer data)
{
(void)widget;
(void)x;
(void)y;
(void)data;
GdkAtom target = gtk_drag_dest_find_target(widget, context, NULL);
if (target != GDK_NONE) {
gtk_drag_get_data(widget, context, target, time);
return TRUE;
}
return FALSE;
}
static void on_drag_data_received(GtkWidget *widget, GdkDragContext *context, gint x, gint y,
GtkSelectionData *selection, guint info, guint time, gpointer data)
{
(void)widget;
(void)x;
(void)y;
(void)info;
AppUI *ui = (AppUI *)data;
if (gtk_selection_data_get_length(selection) > 0) {
char **uris = gtk_selection_data_get_uris(selection);
if (uris) {
for (int i = 0; uris[i] != NULL; i++) {
char *filename = g_filename_from_uri(uris[i], NULL, NULL);
if (filename) {
add_path_to_playlist(ui, filename, FALSE);
g_free(filename);
}
}
g_strfreev(uris);
}
}
gtk_drag_finish(context, TRUE, FALSE, time);
}
static gboolean on_video_area_scroll(GtkWidget *widget, GdkEventScroll *event, gpointer data)
{
(void)widget;
AppUI *ui = (AppUI *)data;
if (ui->prefs->mouse_wheel_seeks) {
if (event->direction == GDK_SCROLL_UP) {
player_seek(ui->player, 5);
} else if (event->direction == GDK_SCROLL_DOWN) {
player_seek(ui->player, -5);
}
} else {
double vol = player_get_volume(ui->player);
if (event->direction == GDK_SCROLL_UP) {
player_set_volume(ui->player, vol + 5);
} else if (event->direction == GDK_SCROLL_DOWN) {
player_set_volume(ui->player, vol - 5);
}
}
return TRUE;
}
static gboolean on_video_area_button_press(GtkWidget *widget, GdkEventButton *event, gpointer data)
{
(void)widget;
AppUI *ui = (AppUI *)data;
if (event->type == GDK_2BUTTON_PRESS && event->button == 1) {
ui_toggle_fullscreen(ui);
return TRUE;
} else if (event->type == GDK_BUTTON_PRESS && event->button == 3) {
on_playback_play_pause(NULL, ui);
return TRUE;
} else if (event->type == GDK_BUTTON_PRESS && event->button == 1 && (event->state & GDK_CONTROL_MASK)) {
ui->sub_dragging = TRUE;
/* Update position immediately */
GtkAllocation alloc;
gtk_widget_get_allocation(ui->video_area, &alloc);
if (alloc.height > 0) {
int pos = (int)((event->y / alloc.height) * 100);
if (pos < 0) pos = 0;
if (pos > 100) pos = 100;
player_set_subtitle_pos(ui->player, pos);
/* Show feedback */
char msg[32];
snprintf(msg, sizeof(msg), "Subtitle Position: %d%%", pos);
const char *cmd[] = {"show-text", msg, "1000", NULL};
mpv_command_async(player_get_mpv_handle(ui->player), 0, cmd);
}
return TRUE;
}
return FALSE;
}
static gboolean on_video_area_button_release(GtkWidget *widget, GdkEventButton *event, gpointer data)
{
(void)widget;
AppUI *ui = (AppUI *)data;
if (event->button == 1) {
ui->sub_dragging = FALSE;
}
return FALSE;
}
static gboolean auto_hide_timer_cb(gpointer data)
{
AppUI *ui = (AppUI *)data;
/* Hide controls only if in fullscreen */
if (ui->is_fullscreen) {
if (ui->fs_window) {
gtk_widget_hide(ui->fs_window);
} else {
gtk_widget_hide(controls_get_widget(ui->controls));
}
}
/* Hide cursor if in fullscreen OR if hide_cursor_windowed is enabled */
if (ui->is_fullscreen || ui->prefs->hide_cursor_windowed) {
GdkWindow *window = gtk_widget_get_window(ui->video_area);
if (window) {
GdkCursor *cursor = gdk_cursor_new(GDK_BLANK_CURSOR);
gdk_window_set_cursor(window, cursor);
gdk_cursor_unref(cursor);
}
}
ui->auto_hide_timer_id = 0;
return FALSE; /* Stop timer */
}
static gboolean on_video_area_motion(GtkWidget *widget, GdkEventMotion *event, gpointer data)
{
(void)widget;
AppUI *ui = (AppUI *)data;
if (ui->sub_dragging) {
GtkAllocation alloc;
gtk_widget_get_allocation(ui->video_area, &alloc);
if (alloc.height > 0) {
int pos = (int)((event->y / alloc.height) * 100);
if (pos < 0) pos = 0;
if (pos > 100) pos = 100;
player_set_subtitle_pos(ui->player, pos);
/* Update UI preferences state so it survives restart/dialog */
if (ui->prefs) ui->prefs->sub_pos = pos;
/* Show feedback */
char msg[32];
snprintf(msg, sizeof(msg), "Subtitle Position: %d%%", pos);
const char *cmd[] = {"show-text", msg, "500", NULL};
mpv_command_async(player_get_mpv_handle(ui->player), 0, cmd);
}
return TRUE;
}
if (ui->is_fullscreen || ui->prefs->hide_cursor_windowed) {
/* Show controls */
if (ui->is_fullscreen && ui->fs_window) {
gtk_widget_show_all(ui->fs_window);
} else {
gtk_widget_show(controls_get_widget(ui->controls));
}
/* Show cursor */
GdkWindow *window = gtk_widget_get_window(ui->video_area);
if (window) {
gdk_window_set_cursor(window, NULL); /* Restore default cursor */
}
/* Reset timer */
if (ui->auto_hide_timer_id > 0) {
g_source_remove(ui->auto_hide_timer_id);
}
ui->auto_hide_timer_id = g_timeout_add(3000, auto_hide_timer_cb, ui);
}
return FALSE;
}
static void add_path_to_playlist(AppUI *ui, const char *path, gboolean clear)
{
if (!ui || !path) return;
if (g_file_test(path, G_FILE_TEST_IS_DIR)) {
GDir *dir = g_dir_open(path, 0, NULL);
if (dir) {
const char *name;
GSList *file_list = NULL;
while ((name = g_dir_read_name(dir)) != NULL) {
if (is_media_file(name)) {
file_list = g_slist_prepend(file_list, g_strdup(name));
}
}
g_dir_close(dir);
if (file_list) {
if (clear) playlist_clear(ui->playlist);
file_list = g_slist_sort(file_list, (GCompareFunc)g_ascii_strcasecmp);
gboolean first = clear;
for (GSList *iter = file_list; iter != NULL; iter = iter->next) {
char *full_path = g_build_filename(path, (char *)iter->data, NULL);
playlist_add_file(ui->playlist, full_path, !first);
first = FALSE;
g_free(full_path);
}
g_slist_free_full(file_list, g_free);
}
}
} else {
if (clear) playlist_clear(ui->playlist);
playlist_add_file(ui->playlist, path, !clear);
}
}
/* Menu population helpers */
static void clear_menu(GtkWidget *menu)
{
GList *children = gtk_container_get_children(GTK_CONTAINER(menu));
GList *iter;
for (iter = children; iter != NULL; iter = iter->next) {
gtk_widget_destroy(GTK_WIDGET(iter->data));
}
g_list_free(children);
}
void update_track_menus(AppUI *ui)
{
if (!ui || !ui->player) return;
/* Update Audio Menu */
clear_menu(ui->audio_menu);
int audio_count = 0;
char **audio_tracks = player_get_audio_track_list(ui->player, &audio_count);
if (audio_count == 0) {
GtkWidget *item = gtk_menu_item_new_with_label("None available");
gtk_widget_set_sensitive(item, FALSE);
gtk_menu_shell_append(GTK_MENU_SHELL(ui->audio_menu), item);
} else {
for (int i = 0; i < audio_count; i++) {
GtkWidget *item = gtk_menu_item_new_with_label(audio_tracks[i]);
g_object_set_data(G_OBJECT(item), "track", GINT_TO_POINTER(i + 1));
g_signal_connect(item, "activate", G_CALLBACK(on_audio_track_selected), ui);
gtk_menu_shell_append(GTK_MENU_SHELL(ui->audio_menu), item);
}
player_free_track_list(audio_tracks, audio_count);
}
gtk_widget_show_all(ui->audio_menu);
/* Update Subtitle Menu */
clear_menu(ui->subtitle_menu);
GtkWidget *sub_settings_item = gtk_menu_item_new_with_label("Subtitle Settings...");
g_signal_connect(sub_settings_item, "activate", G_CALLBACK(on_subtitle_settings_clicked), ui);
gtk_menu_shell_append(GTK_MENU_SHELL(ui->subtitle_menu), sub_settings_item);
GtkWidget *sub_sep = gtk_separator_menu_item_new();
gtk_menu_shell_append(GTK_MENU_SHELL(ui->subtitle_menu), sub_sep);
int sub_count = 0;
char **sub_tracks = player_get_subtitle_track_list(ui->player, &sub_count);
if (sub_count == 0) {
GtkWidget *item = gtk_menu_item_new_with_label("None available");
gtk_widget_set_sensitive(item, FALSE);
gtk_menu_shell_append(GTK_MENU_SHELL(ui->subtitle_menu), item);
} else {
GtkWidget *sub_disable_item = gtk_menu_item_new_with_mnemonic("_Disable");
g_signal_connect(sub_disable_item, "activate", G_CALLBACK(on_subtitle_disable), ui);
gtk_menu_shell_append(GTK_MENU_SHELL(ui->subtitle_menu), sub_disable_item);
gtk_menu_shell_append(GTK_MENU_SHELL(ui->subtitle_menu), gtk_separator_menu_item_new());
for (int i = 0; i < sub_count; i++) {
GtkWidget *item = gtk_menu_item_new_with_label(sub_tracks[i]);
g_object_set_data(G_OBJECT(item), "track", GINT_TO_POINTER(i + 1));
g_signal_connect(item, "activate", G_CALLBACK(on_subtitle_track_selected), ui);
gtk_menu_shell_append(GTK_MENU_SHELL(ui->subtitle_menu), item);
}
player_free_track_list(sub_tracks, sub_count);
}
gtk_widget_show_all(ui->subtitle_menu);
}
static void create_menubar(AppUI *ui)
{
ui->menubar = gtk_menu_bar_new();
GtkAccelGroup *accel_group = gtk_accel_group_new();
gtk_window_add_accel_group(GTK_WINDOW(ui->window), accel_group);
/* File Menu */
GtkWidget *file_menu = gtk_menu_new();
GtkWidget *file_item = gtk_menu_item_new_with_mnemonic("_File");
gtk_menu_item_set_submenu(GTK_MENU_ITEM(file_item), file_menu);
GtkWidget *open_item = gtk_image_menu_item_new_from_stock(GTK_STOCK_OPEN, NULL);
g_signal_connect(open_item, "activate", G_CALLBACK(on_file_open), ui);
gtk_widget_add_accelerator(open_item, "activate", accel_group, GDK_o, GDK_CONTROL_MASK, GTK_ACCEL_VISIBLE);
gtk_menu_shell_append(GTK_MENU_SHELL(file_menu), open_item);
GtkWidget *open_dir_item = gtk_menu_item_new_with_mnemonic("Open _Folder...");
g_signal_connect(open_dir_item, "activate", G_CALLBACK(on_file_open_directory), ui);
gtk_menu_shell_append(GTK_MENU_SHELL(file_menu), open_dir_item);
GtkWidget *open_url_item = gtk_menu_item_new_with_mnemonic("Open _URL...");
g_signal_connect(open_url_item, "activate", G_CALLBACK(on_file_open_url), ui);
gtk_widget_add_accelerator(open_url_item, "activate", accel_group, GDK_l, GDK_CONTROL_MASK, GTK_ACCEL_VISIBLE);
gtk_menu_shell_append(GTK_MENU_SHELL(file_menu), open_url_item);
gtk_menu_shell_append(GTK_MENU_SHELL(file_menu), gtk_separator_menu_item_new());
ui->recent_menu = gtk_recent_chooser_menu_new_for_manager(ui->recent_manager);
GtkRecentFilter *filter = gtk_recent_filter_new();
gtk_recent_filter_add_mime_type(filter, "video/*");
gtk_recent_filter_add_mime_type(filter, "audio/*");
gtk_recent_chooser_set_filter(GTK_RECENT_CHOOSER(ui->recent_menu), filter);
gtk_recent_chooser_set_local_only(GTK_RECENT_CHOOSER(ui->recent_menu), FALSE);
gtk_recent_chooser_set_sort_type(GTK_RECENT_CHOOSER(ui->recent_menu), GTK_RECENT_SORT_MRU);
g_signal_connect(ui->recent_menu, "item-activated", G_CALLBACK(on_recent_item_activated), ui);
GtkWidget *recent_item = gtk_menu_item_new_with_mnemonic("_Recent Files");
gtk_menu_item_set_submenu(GTK_MENU_ITEM(recent_item), ui->recent_menu);
gtk_menu_shell_append(GTK_MENU_SHELL(file_menu), recent_item);
gtk_menu_shell_append(GTK_MENU_SHELL(file_menu), gtk_separator_menu_item_new());
GtkWidget *save_playlist_item = gtk_menu_item_new_with_mnemonic("_Save Playlist...");
g_signal_connect(save_playlist_item, "activate", G_CALLBACK(on_file_save_playlist), ui);
gtk_menu_shell_append(GTK_MENU_SHELL(file_menu), save_playlist_item);
gtk_menu_shell_append(GTK_MENU_SHELL(file_menu), gtk_separator_menu_item_new());
GtkWidget *quit_item = gtk_image_menu_item_new_from_stock(GTK_STOCK_QUIT, NULL);
g_signal_connect(quit_item, "activate", G_CALLBACK(on_file_quit), ui);
gtk_widget_add_accelerator(quit_item, "activate", accel_group, GDK_q, GDK_CONTROL_MASK, GTK_ACCEL_VISIBLE);
gtk_widget_add_accelerator(quit_item, "activate", accel_group, GDK_q, 0, 0);
gtk_menu_shell_append(GTK_MENU_SHELL(file_menu), quit_item);
gtk_menu_shell_append(GTK_MENU_SHELL(ui->menubar), file_item);
/* Playback Menu */
GtkWidget *playback_menu = gtk_menu_new();
GtkWidget *playback_item = gtk_menu_item_new_with_mnemonic("_Playback");
gtk_menu_item_set_submenu(GTK_MENU_ITEM(playback_item), playback_menu);
GtkWidget *play_pause_item = gtk_image_menu_item_new_from_stock(GTK_STOCK_MEDIA_PLAY, NULL);
gtk_menu_item_set_label(GTK_MENU_ITEM(play_pause_item), "_Play/Pause");
g_signal_connect(play_pause_item, "activate", G_CALLBACK(on_playback_play_pause), ui);
gtk_widget_add_accelerator(play_pause_item, "activate", accel_group, GDK_space, 0, GTK_ACCEL_VISIBLE);
gtk_menu_shell_append(GTK_MENU_SHELL(playback_menu), play_pause_item);
GtkWidget *stop_item = gtk_image_menu_item_new_from_stock(GTK_STOCK_MEDIA_STOP, NULL);
gtk_menu_item_set_label(GTK_MENU_ITEM(stop_item), "_Stop");
g_signal_connect(stop_item, "activate", G_CALLBACK(on_playback_stop), ui);
gtk_menu_shell_append(GTK_MENU_SHELL(playback_menu), stop_item);
gtk_menu_shell_append(GTK_MENU_SHELL(playback_menu), gtk_separator_menu_item_new());
GtkWidget *prev_item = gtk_image_menu_item_new_from_stock(GTK_STOCK_MEDIA_PREVIOUS, NULL);
g_signal_connect(prev_item, "activate", G_CALLBACK(on_playback_prev), ui);
gtk_widget_add_accelerator(prev_item, "activate", accel_group, GDK_Page_Up, GDK_CONTROL_MASK, 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, GDK_CONTROL_MASK, 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());
/* Seek Submenu */
GtkWidget *seek_menu = gtk_menu_new();
GtkWidget *seek_item = gtk_menu_item_new_with_mnemonic("_Seek");
gtk_menu_item_set_submenu(GTK_MENU_ITEM(seek_item), seek_menu);
GtkWidget *seek_fwd_item = gtk_menu_item_new_with_mnemonic("Forward 10s");
g_signal_connect(seek_fwd_item, "activate", G_CALLBACK(on_playback_seek_forward), ui);
gtk_widget_add_accelerator(seek_fwd_item, "activate", accel_group, GDK_Right, 0, GTK_ACCEL_VISIBLE);
gtk_menu_shell_append(GTK_MENU_SHELL(seek_menu), seek_fwd_item);
GtkWidget *seek_back_item = gtk_menu_item_new_with_mnemonic("Backward 10s");
g_signal_connect(seek_back_item, "activate", G_CALLBACK(on_playback_seek_backward), ui);
gtk_widget_add_accelerator(seek_back_item, "activate", accel_group, GDK_Left, 0, GTK_ACCEL_VISIBLE);
gtk_menu_shell_append(GTK_MENU_SHELL(seek_menu), seek_back_item);
GtkWidget *frame_step_item = gtk_menu_item_new_with_mnemonic("_Frame Step");
g_signal_connect(frame_step_item, "activate", G_CALLBACK(on_playback_frame_step), ui);
gtk_widget_add_accelerator(frame_step_item, "activate", accel_group, GDK_period, 0, GTK_ACCEL_VISIBLE);
gtk_menu_shell_append(GTK_MENU_SHELL(seek_menu), frame_step_item);
GtkWidget *goto_time_item = gtk_menu_item_new_with_mnemonic("_Go to Time...");
g_signal_connect(goto_time_item, "activate", G_CALLBACK(on_playback_goto_time), ui);
gtk_widget_add_accelerator(goto_time_item, "activate", accel_group, GDK_g, GDK_CONTROL_MASK, GTK_ACCEL_VISIBLE);
gtk_menu_shell_append(GTK_MENU_SHELL(seek_menu), goto_time_item);
gtk_menu_shell_append(GTK_MENU_SHELL(playback_menu), seek_item);
gtk_menu_shell_append(GTK_MENU_SHELL(playback_menu), gtk_separator_menu_item_new());
GtkWidget *speed_item = gtk_menu_item_new_with_mnemonic("Playback S_peed...");
g_signal_connect(speed_item, "activate", G_CALLBACK(on_playback_speed), ui);
gtk_menu_shell_append(GTK_MENU_SHELL(playback_menu), speed_item);
gtk_menu_shell_append(GTK_MENU_SHELL(playback_menu), gtk_separator_menu_item_new());
GtkWidget *ab_menu = gtk_menu_new();
GtkWidget *ab_item = gtk_menu_item_new_with_mnemonic("A-B _Loop");
gtk_menu_item_set_submenu(GTK_MENU_ITEM(ab_item), ab_menu);
GtkWidget *ab_a_item = gtk_menu_item_new_with_mnemonic("Set Point _A");
g_signal_connect(ab_a_item, "activate", G_CALLBACK(on_playback_ab_loop_a), ui);
gtk_menu_shell_append(GTK_MENU_SHELL(ab_menu), ab_a_item);
GtkWidget *ab_b_item = gtk_menu_item_new_with_mnemonic("Set Point _B");
g_signal_connect(ab_b_item, "activate", G_CALLBACK(on_playback_ab_loop_b), ui);
gtk_menu_shell_append(GTK_MENU_SHELL(ab_menu), ab_b_item);
GtkWidget *ab_clear_item = gtk_menu_item_new_with_mnemonic("_Clear Loop");
g_signal_connect(ab_clear_item, "activate", G_CALLBACK(on_playback_ab_loop_clear), ui);
gtk_menu_shell_append(GTK_MENU_SHELL(ab_menu), ab_clear_item);
gtk_menu_shell_append(GTK_MENU_SHELL(playback_menu), ab_item);
gtk_menu_shell_append(GTK_MENU_SHELL(ui->menubar), playback_item);
/* Audio Menu */
GtkWidget *audio_root_menu = gtk_menu_new();
GtkWidget *audio_item = gtk_menu_item_new_with_mnemonic("_Audio");
gtk_menu_item_set_submenu(GTK_MENU_ITEM(audio_item), audio_root_menu);
GtkWidget *audio_tracks_item = gtk_menu_item_new_with_mnemonic("_Select Track");
ui->audio_menu = gtk_menu_new();
gtk_menu_item_set_submenu(GTK_MENU_ITEM(audio_tracks_item), ui->audio_menu);
gtk_menu_shell_append(GTK_MENU_SHELL(audio_root_menu), audio_tracks_item);
GtkWidget *audio_placeholder = gtk_menu_item_new_with_label("None available");
gtk_widget_set_sensitive(audio_placeholder, FALSE);
gtk_menu_shell_append(GTK_MENU_SHELL(ui->audio_menu), audio_placeholder);
gtk_menu_shell_append(GTK_MENU_SHELL(audio_root_menu), gtk_separator_menu_item_new());
ui->mute_item = gtk_check_menu_item_new_with_mnemonic("_Muted");
g_signal_connect(ui->mute_item, "toggled", G_CALLBACK(on_audio_mute), ui);
gtk_menu_shell_append(GTK_MENU_SHELL(audio_root_menu), ui->mute_item);
gtk_menu_shell_append(GTK_MENU_SHELL(ui->menubar), audio_item);
/* Video Menu */
GtkWidget *video_menu = gtk_menu_new();
GtkWidget *video_item = gtk_menu_item_new_with_mnemonic("_Video");
gtk_menu_item_set_submenu(GTK_MENU_ITEM(video_item), video_menu);
/* Subtitles Submenu */
GtkWidget *subtitle_root_menu = gtk_menu_new();
GtkWidget *subtitle_item = gtk_menu_item_new_with_mnemonic("_Subtitles");
gtk_menu_item_set_submenu(GTK_MENU_ITEM(subtitle_item), subtitle_root_menu);
GtkWidget *sub_settings_item = gtk_menu_item_new_with_label("Subtitle Settings...");
g_signal_connect(sub_settings_item, "activate", G_CALLBACK(on_subtitle_settings_clicked), ui);
gtk_menu_shell_append(GTK_MENU_SHELL(subtitle_root_menu), sub_settings_item);
GtkWidget *sub_sep = gtk_separator_menu_item_new();
gtk_menu_shell_append(GTK_MENU_SHELL(subtitle_root_menu), sub_sep);
GtkWidget *sub_tracks_item = gtk_menu_item_new_with_mnemonic("_Select Track");
ui->subtitle_menu = gtk_menu_new();
gtk_menu_item_set_submenu(GTK_MENU_ITEM(sub_tracks_item), ui->subtitle_menu);
gtk_menu_shell_append(GTK_MENU_SHELL(subtitle_root_menu), sub_tracks_item);
GtkWidget *sub_placeholder = gtk_menu_item_new_with_label("None available");
gtk_widget_set_sensitive(sub_placeholder, FALSE);
gtk_menu_shell_append(GTK_MENU_SHELL(ui->subtitle_menu), sub_placeholder);
gtk_menu_shell_append(GTK_MENU_SHELL(subtitle_root_menu), gtk_separator_menu_item_new());
GtkWidget *sub_load_item = gtk_menu_item_new_with_mnemonic("_Load External...");
g_signal_connect(sub_load_item, "activate", G_CALLBACK(on_subtitle_load), ui);
gtk_menu_shell_append(GTK_MENU_SHELL(subtitle_root_menu), sub_load_item);
gtk_menu_shell_append(GTK_MENU_SHELL(video_menu), subtitle_item);
/* Chapters Submenu */
ui->chapter_menu = gtk_menu_new();
GtkWidget *chapter_item = gtk_menu_item_new_with_mnemonic("_Chapters");
gtk_menu_item_set_submenu(GTK_MENU_ITEM(chapter_item), ui->chapter_menu);
GtkWidget *chapter_placeholder = gtk_menu_item_new_with_label("(No chapters)");
gtk_widget_set_sensitive(chapter_placeholder, FALSE);
gtk_menu_shell_append(GTK_MENU_SHELL(ui->chapter_menu), chapter_placeholder);
gtk_menu_shell_append(GTK_MENU_SHELL(video_menu), chapter_item);
gtk_menu_shell_append(GTK_MENU_SHELL(video_menu), gtk_separator_menu_item_new());
/* Aspect ratio submenu */
ui->aspect_menu = gtk_menu_new();
GtkWidget *aspect_item = gtk_menu_item_new_with_mnemonic("_Aspect Ratio");
gtk_menu_item_set_submenu(GTK_MENU_ITEM(aspect_item), ui->aspect_menu);
const char *aspects[] = {"Auto", "4:3", "16:9", "16:10", "1.85:1", "2.35:1", NULL};
const char *aspect_vals[] = {"-1", "4:3", "16:9", "16:10", "1.85:1", "2.35:1", NULL};
for (int i = 0; aspects[i] != NULL; i++) {
GtkWidget *ar_item = gtk_menu_item_new_with_label(aspects[i]);
g_object_set_data(G_OBJECT(ar_item), "aspect", (gpointer)aspect_vals[i]);
g_signal_connect(ar_item, "activate", G_CALLBACK(on_aspect_ratio_selected), ui);
gtk_menu_shell_append(GTK_MENU_SHELL(ui->aspect_menu), ar_item);
}
gtk_menu_shell_append(GTK_MENU_SHELL(video_menu), aspect_item);
/* yt-dlp resolution submenu */
GtkWidget *res_item = gtk_menu_item_new_with_mnemonic("_Stream Quality (yt-dlp)");
GtkWidget *res_menu = gtk_menu_new();
gtk_menu_item_set_submenu(GTK_MENU_ITEM(res_item), res_menu);
const char *res_labels[] = {"Best", "2160p (4K)", "1440p (2K)", "1080p", "720p", "480p", "360p", NULL};
int res_vals[] = {0, 2160, 1440, 1080, 720, 480, 360};
for (int i = 0; res_labels[i] != NULL; i++) {
GtkWidget *ri = gtk_menu_item_new_with_label(res_labels[i]);
g_object_set_data(G_OBJECT(ri), "res", GINT_TO_POINTER(res_vals[i]));
g_signal_connect(ri, "activate", G_CALLBACK(on_ytdl_resolution_selected), ui);
gtk_menu_shell_append(GTK_MENU_SHELL(res_menu), ri);
}
gtk_menu_shell_append(GTK_MENU_SHELL(video_menu), res_item);
gtk_menu_shell_append(GTK_MENU_SHELL(video_menu), gtk_separator_menu_item_new());
GtkWidget *screenshot_item = gtk_menu_item_new_with_mnemonic("_Screenshot");
g_signal_connect(screenshot_item, "activate", G_CALLBACK(on_view_screenshot), ui);
gtk_widget_add_accelerator(screenshot_item, "activate", accel_group, GDK_s, GDK_CONTROL_MASK | GDK_SHIFT_MASK, GTK_ACCEL_VISIBLE);
gtk_menu_shell_append(GTK_MENU_SHELL(video_menu), screenshot_item);
gtk_menu_shell_append(GTK_MENU_SHELL(ui->menubar), video_item);
/* View Menu */
GtkWidget *view_menu = gtk_menu_new();
GtkWidget *view_item = gtk_menu_item_new_with_mnemonic("_View");
gtk_menu_item_set_submenu(GTK_MENU_ITEM(view_item), view_menu);
GtkWidget *fullscreen_item = gtk_menu_item_new_with_mnemonic("_Fullscreen");
g_signal_connect(fullscreen_item, "activate", G_CALLBACK(on_view_fullscreen), ui);
gtk_widget_add_accelerator(fullscreen_item, "activate", accel_group, GDK_f, 0, GTK_ACCEL_VISIBLE);
gtk_widget_add_accelerator(fullscreen_item, "activate", accel_group, GDK_F11, 0, GTK_ACCEL_VISIBLE);
gtk_menu_shell_append(GTK_MENU_SHELL(view_menu), fullscreen_item);
GtkWidget *playlist_toggle_item = gtk_menu_item_new_with_mnemonic("_Playlist");
g_signal_connect(playlist_toggle_item, "activate", G_CALLBACK(on_view_playlist), ui);
gtk_widget_add_accelerator(playlist_toggle_item, "activate", accel_group, GDK_p, GDK_CONTROL_MASK, GTK_ACCEL_VISIBLE);
gtk_menu_shell_append(GTK_MENU_SHELL(view_menu), playlist_toggle_item);
GtkWidget *chapters_toggle_item = gtk_menu_item_new_with_mnemonic("C_hapters");
g_signal_connect(chapters_toggle_item, "activate", G_CALLBACK(on_view_chapters), ui);
gtk_menu_shell_append(GTK_MENU_SHELL(view_menu), chapters_toggle_item);
GtkWidget *on_top_item = gtk_check_menu_item_new_with_mnemonic("Always on _Top");
gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(on_top_item), ui->always_on_top);
g_signal_connect(on_top_item, "toggled", G_CALLBACK(on_view_always_on_top), ui);
gtk_menu_shell_append(GTK_MENU_SHELL(view_menu), on_top_item);
gtk_menu_shell_append(GTK_MENU_SHELL(ui->menubar), view_item);
/* Tools Menu */
GtkWidget *tools_menu = gtk_menu_new();
GtkWidget *tools_item = gtk_menu_item_new_with_mnemonic("_Tools");
gtk_menu_item_set_submenu(GTK_MENU_ITEM(tools_item), tools_menu);
GtkWidget *plugins_menu = plugin_manager_get_menu(ui->plugin_manager);
GtkWidget *plugins_item = gtk_menu_item_new_with_mnemonic("_Plugins");
gtk_menu_item_set_submenu(GTK_MENU_ITEM(plugins_item), plugins_menu);
gtk_menu_shell_append(GTK_MENU_SHELL(tools_menu), plugins_item);
gtk_menu_shell_append(GTK_MENU_SHELL(tools_menu), gtk_separator_menu_item_new());
GtkWidget *prefs_item = gtk_image_menu_item_new_from_stock(GTK_STOCK_PREFERENCES, NULL);
g_signal_connect(prefs_item, "activate", G_CALLBACK(on_view_preferences), ui);
gtk_menu_shell_append(GTK_MENU_SHELL(tools_menu), prefs_item);
gtk_menu_shell_append(GTK_MENU_SHELL(ui->menubar), tools_item);
/* Help Menu */
GtkWidget *help_menu = gtk_menu_new();
GtkWidget *help_item = gtk_menu_item_new_with_mnemonic("_Help");
gtk_menu_item_set_submenu(GTK_MENU_ITEM(help_item), help_menu);
gtk_menu_item_set_right_justified(GTK_MENU_ITEM(help_item), FALSE);
GtkWidget *key_item = gtk_menu_item_new_with_label("Keyboard Shortcuts");
g_signal_connect(key_item, "activate", G_CALLBACK(on_help_keybinds), ui);
gtk_menu_shell_append(GTK_MENU_SHELL(help_menu), key_item);
GtkWidget *about_item = gtk_image_menu_item_new_from_stock(GTK_STOCK_ABOUT, NULL);
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(ui->menubar), help_item);
}
/* Menu callbacks */
static void on_audio_mute(GtkMenuItem *item, gpointer data)
{
AppUI *ui = (AppUI *)data;
player_set_muted(ui->player, gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(item)));
}
static void on_file_open(GtkMenuItem *item, gpointer data)
{
(void)item;
ui_open_file((AppUI *)data);
}
static void on_file_open_directory(GtkMenuItem *item, gpointer data)
{
(void)item;
ui_open_directory((AppUI *)data);
}
static void on_file_open_url(GtkMenuItem *item, gpointer data)
{
(void)item;
ui_open_url((AppUI *)data);
}
static void on_file_save_playlist(GtkMenuItem *item, gpointer data)
{
(void)item;
AppUI *ui = (AppUI *)data;
char *filename = dialogs_save_playlist(GTK_WINDOW(ui->window), ui->prefs);
if (filename) {
playlist_save_to_file(ui->playlist, filename);
g_free(filename);
}
}
static void on_file_quit(GtkMenuItem *item, gpointer data)
{
(void)item;
(void)data;
gtk_main_quit();
}
static void on_playback_play_pause(GtkMenuItem *item, gpointer data)
{
(void)item;
AppUI *ui = (AppUI *)data;
gboolean eof = player_get_eof_reached(ui->player);
gboolean idle = player_is_idle(ui->player);
fprintf(stderr, "DEBUG: on_playback_play_pause triggered. eof=%d, idle=%d\n", eof, idle);
if (eof) {
fprintf(stderr, "DEBUG: EOF reached, seeking to 0 and playing\n");
player_seek_absolute(ui->player, 0);
player_play(ui->player);
controls_update_pause_state(ui->controls, FALSE);
} else if (idle) {
fprintf(stderr, "DEBUG: Player idle, re-triggering playback\n");
/* If mpv playlist is empty, refill it from our UI playlist */
if (player_get_playlist_count(ui->player) == 0) {
fprintf(stderr, "DEBUG: mpv playlist empty, refilling\n");
playlist_refill_player(ui->playlist);
}
if (playlist_get_count(ui->playlist) > 0) {
int index = playlist_get_current_index(ui->playlist);
if (index < 0) index = 0;
playlist_play_index(ui->playlist, index);
player_play(ui->player);
controls_update_pause_state(ui->controls, FALSE);
}
} else {
fprintf(stderr, "DEBUG: Normal playback, toggling pause\n");
player_toggle_pause(ui->player);
}
}
static void on_playback_stop(GtkMenuItem *item, gpointer data)
{
(void)item;
AppUI *ui = (AppUI *)data;
player_stop(ui->player);
/* Immediately reset UI */
controls_reset(ui->controls);
ui_update_title(ui, "Kino");
}
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_chapter_selected(GtkMenuItem *item, gpointer data)
{
AppUI *ui = (AppUI *)data;
int index = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(item), "chapter-index"));
player_set_chapter(ui->player, index);
}
static void update_chapter_menu(AppUI *ui)
{
if (!ui || !ui->chapter_menu) return;
if (ui->prefs && !ui->prefs->enable_chapters) return;
/* Clear old menu */
GList *children = gtk_container_get_children(GTK_CONTAINER(ui->chapter_menu));
for (GList *iter = children; iter != NULL; iter = iter->next) {
gtk_widget_destroy(GTK_WIDGET(iter->data));
}
g_list_free(children);
/* Clear Side panel tree */
gtk_list_store_clear(ui->chapters_store);
int count = 0;
char **chapters = player_get_chapter_list(ui->player, &count);
double *times = player_get_chapter_times(ui->player, &count);
/* Sync markers to controls */
controls_set_chapters(ui->controls, count, times);
if (times) g_free(times);
if (count > 0 && chapters) {
int current = player_get_current_chapter(ui->player);
for (int i = 0; i < count; i++) {
GtkWidget *item = gtk_image_menu_item_new_with_label(chapters[i]);
if (i == current) {
GtkWidget *img = gtk_image_new_from_stock(GTK_STOCK_GO_FORWARD, GTK_ICON_SIZE_MENU);
gtk_image_menu_item_set_image(GTK_IMAGE_MENU_ITEM(item), img);
gtk_image_menu_item_set_always_show_image(GTK_IMAGE_MENU_ITEM(item), TRUE);
}
g_object_set_data(G_OBJECT(item), "chapter-index", GINT_TO_POINTER(i));
g_signal_connect(item, "activate", G_CALLBACK(on_chapter_selected), ui);
gtk_menu_shell_append(GTK_MENU_SHELL(ui->chapter_menu), item);
GtkTreeIter iter;
gtk_list_store_append(ui->chapters_store, &iter);
gtk_list_store_set(ui->chapters_store, &iter, 0, i, 1, chapters[i], -1);
}
player_free_track_list(chapters, count);
} else {
GtkWidget *placeholder = gtk_menu_item_new_with_label("(No chapters)");
gtk_widget_set_sensitive(placeholder, FALSE);
gtk_menu_shell_append(GTK_MENU_SHELL(ui->chapter_menu), placeholder);
GtkTreeIter iter;
gtk_list_store_append(ui->chapters_store, &iter);
gtk_list_store_set(ui->chapters_store, &iter, 0, -1, 1, "(No chapters)", -1);
}
gtk_widget_show_all(ui->chapter_menu);
}
static void on_playback_seek_forward(GtkMenuItem *item, gpointer data)
{
(void)item;
AppUI *ui = (AppUI *)data;
player_seek(ui->player, 10);
}
static void on_playback_seek_backward(GtkMenuItem *item, gpointer data)
{
(void)item;
AppUI *ui = (AppUI *)data;
player_seek(ui->player, -10);
}
static void on_playback_frame_step(GtkMenuItem *item, gpointer data)
{
(void)item;
AppUI *ui = (AppUI *)data;
player_frame_step(ui->player);
}
static void on_playback_goto_time(GtkMenuItem *item, gpointer data)
{
(void)item;
AppUI *ui = (AppUI *)data;
double current = player_get_position(ui->player);
double duration = player_get_duration(ui->player);
double new_time = dialogs_goto_time(GTK_WINDOW(ui->window), current, duration);
if (new_time >= 0) {
player_seek_absolute(ui->player, new_time);
}
}
static void on_playback_speed(GtkMenuItem *item, gpointer data)
{
(void)item;
AppUI *ui = (AppUI *)data;
double current = player_get_speed(ui->player);
double new_speed = dialogs_select_speed(GTK_WINDOW(ui->window), current);
if (new_speed > 0) {
player_set_speed(ui->player, new_speed);
}
}
static void on_playback_ab_loop_a(GtkMenuItem *item, gpointer data)
{
(void)item;
AppUI *ui = (AppUI *)data;
player_set_ab_loop_a(ui->player);
}
static void on_playback_ab_loop_b(GtkMenuItem *item, gpointer data)
{
(void)item;
AppUI *ui = (AppUI *)data;
player_set_ab_loop_b(ui->player);
}
static void on_playback_ab_loop_clear(GtkMenuItem *item, gpointer data)
{
(void)item;
AppUI *ui = (AppUI *)data;
player_clear_ab_loop(ui->player);
}
static void on_audio_track_selected(GtkMenuItem *item, gpointer data)
{
AppUI *ui = (AppUI *)data;
int track = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(item), "track"));
player_set_audio_track(ui->player, track);
}
static void on_subtitle_track_selected(GtkMenuItem *item, gpointer data)
{
AppUI *ui = (AppUI *)data;
int track = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(item), "track"));
player_set_subtitle_track(ui->player, track);
}
static void on_subtitle_load(GtkMenuItem *item, gpointer data)
{
(void)item;
AppUI *ui = (AppUI *)data;
ui_load_subtitle(ui);
}
static void on_subtitle_disable(GtkMenuItem *item, gpointer data)
{
(void)item;
AppUI *ui = (AppUI *)data;
player_set_subtitle_track(ui->player, 0); /* 0 = no subtitles */
}
static void on_view_fullscreen(GtkMenuItem *item, gpointer data)
{
(void)item;
AppUI *ui = (AppUI *)data;
ui_toggle_fullscreen(ui);
}
static void on_view_playlist(GtkMenuItem *item, gpointer data)
{
(void)item;
AppUI *ui = (AppUI *)data;
ui_toggle_playlist(ui);
}
static void on_view_chapters(GtkMenuItem *item, gpointer data)
{
(void)item;
AppUI *ui = (AppUI *)data;
ui_toggle_chapters(ui);
}
static void on_view_always_on_top(GtkMenuItem *item, gpointer data)
{
(void)item;
AppUI *ui = (AppUI *)data;
ui_toggle_always_on_top(ui);
}
static void on_view_screenshot(GtkMenuItem *item, gpointer data)
{
(void)item;
AppUI *ui = (AppUI *)data;
/* Use instant screenshot (saves to configured directory) */
player_screenshot(ui->player);
}
static void on_aspect_ratio_selected(GtkMenuItem *item, gpointer data)
{
AppUI *ui = (AppUI *)data;
const char *aspect = g_object_get_data(G_OBJECT(item), "aspect");
player_set_aspect_ratio(ui->player, aspect);
}
static void on_view_preferences(GtkMenuItem *item, gpointer data)
{
(void)item;
AppUI *ui = (AppUI *)data;
dialogs_show_preferences(GTK_WINDOW(ui->window), ui->prefs, ui->plugin_manager, ui->keybinds, -1, ui_apply_preferences_internal, ui);
}
static void on_subtitle_settings_clicked(GtkMenuItem *item, gpointer data)
{
(void)item;
AppUI *ui = (AppUI *)data;
/* Open preferences directly on the Subtitles tab (index 1) */
dialogs_show_preferences(GTK_WINDOW(ui->window), ui->prefs, ui->plugin_manager, ui->keybinds, 1, ui_apply_preferences_internal, ui);
}
static void on_help_keybinds(GtkMenuItem *item, gpointer data)
{
(void)item;
AppUI *ui = (AppUI *)data;
dialogs_show_keybinds(GTK_WINDOW(ui->window), ui->keybinds);
}
static void on_help_about(GtkMenuItem *item, gpointer data)
{
(void)item;
AppUI *ui = (AppUI *)data;
dialogs_show_about(GTK_WINDOW(ui->window));
}
static void on_controls_play_pause(gpointer data)
{
on_playback_play_pause(NULL, data);
}
static void on_recent_item_activated(GtkRecentChooser *chooser, gpointer data)
{
AppUI *ui = (AppUI *)data;
char *uri = gtk_recent_chooser_get_current_uri(chooser);
if (uri) {
char *path = g_filename_from_uri(uri, NULL, NULL);
if (path) {
add_path_to_playlist(ui, path, TRUE);
if (playlist_get_count(ui->playlist) > 0) {
playlist_play_index(ui->playlist, 0);
}
g_free(path);
} else if (strstr(uri, "://")) {
/* Handle URL */
playlist_clear(ui->playlist);
playlist_add_file(ui->playlist, uri, FALSE);
playlist_play_index(ui->playlist, 0);
}
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);
}
void ui_show_script_manager(AppUI *ui)
{
if (!ui) return;
dialogs_show_script_manager(GTK_WINDOW(ui->window), ui->player);
}
static void on_ytdl_resolution_selected(GtkMenuItem *item, gpointer data)
{
AppUI *ui = (AppUI *)data;
int res = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(item), "res"));
if (ui->prefs->ytdl_max_resolution == res) return;
ui->prefs->ytdl_max_resolution = res;
preferences_save(ui->prefs);
ui_apply_ytdl_resolution(ui);
}
static void on_side_panel_close_clicked(GtkButton *button, gpointer data)
{
(void)button;
AppUI *ui = (AppUI *)data;
ui->playlist_visible = FALSE;
ui->prefs->show_playlist = FALSE;
gtk_widget_hide(ui->side_notebook);
preferences_save(ui->prefs);
}
static void on_chapter_row_activated(GtkTreeView *tree_view, GtkTreePath *path, GtkTreeViewColumn *column, gpointer data)
{
(void)tree_view;
(void)column;
AppUI *ui = (AppUI *)data;
if (!ui || !ui->player) return;
GtkTreeIter iter;
if (gtk_tree_model_get_iter(GTK_TREE_MODEL(ui->chapters_store), &iter, path)) {
int index = -1;
gtk_tree_model_get(GTK_TREE_MODEL(ui->chapters_store), &iter, 0, &index, -1);
if (index >= 0) {
player_set_chapter(ui->player, index);
}
}
}