#include "ui.h" #include #include #include #include #include #include #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); } } }