/* * Scallywag-NG: Half the scally, and twice the wag. * Copyright (C) 2025 SILO GROUP LLC * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "window.h" #include "callbacks.h" #include "../app.h" #include #define LOGO_RESOURCE_PATH "/org/scallywag/logo.svg" // Column indices for results store enum { COL_SEEDERS = 0, COL_LEECHERS, COL_SIZE, COL_TITLE, COL_AUTHOR, COL_URL, NUM_COLS }; // Callback for proxy section toggle static void on_proxy_toggle(GtkToggleButton *toggle, gpointer user_data) { GtkWidget *proxy_box = GTK_WIDGET(user_data); gboolean active = gtk_toggle_button_get_active(toggle); if (active) { gtk_widget_show(proxy_box); // Show all children explicitly GList *children = gtk_container_get_children(GTK_CONTAINER(proxy_box)); for (GList *l = children; l != NULL; l = l->next) { gtk_widget_show(GTK_WIDGET(l->data)); } g_list_free(children); } else { gtk_widget_hide(proxy_box); } gtk_button_set_label(GTK_BUTTON(toggle), active ? "\342\226\274 Proxy" : "\342\226\266 Proxy"); } // Handle horizontal scroll wheel events static gboolean on_scroll_event(GtkWidget *widget, GdkEventScroll *event, gpointer data) { (void)widget; GtkScrolledWindow *scrolled = GTK_SCROLLED_WINDOW(data); GtkAdjustment *hadj = gtk_scrolled_window_get_hadjustment(scrolled); gdouble step = gtk_adjustment_get_step_increment(hadj); gdouble value = gtk_adjustment_get_value(hadj); // Handle Shift+scroll or horizontal scroll for horizontal movement if (event->state & GDK_SHIFT_MASK || event->direction == GDK_SCROLL_LEFT || event->direction == GDK_SCROLL_RIGHT) { if (event->direction == GDK_SCROLL_UP || event->direction == GDK_SCROLL_LEFT) { gtk_adjustment_set_value(hadj, value - step * 3); } else if (event->direction == GDK_SCROLL_DOWN || event->direction == GDK_SCROLL_RIGHT) { gtk_adjustment_set_value(hadj, value + step * 3); } else if (event->direction == GDK_SCROLL_SMOOTH) { gtk_adjustment_set_value(hadj, value + event->delta_x * step * 3); } return TRUE; // Event handled } return FALSE; // Let default handler process vertical scroll } static void setup_treeview_columns(GtkTreeView *treeview) { const char *titles[] = {"Seeders", "Leechers", "Size", "Title", "Author"}; // Don't show URL column (COL_URL) - data is still in store for downloads for (int i = 0; i < COL_URL; i++) { GtkCellRenderer *renderer = gtk_cell_renderer_text_new(); GtkTreeViewColumn *column; // Use markup for Author column to render italic anonymous users if (i == COL_AUTHOR) { column = gtk_tree_view_column_new_with_attributes( titles[i], renderer, "markup", i, NULL); } else { column = gtk_tree_view_column_new_with_attributes( titles[i], renderer, "text", i, NULL); } gtk_tree_view_column_set_resizable(column, TRUE); // Make title column expand if (i == COL_TITLE) { gtk_tree_view_column_set_expand(column, TRUE); } gtk_tree_view_append_column(treeview, column); } } GtkWidget *window_create(App *app) { // Set up CSS for alternating row colors GtkCssProvider *css_provider = gtk_css_provider_new(); gtk_css_provider_load_from_data(css_provider, "treeview.view row:nth-child(even) { background-color: rgba(0, 26, 45, 0.15); }" "treeview.view row:nth-child(even) cell { background-color: rgba(0, 26, 45, 0.15); }", -1, NULL); gtk_style_context_add_provider_for_screen(gdk_screen_get_default(), GTK_STYLE_PROVIDER(css_provider), GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); g_object_unref(css_provider); // Main window GtkWidget *window = gtk_window_new(GTK_WINDOW_TOPLEVEL); gtk_window_set_title(GTK_WINDOW(window), "Scallywag"); gtk_window_set_default_size(GTK_WINDOW(window), 600, 500); g_signal_connect(window, "destroy", G_CALLBACK(on_window_destroy), app); // Main vertical box with spacing GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8); gtk_container_set_border_width(GTK_CONTAINER(vbox), 12); gtk_container_add(GTK_CONTAINER(window), vbox); // Logo (clickable - toggles loading dialog) GError *error = NULL; GdkPixbuf *pixbuf = gdk_pixbuf_new_from_resource_at_scale( LOGO_RESOURCE_PATH, 120, -1, TRUE, &error); if (pixbuf) { GtkWidget *logo = gtk_image_new_from_pixbuf(pixbuf); GtkWidget *event_box = gtk_event_box_new(); gtk_container_add(GTK_CONTAINER(event_box), logo); gtk_widget_set_halign(event_box, GTK_ALIGN_CENTER); gtk_box_pack_start(GTK_BOX(vbox), event_box, FALSE, FALSE, 0); g_signal_connect(event_box, "button-press-event", G_CALLBACK(on_logo_clicked), app); g_object_unref(pixbuf); } if (error) g_error_free(error); // Program name label GtkWidget *title_label = gtk_label_new(NULL); gtk_label_set_markup(GTK_LABEL(title_label), "Scallywag"); gtk_widget_set_halign(title_label, GTK_ALIGN_CENTER); gtk_widget_set_margin_bottom(title_label, 12); gtk_box_pack_start(GTK_BOX(vbox), title_label, FALSE, FALSE, 0); // Search row GtkWidget *search_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8); gtk_box_pack_start(GTK_BOX(vbox), search_box, FALSE, TRUE, 0); GtkWidget *entry_search = gtk_entry_new(); gtk_entry_set_placeholder_text(GTK_ENTRY(entry_search), "Enter search terms..."); gtk_box_pack_start(GTK_BOX(search_box), entry_search, TRUE, TRUE, 0); g_signal_connect(entry_search, "activate", G_CALLBACK(on_search_activate), app); GtkWidget *btn_search = gtk_button_new_with_label("Search"); gtk_box_pack_start(GTK_BOX(search_box), btn_search, FALSE, FALSE, 0); g_signal_connect(btn_search, "clicked", G_CALLBACK(on_search_clicked), app); // Collapsible proxy section GtkWidget *proxy_toggle = gtk_toggle_button_new_with_label("\342\226\266 Proxy"); gtk_widget_set_halign(proxy_toggle, GTK_ALIGN_START); gtk_button_set_relief(GTK_BUTTON(proxy_toggle), GTK_RELIEF_NONE); gtk_box_pack_start(GTK_BOX(vbox), proxy_toggle, FALSE, FALSE, 0); GtkWidget *proxy_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8); gtk_widget_set_margin_start(proxy_box, 16); gtk_box_pack_start(GTK_BOX(vbox), proxy_box, FALSE, FALSE, 0); GtkWidget *combo_proxy = gtk_combo_box_text_new(); gtk_box_pack_start(GTK_BOX(proxy_box), combo_proxy, TRUE, TRUE, 0); g_signal_connect(combo_proxy, "changed", G_CALLBACK(on_proxy_changed), app); GtkWidget *btn_refresh = gtk_button_new_with_label("Refresh"); gtk_box_pack_start(GTK_BOX(proxy_box), btn_refresh, FALSE, FALSE, 0); g_signal_connect(btn_refresh, "clicked", G_CALLBACK(on_refresh_clicked), app); // Connect toggle after children are added, then hide g_signal_connect(proxy_toggle, "toggled", G_CALLBACK(on_proxy_toggle), proxy_box); gtk_widget_set_no_show_all(proxy_box, TRUE); // Prevent show_all from main window gtk_widget_hide(proxy_box); // Scrolled window for results GtkWidget *scrolled = gtk_scrolled_window_new(NULL, NULL); gtk_scrolled_window_set_shadow_type(GTK_SCROLLED_WINDOW(scrolled), GTK_SHADOW_IN); gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolled), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); gtk_box_pack_start(GTK_BOX(vbox), scrolled, TRUE, TRUE, 0); // Tree view for results GtkWidget *treeview = gtk_tree_view_new_with_model(GTK_TREE_MODEL(app->results_store)); setup_treeview_columns(GTK_TREE_VIEW(treeview)); gtk_container_add(GTK_CONTAINER(scrolled), treeview); g_signal_connect(treeview, "row-activated", G_CALLBACK(on_row_activated), app); g_signal_connect(treeview, "scroll-event", G_CALLBACK(on_scroll_event), scrolled); gtk_widget_add_events(treeview, GDK_SCROLL_MASK | GDK_SMOOTH_SCROLL_MASK); // Get More Results button (centered, initially disabled) GtkWidget *btn_more = gtk_button_new_with_label("Get More Results"); gtk_widget_set_halign(btn_more, GTK_ALIGN_CENTER); gtk_widget_set_margin_top(btn_more, 4); gtk_widget_set_margin_bottom(btn_more, 4); gtk_widget_set_sensitive(btn_more, FALSE); gtk_box_pack_start(GTK_BOX(vbox), btn_more, FALSE, FALSE, 0); g_signal_connect(btn_more, "clicked", G_CALLBACK(on_more_clicked), app); // Download button (default action) GtkWidget *btn_download = gtk_button_new_with_label("Download"); gtk_widget_set_can_default(btn_download, TRUE); gtk_box_pack_start(GTK_BOX(vbox), btn_download, FALSE, TRUE, 0); g_signal_connect(btn_download, "clicked", G_CALLBACK(on_download_clicked), app); // Status bar row with page counter on right GtkWidget *status_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8); gtk_box_pack_start(GTK_BOX(vbox), status_box, FALSE, TRUE, 0); GtkWidget *statusbar = gtk_statusbar_new(); gtk_box_pack_start(GTK_BOX(status_box), statusbar, TRUE, TRUE, 0); GtkWidget *lbl_page_count = gtk_label_new("pages 0/0"); gtk_widget_set_margin_end(lbl_page_count, 8); gtk_box_pack_start(GTK_BOX(status_box), lbl_page_count, FALSE, FALSE, 0); // Store widget references in app app->window = window; app->combo_proxy = combo_proxy; app->entry_search = entry_search; app->treeview_results = treeview; app->btn_search = btn_search; app->btn_refresh = btn_refresh; app->btn_download = btn_download; app->btn_more = btn_more; app->lbl_page_count = lbl_page_count; app->statusbar = statusbar; // Set download as the default button gtk_window_set_default(GTK_WINDOW(window), btn_download); return window; }