2292 lines
84 KiB
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);
|
|
}
|
|
}
|
|
}
|