/*
* 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;
}