diff --git a/Kino b/Kino index 91258d2..3bcb878 100755 Binary files a/Kino and b/Kino differ diff --git a/src/dialogs.c b/src/dialogs.c index fce9301..f5ed568 100644 --- a/src/dialogs.c +++ b/src/dialogs.c @@ -982,6 +982,8 @@ Preferences* preferences_new(void) prefs->sub_shadow_opacity = 100; prefs->sub_shadow_enabled = TRUE; prefs->sub_bold = FALSE; + prefs->sub_italic = FALSE; + prefs->ytdl_max_resolution = 1080; return prefs; } @@ -1040,6 +1042,9 @@ void preferences_load(Preferences *prefs) load_bool_pref(keyfile, "sub_border_enabled", &prefs->sub_border_enabled); load_bool_pref(keyfile, "sub_shadow_enabled", &prefs->sub_shadow_enabled); + prefs->ytdl_max_resolution = g_key_file_get_integer(keyfile, "General", "ytdl_max_resolution", &error); + if (error) { prefs->ytdl_max_resolution = 1080; g_clear_error(&error); } + char *screenshot_dir = g_key_file_get_string(keyfile, "General", "screenshot_directory", &error); if (!error && screenshot_dir) { g_free(prefs->screenshot_directory); @@ -1158,9 +1163,11 @@ void preferences_save(Preferences *prefs) }; for (int i = 0; bool_prefs[i].key != NULL; i++) { - g_key_file_set_boolean(keyfile, "General", bool_prefs[i].key, bool_prefs[i].val); + g_key_file_set_boolean(keyfile, (i < 13) ? "General" : "Subtitles", bool_prefs[i].key, bool_prefs[i].val); } + g_key_file_set_integer(keyfile, "General", "ytdl_max_resolution", prefs->ytdl_max_resolution); + if (prefs->last_dir) { g_key_file_set_string(keyfile, "General", "last_dir", prefs->last_dir); } @@ -1473,6 +1480,23 @@ gboolean dialogs_show_preferences(GtkWindow *parent, Preferences *prefs, PluginM gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(format_combo), "webp"); gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(format_combo), "png"); gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(format_combo), "jpg"); + gtk_box_pack_start(GTK_BOX(format_hbox), format_combo, FALSE, FALSE, 0); + gtk_box_pack_start(GTK_BOX(misc_vbox), format_hbox, FALSE, FALSE, 0); + + /* Resolution Row */ + GtkWidget *res_hbox = gtk_hbox_new(FALSE, 10); + gtk_box_pack_start(GTK_BOX(res_hbox), gtk_label_new("Max Resolution (yt-dlp):"), FALSE, FALSE, 0); + GtkWidget *res_combo = gtk_combo_box_text_new(); + const char *res_labels[] = {"Best", "2160p (4K)", "1440p (2K)", "1080p", "720p", "480p", "360p", NULL}; + int res_values[] = {0, 2160, 1440, 1080, 720, 480, 360}; + int active_idx = 0; + for (int i = 0; res_labels[i] != NULL; i++) { + gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(res_combo), res_labels[i]); + if (prefs->ytdl_max_resolution == res_values[i]) active_idx = i; + } + gtk_combo_box_set_active(GTK_COMBO_BOX(res_combo), active_idx); + gtk_box_pack_start(GTK_BOX(res_hbox), res_combo, FALSE, FALSE, 0); + gtk_box_pack_start(GTK_BOX(misc_vbox), res_hbox, FALSE, FALSE, 0); if (g_strcmp0(prefs->screenshot_format, "webp") == 0) gtk_combo_box_set_active(GTK_COMBO_BOX(format_combo), 0); else if (g_strcmp0(prefs->screenshot_format, "png") == 0) gtk_combo_box_set_active(GTK_COMBO_BOX(format_combo), 1); @@ -1552,6 +1576,10 @@ gboolean dialogs_show_preferences(GtkWindow *parent, Preferences *prefs, PluginM prefs->enable_volume_boost = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(vol_boost_check)); prefs->mouse_wheel_seeks = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(mouse_wheel_check)); + int res_idx = gtk_combo_box_get_active(GTK_COMBO_BOX(res_combo)); + int res_values[] = {0, 2160, 1440, 1080, 720, 480, 360}; + if (res_idx >= 0 && res_idx < 7) prefs->ytdl_max_resolution = res_values[res_idx]; + /* Subtitles */ prefs->sub_delay = gtk_spin_button_get_value(GTK_SPIN_BUTTON(delay_spin)); prefs->sub_pos = gtk_spin_button_get_value_as_int(GTK_SPIN_BUTTON(subpos_spin)); diff --git a/src/dialogs.h b/src/dialogs.h index c66a432..ea0fc3b 100644 --- a/src/dialogs.h +++ b/src/dialogs.h @@ -47,6 +47,7 @@ typedef struct { gboolean sub_shadow_enabled; gboolean sub_bold; gboolean sub_italic; + int ytdl_max_resolution; } Preferences; Preferences* preferences_new(void); diff --git a/src/ui.c b/src/ui.c index 7d1056b..b5aa2ab 100644 --- a/src/ui.c +++ b/src/ui.c @@ -70,6 +70,11 @@ struct AppUI { Controls *controls; Playlist *playlist; + /* Side Panel */ + GtkWidget *side_notebook; + GtkWidget *chapters_tree; + GtkListStore *chapters_store; + /* Player */ Player *player; @@ -158,9 +163,14 @@ 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; @@ -287,18 +297,57 @@ AppUI* ui_new(void) 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); - gtk_widget_set_size_request(playlist_widget, 250, -1); - gtk_paned_pack2(GTK_PANED(ui->hpaned), playlist_widget, FALSE, TRUE); + 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); - /* Handle visibility manually */ - gtk_widget_set_no_show_all(playlist_widget, TRUE); + /* 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(playlist_widget); + gtk_widget_show(ui->side_notebook); } /* Controls */ @@ -322,6 +371,8 @@ AppUI* ui_new(void) 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\""); } @@ -421,6 +472,20 @@ void ui_apply_subtitle_preferences(AppUI *ui) 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; @@ -449,8 +514,11 @@ static void ui_apply_preferences_internal(gpointer data) 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; @@ -478,12 +546,13 @@ void ui_set_preferences(AppUI *ui, Preferences *prefs) } /* Update playlist visibility if UI is already realized */ - if (ui->playlist) { - GtkWidget *playlist_widget = playlist_get_widget(ui->playlist); - if (ui->prefs->show_playlist) { - gtk_widget_show(playlist_widget); + 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(playlist_widget); + gtk_widget_hide(ui->side_notebook); } } } @@ -675,7 +744,7 @@ void ui_toggle_fullscreen(AppUI *ui) if (ui->is_fullscreen) { gtk_widget_hide(ui->menubar); if (ui->playlist_visible) { - gtk_widget_hide(playlist_get_widget(ui->playlist)); + gtk_widget_hide(ui->side_notebook); } /* Create overlay window for controls */ @@ -728,7 +797,7 @@ void ui_toggle_fullscreen(AppUI *ui) gtk_widget_show(ui->menubar); gtk_widget_show(controls_widget); if (ui->playlist_visible) { - gtk_widget_show(playlist_get_widget(ui->playlist)); + gtk_widget_show(ui->side_notebook); } /* Stop auto-hide timer unless windowed hiding is on */ @@ -748,13 +817,29 @@ void ui_toggle_playlist(AppUI *ui) { if (!ui) return; - ui->playlist_visible = !ui->playlist_visible; - - GtkWidget *playlist_widget = playlist_get_widget(ui->playlist); - if (ui->playlist_visible) { - gtk_widget_show(playlist_widget); + 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 { - gtk_widget_hide(playlist_widget); + 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); } } @@ -1639,6 +1724,23 @@ static void create_menubar(AppUI *ui) } 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"); @@ -1659,12 +1761,15 @@ static void create_menubar(AppUI *ui) 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_check_menu_item_new_with_mnemonic("_Playlist"); - gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(playlist_toggle_item), ui->prefs->show_playlist); - g_signal_connect(playlist_toggle_item, "toggled", G_CALLBACK(on_view_playlist), ui); + 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); @@ -1833,6 +1938,9 @@ static void update_chapter_menu(AppUI *ui) } 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); @@ -1855,12 +1963,20 @@ static void update_chapter_menu(AppUI *ui) 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); @@ -1976,6 +2092,13 @@ static void on_view_playlist(GtkMenuItem *item, gpointer 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; @@ -2128,3 +2251,41 @@ void ui_show_script_manager(AppUI *ui) 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); + } + } +}