#!/usr/bin/perl

use strict;
use warnings;
use Gtk2 '-init';
use Glib qw/TRUE FALSE/;
use AnyEvent;
use AnyEvent::HTTP;
use XML::LibXML;
use XML::Feed;
use File::Path qw(make_path);
use File::Spec;
use POSIX qw(strftime);
use MIME::Base64;
use Storable;
# DB-MOD: Add DBI for database interaction.
use DBI;
# SEARCH4FEEDS-MOD: Add modules for URL and path parsing.
use URI;
use File::Basename 'dirname';

# DEBUG-FLAG-MOD: Add a flag to control loading saved widths. Set to 0 to override bad saved values.
my $loadcolumnwidthsfromconfig = 1;

# --- Define and create config directories on startup ---
my $config_dir    = File::Spec->catdir(glob('~'), '.config', 'feeed');
my $feeds_dir     = File::Spec->catdir($config_dir, 'feeds');
my $storable_file = File::Spec->catfile($config_dir, '.feeedconfig');
# DB-MOD: Define path for the SQLite database and declare the handle.
my $db_file       = File::Spec->catfile($config_dir, 'feeds.sqlite');
my $dbh;

make_path($feeds_dir) unless -d $feeds_dir;

# DB-MOD: Initialize the database on startup.
init_database();

# ICON-BUTTON-MOD: To generate: base64 -w 0 icon.png > iconbase64.txt
my $icon_data_base64 = 'iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEgAACxIB0t1+/AAAAAd0SU1FB+kJDQUDF6zQTqkAAAERSURBVEjH5VTBbYQwEJy1LFniy4tmXEGOCnLt8KINCoE++NMAykGMJ4/TIS6XM4YQKUpW2geMvcvODCsAiB8MtedSkiTRZzUAkI9DiAjWsOgGMRe3FH3aYPm1y4KknzGSUErt00BE5rx/ryAiMMZsKv4wQSieTbcW8itteihFh9o0yOVOm6o4ga82JYlxHDc1WBX5RpGIQGsN59w/s6n+boHX8xnvC126rkNd1/fTh/KrCOGXyxtPpxcaY25n4hrE4NZaej+RJPM8JwAeKnLTNBC5On+apu0/WpqmwW1qrYWnhxIFtVz3MRQ55+b8jHvv6Zxj3/ckybIsmWVZHEVrni+KAsMwzM9t26Kqqj+0rj8AgqnqSf0dhWwAAAAASUVORK5CYII=';
#my $icon_data = decode_base64($icon_data_base64);

# --- Main Window and Layout ---
my $window = Gtk2::Window->new('toplevel');
$window->set_title("feeed");
$window->set_default_size(1450, 800);
# DB-MOD: Ensure the database connection is closed gracefully on exit.
$window->signal_connect(delete_event => sub { $dbh->disconnect if $dbh; Gtk2->main_quit; });

# ICON-BUTTON-MOD: Use the new robust loader to set the window icon.
my $pixbuf_icon = load_pixbuf_from_base64($icon_data_base64);
if ($pixbuf_icon) {
    Gtk2::Window->set_default_icon($pixbuf_icon);
}

my $vbox = Gtk2::VBox->new(FALSE, 2);
$window->add($vbox);

# --- Toolbar Setup ---
my $toolbar = Gtk2::Toolbar->new;
$vbox->pack_start($toolbar, FALSE, FALSE, 0);

# FILE-MENU-MOD: Add the new File menu button to the far left.
my $file_menu_button_item = create_file_menu_button();
$toolbar->insert($file_menu_button_item, 0);
$toolbar->insert(Gtk2::SeparatorToolItem->new, 1);


my $import_button = Gtk2::ToolButton->new_from_stock('gtk-open');
$import_button->set_tooltip_text('Import OPML File');
$toolbar->insert($import_button, 2);

my $save_button = Gtk2::ToolButton->new_from_stock('gtk-save');
$save_button->set_tooltip_text('Save Feeds as OPML');
$toolbar->insert($save_button, 3);

# MULTI-REFRESH-MOD: Add a new button for concurrent refreshing.
my $refresh_all_button = Gtk2::ToolButton->new_from_stock('gtk-execute');
$refresh_all_button->set_tooltip_text('Refresh All Concurrently');
$toolbar->insert($refresh_all_button, 4);

my $refresh_button = Gtk2::ToolButton->new_from_stock('gtk-refresh');
$toolbar->insert($refresh_button, -1);

my $progress_bar = Gtk2::ProgressBar->new;
$progress_bar->set_size_request(250, -1);
my $progress_item = Gtk2::ToolItem->new;
$progress_item->add($progress_bar);
$toolbar->insert($progress_item, -1);

my $skip_button = Gtk2::ToolButton->new_from_stock('gtk-media-next');
$skip_button->set_tooltip_text('Skip Current Feed');
$toolbar->insert($skip_button, -1);

my $pause_button = Gtk2::ToolButton->new_from_stock('gtk-media-pause');
$pause_button->set_tooltip_text('Pause');
$toolbar->insert($pause_button, -1);

my $cancel_button = Gtk2::ToolButton->new_from_stock('gtk-stop');
$cancel_button->set_tooltip_text('Cancel');
$toolbar->insert($cancel_button, -1);

# DEFAULTS-V2-MOD: The 'Enable Cache' checkbox is removed.

# DEFAULTS-V2-MOD: Change the label text.
my $cache_label = Gtk2::Label->new("Update Feeds again after (min):");
my $cache_label_item = Gtk2::ToolItem->new;
$cache_label_item->add($cache_label);
$toolbar->insert($cache_label_item, -1);

my $cache_expire_entry = Gtk2::Entry->new;
$cache_expire_entry->set_width_chars(5);
my $cache_entry_item = Gtk2::ToolItem->new;
$cache_entry_item->add($cache_expire_entry);
$toolbar->insert($cache_entry_item, -1);

my $cache_set_button = Gtk2::Button->new("Set");
# DEFAULTS-V2-MOD: Make button stand out.
$cache_set_button->set_relief('normal');
my $cache_button_item = Gtk2::ToolItem->new;
$cache_button_item->add($cache_set_button);
$toolbar->insert($cache_button_item, -1);

# DEFAULTS-V2-MOD: Add a separator after the cache settings.
$toolbar->insert(Gtk2::SeparatorToolItem->new, -1);

# PRUNE-QUERY-MOD: Add Query button
my $query_button = Gtk2::Button->new("SQL Query");
# DEFAULTS-V2-MOD: Make button stand out.
$query_button->set_relief('normal');
my $query_button_item = Gtk2::ToolItem->new;
$query_button_item->add($query_button);
$toolbar->insert($query_button_item, -1);

# PAGINATION-MOD: Add "Show New" and "Next Page" buttons.
my $show_new_button = Gtk2::ToolButton->new_from_stock('gtk-dnd-multiple');
$show_new_button->set_tooltip_text('Show All New Entries');
$toolbar->insert($show_new_button, -1);

my $next_page_button = Gtk2::ToolButton->new_from_stock('gtk-go-forward');
# QUERY-LIMIT-MOD: Update tooltip text to be generic.
$next_page_button->set_tooltip_text('Show Next Page of Query Results');
$toolbar->insert($next_page_button, -1);


# IGNORE-BROKEN-MOD: Changed the label of the checkbox.
my $mark_broken_check = Gtk2::CheckButton->new("Mark/Ignore broken feeds");
# DEFAULTS-MOD: Set the checkbox to be active by default.
$mark_broken_check->set_active(TRUE);
my $check_item = Gtk2::ToolItem->new;
$check_item->add($mark_broken_check);
$toolbar->insert($check_item, -1);

# HIDE-UNPARSEABLE-MOD: Add a checkbox to filter out "Unparseable Feed Content" entries.
my $hide_unparseable_check = Gtk2::CheckButton->new("Hide Unparseable");
$hide_unparseable_check->set_active(TRUE); # Default to hiding them for a cleaner view.
my $hide_check_item = Gtk2::ToolItem->new;
$hide_check_item->add($hide_unparseable_check);
$toolbar->insert($hide_check_item, -1);

my $spacer = Gtk2::SeparatorToolItem->new;
$spacer->set_draw(FALSE);
$spacer->set_expand(TRUE);
$toolbar->insert($spacer, -1);

# --- MODIFICATION: Add a close button to the top-right ---
my $close_button = Gtk2::ToolButton->new_from_stock('gtk-close');
$close_button->set_tooltip_text('Quit');
$close_button->signal_connect(clicked => sub { Gtk2->main_quit; });
$toolbar->insert($close_button, -1); # Insert at the end, to the right of the spacer.

# --- Paned Layout ---
my $hpaned = Gtk2::HPaned->new;
$vbox->pack_start($hpaned, TRUE, TRUE, 0);

# --- Left Pane: TreeView ---
# BOLD-FIX: Change weight column (5) type from String to Int.
my $tree_store = Gtk2::TreeStore->new(qw/Glib::String Glib::String Glib::Boolean Glib::String Glib::Int Glib::Int/);
my $tree_view = Gtk2::TreeView->new($tree_store);
my $scrolled_win_left = Gtk2::ScrolledWindow->new;
$scrolled_win_left->set_policy('automatic', 'automatic');
$scrolled_win_left->add($tree_view);

# SEARCH-MOD: Create a VBox for the left pane to hold the search entry and the tree.
my $left_vbox = Gtk2::VBox->new(FALSE, 2);

my $search_entry = Gtk2::Entry->new;
my $search_placeholder = "Search..."; # GTK2-FIX: Define placeholder text.
# GTK2-FIX: Manually set initial placeholder text and color.
$search_entry->set_text($search_placeholder);
$search_entry->modify_text('normal', Gtk2::Gdk::Color->parse('gray'));

# SEARCH-MOD: Pack the new search entry and the existing tree view into the VBox.
$left_vbox->pack_start($search_entry, FALSE, FALSE, 2);
$left_vbox->pack_start($scrolled_win_left, TRUE, TRUE, 0);

# SEARCH-MOD: Add the new VBox to the main horizontal pane.
$hpaned->add1($left_vbox);

# BOLD-MOD: Add the 'weight' attribute, linking it to the new column 5.
my $tree_column = Gtk2::TreeViewColumn->new_with_attributes("Feeds",
    Gtk2::CellRendererText->new, text => 0, background => 3, weight => 5);
$tree_view->append_column($tree_column);

# --- Right Pane Widgets (Dynamic) ---
# REFACTOR-MOD: The right pane is now a stable VBox that holds both views.
my $right_pane_container = Gtk2::VBox->new(FALSE, 0);
$hpaned->add2($right_pane_container);

# REFACTOR-MOD: Widgets for the single-feed view.
my $text_buffer = Gtk2::TextBuffer->new;
my $text_view = Gtk2::TextView->new_with_buffer($text_buffer);
$text_view->set_editable(FALSE);
$text_view->set_cursor_visible(FALSE);
$text_view->set_wrap_mode('word');
my $scrolled_win_single_view = Gtk2::ScrolledWindow->new;
$scrolled_win_single_view->set_policy('automatic', 'automatic');
$scrolled_win_single_view->add($text_view);

# NEW-PERSIST-MOD: Add a new boolean column (9) to the headline store for is_new status.
my $headline_store = Gtk2::ListStore->new(
    qw/Glib::String Glib::String Glib::String Glib::String Glib::String Glib::Int Glib::String Glib::Int Glib::Int Glib::Boolean/
);
my $headline_view = Gtk2::TreeView->new($headline_store);
# --- MODIFICATION: Set selection mode to multiple ---
$headline_view->get_selection->set_mode('multiple');

# REFACTOR-MOD: Widgets for the folder (multi-feed) view.
my $vpaned_right = Gtk2::VPaned->new;
my $scrolled_win_top_right = Gtk2::ScrolledWindow->new;
$scrolled_win_top_right->set_policy('automatic', 'automatic');
$scrolled_win_top_right->add($headline_view);

my $scrolled_win_bottom_right = Gtk2::ScrolledWindow->new;
$scrolled_win_bottom_right->set_policy('automatic', 'automatic');
my $folder_text_view = Gtk2::TextView->new; # A dedicated text view for the folder pane.
$folder_text_view->set_editable(FALSE);
$folder_text_view->set_cursor_visible(FALSE);
$folder_text_view->set_wrap_mode('word');
$scrolled_win_bottom_right->add($folder_text_view);

$vpaned_right->add1($scrolled_win_top_right);
$vpaned_right->add2($scrolled_win_bottom_right);

# REFACTOR-MOD: Add both stable views to the right pane container.
$right_pane_container->pack_start($scrolled_win_single_view, TRUE, TRUE, 0);
$right_pane_container->pack_start($vpaned_right, TRUE, TRUE, 0);


# BOLD-MOD: Add 'weight' attribute to the feed column.
my $feed_renderer = Gtk2::CellRendererText->new;
my $feed_column = Gtk2::TreeViewColumn->new_with_attributes("Feed", $feed_renderer, text => 0, weight => 8);
$feed_column->set_sort_column_id(0);
$feed_column->set_resizable(TRUE);
$feed_column->set_sizing('fixed');
$headline_view->append_column($feed_column);

# BOLD-MOD: Add 'weight' attribute to the headline column.
my $headline_renderer = Gtk2::CellRendererText->new;
$headline_renderer->set_property('underline', 'single');
$headline_renderer->set_property('foreground', 'blue');
my $headline_column = Gtk2::TreeViewColumn->new_with_attributes("Headline", $headline_renderer, text => 1, weight => 8);
$headline_column->set_sort_column_id(1);
$headline_column->set_resizable(TRUE);
$headline_column->set_sizing('fixed');
$headline_view->append_column($headline_column);

my $date_renderer = Gtk2::CellRendererText->new;
my $date_column = Gtk2::TreeViewColumn->new_with_attributes("Date", $date_renderer, text => 2);
$date_column->set_sort_column_id(5);
$date_column->set_resizable(TRUE);
$date_column->set_sizing('fixed');
$headline_view->append_column($date_column);

# DATE-ADDED-MOD: Create and add the new "Date Added" column to the TreeView.
my $date_added_renderer = Gtk2::CellRendererText->new;
my $date_added_column = Gtk2::TreeViewColumn->new_with_attributes("Date Added", $date_added_renderer, text => 6);
$date_added_column->set_sort_column_id(7);
$date_added_column->set_resizable(TRUE);
$date_added_column->set_sizing('fixed');
$headline_view->append_column($date_added_column);

# --- Hyperlink Tag Setup ---
# CRASH-FIX: Create two separate tag objects, one for each text buffer.
my $single_view_link_tag = Gtk2::TextTag->new('hyperlink');
$single_view_link_tag->set_property('foreground', 'blue');
$single_view_link_tag->set_property('underline', 'single');
$text_buffer->get_tag_table->add($single_view_link_tag);

my $folder_view_link_tag = Gtk2::TextTag->new('hyperlink');
$folder_view_link_tag->set_property('foreground', 'blue');
$folder_view_link_tag->set_property('underline', 'single');
$folder_text_view->get_buffer->get_tag_table->add($folder_view_link_tag);

# --- State Variables ---
my %feed_data;
# MULTI-REFRESH-MOD: These global state variables are now deprecated in favor of per-job state.
# They are kept for now to support the single-update functions which are now wrappers.
my @download_queue;
my $total_feeds_in_queue = 0;
my $feeds_processed = 0;
my $is_updating = FALSE;
my $is_paused = FALSE;
my $is_cancelled = FALSE;
my $task_counter = 0;
my $current_task_id;
# SINGLE-CTRL-FIX: Add a global variable to hold the current job for the main UI.
my $current_single_update_job;

# BROKEN-FEEDS-MOD: The old %broken_feeds hash is replaced by a DB-backed status cache.
my %broken_feed_status;
# DEFAULTS-MOD: Change the default expiration to 1440 minutes (24 hours).
my $cache_expire_minutes = 1440;
# LIVE-REFRESH: State variables for managing live updates.
my $current_update_root_iter; 
my $live_refresh_timer_id;
# PRUNE-QUERY-MOD: Add variables for pruning settings.
# DEFAULTS-V2-MOD: Pruning is now disabled by default.
my $pruning_enabled = 0;
my $prune_days = 64;

# --- Search State Variables ---
my @search_results;
my $current_search_index = -1;
my $last_search_term = '';
my $last_successful_select_query = '';
# PAGINATION-MOD: Add state variable for pagination offset.
my $current_query_offset = 0;
# QUERY-LIMIT-MOD: Add a variable for the query result limit.
my $query_limit = 1000;


# COL-WIDTH-MOD: Add state variables for column width persistence.
my %column_widths;
my $save_widths_timer_id; # For debouncing the save operation.
# SQUISHED-FIX: Add a "gatekeeper" flag to prevent the resize handler from running during startup.
my $initial_layout_complete = FALSE; 


# FILE-MENU-MOD: Declare a variable to hold the "Next Page" menu item.
my $next_page_menu_item;

# --- MODIFICATION: State variable for shift-selection ---
my $last_clicked_headline_path; # For shift-selection in the headline view.

# DEFAULTS-MOD: Set the initial text of the expiration entry.
$cache_expire_entry->set_text($cache_expire_minutes);

# --- Signal Connections ---
$import_button->signal_connect(clicked => \&import_opml_file);
$save_button->signal_connect(clicked => \&save_opml_file);
$refresh_button->signal_connect(clicked => \&refresh_all_feeds);
$pause_button->signal_connect(clicked => \&on_pause_clicked);
$cancel_button->signal_connect(clicked => \&on_cancel_clicked);
$skip_button->signal_connect(clicked => \&on_skip_clicked);
$mark_broken_check->signal_connect(toggled => \&update_all_row_colors);
$cache_set_button->signal_connect(clicked => \&on_set_cache_clicked);
$tree_view->get_selection->signal_connect(changed => \&feed_selection_changed);
$tree_view->signal_connect('button-press-event' => \&on_tree_view_click);
# MULTI-REFRESH-MOD: Connect the new button.
$refresh_all_button->signal_connect(clicked => \&on_refresh_all_concurrently_clicked);

# PAGINATION-MOD: Connect signals for the new buttons.
$show_new_button->signal_connect(clicked => \&on_show_new_clicked);
$next_page_button->signal_connect(clicked => \&on_next_page_clicked);


# REFACTOR-MOD: Connect signals to BOTH text views now.
for my $view ($text_view, $folder_text_view) {
    $view->add_events([qw/button-press-mask pointer-motion-mask/]);
    $view->signal_connect( 'event-after' => \&check_for_link_click );
    $view->signal_connect( 'motion-notify-event' => \&check_for_link_hover );
}

# --- MODIFICATION: The 'changed' signal is no longer used, only the click handler. ---
$headline_view->signal_connect('button-press-event' => \&on_headline_view_click);

# SEARCH-MOD: Connect signals for the search entry field.
$search_entry->signal_connect(activate => \&on_search_activate);
$search_entry->signal_connect(changed => \&on_search_text_changed);
# GTK2-FIX: Connect focus signals to handle the manual placeholder.
$search_entry->signal_connect('focus-in-event' => \&on_search_focus_in);
$search_entry->signal_connect('focus-out-event' => \&on_search_focus_out);

# PRUNE-QUERY-MOD: Connect the new toolbar buttons to their functions.
$query_button->signal_connect(clicked => sub { create_sql_query_popup(); });

# HIDE-UNPARSEABLE-MOD: Connect the new checkbox's toggled signal.
$hide_unparseable_check->signal_connect(toggled => \&on_hide_unparseable_toggled);

# COL-WIDTH-MOD: Connect the 'notify::width' signal for each resizable column.
for my $col ($feed_column, $headline_column, $date_column, $date_added_column) {
    $col->signal_connect('notify::width' => \&on_column_resized);
}


################################################################################
# DB-MOD: Database Initialization and Schema
################################################################################

sub init_database {
    my $dsn = "dbi:SQLite:dbname=$db_file";
    # UTF8-FIX: Add sqlite_unicode => 1 to the connection options.
    $dbh = DBI->connect($dsn, "", "", { 
        RaiseError => 1, 
        AutoCommit => 1,
        sqlite_unicode => 1,
    }) or die "Could not connect to database: " . DBI->errstr;

    # Enable WAL mode for better performance.
    $dbh->do('PRAGMA journal_mode=WAL;');

    # Create tables if they don't exist.
    # BROKEN-FEEDS-MOD: Added the 'broken' column to the feeds table.
    $dbh->do(q{
        CREATE TABLE IF NOT EXISTS feeds (
            id INTEGER PRIMARY KEY,
            parent_id INTEGER,
            name TEXT NOT NULL,
            url TEXT,
            is_folder BOOLEAN NOT NULL,
            broken BOOLEAN NOT NULL DEFAULT 0,
            FOREIGN KEY(parent_id) REFERENCES feeds(id) ON DELETE CASCADE
        )
    });
    # NEW-PERSIST-MOD: Add the is_new column with a default of 1 (true).
    $dbh->do(q{
        CREATE TABLE IF NOT EXISTS entries (
            id INTEGER PRIMARY KEY,
            feed_id INTEGER NOT NULL,
            title TEXT,
            link TEXT UNIQUE,
            published_date INTEGER,
            content TEXT,
            date_added INTEGER NOT NULL DEFAULT (strftime('%s', 'now')),
            is_new BOOLEAN NOT NULL DEFAULT 1,
            FOREIGN KEY(feed_id) REFERENCES feeds(id) ON DELETE CASCADE
        )
    });

    # --- Schema Migration for 'broken' column ---
    my $broken_exists = 0;
    my $sth_broken = $dbh->prepare('PRAGMA table_info(feeds)');
    $sth_broken->execute;
    while (my $row = $sth_broken->fetchrow_hashref) {
        if ($row->{name} eq 'broken') {
            $broken_exists = 1;
            last;
        }
    }
    $sth_broken->finish;
    if (!$broken_exists) {
        warn "Adding 'broken' column to feeds table...";
        $dbh->do('ALTER TABLE feeds ADD COLUMN broken BOOLEAN NOT NULL DEFAULT 0');
    }

    # --- Schema Migration for 'is_new' column ---
    my $is_new_exists = 0;
    my $sth_is_new = $dbh->prepare('PRAGMA table_info(entries)');
    $sth_is_new->execute;
    while (my $row = $sth_is_new->fetchrow_hashref) {
        if ($row->{name} eq 'is_new') {
            $is_new_exists = 1;
            last;
        }
    }
    $sth_is_new->finish;
    if (!$is_new_exists) {
        warn "Adding 'is_new' column to entries table...";
        $dbh->do('ALTER TABLE entries ADD COLUMN is_new BOOLEAN NOT NULL DEFAULT 1');
    }

    # Create indices for faster queries
    $dbh->do('CREATE INDEX IF NOT EXISTS idx_feeds_parent_id ON feeds(parent_id);');
    $dbh->do('CREATE INDEX IF NOT EXISTS idx_entries_feed_id ON entries(feed_id);');
    $dbh->do('CREATE INDEX IF NOT EXISTS idx_entries_published_date ON entries(published_date);');
    $dbh->do('CREATE INDEX IF NOT EXISTS idx_entries_date_added ON entries(date_added);');
    # NEW-PERSIST-MOD: Add an index for quickly counting new entries.
    $dbh->do('CREATE INDEX IF NOT EXISTS idx_entries_feed_id_is_new ON entries(feed_id, is_new);');
}

################################################################################
# Persistence and File I/O
################################################################################

# ICON-BUTTON-MOD: New reusable function to safely load a pixbuf from base64 data.
sub load_pixbuf_from_base64 {
    my ($base64_data) = @_;
    return undef unless $base64_data;

    my $pixbuf;
    eval {
        my $loader = Gtk2::Gdk::PixbufLoader->new();
        $loader->write(decode_base64($base64_data));
        $loader->close();
        $pixbuf = $loader->get_pixbuf();
    };
    if ($@) {
        warn "Failed to load pixbuf from Base64 data: $@";
        return undef;
    }
    return $pixbuf;
}

# FILE-MENU-MOD: New subroutine to create the file menu button and its dropdown.
sub create_file_menu_button {
    my $button;
    my $pixbuf = load_pixbuf_from_base64($icon_data_base64);

    if ($pixbuf) {
        # If successful, create a button with the custom image.
        my $image = Gtk2::Image->new_from_pixbuf($pixbuf);
        $button = Gtk2::ToolButton->new($image, undef);
    } else {
        # Fallback to the stock icon if loading failed.
        $button = Gtk2::ToolButton->new_from_stock('gtk-yes');
    }
    
    $button->set_tooltip_text("About Feeed");
    $button->signal_connect(clicked => \&show_about_dialog);

    my $menu = Gtk2::Menu->new;

    # --- Import/Export ---
    my $import_item = Gtk2::MenuItem->new_with_mnemonic("_Import OPML File...");
    $import_item->signal_connect(activate => \&import_opml_file);
    $menu->append($import_item);

    my $save_item = Gtk2::MenuItem->new_with_mnemonic("_Save OPML As...");
    $save_item->signal_connect(activate => \&save_opml_file);
    $menu->append($save_item);

    $menu->append(Gtk2::SeparatorMenuItem->new);

    # --- Refreshing ---
    my $refresh_current_item = Gtk2::MenuItem->new_with_mnemonic("_Refresh Current/Selected");
    $refresh_current_item->signal_connect(activate => \&refresh_all_feeds);
    $menu->append($refresh_current_item);

    my $refresh_all_item = Gtk2::MenuItem->new_with_mnemonic("Refresh _All Concurrently...");
    $refresh_all_item->signal_connect(activate => \&on_refresh_all_concurrently_clicked);
    $menu->append($refresh_all_item);
    
    $menu->append(Gtk2::SeparatorMenuItem->new);
    
    # --- Querying ---
    my $show_new_item = Gtk2::MenuItem->new_with_mnemonic("Show _New Entries");
    $show_new_item->signal_connect(activate => \&on_show_new_clicked);
    $menu->append($show_new_item);
    
    # We need a reference to this item to toggle its sensitivity.
    $next_page_menu_item = Gtk2::MenuItem->new_with_mnemonic("Show _Next Page of Results");
    $next_page_menu_item->signal_connect(activate => \&on_next_page_clicked);
    $menu->append($next_page_menu_item);

    $menu->append(Gtk2::SeparatorMenuItem->new);

    # --- Tools ---
    my $pruning_item = Gtk2::MenuItem->new_with_mnemonic("_Pruning...");
    $pruning_item->signal_connect(activate => \&create_pruning_popup);
    $menu->append($pruning_item);

    my $sql_item = Gtk2::MenuItem->new_with_mnemonic("_SQL Query...");
    $sql_item->signal_connect(activate => sub { create_sql_query_popup(); });
    $menu->append($sql_item);

    # --- MODIFICATION: Add Check/Fix Broken menu item ---
    my $check_broken_item = Gtk2::MenuItem->new_with_mnemonic("Check/_Fix Broken...");
    $check_broken_item->signal_connect(activate => \&create_check_fix_broken_window);
    $menu->append($check_broken_item);

    $menu->append(Gtk2::SeparatorMenuItem->new);

    # --- Quit ---
    my $quit_item = Gtk2::MenuItem->new_with_mnemonic("_Quit");
    $quit_item->signal_connect(activate => sub { Gtk2->main_quit; });
    $menu->append($quit_item);
    
    my $arrow = Gtk2::Arrow->new('down', 'etched-in');
    my $arrow_button = Gtk2::ToolButton->new($arrow, undef);
    $arrow_button->set_tooltip_text("File Menu");
    $arrow_button->signal_connect(clicked => sub {
        my ($widget) = @_;
        $menu->show_all;
        $menu->popup(undef, undef, sub {
            my ($m, $x, $y, $p) = @_;
            # CRASH-FIX: Capture list return from get_origin into an array.
            my @coords = $widget->get_window->get_origin;
            my $alloc = $widget->get_allocation;
            ($x, $y, $p) = ($coords[0], $coords[1] + $alloc->height, 1);
            return ($x, $y, $p);
        }, $widget, 1, Gtk2->get_current_event_time());
    });

    my $hbox = Gtk2::HBox->new(FALSE, 0);
    $hbox->pack_start($button, FALSE, FALSE, 0);
    $hbox->pack_start($arrow_button, FALSE, FALSE, 0);

    my $tool_item = Gtk2::ToolItem->new;
    $tool_item->add($hbox);
    
    return $tool_item;
}

# ABOUT-DIALOG-MOD: New subroutine to display the about dialog.
sub show_about_dialog {
    my $dialog = Gtk2::AboutDialog->new;
    $dialog->set_program_name("feeed");
    $dialog->set_version("1.0");
    
    # CREDITS-BUTTON-REMOVED: Combine author info into the copyright string
    # and remove the set_authors call to prevent the Credits page from appearing.
    $dialog->set_copyright("(c) 2025 superkuh\nsuperkuh\@superkuh.com");
    
    $dialog->set_comments("A GTK2 RSS/Atom Feed Reader");
    $dialog->set_website("http://superkuh.com/");
    $dialog->set_website_label("superkuh.com"); # Set a cleaner label for the link
    
    $dialog->set_logo($pixbuf_icon) if $pixbuf_icon;

    $dialog->run;
    $dialog->destroy;
}


# COL-WIDTH-MOD: New subroutine to handle column resizing and trigger a debounced save.
# SQUISHED-FIX: This handler will now do nothing until the initial layout is complete.
sub on_column_resized {
    my ($column) = @_;
    
    # SQUISHED-FIX: Do not save any widths during the application's initial drawing phase.
    return unless $initial_layout_complete;

    # Debounce: if a save is already scheduled, cancel it.
    Glib::Source->remove($save_widths_timer_id) if $save_widths_timer_id;

    # Update the in-memory hash immediately with the new width.
    my $title = $column->get_title;
    my $width = $column->get_width;

    # Map the column's title to a consistent key for the config hash.
    return unless $title;
    my %title_map = (
        'Feed'       => 'feed',
        'Headline'   => 'headline',
        'Date'       => 'date',
        'Date Added' => 'date_added'
    );
    my $key = $title_map{$title};
    return unless $key;

    $column_widths{$key} = $width;

    # Schedule a save to disk in 1 second.
    $save_widths_timer_id = Glib::Timeout->add(1000, sub {
        save_config_to_storable();
        $save_widths_timer_id = undef; # Clear the timer ID after it runs.
        return FALSE; # Ensure this timer only runs once.
    });
}

sub on_set_cache_clicked {
    my $minutes = $cache_expire_entry->get_text;
    if ($minutes =~ /^\d+$/) {
        $cache_expire_minutes = $minutes;
        save_config_to_storable();
    } else {
        warn "Invalid input: Please enter a number for cache minutes.";
    }
}

# PRUNE-QUERY-MOD: Updated to save pruning settings.
# COL-WIDTH-MOD: Added column_widths to the saved data.
sub save_config_to_storable {
    my $config_data = {
        cache_minutes    => $cache_expire_minutes,
        pruning_enabled  => $pruning_enabled,
        prune_days       => $prune_days,
        column_widths    => \%column_widths, # Save the hash reference of column widths.
    };
    eval { store($config_data, $storable_file); };
    if ($@) { warn "Could not save config to $storable_file: $@"; }
}

# DB-MOD: This function is no longer used to save the tree.
sub walk_and_build_datastructure {
    my ($iter) = @_;
    my ($name, $url, $is_folder) = $tree_store->get($iter, 0, 1, 2);
    my $node = { name => $name, url => $url };
    if ($is_folder) {
        $node->{children} = [];
        my $child_iter = $tree_store->iter_children($iter);
        while ($child_iter) {
            push @{$node->{children}}, walk_and_build_datastructure($child_iter);
            $child_iter = $tree_store->iter_next($child_iter);
        }
    }
    return $node;
}

# DB-MOD: Major overhaul of loading logic.
# COL-WIDTH-MOD: Added loading for column widths.
# DEBUG-FLAG-MOD: Added logic to respect the $loadcolumnwidthsfromconfig flag.
sub load_config_from_storable {
    # First, check if there are any feeds in the database.
    my $sth = $dbh->prepare('SELECT COUNT(*) FROM feeds');
    $sth->execute;
    my ($feed_count) = $sth->fetchrow_array;
    $sth->finish;

    # FIRST-RUN-MOD: Check for the true "first run" condition.
    if ($feed_count == 0 && !(-f $storable_file)) {
        warn "First run detected. Creating default 'Feeds' folder.";
        # Create the default folder in the database.
        my $insert_sth = $dbh->prepare('INSERT INTO feeds (name, is_folder) VALUES (?, ?)');
        $insert_sth->execute('Feeds', 1);
        $insert_sth->finish;
        # Now populate the tree from the DB, which now contains one folder.
        populate_treestore_from_db();
    }
    elsif ($feed_count > 0) {
        # If feeds exist in DB, load from there.
        populate_treestore_from_db();
    } elsif (-f $storable_file) {
        # If DB is empty but storable exists, perform one-time migration.
        warn "Empty database found. Migrating from $storable_file...";
        my $config_data;
        eval { $config_data = retrieve($storable_file); };
        if ($@ || !ref $config_data) {
            warn "Could not load or parse old config from $storable_file: $@";
            return;
        }
        
        my $tree_data = (ref $config_data eq 'HASH') ? $config_data->{tree_data} : $config_data;

        if (ref $tree_data eq 'ARRAY') {
            $dbh->begin_work;
            eval {
                populate_treestore_from_data(undef, $tree_data, undef);
                $dbh->commit;
                warn "Migration successful.";
            };
            if ($@) {
                warn "Migration failed: $@. Rolling back.";
                $dbh->rollback;
            }
        }
    }

    # Load non-tree settings from the storable file regardless.
    if (-f $storable_file) {
        my $config_data;
        eval { $config_data = retrieve($storable_file); };
        if (ref $config_data eq 'HASH') {
            $cache_expire_minutes = $config_data->{cache_minutes} // 1440;
            $cache_expire_entry->set_text($cache_expire_minutes);
            
            $pruning_enabled = $config_data->{pruning_enabled} // 0;
            $prune_days = $config_data->{prune_days} // 64;

            # COL-WIDTH-MOD: Load column widths with sane defaults.
            if ($loadcolumnwidthsfromconfig) {
                 warn "Loading column widths from config file.";
                my $default_widths = { feed => 200, headline => 450, date => 150, date_added => 150 };
                %column_widths = %{ $config_data->{column_widths} // $default_widths };
            } else {
                warn "Overriding column widths with hardcoded defaults due to debug flag.";
                %column_widths = ( feed => 200, headline => 450, date => 150, date_added => 150 );
            }
        }
    } else {
        # If no config file exists at all, set the defaults regardless of the flag.
        %column_widths = ( feed => 200, headline => 450, date => 150, date_added => 150 );
    }
    
    # BROKEN-FEEDS-MOD: Load the broken status for all feeds into the cache.
    load_broken_feed_status_from_db();

    update_all_row_colors();
}

# DB-MOD: New function to populate the TreeStore by reading from the SQLite DB.
sub populate_treestore_from_db {
    my $sth = $dbh->prepare('SELECT id, parent_id, name, url, is_folder FROM feeds ORDER BY id');
    $sth->execute;

    my %nodes;
    my @root_ids;

    while (my $row = $sth->fetchrow_hashref) {
        $nodes{$row->{id}} = $row;
        if (!defined $row->{parent_id}) {
            push @root_ids, $row->{id};
        } else {
            $nodes{$row->{parent_id}}->{children} ||= [];
            push @{$nodes{$row->{parent_id}}->{children}}, $row->{id};
        }
    }
    $sth->finish;

    $tree_store->clear;
    for my $id (@root_ids) {
        add_node_to_tree_recursively(\%nodes, $id, undef);
    }
}

# DB-MOD: Helper function for populate_treestore_from_db
sub add_node_to_tree_recursively {
    my ($nodes, $id, $parent_iter) = @_;
    my $node = $nodes->{$id};

    my $iter = $tree_store->append($parent_iter);
    # BOLD-FIX: Use integer constant for weight.
    $tree_store->set($iter, 0, $node->{name}, 1, $node->{url}, 2, $node->{is_folder}, 4, $node->{id}, 5, 400);

    if ($node->{children}) {
        for my $child_id (@{$node->{children}}) {
            add_node_to_tree_recursively($nodes, $child_id, $iter);
        }
    }
}


# DB-MOD: This function now also populates the database during the initial migration.
sub populate_treestore_from_data {
    my ($parent_iter, $nodes, $parent_id) = @_;
    for my $node (@$nodes) {
        my $is_folder = exists $node->{children};
        
        # Insert into database
        my $sth = $dbh->prepare('INSERT INTO feeds (parent_id, name, url, is_folder) VALUES (?, ?, ?, ?)');
        $sth->execute($parent_id, $node->{name}, $node->{url}, $is_folder);
        my $new_id = $dbh->last_insert_id("","","feeds","id");
        $sth->finish;

        # Insert into Gtk2::TreeStore
        my $iter = $tree_store->append($parent_iter);
        # BOLD-FIX: Use integer constant for weight.
        $tree_store->set($iter, 0, $node->{name}, 1, $node->{url}, 2, $is_folder, 4, $new_id, 5, 400);
        
        if ($is_folder) {
            populate_treestore_from_data($iter, $node->{children}, $new_id);
        }
    }
}

sub save_opml_file {
    my $dialog = Gtk2::FileChooserDialog->new("Save Feeds As OPML", $window, 'save',
                                            'gtk-cancel' => 'cancel', 'gtk-save' => 'ok');
    $dialog->set_current_name("my_feeds.opml");
    if ($dialog->run eq 'ok') {
        my $filename = $dialog->get_filename;
        open my $fh, '>:utf8', $filename or do {
            warn "Could not open $filename for writing: $!";
            $dialog->destroy;
            return;
        };
        my $date = strftime "%a %b %d %H:%M:%S %Y", localtime;
        print $fh qq(<?xml version="1.0" encoding="UTF-8"?>\n);
        print $fh qq(<opml version="2.0">\n);
        print $fh qq(    <head>\n);
        print $fh qq(        <title>Feeds Exported from feeed</title>\n);
        print $fh qq(        <dateModified>$date</dateModified>\n);
        print $fh qq(    </head>\n);
        print $fh qq(    <body>\n);
        my $root_iter = $tree_store->get_iter_first;
        while ($root_iter) {
            walk_and_generate_opml($fh, $root_iter, "        ");
            $root_iter = $tree_store->iter_next($root_iter);
        }
        print $fh qq(    </body>\n);
        print $fh qq(</opml>\n);
        close $fh;
    }
    $dialog->destroy;
}

# EXPORT-MOD: New subroutine to handle exporting a single folder.
sub export_folder_as_opml {
    my ($folder_iter) = @_;
    
    my ($folder_name) = $tree_store->get($folder_iter, 0);
    my $safe_filename = sanitize_filename($folder_name) . ".opml";

    my $dialog = Gtk2::FileChooserDialog->new("Export Folder as OPML", $window, 'save',
                                            'gtk-cancel' => 'cancel', 'gtk-save' => 'ok');
    $dialog->set_current_name($safe_filename);
    
    if ($dialog->run eq 'ok') {
        my $filename = $dialog->get_filename;
        open my $fh, '>:utf8', $filename or do {
            warn "Could not open $filename for writing: $!";
            $dialog->destroy;
            return;
        };

        my $date = strftime "%a %b %d %H:%M:%S %Y", localtime;
        print $fh qq(<?xml version="1.0" encoding="UTF-8"?>\n);
        print $fh qq(<opml version="2.0">\n);
        print $fh qq(    <head>\n);
        # OPML-TITLE-FIX: Changed the title to just the folder name.
        print $fh qq(        <title>$folder_name</title>\n);
        print $fh qq(        <dateModified>$date</dateModified>\n);
        print $fh qq(    </head>\n);
        print $fh qq(    <body>\n);

        # Only iterate over the children of the selected folder.
        my $child_iter = $tree_store->iter_children($folder_iter);
        while ($child_iter) {
            walk_and_generate_opml($fh, $child_iter, "        ");
            $child_iter = $tree_store->iter_next($child_iter);
        }

        print $fh qq(    </body>\n);
        print $fh qq(</opml>\n);
        close $fh;
    }
    $dialog->destroy;
}

sub walk_and_generate_opml {
    my ($fh, $iter, $indent) = @_;
    my ($name, $url, $is_folder) = $tree_store->get($iter, 0, 1, 2);
    $name =~ s/&/&amp;/g; $name =~ s/</&lt;/g; $name =~ s/>/&gt;/g; $name =~ s/"/&quot;/g;
    $url =~ s/&/&amp;/g; $url =~ s/</&lt;/g; $url =~ s/>/&gt;/g; $url =~ s/"/&quot;/g;
    if ($is_folder) {
        print $fh qq($indent<outline text="$name">\n);
        my $child_iter = $tree_store->iter_children($iter);
        while ($child_iter) {
            walk_and_generate_opml($fh, $child_iter, $indent . "    ");
            $child_iter = $tree_store->iter_next($child_iter);
        }
        print $fh qq($indent</outline>\n);
    } else {
        print $fh qq($indent<outline type="rss" text="$name" xmlUrl="$url" />\n);
    }
}

################################################################################
# Download Queue and UI Control
################################################################################

# --- MODIFICATION: New subroutines for the Check/Fix Broken feature ---

# New helper to get a cache path from a feed ID, without needing a TreeStore iter.
sub get_feed_cache_path_from_db {
    my ($feed_id) = @_;
    my @path_parts;

    my $current_id = $feed_id;
    my $sth = $dbh->prepare('SELECT name, parent_id FROM feeds WHERE id = ?');

    while (defined $current_id) {
        $sth->execute($current_id);
        my ($name, $parent_id) = $sth->fetchrow_array;
        last unless defined $name;

        unshift @path_parts, sanitize_filename($name);
        $current_id = $parent_id;
    }
    $sth->finish;

    return File::Spec->catfile($feeds_dir, @path_parts);
}

# New function to run search4feeds logic in the background and use a callback.
sub perform_background_search4feeds {
    my ($original_url, $final_callback) = @_;
    return unless ($original_url and $final_callback);

    my @urls_to_check;
    my $uri = eval { URI->new($original_url) };
    unless ($uri) {
        $final_callback->("Invalid original URL: $original_url", 0);
        return;
    }
    
    my $scheme = $uri->scheme;
    my $domain = $uri->host;
    my $path = $uri->path;
    my $dir_path = $path;
    if ($path =~ m{/[^/]+\.[^/]+$}) {
        $dir_path = dirname($path);
    }
    $dir_path .= '/' if substr($dir_path, -1) ne '/';
    my @feed_suffixes = ('rss', 'feed.xml', 'rss.xml', 'index.xml', 'feed');
    my %seen_urls;
    for my $suffix (@feed_suffixes) {
        my $new_url = "$scheme://$domain/$suffix";
        unless ($seen_urls{$new_url}) {
            push @urls_to_check, $new_url;
            $seen_urls{$new_url} = 1;
        }
    }
    if ($dir_path ne '/') {
         for my $suffix (@feed_suffixes) {
            my $new_url = "$scheme://$domain$dir_path$suffix";
            unless ($seen_urls{$new_url}) {
                push @urls_to_check, $new_url;
                $seen_urls{$new_url} = 1;
            }
        }
    }
    my @path_parts = grep { $_ } split '/', $path;
    if (@path_parts) {
        my $potential_subdomain = $path_parts[0];
        if ($potential_subdomain !~ /\./) {
            my $new_host = "$potential_subdomain.$domain";
            for my $suffix (@feed_suffixes) {
                my $new_url = "$scheme://$new_host/$suffix";
                unless ($seen_urls{$new_url}) {
                    push @urls_to_check, $new_url;
                    $seen_urls{$new_url} = 1;
                }
            }
        }
    }
    
    my $results_text = "Searching for feeds based on: $original_url\n\n";
    $results_text .= "Checking the following potential URLs:\n" . join("\n", @urls_to_check) . "\n\n---\n\n";

    my $total_urls = scalar(@urls_to_check);
    my $completed_count = 0;
    my $found_count = 0;

    if ($total_urls == 0) {
        $results_text .= "Could not generate any URLs to check.";
        $final_callback->($results_text, 0);
        return;
    }

    for my $url_to_check (@urls_to_check) {
        AnyEvent::HTTP::http_get $url_to_check, timeout => 10, sub {
            my ($body, $hdr) = @_;
            $completed_count++;
            
            if ($hdr->{Status} =~ /^2/) {
                my $feed;
                eval { $feed = XML::Feed->parse(\$body) };
                if ($feed && scalar($feed->entries) > 0) { 
                    $found_count++;
                    my $title = $feed->title || "Untitled Feed";
                    $results_text .= "VALID FEED FOUND:\n$url_to_check\nTitle: $title\nEntries: " . scalar($feed->entries) . "\n\n";
                }
            }

            if ($completed_count == $total_urls) {
                $results_text .= "---\nSearch complete. Found $found_count valid feed(s).";
                $final_callback->($results_text, $found_count > 0);
            }
        };
    }
}

# The callback that runs after the initial update of broken feeds finishes.
sub on_broken_update_finished {
    my ($pbar, $results_vbox) = @_;

    Glib::Idle->add(sub {
        # Re-query the DB for feeds that are still broken
        my $sth = $dbh->prepare('SELECT id, name, url FROM feeds WHERE broken = 1');
        $sth->execute;
        my @still_broken_feeds = @{$sth->fetchall_arrayref({})};
        $sth->finish;

        if (!@still_broken_feeds) {
            $pbar->set_text("All broken feeds were successfully updated!");
            return FALSE;
        }

        $pbar->set_text("Searching for alternatives for remaining broken feeds...");
        
        for my $feed (@still_broken_feeds) {
            my $hbox = Gtk2::HBox->new(FALSE, 5);
            my $image = Gtk2::Image->new;
            my $expander = Gtk2::Expander->new($feed->{name});
            my $sw = Gtk2::ScrolledWindow->new(undef, undef);
            $sw->set_policy('automatic', 'automatic');
            $sw->set_shadow_type('in');
            $sw->set_size_request(-1, 150);
            my $tv = Gtk2::TextView->new;
            $tv->set_editable(FALSE);
            $tv->set_cursor_visible(FALSE);
            $tv->set_wrap_mode('word');
            my $buffer = $tv->get_buffer;

            $hbox->pack_start($image, FALSE, FALSE, 0);
            $hbox->pack_start($expander, TRUE, TRUE, 0);
            $sw->add($tv);
            $expander->add($sw);
            $results_vbox->pack_start($hbox, FALSE, FALSE, 0);

            my $search_callback = sub {
                my ($results_string, $was_successful) = @_;
                Glib::Idle->add(sub {
                    $buffer->set_text($results_string);
                    $image->set_from_stock($was_successful ? 'gtk-yes' : 'gtk-no', 'button');
                    return FALSE;
                });
            };

            perform_background_search4feeds($feed->{url}, $search_callback);
        }

        $results_vbox->show_all;
        return FALSE;
    });
}

# The main entry point for the "Check/Fix Broken" feature.
sub create_check_fix_broken_window {
    my $popup = Gtk2::Window->new('toplevel');
    $popup->set_title("Check/Fix Broken Feeds");
    $popup->set_default_size(600, 500);
    $popup->set_position('center-on-parent');
    $popup->set_transient_for($window);
    $popup->set_destroy_with_parent(1);
    $popup->set_border_width(10);

    my $main_vbox = Gtk2::VBox->new(FALSE, 6);
    $popup->add($main_vbox);

    my $pbar = Gtk2::ProgressBar->new;
    $main_vbox->pack_start($pbar, FALSE, FALSE, 0);

    my $sw = Gtk2::ScrolledWindow->new;
    $sw->set_policy('automatic', 'automatic');
    $main_vbox->pack_start($sw, TRUE, TRUE, 0);

    my $results_vbox = Gtk2::VBox->new(FALSE, 5);
    $sw->add_with_viewport($results_vbox);

    my $sth = $dbh->prepare('SELECT id, name, url FROM feeds WHERE broken = 1');
    $sth->execute;
    my @broken_feeds = @{$sth->fetchall_arrayref({})};
    $sth->finish;

    if (!@broken_feeds) {
        $pbar->set_text("No broken feeds found.");
        $popup->show_all;
        return;
    }

    my $job = {
        progress_bar  => $pbar,
        download_queue=> [],
        is_updating   => TRUE,
        task_counter  => 0,
        is_cancelled  => FALSE,
        is_paused     => FALSE,
    };

    for my $feed_info (@broken_feeds) {
        my $cache_path = get_feed_cache_path_from_db($feed_info->{id});
        push @{$job->{download_queue}}, {
            name       => $feed_info->{name},
            url        => $feed_info->{url},
            cache_path => $cache_path,
            iter       => undef,
            retries    => 0,
            feed_id    => $feed_info->{id},
        };
    }
    
    $job->{total_feeds_in_queue} = scalar(@{$job->{download_queue}});
    $job->{feeds_processed} = 0;

    my $finish_callback = sub {
        on_broken_update_finished($pbar, $results_vbox);
    };
    
    $pbar->set_text("Attempting to update " . scalar(@broken_feeds) . " broken feeds...");
    $popup->show_all;

    process_download_job_queue($job, $finish_callback);
}


# PAGINATION-MOD: New subroutines for the "Show New" and "Next Page" buttons.
sub on_show_new_clicked {
    # Define the specific query for finding new items.
    my $sql = "SELECT id, title FROM entries WHERE is_new = 1 ORDER BY date_added DESC";
    
    # Set the global state for the query display function.
    $last_successful_select_query = $sql;
    $current_query_offset = 0; # Reset pagination
    
    # Call the main display function.
    show_query_results_in_main_view();
}

sub on_next_page_clicked {
    # This button should only work if there's a previous query to paginate.
    return unless $last_successful_select_query;
    
    # Increment the offset for the next page.
    $current_query_offset += $query_limit;
    
    # Take the last query and remove any existing LIMIT/OFFSET clauses.
    my $base_query = $last_successful_select_query;
    $base_query =~ s/\s+LIMIT\s+\d+(\s+OFFSET\s+\d+)?\s*$//i;
    
    # Append the new LIMIT and OFFSET.
    my $new_query = $base_query . " LIMIT $query_limit OFFSET $current_query_offset";
    
    # Update the global state so the *next* click on "Next Page" works correctly.
    $last_successful_select_query = $new_query;
    
    # Call the main display function.
    show_query_results_in_main_view();
}

# MULTI-REFRESH-CTRL-MOD: This is the orchestrator for the concurrent refresh feature.
sub on_refresh_all_concurrently_clicked {
    my $popup = Gtk2::Window->new('toplevel');
    $popup->set_title("Refresh All Progress");
    $popup->set_default_size(500, 300);
    $popup->set_position('center-on-parent');
    $popup->set_transient_for($window);
    $popup->set_destroy_with_parent(1);
    $popup->set_border_width(10);

    # MULTI-REFRESH-CTRL-MOD: Create a main VBox for controls and progress bars.
    my $main_vbox = Gtk2::VBox->new(FALSE, 6);
    $popup->add($main_vbox);

    # MULTI-REFRESH-CTRL-MOD: Create HBox for control buttons.
    my $controls_hbox = Gtk2::HBox->new(FALSE, 5);
    my $force_all_button = Gtk2::Button->new("Force All");
    my $pause_all_button = Gtk2::Button->new("Pause");
    # WINDOW-CLOSE-FIX: Rename "Cancel" to "Close" to better reflect its new function.
    my $close_all_button = Gtk2::Button->new("Close");
    $controls_hbox->pack_start($force_all_button, TRUE, TRUE, 0);
    $controls_hbox->pack_start($pause_all_button, TRUE, TRUE, 0);
    $controls_hbox->pack_start($close_all_button, TRUE, TRUE, 0);
    $main_vbox->pack_start($controls_hbox, FALSE, FALSE, 0);
    $main_vbox->pack_start(Gtk2::HSeparator->new, FALSE, FALSE, 5);

    my $sw = Gtk2::ScrolledWindow->new;
    $sw->set_policy('automatic', 'automatic');
    $main_vbox->pack_start($sw, TRUE, TRUE, 0);

    my $progress_vbox = Gtk2::VBox->new(FALSE, 5);
    $sw->add_with_viewport($progress_vbox);

    # MULTI-REFRESH-CTRL-MOD: State variables scoped to this specific refresh operation.
    my @active_jobs;
    my $active_job_count = 0;
    my $is_globally_paused = FALSE;

    # WINDOW-CLOSE-FIX: The finish callback NO LONGER closes the window.
    # It just decrements the counter.
    my $finish_callback = sub {
        $active_job_count--;
    };

    my $iter = $tree_store->get_iter_first;
    while ($iter) {
        my $name = $tree_store->get($iter, 0);
        $name =~ s/\s*\(\d+\)$//; # Clean name for label

        my $hbox = Gtk2::HBox->new(FALSE, 5);
        my $label = Gtk2::Label->new($name);
        $label->set_size_request(120, -1);
        my $pbar = Gtk2::ProgressBar->new;
        $hbox->pack_start($label, FALSE, FALSE, 0);
        $hbox->pack_start($pbar, TRUE, TRUE, 0);
        $progress_vbox->pack_start($hbox, FALSE, FALSE, 0);
        
        # Create a new job object for this top-level item.
        my $job = {
            root_iter            => $iter->copy,
            progress_bar         => $pbar,
            download_queue       => [],
            total_feeds_in_queue => 0,
            feeds_processed      => 0,
            is_updating          => FALSE,
            task_counter         => 0,
            current_task_id      => undef,
            # MULTI-REFRESH-CTRL-MOD: Add per-job control flags.
            is_paused            => FALSE,
            is_cancelled         => FALSE,
        };
        push @active_jobs, $job;
        
        $iter = $tree_store->iter_next($iter);
    }
    
    # MULTI-REFRESH-CTRL-MOD: Signal handlers for the new control buttons.
    $force_all_button->signal_connect(clicked => sub {
        warn "Forcing update for all concurrent jobs...";
        # Restart every job, but with the 'force' flag set to 1.
        for my $job (@active_jobs) {
            $job->{is_cancelled} = TRUE; # Stop any current downloads for this job.
            # WINDOW-CLOSE-FIX: Increment the active job counter as we restart one.
            $active_job_count++;
            start_single_update_job($job, $finish_callback, 1); # Pass 1 to force update.
        }
    });

    $pause_all_button->signal_connect(clicked => sub {
        $is_globally_paused = !$is_globally_paused;
        if ($is_globally_paused) {
            $pause_all_button->set_label("Resume");
            for my $job (@active_jobs) { $job->{is_paused} = TRUE; }
        } else {
            $pause_all_button->set_label("Pause");
            for my $job (@active_jobs) {
                $job->{is_paused} = FALSE;
                # Kickstart the download loop again for any job that was paused.
                process_download_job_queue($job, $finish_callback);
            }
        }
    });

    # WINDOW-CLOSE-FIX: The "Close" button now cancels all jobs and destroys the window.
    $close_all_button->signal_connect(clicked => sub {
        for my $job (@active_jobs) {
            $job->{is_cancelled} = TRUE;
        }
        $popup->destroy;
    });

    $popup->signal_connect(delete_event => sub {
        # Ensure jobs are cancelled if the user closes the window.
        for my $job (@active_jobs) {
            $job->{is_cancelled} = TRUE;
        }
        return FALSE; # Allow the window to be destroyed.
    });


    $popup->show_all;

    if (@active_jobs) {
        $active_job_count = scalar(@active_jobs);
        for my $job (@active_jobs) {
            # Start initial update (not forced).
            start_single_update_job($job, $finish_callback, 0);
        }
    } else {
        # If there are literally no jobs, it's safe to close.
        $popup->destroy;
    }
}


# SINGLE-CTRL-FIX: The main UI control buttons now modify the current job's state.
sub on_pause_clicked {
    return unless $current_single_update_job and $current_single_update_job->{is_updating};
    
    $current_single_update_job->{is_paused} = !$current_single_update_job->{is_paused};
    
    if ($current_single_update_job->{is_paused}) {
        $pause_button->set_stock_id('gtk-media-play');
        $pause_button->set_tooltip_text('Resume');
    } else {
        $pause_button->set_stock_id('gtk-media-pause');
        $pause_button->set_tooltip_text('Pause');
        # Resume the download loop for the specific job.
        process_download_job_queue($current_single_update_job, \&finish_update_process);
    }
}

sub on_cancel_clicked {
    return unless $current_single_update_job and $current_single_update_job->{is_updating};
    $current_single_update_job->{is_cancelled} = TRUE;
    # Directly call the finish function to clean up the UI.
    finish_update_process();
}

sub on_skip_clicked {
    return unless $current_single_update_job and $current_single_update_job->{is_updating};
    if ($current_single_update_job->{is_paused}) { on_pause_clicked(); return; }

    # Invalidate the current task for this job and immediately process the next item.
    $current_single_update_job->{current_task_id} = undef;
    $current_single_update_job->{feeds_processed}++;
    my $fraction = $current_single_update_job->{total_feeds_in_queue} > 0 ? 
                   $current_single_update_job->{feeds_processed} / $current_single_update_job->{total_feeds_in_queue} : 1.0;
    $progress_bar->set_fraction($fraction);
    process_download_job_queue($current_single_update_job, \&finish_update_process);
}


# SINGLE-CTRL-FIX: This function now populates the global job variable.
sub start_update_process {
    my ($iter) = @_;
    return if $is_updating; # Use the old global flag as a simple mutex.
    $is_updating = TRUE;

    $progress_bar->set_fraction(0);
    $progress_bar->set_text("Starting...");
    $progress_item->show;
    $pause_button->set_stock_id('gtk-media-pause');
    $pause_button->set_tooltip_text('Pause');
    $pause_button->show;
    $cancel_button->show;
    $skip_button->show;

    # Create a job object for the main UI.
    $current_single_update_job = {
        root_iter            => $iter,
        progress_bar         => $progress_bar,
        is_updating          => TRUE,
    };
    
    start_single_update_job($current_single_update_job, \&finish_update_process, 1);
}

# LIVE-REFRESH: This function handles the cleanup and final refresh.
sub finish_update_process {
    $progress_item->hide;
    $pause_button->hide;
    $cancel_button->hide;
    $skip_button->hide;
    
    # SINGLE-CTRL-FIX: Clear the global job reference.
    $current_single_update_job = undef;
    $is_updating = FALSE;
    
    # LIVE-REFRESH: Stop the live refresh timer.
    if ($live_refresh_timer_id) {
        Glib::Source->remove($live_refresh_timer_id);
        $live_refresh_timer_id = undef;
    }

    # LIVE-REFRESH: Perform a final refresh of the view if the updated item is still selected.
    if ($current_update_root_iter and is_iter_selected($current_update_root_iter)) {
        my $is_folder = $tree_store->get($current_update_root_iter, 2);
        if ($is_folder) {
            display_folder_contents($current_update_root_iter);
        } else {
            display_feed_contents($current_update_root_iter);
        }
    }
    $current_update_root_iter = undef;
}

# MULTI-REFRESH-CTRL-MOD: Now accepts a 'force_update' flag.
sub start_single_update_job {
    my ($job, $finish_callback, $force_update) = @_;

    # Initialize or reset the state within the job object.
    $job->{download_queue}       = [];
    $job->{total_feeds_in_queue} = 0;
    $job->{feeds_processed}      = 0;
    $job->{is_updating}          = TRUE;
    $job->{task_counter}         = 0;
    $job->{is_cancelled}         = FALSE; # Ensure it's not cancelled on restart.
    $job->{is_paused}            = FALSE; # Ensure it's not paused on restart.
    $current_update_root_iter = $job->{root_iter}; # For live refresh

    $job->{progress_bar}->set_fraction(0);
    $job->{progress_bar}->set_text("Collecting feeds...");
    
    # Collect feeds just for this job, respecting the force flag.
    collect_feeds_for_job_queue_recursive($job->{root_iter}, $job->{download_queue}, $force_update);
    $job->{total_feeds_in_queue} = scalar(@{$job->{download_queue}});
    
    if ($job->{total_feeds_in_queue} == 0) {
        $job->{progress_bar}->set_fraction(1.0);
        $job->{progress_bar}->set_text("Nothing to update");
        $finish_callback->();
        return;
    }

    # Kick off the processing loop for this job.
    process_download_job_queue($job, $finish_callback);
}

# MULTI-REFRESH-MOD: A new recursive feed collector that populates a specific queue.
sub collect_feeds_for_job_queue_recursive {
    my ($iter, $queue_ref, $force_update) = @_;
    my $is_folder = $tree_store->get($iter, 2);
    if ($is_folder) {
        my $child_iter = $tree_store->iter_children($iter);
        while ($child_iter) {
            collect_feeds_for_job_queue_recursive($child_iter, $queue_ref, $force_update);
            $child_iter = $tree_store->iter_next($child_iter);
        }
    } else {
        my $feed_id = $tree_store->get($iter, 4);
        if ($mark_broken_check->get_active() and !$force_update and exists $broken_feed_status{$feed_id}) {
            return;
        }

        my $should_download = TRUE;
        if (!$force_update) {
            my $cache_path = get_feed_cache_path($iter);
            if (-f $cache_path) {
                my $mtime = (stat($cache_path))[9];
                if (time() - $mtime <= ($cache_expire_minutes * 60)) {
                    $should_download = FALSE;
                }
            }
        }
        
        if ($should_download) {
            push @$queue_ref, {
                name       => $tree_store->get($iter, 0),
                url        => $tree_store->get($iter, 1),
                cache_path => get_feed_cache_path($iter),
                iter       => $iter,
                retries    => 0,
                feed_id    => $feed_id,
            };
        }
    }
}


# MULTI-REFRESH-CTRL-MOD: The core download loop, now respects pause/cancel flags.
# UI-FREEZE-FIX: UI update calls are moved to the end of the job to prevent thrashing.
sub process_download_job_queue {
    my ($job, $finish_callback) = @_;

    # Check control flags at the start of each iteration.
    return if $job->{is_cancelled};
    return if $job->{is_paused};

    if (!@{$job->{download_queue}}) {
        $job->{is_updating} = FALSE;
        $job->{progress_bar}->set_fraction(1.0);
        $job->{progress_bar}->set_text("Finished");

        # UI-FREEZE-FIX: The UI is now updated only ONCE when the entire job is complete.
        update_tree_view_labels();
        update_all_row_colors();

        $finish_callback->();
        return;
    }

    my $feed_info = shift @{$job->{download_queue}};
    $job->{task_counter}++;
    $feed_info->{task_id} = $job->{task_counter};
    $job->{current_task_id} = $feed_info->{task_id};
    
    my $remaining = scalar(@{$job->{download_queue}}) + 1;
    my $name = $feed_info->{name};
    $name =~ s/\s*\(\d+\)$//;
    my $max_len = 25;
    $name = (length($name) > $max_len) ? substr($name, 0, $max_len-3) . '...' : $name;
    $job->{progress_bar}->set_text("$name ($remaining)");

    fetch_and_parse_feed($feed_info, sub {
        my ($download_success, $content, $feed_obj) = @_;
        
        Glib::Idle->add(sub {
            return FALSE if !defined($job->{current_task_id}) or $job->{current_task_id} ne $feed_info->{task_id};
            # Also check for cancellation before processing.
            return FALSE if $job->{is_cancelled};

            if ($download_success) {
                # DIR-CREATE-FIX: Ensure the cache directory exists before trying to write.
                # This is the new, robust way to create directories just-in-time.
                my $dir = dirname($feed_info->{cache_path});
                make_path($dir) unless -d $dir;

                open my $fh, '>:utf8', $feed_info->{cache_path} or warn "Could not write to cache file $feed_info->{cache_path}: $!";
                if ($fh) { print $fh $content; close $fh; }
                
                my $parse_success = update_db_from_feed_content($feed_info->{feed_id}, $content, $feed_obj);
                
                if ($parse_success) {
                    $dbh->do('UPDATE feeds SET broken = 0 WHERE id = ?', undef, $feed_info->{feed_id});
                    delete $broken_feed_status{$feed_info->{feed_id}};
                }
                $job->{feeds_processed}++;
            } else {
                if ($feed_info->{retries} < 2) {
                    $feed_info->{retries}++;
                    unshift @{$job->{download_queue}}, $feed_info;
                } else {
                    $dbh->do('UPDATE feeds SET broken = 1 WHERE id = ?', undef, $feed_info->{feed_id});
                    $broken_feed_status{$feed_info->{feed_id}} = 1;
                    $job->{feeds_processed}++;
                }
            }
            
            my $fraction = $job->{total_feeds_in_queue} > 0 ? $job->{feeds_processed} / $job->{total_feeds_in_queue} : 1.0;
            $job->{progress_bar}->set_fraction($fraction);
            
            process_download_job_queue($job, $finish_callback);
            return FALSE;
        });
    });
}


# REFACTOR-MOD: New unified function for fetching and initial parsing.
sub fetch_and_parse_feed {
    my ($feed_info, $callback) = @_;
    my ($url) = ($feed_info->{url});
    my %headers = (
        'User-Agent' => 'Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0',    
        'Accept'     => 'application/xml,application/rss+xml,application/atom+xml,*/*',
    );
    AnyEvent::HTTP::http_get $url, timeout => 10, headers => \%headers, tls_ctx => { verify => 0, verify_hostname => 0 }, sub {
        my ($body, $hdr) = @_;
        if ($hdr->{Status} =~ /^2/) {
            my $feed_obj;
            eval { $feed_obj = XML::Feed->parse(\$body) };
            $callback->(1, $body, $feed_obj);
        } else {
            warn "Failed to fetch feed at $url: $hdr->{Status} $hdr->{Reason}";
            $callback->(0, undef, undef);
        }
    };
}

# --- MODIFICATION: Updated logic for valid but empty feeds ---
sub update_db_from_feed_content {
    my ($feed_id, $content, $feed_obj) = @_;
    return 0 unless defined $feed_id and defined $content;

    my @entries_to_process;
    my $parse_successful = 1;
    my $initial_parse_was_valid = 0; # New flag to track feed validity

# --- Attempt 1: Use the standard parser ---
    if ($feed_obj and $feed_obj->isa('XML::Feed')) {
        $initial_parse_was_valid = 1; # Mark that the XML itself is good
        
        # --- MODIFICATION: Clean up old error entries upon successful parse ---
        my $sth_cleanup = $dbh->prepare("DELETE FROM entries WHERE feed_id = ? AND title = 'Unparseable Feed Content'");
        $sth_cleanup->execute($feed_id);
        $sth_cleanup->finish;
        # ----------------------------------------------------------------------
        
        my $fallback_summaries = regexfallbackforparsingoutdescriptionsummary($content);
        for my $entry ($feed_obj->entries) {
            my $date_obj = $entry->issued || $entry->modified;
            push @entries_to_process, {
                link    => $entry->link || $entry->id,
                title   => $entry->title,
                date    => $date_obj ? $date_obj->epoch : 0,
                content => format_post_content_string($entry, $fallback_summaries)
            };
        }
    }

    # --- Attempt 2: Regex Fallback Parser (if first attempt yielded nothing) ---
    if (!@entries_to_process) {
        warn "Initial parse found 0 entries for feed ID $feed_id. Attempting regex fallback." unless $initial_parse_was_valid;
        my @synthetic_entries = regex_fallback_parser($content);
        if (@synthetic_entries) {
            warn "Regex fallback successful, found " . scalar(@synthetic_entries) . " potential entries.";
            @entries_to_process = @synthetic_entries;
        }
    }

    # --- Final Resort: Create error entry ONLY if the initial parse was invalid AND we still have no entries ---
    if (!@entries_to_process and !$initial_parse_was_valid) {
        warn "All parsing attempts failed for feed ID $feed_id. Storing raw content as a single entry.";
        $dbh->do('UPDATE feeds SET broken = 1 WHERE id = ?', undef, $feed_id);
        $broken_feed_status{$feed_id} = 1;
        $parse_successful = 0;
        
        my $error_title = "Unparseable Feed Content";
        my $error_summary = "The original feed content could not be parsed. The raw text is included below for debugging.\n\n" . $content;
        my $error_link = "urn:uuid:$feed_id:" . time();
        my $error_date = time();
        my $date_str = strftime('%Y-%m-%d %H:%M:%S', localtime($error_date));
        my $final_content_string = "Title: $error_title\nLink: $error_link\nDate: $date_str\nSummary: $error_summary\n";
        push @entries_to_process, { title => $error_title, link => $error_link, date => $error_date, content => $final_content_string };
    }

    # --- Database Insertion ---
    if (@entries_to_process) {
        my $sth = $dbh->prepare(q{
            INSERT OR IGNORE INTO entries (feed_id, title, link, published_date, content)
            VALUES (?, ?, ?, ?, ?)
        });

        $dbh->begin_work;
        eval {
            for my $entry_data (@entries_to_process) {
                next unless ($entry_data->{link} and $entry_data->{link} ne '');
                $sth->execute(
                    $feed_id, $entry_data->{title}, $entry_data->{link}, $entry_data->{date}, $entry_data->{content}
                );
            }
            $dbh->commit;
        };
        if ($@) {
            warn "Database transaction failed for feed ID $feed_id: $@. Rolling back.";
            $dbh->rollback;
        }
        $sth->finish;
    }

    return $parse_successful;
}

# This is now a deprecated function, replaced by the recursive job collector.
sub collect_feeds_for_queue {}

################################################################################
# Core Application Logic
################################################################################

# ... (The rest of the file is unchanged, as previously stated, but included now for completeness) ...

# HIDE-UNPARSEABLE-MOD: New sub to handle the checkbox toggle.
sub on_hide_unparseable_toggled {
    # Get the current selection and re-trigger the display logic.
    # This will cause the appropriate display function to re-run its query
    # with the new checkbox state.
    feed_selection_changed($tree_view->get_selection);
}

# BROKEN-FEEDS-MOD: New function to populate the in-memory cache of broken feeds.
sub load_broken_feed_status_from_db {
    %broken_feed_status = ();
    my $sth = $dbh->prepare('SELECT id FROM feeds WHERE broken = 1');
    $sth->execute;
    while (my ($feed_id) = $sth->fetchrow_array) {
        $broken_feed_status{$feed_id} = 1;
    }
    $sth->finish;
}

# LIVE-REFRESH: This is the function called by the timer.
sub live_refresh_view {
    # Check if the item that started the update is still the one selected.
    if ($current_update_root_iter and is_iter_selected($current_update_root_iter)) {
        # Only refresh folders, as single feeds don't show progress.
        my $is_folder = $tree_store->get($current_update_root_iter, 2);
        if ($is_folder) {
            display_folder_contents($current_update_root_iter);
        }
    }
    return TRUE; # Return true to keep the timer running.
}

# LIVE-REFRESH: Helper subroutine to check if a given iter is currently selected.
sub is_iter_selected {
    my ($iter_to_check) = @_;
    return FALSE unless $iter_to_check;

    my ($model, $selected_iter) = $tree_view->get_selection->get_selected;
    return FALSE unless $selected_iter;

    # Compare iterators by their string representation, which is unique and safe.
    if ($model->get_string_from_iter($selected_iter) eq 
        $model->get_string_from_iter($iter_to_check)) {
        return TRUE;
    }
    return FALSE;
}

sub update_all_row_colors {
    my $root_iter = $tree_store->get_iter_first;
    while ($root_iter) {
        apply_background_color($root_iter);
        $root_iter = $tree_store->iter_next($root_iter);
    }
}

sub apply_background_color {
    my ($iter) = @_;
    my $is_folder = $tree_store->get($iter, 2);
    if ($is_folder) {
        my $child_iter = $tree_store->iter_children($iter);
        while ($child_iter) {
            apply_background_color($child_iter);
            $child_iter = $tree_store->iter_next($child_iter);
        }
    } else {
        my $color = undef;
        # BROKEN-FEEDS-MOD: Check the in-memory cache using the feed_id instead of the old hash.
        my $feed_id = $tree_store->get($iter, 4);
        if ($mark_broken_check->get_active and exists $broken_feed_status{$feed_id}) {
            $color = '#FFDDDD';
        } else {
            my $cache_path = get_feed_cache_path($iter);
            unless (-f $cache_path) {
                $color = '#EEEEEE';
            }
        }
        $tree_store->set($iter, 3, $color);
    }
}

sub sanitize_filename {
    my ($filename) = @_;
    $filename =~ s/[^\w\d\.\-_]/_/g;
    return $filename;
}

sub get_feed_cache_path {
    my ($iter) = @_;
    my @path_parts;
    my $name = $tree_store->get($iter, 0);
    
    # COUNT-FIX: Strip the " (##)" count from the name before using it for the filename.
    # This fixes the regression where updated feeds remained gray.
    $name =~ s/\s*\(\d+\)$//;

    unshift @path_parts, sanitize_filename($name);
    my $parent_iter = $tree_store->iter_parent($iter);
    while ($parent_iter) {
        my $parent_name = $tree_store->get($parent_iter, 0);
        # COUNT-FIX: Also strip counts from parent folder names.
        $parent_name =~ s/\s*\(\d+\)$//;
        unshift @path_parts, sanitize_filename($parent_name);
        $parent_iter = $tree_store->iter_parent($parent_iter);
    }
    return File::Spec->catfile($feeds_dir, @path_parts);
}

# NEW-PERSIST-MOD: This function now updates labels based on DB counts.
sub update_tree_view_labels {
    my $iter = $tree_store->get_iter_first;
    while ($iter) {
        recursively_update_counts_and_labels($iter);
        $iter = $tree_store->iter_next($iter);
    }
}

# NEW-PERSIST-MOD: Recursive helper to calculate and apply counts from the database.
sub recursively_update_counts_and_labels {
    my ($iter) = @_;
    my $is_folder = $tree_store->get($iter, 2);
    my ($base_name) = $tree_store->get($iter, 0);
    $base_name =~ s/\s*\(\d+\)$//; # Strip old count to get the original name.

    my $count = 0;
    if ($is_folder) {
        my $child_iter = $tree_store->iter_children($iter);
        while ($child_iter) {
            $count += recursively_update_counts_and_labels($child_iter);
            $child_iter = $tree_store->iter_next($child_iter);
        }
    } else {
        my $feed_id = $tree_store->get($iter, 4);
        # Query the database for the count of new entries for this specific feed.
        my $sth = $dbh->prepare('SELECT COUNT(*) FROM entries WHERE feed_id = ? AND is_new = 1');
        $sth->execute($feed_id);
        ($count) = $sth->fetchrow_array;
        $sth->finish;
    }

    my $new_label = $count > 0 ? "$base_name ($count)" : $base_name;
    my $weight = ($count > 0) ? 700 : 400;

    $tree_store->set($iter, 0, $new_label, 5, $weight);

    return $count;
}

# NEW-PERSIST-MOD: "Mark Read" now updates the is_new flag in the database.
sub on_mark_read_clicked {
    my ($iter) = @_;

    my $feed_id = $tree_store->get($iter, 4);
    return unless $feed_id;
    
    my @feed_ids_to_mark = get_all_child_feed_ids($feed_id);
    # Also include the parent folder's ID if it's a folder, as it could be a container for other folders.
    push @feed_ids_to_mark, $feed_id if $tree_store->get($iter, 2);
    return unless @feed_ids_to_mark;

    my $placeholders = join(',', ('?') x @feed_ids_to_mark);
    my $sth = $dbh->prepare("UPDATE entries SET is_new = 0 WHERE feed_id IN ($placeholders)");
    $sth->execute(@feed_ids_to_mark);
    $sth->finish;

    # After updating the database, update the labels in the UI.
    update_tree_view_labels();
}

################################################################################
# Search Functionality
################################################################################

# ... (The search functions are identical to the last version) ...
# ... (They are included here for completeness) ...

# GTK2-FIX: New subroutine to handle focus-in event for placeholder text.
sub on_search_focus_in {
    my ($entry) = @_;
    if ($entry->get_text eq $search_placeholder) {
        $entry->set_text('');
        $entry->modify_text('normal', Gtk2::Gdk::Color->parse('black'));
    }
    return FALSE; # Allow other handlers to run
}

# GTK2-FIX: New subroutine to handle focus-out event for placeholder text.
sub on_search_focus_out {
    my ($entry) = @_;
    if ($entry->get_text eq '') {
        $entry->set_text($search_placeholder);
        $entry->modify_text('normal', Gtk2::Gdk::Color->parse('gray'));
    }
    return FALSE; # Allow other handlers to run
}

# SEARCH-MOD: Reset the search state whenever the text in the entry box changes.
sub on_search_text_changed {
    @search_results = ();
    $current_search_index = -1;
    $last_search_term = '';
}

# SEARCH-MOD: Recursively traverses the TreeStore to find nodes matching the search term.
sub find_matching_nodes_recursive {
    my ($iter, $term_lc) = @_;
    
    # Check the current node.
    my ($name) = $tree_store->get($iter, 0);
    if (defined $name && lc($name) =~ /\Q$term_lc\E/) {
        # Use copy() because iters can be invalidated.
        push @search_results, $iter->copy;
    }

    # Recurse for children if they exist.
    if ($tree_store->iter_has_child($iter)) {
        my $child_iter = $tree_store->iter_children($iter);
        while ($child_iter) {
            find_matching_nodes_recursive($child_iter, $term_lc);
            $child_iter = $tree_store->iter_next($child_iter);
        }
    }
}

# SEARCH-MOD: Handles the 'Enter' key press in the search box.
sub on_search_activate {
    my ($entry) = @_;
    my $term = $entry->get_text;
    # GTK2-FIX: Don't search if the entry is empty or still has the placeholder.
    return if ($term eq '' or $term eq $search_placeholder);
    my $term_lc = lc($term);

    # If this is a new search term or the first time searching, populate the results.
    if ($term ne $last_search_term) {
        on_search_text_changed(); # Ensure state is clean.
        $last_search_term = $term;

        # Start the recursive search from the root level.
        my $root_iter = $tree_store->get_iter_first;
        while ($root_iter) {
            find_matching_nodes_recursive($root_iter, $term_lc);
            $root_iter = $tree_store->iter_next($root_iter);
        }
    }

    # If no results were found, do nothing.
    return if !@search_results;

    # Cycle to the next search result.
    $current_search_index++;
    # Loop back to the beginning if we've passed the last result.
    if ($current_search_index >= @search_results) {
        $current_search_index = 0;
    }

    # Get the iter for the current result and select it in the TreeView.
    my $iter_to_select = $search_results[$current_search_index];
    if ($iter_to_select and $tree_store->iter_is_valid($iter_to_select)) {
        my $path = $tree_store->get_path($iter_to_select);
        
        # Expand all parent nodes to make the selection visible.
        $tree_view->expand_to_path($path);
        
        # Select the item.
        $tree_view->get_selection->select_path($path);

        # Scroll the view so the selected item is visible.
        $tree_view->scroll_to_cell($path, undef, TRUE, 0.5, 0.0);
    }
}

# --- MODIFICATION: New subroutine to handle the entire Wayback Machine search process ---
sub perform_wayback_search {
    my ($original_url, $final_callback) = @_;
    my $to_date = '20201231'; # Limit to snapshots before 2021
    my $cdx_url = "https://web.archive.org/cdx/search/cdx?url=$original_url&to=$to_date";

    # Step 1: Query the CDX API for snapshots
    http_get $cdx_url, sub {
        my ($body, $headers) = @_;
        
        return $final_callback->("CDX API request failed: $headers->{Status}", 0) unless ($headers->{Status} =~ /^2/);

        my @lines = grep { $_ && !/^urlkey/ } split /\n/, $body;
        return $final_callback->("No snapshots found in the Internet Archive before $to_date.", 0) unless @lines;

        # The last line is the latest snapshot within the date range
        my @fields = split /\s+/, $lines[-1];
        my ($timestamp, $original_feed_url) = @fields[1, 2];
        my $playback_url = "https://web.archive.org/web/$timestamp/$original_feed_url";

        # Step 2: Fetch the HTML playback page
        http_get $playback_url, sub {
            my ($playback_body, $playback_headers) = @_;
            return $final_callback->("Failed to fetch playback page: $playback_headers->{Status}", 0) unless ($playback_headers->{Status} =~ /^2/);

            # Step 3: Extract the real feed URL from the iframe
            if ($playback_body =~ m{<iframe\s+id="playback"\s+src="([^"]+)"}i) {
                my $iframe_src = $1;
                
                # Step 4: Fetch the actual feed content from the iframe URL
                http_get $iframe_src, sub {
                    my ($feed_body, $feed_headers) = @_;
                    return $final_callback->("Failed to fetch iframe content: $feed_headers->{Status}", 0) unless ($feed_headers->{Status} =~ /^2/);

                    # Step 5: Try to parse the final content
                    my $feed;
                    eval { $feed = XML::Feed->parse(\$feed_body) };
                    if ($feed && scalar($feed->entries) > 0) {
                        my $title = $feed->title || "Untitled Feed";
                        my $result_str = "VALID ARCHIVED/DEAD FEED FOUND:\n$iframe_src\nTitle: $title\nEntries: " . scalar($feed->entries) . "\n\n";
                        $final_callback->($result_str, 1);
                    } else {
                        $final_callback->("Found an archived snapshot, but its content was not a valid feed.", 0);
                    }
                };
            } else {
                $final_callback->("Could not find the content iframe on the playback page.", 0);
            }
        };
    };
}


# --- MODIFICATION: Updated to chain the Wayback Machine search after the live search ---
sub on_search_for_feeds_clicked {
    my ($iter) = @_;
    my $original_url = $tree_store->get($iter, 1);
    return unless $original_url;

    # --- Create the results window ---
    my $popup = Gtk2::Window->new('toplevel');
    $popup->set_title("Feed Search Results for: $original_url");
    $popup->set_default_size(600, 400);
    $popup->set_position('center-on-parent');
    $popup->set_transient_for($window);
    $popup->set_destroy_with_parent(1);
    $popup->set_border_width(10);

    my $sw = Gtk2::ScrolledWindow->new(undef, undef);
    $sw->set_policy('automatic', 'automatic');
    $sw->set_shadow_type('in');

    my $tv = Gtk2::TextView->new();
    $tv->set_editable(TRUE); 
    $tv->set_cursor_visible(FALSE);
    $tv->set_wrap_mode('word');
    my $buffer = $tv->get_buffer();
    $sw->add($tv);
    $popup->add($sw);

    my $iter_end = $buffer->get_end_iter;
    $buffer->insert($iter_end, "Searching for live feeds based on: $original_url\n\n");

    # --- Generate URLs to check ---
    my @urls_to_check;
    my $uri = eval { URI->new($original_url) };
    unless ($uri) {
        $buffer->insert($buffer->get_end_iter, "Invalid original URL provided.");
        $popup->show_all;
        return;
    }
    
    my $scheme = $uri->scheme;
    my $domain = $uri->host;
    my $path = $uri->path;
    my $dir_path = $path;
    if ($path =~ m{/[^/]+\.[^/]+$}) {
        $dir_path = dirname($path);
    }
    $dir_path .= '/' if substr($dir_path, -1) ne '/';
    my @feed_suffixes = ('rss', 'feed.xml', 'rss.xml', 'index.xml', 'feed');
    my %seen_urls;
    for my $suffix (@feed_suffixes) {
        my $new_url = "$scheme://$domain/$suffix";
        unless ($seen_urls{$new_url}) {
            push @urls_to_check, $new_url;
            $seen_urls{$new_url} = 1;
        }
    }
    if ($dir_path ne '/') {
         for my $suffix (@feed_suffixes) {
            my $new_url = "$scheme://$domain$dir_path$suffix";
            unless ($seen_urls{$new_url}) {
                push @urls_to_check, $new_url;
                $seen_urls{$new_url} = 1;
            }
        }
    }
    my @path_parts = grep { $_ } split '/', $path;
    if (@path_parts) {
        my $potential_subdomain = $path_parts[0];
        if ($potential_subdomain !~ /\./) {
            my $new_host = "$potential_subdomain.$domain";
            for my $suffix (@feed_suffixes) {
                my $new_url = "$scheme://$new_host/$suffix";
                unless ($seen_urls{$new_url}) {
                    push @urls_to_check, $new_url;
                    $seen_urls{$new_url} = 1;
                }
            }
        }
    }

    $iter_end = $buffer->get_end_iter;
    $buffer->insert($iter_end, "Checking the following potential URLs:\n" . join("\n", @urls_to_check) . "\n\n---\n\n");
    $popup->show_all;
    
    # --- Asynchronously check each URL ---
    my $total_urls = scalar(@urls_to_check);
    my $completed_count = 0;
    my $found_count = 0;

    # The callback for the Wayback search phase
    my $wayback_callback = sub {
        my ($result_string, $success) = @_;
        Glib::Idle->add(sub {
            my $end_iter = $buffer->get_end_iter;
            $buffer->insert($end_iter, $result_string);
            return FALSE;
        });
    };

    if ($total_urls == 0) {
        my $end_iter = $buffer->get_end_iter;
        $buffer->insert($end_iter, "Live search complete. Found 0 valid feed(s).\n\n---\n\nSearching Internet Archive...");
        perform_wayback_search($original_url, $wayback_callback);
        return;
    }

    for my $url_to_check (@urls_to_check) {
        AnyEvent::HTTP::http_get $url_to_check, timeout => 10, sub {
            my ($body, $hdr) = @_;
            $completed_count++;
            
            if ($hdr->{Status} =~ /^2/) {
                my $feed;
                eval { $feed = XML::Feed->parse(\$body) };
                if ($feed && scalar($feed->entries) > 0) { 
                    $found_count++;
                    my $end_iter = $buffer->get_end_iter;
                    my $title = $feed->title || "Untitled Feed";
                    $buffer->insert($end_iter, "VALID FEED FOUND:\n$url_to_check\nTitle: $title\nEntries: " . scalar($feed->entries) . "\n\n");
                }
            }

            if ($completed_count == $total_urls) {
                my $end_iter = $buffer->get_end_iter;
                $buffer->insert($end_iter, "---\nLive search complete. Found $found_count valid feed(s).\n\n---\n\nSearching Internet Archive...\n\n");
                # Chain the next asynchronous operation here
                perform_wayback_search($original_url, $wayback_callback);
            }
        };
    }
}


sub on_delete_local_file_clicked {
    my ($iter) = @_;
    my $cache_path = get_feed_cache_path($iter);
    
    if (-f $cache_path) {
        if (unlink $cache_path) {
            warn "Deleted local file: $cache_path\n";
            # Update the row's color to gray to show it's no longer cached.
            apply_background_color($iter);
        } else {
            warn "Failed to delete local file: $cache_path: $!\n";
        }
    } else {
        warn "Local file does not exist, cannot delete: $cache_path\n";
    }
}

sub on_tree_view_click {
    my ($widget, $event) = @_;

    # --- Handle Right-Click for Context Menu ---
    if ($event->button == 3) {
        my ($path) = $widget->get_path_at_pos($event->x, $event->y);
        my $menu = Gtk2::Menu->new;

        if ($path) { # Click was on an item
            my $iter = $tree_store->get_iter($path);
            my $is_folder = $tree_store->get($iter, 2);

            # --- Common Items for Top of Both Menus ---
            my $update_item = Gtk2::MenuItem->new("Update");
            $update_item->signal_connect(activate => sub { on_update_clicked($iter) });
            $menu->append($update_item);
            
            my $mark_read_item = Gtk2::MenuItem->new("Mark Read");
            $mark_read_item->signal_connect(activate => sub { on_mark_read_clicked($iter) });
            $menu->append($mark_read_item);

            # --- Menu for Folders/Directories ---
            if ($is_folder) {
                my $change_name_item = Gtk2::MenuItem->new("Change Name");
                $change_name_item->signal_connect(activate => sub { on_change_name_clicked($iter) });
                $menu->append($change_name_item);
                
                my $add_item = Gtk2::MenuItem->new("Add Feed");
                $add_item->signal_connect(activate => sub { add_feed_to_folder($iter) });
                $menu->append($add_item);
                
                # --- MODIFICATION: Add "Add Folder" to folder context menu ---
                my $add_folder_item = Gtk2::MenuItem->new("Add Folder");
                $add_folder_item->signal_connect(activate => sub { add_folder_to_folder($iter) });
                $menu->append($add_folder_item);
                
                my $export_item = Gtk2::MenuItem->new("Export OPML");
                $export_item->signal_connect(activate => sub { export_folder_as_opml($iter) });
                $menu->append($export_item);
                
                my $delete_folder_item = Gtk2::MenuItem->new("Delete Folder");
                $delete_folder_item->signal_connect(activate => sub { remove_folder($iter) });
                $menu->append($delete_folder_item);

            # --- Menu for Individual Feeds ---
            } else {
                $menu->append(Gtk2::SeparatorMenuItem->new);

                my $change_name_item = Gtk2::MenuItem->new("Change Name");
                $change_name_item->signal_connect(activate => sub { on_change_name_clicked($iter) });
                $menu->append($change_name_item);
                
                my $edit_item = Gtk2::MenuItem->new("Edit URL");
                $edit_item->signal_connect(activate => sub { edit_feed_url($iter) });
                $menu->append($edit_item);

                my $cache_path = get_feed_cache_path($iter);
                my $view_source_item = Gtk2::MenuItem->new("View Source");
                $view_source_item->signal_connect(activate => sub { view_feed_source($iter) });
                $view_source_item->set_sensitive(-f $cache_path);
                $menu->append($view_source_item);
                
                my $test_item = Gtk2::MenuItem->new("Run Test");
                $test_item->signal_connect(activate => sub { run_feed_test($iter) });
                $menu->append($test_item);

                my $search_item = Gtk2::MenuItem->new("search4feeds");
                $search_item->signal_connect(activate => sub { on_search_for_feeds_clicked($iter) });
                $menu->append($search_item);
                
                my $del_local_file_item = Gtk2::MenuItem->new("del local file");
                $del_local_file_item->signal_connect(activate => sub { on_delete_local_file_clicked($iter) });
                $del_local_file_item->set_sensitive(-f $cache_path);
                $menu->append($del_local_file_item);

                my $remove_item = Gtk2::MenuItem->new("Remove");
                $remove_item->signal_connect(activate => sub { remove_feed($iter) });
                $menu->append($remove_item);

                if (-f $cache_path) {
                    $menu->append(Gtk2::SeparatorMenuItem->new);
                    my $mtime = (stat($cache_path))[9];
                    my $date_str = strftime "%Y-%m-%d %H:%M:%S", localtime($mtime);
                    my $date_item = Gtk2::MenuItem->new("Cached: $date_str");
                    $date_item->set_sensitive(FALSE);
                    $menu->append($date_item);
                }
            }
        } else { # Click was on whitespace
            my $add_folder_item = Gtk2::MenuItem->new("Add Folder");
            $add_folder_item->signal_connect(activate => \&add_folder_to_root);
            $menu->append($add_folder_item);
            my $add_feed_item = Gtk2::MenuItem->new("Add Feed");
            $add_feed_item->signal_connect(activate => \&add_feed_to_root);
            $menu->append($add_feed_item);
        }
        
        $menu->show_all;
        $menu->popup(undef, undef, undef, undef, $event->button, $event->time);
        return TRUE;
    }
    
    # --- Handle Left-Click ---
    if ($event->button == 1) {
        my ($path) = $widget->get_path_at_pos($event->x, $event->y);
        return FALSE unless $path;
        my $iter = $tree_store->get_iter($path);
        return FALSE unless $iter;

        # SCROLL-TOP-FIX: If the user manually clicks a folder, schedule a scroll-to-top action.
        my $is_folder = $tree_store->get($iter, 2);
        if ($is_folder) {
            # We use a short timeout to ensure this runs *after* the list is populated
            # by the feed_selection_changed signal, preventing a race condition.
            Glib::Timeout->add(10, sub {
                my $adj = $scrolled_win_top_right->get_vadjustment;
                $adj->set_value(0) if $adj;
                return FALSE; # Timer runs only once.
            });
        }
        
        # This is the original re-selection logic. It's still useful for single feeds.
        my $selection = $widget->get_selection;
        my ($model, $selected_iter) = $selection->get_selected;

        if ($selected_iter and $model->get_string_from_iter($selected_iter) eq $model->get_string_from_iter($iter)) {
            if ($is_folder) {
                my $cell_area = $widget->get_cell_area($path, $widget->get_column(0));
                if ($event->x >= $cell_area->x) {
                    feed_selection_changed($selection);
                    return TRUE;
                }
            }
        }
    }

    return FALSE;
}

sub find_feed_iter_by_name {
    my ($target_name) = @_;
    my $iter = $tree_store->get_iter_first;
    while ($iter) {
        my $found_iter = find_feed_iter_by_name_recursive($iter, $target_name);
        return $found_iter if $found_iter;
        $iter = $tree_store->iter_next($iter);
    }
    return undef;
}

sub find_feed_iter_by_name_recursive {
    my ($iter, $target_name) = @_;
    
    my ($is_folder) = $tree_store->get($iter, 2);
    if ($is_folder) {
        my $child_iter = $tree_store->iter_children($iter);
        while ($child_iter) {
            my $found_iter = find_feed_iter_by_name_recursive($child_iter, $target_name);
            return $found_iter if $found_iter;
            $child_iter = $tree_store->iter_next($child_iter);
        }
    } else {
        # It's a feed, let's check its name.
        my ($current_name) = $tree_store->get($iter, 0);
        # Get the base name, without any unread count.
        $current_name =~ s/\s*\(\d+\)$//;
        
        if ($current_name eq $target_name) {
            return $iter->copy; # Return a copy as the original iter will be invalidated.
        }
    }
    
    return undef;
}

sub on_change_name_clicked {
    my ($iter) = @_;

    # 1. Get current data and determine the base name (without the unread count)
    my ($current_label, $feed_id) = $tree_store->get($iter, 0, 4);
    return unless defined $current_label;

    my $base_name = $current_label;
    my $count_suffix = '';
    # --- FIX: Capture the preceding space within the suffix itself ---
    if ($base_name =~ s/(\s*\(\d+\))$//) {
        $count_suffix = $1; # Now captures " (343)" instead of just "(343)"
    }

    # 2. Create and show a dialog to get the new name from the user
    my $dialog = Gtk2::Dialog->new("Change Name", $window, 'modal',
                                   'gtk-ok'     => 'ok',
                                   'gtk-cancel' => 'cancel');
    my $entry = Gtk2::Entry->new;
    $entry->set_text($base_name);
    $entry->set_activates_default(TRUE);
    $dialog->vbox->pack_start($entry, TRUE, TRUE, 5);
    $dialog->set_default_response('ok');
    $dialog->show_all;

    if ($dialog->run eq 'ok') {
        my $new_base_name = $entry->get_text;

        # 3. Validate the user's input
        $new_base_name =~ s/^\s+|\s+$//g; # Trim whitespace
        if ($new_base_name eq '' or $new_base_name eq $base_name) {
            $dialog->destroy;
            return; # Do nothing if the name is empty or unchanged
        }

        # 4. Begin the coordinated rename process
        
        # --- Step A: Get the old cache path BEFORE changing the name in the UI ---
        my $old_cache_path = get_feed_cache_path($iter);

        # --- Step B: Update the name in the database ---
        my $sth = $dbh->prepare('UPDATE feeds SET name = ? WHERE id = ?');
        $sth->execute($new_base_name, $feed_id);
        $sth->finish;

        # --- Step C: Update the name in the UI, preserving the unread count ---
        my $new_label = $new_base_name . $count_suffix;
        $tree_store->set($iter, 0, $new_label);

        # --- Step D: Rename the corresponding cache file or directory on the filesystem ---
        my $new_cache_path = get_feed_cache_path($iter);

        if (-e $old_cache_path) {
            # Perl's built-in rename works for both files and directories
            unless (rename $old_cache_path, $new_cache_path) {
                warn "Failed to rename cache from '$old_cache_path' to '$new_cache_path': $!";
            } else {
                warn "Renamed cache from '$old_cache_path' to '$new_cache_path'";
            }
        }
    }
    $dialog->destroy;
}

# --- MODIFICATION: Refactored folder creation logic ---
sub add_folder_to_folder {
    my ($parent_iter) = @_; # This can be undef for root folders
    my $dialog = Gtk2::Dialog->new("Add New Folder", $window, 'modal',
                                   'gtk-ok'     => 'ok',
                                   'gtk-cancel' => 'cancel');
    my $entry = Gtk2::Entry->new;
    $entry->set_activates_default(TRUE);
    $dialog->vbox->pack_start($entry, TRUE, TRUE, 5);
    $dialog->set_default_response('ok');
    $dialog->show_all;

    if ($dialog->run eq 'ok') {
        my $name_text = $entry->get_text;
        if ($name_text) {
            my $parent_id = defined $parent_iter ? $tree_store->get($parent_iter, 4) : undef;

            my $sth = $dbh->prepare('INSERT INTO feeds (parent_id, name, url, is_folder) VALUES (?, ?, ?, ?)');
            $sth->execute($parent_id, $name_text, '', 1);
            my $new_id = $dbh->last_insert_id("", "", "feeds", "id");
            $sth->finish;

            my $new_iter = $tree_store->append($parent_iter);
            $tree_store->set($new_iter, 0, $name_text, 1, '', 2, TRUE, 4, $new_id, 5, 400);
        }
    }
    $dialog->destroy;
}

sub add_folder_to_root {
    add_folder_to_folder(undef);
}

# FOLDER-MGMT-MOD: New function to handle deleting a folder.
sub remove_folder {
    my ($iter) = @_;
    my $name = $tree_store->get($iter, 0);
    
    my $dialog = Gtk2::MessageDialog->new($window, ['modal', 'destroy-with-parent'], 'question',
        'ok-cancel', "Are you sure you want to delete the folder \"$name\" and all of its contents? This cannot be undone.");
    my $response = $dialog->run;
    $dialog->destroy;
    return if $response ne 'ok';

    # Now, delete from DB and UI.
    my $feed_id = $tree_store->get($iter, 4);
    my $sth = $dbh->prepare('DELETE FROM feeds WHERE id = ?');
    $sth->execute($feed_id);
    $sth->finish;

    $tree_store->remove($iter);
}

sub add_feed_to_folder {
    my ($parent_iter) = @_;
    my $dialog = Gtk2::Dialog->new("Add New Feed", $window, 'modal',
                                   'gtk-ok'     => 'ok',
                                   'gtk-cancel' => 'cancel');
    my $table = Gtk2::Table->new(2, 2, FALSE);
    $dialog->vbox->pack_start($table, TRUE, TRUE, 5);
    my $name_label = Gtk2::Label->new_with_mnemonic("_Name (optional):");
    my $name_entry = Gtk2::Entry->new;
    $name_label->set_mnemonic_widget($name_entry);
    my $url_label = Gtk2::Label->new_with_mnemonic("_URL:");
    my $url_entry = Gtk2::Entry->new;
    $url_label->set_mnemonic_widget($url_entry);
    $url_entry->set_activates_default(TRUE);
    $table->attach_defaults($name_label, 0, 1, 0, 1);
    $table->attach_defaults($name_entry, 1, 2, 0, 1);
    $table->attach_defaults($url_label, 0, 1, 1, 2);
    $table->attach_defaults($url_entry, 1, 2, 1, 2);
    $dialog->set_default_response('ok');
    $dialog->show_all;
    if ($dialog->run eq 'ok') {
        my $name_text = $name_entry->get_text;
        my $url_text = $url_entry->get_text;
        if ($url_text) {
            my $new_name = ($name_text ne '') ? $name_text : $url_text;
            
            # DB-MOD: Add feed to database
            my $parent_id = defined $parent_iter ? $tree_store->get($parent_iter, 4) : undef;
            my $sth = $dbh->prepare('INSERT INTO feeds (parent_id, name, url, is_folder) VALUES (?, ?, ?, ?)');
            $sth->execute($parent_id, $new_name, $url_text, 0);
            my $new_id = $dbh->last_insert_id("","","feeds","id");
            $sth->finish;

            # DB-MOD: Add feed to UI TreeStore with new ID.
            my $new_iter = $tree_store->append($parent_iter);
            # BOLD-FIX: Use integer constant for weight.
            $tree_store->set($new_iter, 0, $new_name, 1, $url_text, 2, FALSE, 4, $new_id, 5, 400);
            
            # REFACTOR-MOD: Use the new unified fetcher to get the title.
            if ($name_text eq '') {
                fetch_and_parse_feed({ url => $url_text }, sub {
                    my ($success, $content, $feed_obj) = @_;
                    if ($success and $feed_obj) {
                        if (my $title = $feed_obj->title) {
                            $tree_store->set($new_iter, 0, $title);
                            my $update_sth = $dbh->prepare('UPDATE feeds SET name = ? WHERE id = ?');
                            $update_sth->execute($title, $new_id);
                            $update_sth->finish;
                        }
                    }
                });
            }
            update_all_row_colors();
        }
    }
    $dialog->destroy;
}

sub view_feed_source {
    my ($iter) = @_;
    my $name = $tree_store->get($iter, 0);
    my $source_window = Gtk2::Window->new('toplevel');
    $source_window->set_title("View Source: $name");
    $source_window->set_default_size(700, 500);
    $source_window->set_transient_for($window);
    $source_window->set_destroy_with_parent(TRUE);
    my $scrolled_win = Gtk2::ScrolledWindow->new;
    $scrolled_win->set_policy('automatic', 'automatic');
    $source_window->add($scrolled_win);
    my $source_buffer = Gtk2::TextBuffer->new;
    my $source_view = Gtk2::TextView->new_with_buffer($source_buffer);
    $source_view->set_editable(FALSE);
    $source_view->set_wrap_mode('word');
    $scrolled_win->add($source_view);
    my $cache_path = get_feed_cache_path($iter);
    my $content = "Could not read cache file: $cache_path";
    if (-f $cache_path) {
        open my $fh, '<:utf8', $cache_path;
        if ($fh) {
            $content = do { local $/; <$fh> };
            close $fh;
        }
    }
    $source_buffer->set_text($content);
    $source_window->show_all;
}

sub on_update_clicked {
    my ($iter) = @_;
    return if $is_updating;
    
    # Force the update by deleting the cache files first.
    my @feeds_to_force;
    find_all_feeds_under_iter($iter, \@feeds_to_force);
    for my $feed_iter (@feeds_to_force) {
        my $cache_path = get_feed_cache_path($feed_iter);
        unlink $cache_path if -f $cache_path;
        my $url = $tree_store->get($feed_iter, 1);
        delete $feed_data{$url} if $url;
    }

    # Now, simply call the single-update starter function. It will handle
    # creating the job, collecting the queue, and starting the download loop correctly.
    start_update_process($iter); 
    
}

sub remove_feed {
    my ($iter) = @_;
    my $feed_id = $tree_store->get($iter, 4);
    my $url = $tree_store->get($iter, 1);

    delete $feed_data{$url} if $url;
    # BROKEN-FEEDS-MOD: Update the in-memory cache when a feed is removed.
    delete $broken_feed_status{$feed_id} if $feed_id;
    
    my $cache_path = get_feed_cache_path($iter);
    unlink $cache_path if -f $cache_path;

    # DB-MOD: Remove from DB, which will cascade delete entries.
    my $sth = $dbh->prepare('DELETE FROM feeds WHERE id = ?');
    $sth->execute($feed_id);
    $sth->finish;

    # DB-MOD: Remove from UI.
    $tree_store->remove($iter);
}

sub edit_feed_url {
    my ($iter) = @_;
    my $old_url = $tree_store->get($iter, 1);
    my $dialog = Gtk2::Dialog->new("Edit Feed URL", $window, 'modal',
                                   'gtk-ok' => 'ok', 'gtk-cancel' => 'cancel');
    my $entry = Gtk2::Entry->new;
    $entry->set_text($old_url);
    $dialog->get_content_area->pack_start($entry, TRUE, TRUE, 5);
    $dialog->show_all;
    if ($dialog->run eq 'ok') {
        my $new_url = $entry->get_text;
        if ($new_url ne $old_url) {
            my $feed_id = $tree_store->get($iter, 4);

            # --- FIX: Purge all old data, both on disk and in the DB ---
            
            # 1. Delete the old cache file from disk. This is the crucial step
            #    to force a re-download from the new URL.
            my $cache_path = get_feed_cache_path($iter);
            unlink $cache_path if -f $cache_path;

            # 2. Delete all existing entries from the database for this feed.
            my $del_sth = $dbh->prepare('DELETE FROM entries WHERE feed_id = ?');
            $del_sth->execute($feed_id);
            $del_sth->finish;
            
            # 3. Reset the UI count display.
            update_tree_view_labels();
            # --------------------------------------------------------

            # Update the UI and the feeds table with the new URL.
            $tree_store->set($iter, 1, $new_url);
            my $update_sth = $dbh->prepare('UPDATE feeds SET url = ? WHERE id = ?');
            $update_sth->execute($new_url, $feed_id);
            $update_sth->finish;

            delete $feed_data{$old_url};
            # The background color will now correctly be set to gray because the cache file is gone.
            update_all_row_colors();
        }
    }
    $dialog->destroy;
}

sub import_entries_from_test {
    my ($feed_id, $feed_obj) = @_;
    return unless ($feed_id and $feed_obj and $feed_obj->isa('XML::Feed'));

    warn "\n--- import_entries_from_test called for feed ID $feed_id ---";

    # --- Step 1: Gather all unique links from the incoming feed data ---
    my @links_to_import;
    for my $entry ($feed_obj->entries) {
        my $link = $entry->link || $entry->id;
        push @links_to_import, $link if defined $link;
    }

    if (!@links_to_import) {
        warn "  > No valid links found in the feed to import. Aborting.";
        return;
    }

    # --- Step 2: Purge ALL old data that conflicts with the incoming links, regardless of feed_id. ---
    # This is the crucial fix to prevent UNIQUE constraint violations from data contamination.
    warn "  > Deleting any existing entries that have the same links...";
    my $placeholders = join(',', ('?') x @links_to_import);
    my $del_sth = $dbh->prepare("DELETE FROM entries WHERE link IN ($placeholders)");
    my $deleted_rows = $del_sth->execute(@links_to_import);
    $del_sth->finish;
    warn "    -> $deleted_rows conflicting old rows deleted from database.";
    
    # --- Step 3: Prepare to insert the new entries. ---
    my $insert_sth = $dbh->prepare(q{
        INSERT INTO entries (feed_id, title, link, published_date, content)
        VALUES (?, ?, ?, ?, ?)
    });

    # --- Step 4: Loop through the good entries and insert them. ---
    my $imported_count = 0;
    for my $entry ($feed_obj->entries) {
        my $title = $entry->title || "Untitled";
        my $link = $entry->link || $entry->id;
        
        next unless defined $link;

        my $full_content = format_post_content_string($entry, undef);
        my $date_obj = $entry->issued || $entry->modified;

        eval {
            $insert_sth->execute(
                $feed_id,
                $title,
                $link,
                $date_obj ? $date_obj->epoch : 0,
                $full_content
            );
            $imported_count++;
        };
        if ($@) {
            # This should no longer happen, but is kept for safety.
            warn "    -> FAILED to insert '${title}': $@";
        }
    }
    $insert_sth->finish;
    
    # --- Step 5: Update the UI ---
    update_tree_view_labels();
    warn "--- Finished import: $imported_count entries successfully imported for feed ID $feed_id. ---\n";
    
    # Force the main view to refresh itself to show the newly imported data.
    feed_selection_changed($tree_view->get_selection);
}

sub run_feed_test {
    my ($iter) = @_;
    my $url = $tree_store->get($iter, 1);
    my $feed_id = $tree_store->get($iter, 4);

    my $test_window = Gtk2::Window->new('toplevel');
    $test_window->set_title("Feed Test: $url");
    $test_window->set_default_size(600, 500);

    my $vbox = Gtk2::VBox->new(FALSE, 5);
    $test_window->add($vbox);
    
    my $scrolled_win = Gtk2::ScrolledWindow->new;
    $scrolled_win->set_policy('automatic', 'automatic');
    my $test_buffer = Gtk2::TextBuffer->new;
    my $test_view = Gtk2::TextView->new_with_buffer($test_buffer);
    $test_view->set_editable(FALSE);
    $test_view->set_wrap_mode('word');
    $scrolled_win->add($test_view);

    my $import_button = Gtk2::Button->new("Import These Entries");
    $import_button->set_sensitive(FALSE);

    $vbox->pack_start($scrolled_win, TRUE, TRUE, 0);
    $vbox->pack_start($import_button, FALSE, FALSE, 0);

    $test_window->show_all;
    
    $test_buffer->set_text("Fetching: $url...");
    
    my $successful_feed_obj;

    generate_feed_test_debug_string($url, sub {
        my ($debug_output, $feed_obj) = @_;
        $test_buffer->set_text($debug_output);

        # --- DEBUGGING PRINTS ---
        warn "--- run_feed_test callback received ---";
        warn "  Is \$feed_obj defined? " . (defined $feed_obj ? "Yes" : "No");
        if (defined $feed_obj) {
            warn "  ref(\$feed_obj): " . ref($feed_obj);
            warn "  \$feed_obj->isa('XML::Feed'): " . ($feed_obj->isa('XML::Feed') ? "Yes" : "No");
            warn "  Entry count: " . scalar($feed_obj->entries);
        }
        # ------------------------

        # FIX: Use ->isa() to correctly check for any XML::Feed subclass.
        if ($feed_obj and $feed_obj->isa('XML::Feed') and scalar($feed_obj->entries) > 0) {
            warn "  SUCCESS: Condition met. Enabling import button.";
            $successful_feed_obj = $feed_obj;
            $import_button->set_sensitive(TRUE);
        } else {
            warn "  FAILURE: Condition not met. Button will remain disabled.";
        }
    });

    $import_button->signal_connect(clicked => sub {
        if ($successful_feed_obj) {
            import_entries_from_test($feed_id, $successful_feed_obj);
            $test_window->destroy();
        }
    });
}

sub generate_feed_test_debug_string {
    my ($url, $callback) = @_;
    my $debug_output = "Running test for: $url\n\n";
    my %headers = (
        'User-Agent' => 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:125.0) Gecko/20100101 Firefox/125.0',
        'Accept'     => 'application/xml,application/rss+xml,application/atom+xml,*/*',
    );
    
    # Show what we're sending
    $debug_output .= "--- REQUEST HEADERS BEING SENT ---\n";
    for my $key (sort keys %headers) {
        $debug_output .= "$key: $headers{$key}\n";
    }
    $debug_output .= "\n";
    
    AnyEvent::HTTP::http_get $url, timeout => 5, headers => \%headers, tls_ctx => { verify => 0, verify_hostname => 0 }, sub {
        my ($body, $hdr) = @_;
        my $parsed_feed; # Variable to hold the result
        
        # Always show the actual sent headers if available, otherwise show what we intended to send
        $debug_output .= "--- ACTUAL REQUEST HEADERS SENT ---\n";
        if ($hdr->{sent_headers}) {
            $debug_output .= $hdr->{sent_headers} . "\n";
        } else {
            $debug_output .= "Headers not captured by AnyEvent::HTTP, but we sent:\n";
            for my $key (sort keys %headers) {
                $debug_output .= "$key: $headers{$key}\n";
            }
        }
        $debug_output .= "\n";
        
        $debug_output .= "--- RESPONSE ---\n" . "Status: " . $hdr->{Status} . " " . $hdr->{Reason} . "\n\n";
        
        # Always show response headers
        $debug_output .= "--- RESPONSE HEADERS RECEIVED ---\n";
        if ($hdr->{raw_headers}) {
            $debug_output .= $hdr->{raw_headers} . "\n";
        } elsif ($hdr->{HTTPVersion} || $hdr->{'content-type'} || $hdr->{'content-length'} || $hdr->{server}) {
            # Reconstruct from individual header fields that AnyEvent::HTTP does provide
            $debug_output .= "HTTP/" . ($hdr->{HTTPVersion} || "1.1") . " " . $hdr->{Status} . " " . $hdr->{Reason} . "\n";
            for my $key (sort keys %$hdr) {
                next if $key =~ /^(Status|Reason|HTTPVersion|URL|sent_headers|raw_headers)$/;
                $debug_output .= "$key: $hdr->{$key}\n" if defined $hdr->{$key};
            }
        } else {
            $debug_output .= "No response headers captured (connection may have failed at TCP level)\n";
        }
        $debug_output .= "\n";
        
        $debug_output .= "--- RESPONSE CONTENT (first 1024 bytes) ---\n";
        if ($hdr->{Status} =~ /^2/) {
            $debug_output .= substr($body, 0, 1024) . "...\n\n";
            $debug_output .= "--- PARSE TEST ---\n";
            my $feed = XML::Feed->parse(\$body);
            if ($feed) {
                $parsed_feed = $feed; # Store successful parse
                $debug_output .= "Successfully parsed feed.\n";
                $debug_output .= "Title: " . ($feed->title || 'N/A') . "\n";
                my $entry_count = scalar($feed->entries);
                $debug_output .= "Entries found: " . $entry_count . "\n";

                # FIX: If entries were found, list their titles for debugging.
                if ($entry_count > 0) {
                    $debug_output .= "\n--- FOUND ENTRIES ---\n";
                    for my $entry ($feed->entries) {
                        $debug_output .= "- " . ($entry->title || "Untitled Entry") . "\n";
                    }
                }
            } else {
                $debug_output .= "Failed to parse feed.\n" . "Error: " . XML::Feed->errstr . "\n";
            }
        } else {
            if ($body) {
                $debug_output .= substr($body, 0, 1024) . "...\n";
            } else {
                $debug_output .= "No response content received.\n";
            }
        }
        # Pass back both the text and the feed object (or undef on failure)
        $callback->($debug_output, $parsed_feed) if $callback;
    };
}

sub import_opml_file {
    my $dialog = Gtk2::FileChooserDialog->new("Import OPML File", $window, 'open',
                                            'gtk-cancel' => 'cancel', 'gtk-open' => 'ok');
    my $filter = Gtk2::FileFilter->new;
    $filter->set_name("OPML files");
    $filter->add_pattern("*.opml");
    $dialog->add_filter($filter);
    if ($dialog->run eq 'ok') {
        my $filename = $dialog->get_filename;
        parse_opml($filename);
    }
    $dialog->destroy;
}

sub parse_opml {
    my ($filename) = @_;
    my $parser = XML::LibXML->new;
    my $doc;
    eval { $doc = $parser->parse_file($filename); };
    if ($@) { return; }
    
    # DB-MOD: Clear existing data for a fresh import.
    $tree_store->clear;
    $dbh->do('DELETE FROM feeds');
    $dbh->do('DELETE FROM entries');

    my ($body) = $doc->findnodes('/opml/body');
    return unless $body;

    # DB-MOD: Process nodes and insert into DB in a transaction.
    $dbh->begin_work;
    eval {
        # DIR-CREATE-FIX: Pass the root directory for feed caches.
        process_outline_nodes($body, undef, $feeds_dir, undef);
        $dbh->commit;
    };
    if ($@) {
        warn "OPML import failed: $@. Rolling back.";
        $dbh->rollback;
    }

    update_all_row_colors();
}


sub process_outline_nodes {
    my ($parent_node, $parent_iter, $parent_path, $parent_id) = @_;
    for my $outline ($parent_node->findnodes('./outline')) {
        my $text = $outline->getAttribute('text') || $outline->getAttribute('title') || 'Untitled';
        my $xml_url = $outline->getAttribute('xmlUrl');
        
        my $is_folder = !$xml_url;

        # DB-MOD: Insert into database
        my $sth = $dbh->prepare('INSERT INTO feeds (parent_id, name, url, is_folder) VALUES (?, ?, ?, ?)');
        $sth->execute($parent_id, $text, $xml_url, $is_folder);
        my $new_id = $dbh->last_insert_id("","","feeds","id");
        $sth->finish;

        # DB-MOD: Insert into UI TreeStore
        my $iter = $tree_store->append($parent_iter);
        # BOLD-FIX: Use integer constant for weight.
        $tree_store->set($iter, 0, $text, 1, $xml_url, 2, $is_folder, 4, $new_id, 5, 400);
        
        if ($is_folder) {
            # DIR-CREATE-FIX: The make_path call has been removed from here.
            # Directories will now be created just-in-time before writing a cache file.
            # We still recurse to process the rest of the OPML file.
            my $sanitized_name = sanitize_filename($text);
            my $current_path = File::Spec->catdir($parent_path, $sanitized_name);
            process_outline_nodes($outline, $iter, $current_path, $new_id);
        }
    }
}


sub refresh_all_feeds {
    return if $is_updating;
    
    # --- BUGFIX: Determine the correct item to refresh. ---
    # 1. First, try to get the currently selected item from the tree view.
    my (undef, $selected_iter) = $tree_view->get_selection->get_selected;
    
    my $iter_to_refresh;
    if ($selected_iter) {
        # If an item is selected, that's what we'll refresh.
        $iter_to_refresh = $selected_iter;
    } else {
        # If nothing is selected, fall back to the original behavior: refresh all from the root.
        $iter_to_refresh = $tree_store->get_iter_first;
    }

    # Do nothing if the tree is completely empty and there's nothing to refresh.
    return unless $iter_to_refresh;

    # Force the update by deleting cache files for the chosen scope (selection or all).
    my @feeds_to_force;
    find_all_feeds_under_iter($iter_to_refresh, \@feeds_to_force);
    for my $feed_iter (@feeds_to_force) {
        my $cache_path = get_feed_cache_path($feed_iter);
        unlink $cache_path if -f $cache_path;
    }
    %feed_data = ();

    # Now, call the single-update starter function on the CORRECT item.
    start_update_process($iter_to_refresh);
}

sub feed_selection_changed {
    my ($selection) = @_;

    my ($model, $iter) = $selection->get_selected;
    return unless $iter;

    my $is_folder = $model->get($iter, 2);

    # PAGINATION-MOD: Disable the "Next Page" button during normal browsing.
    $next_page_button->set_sensitive(FALSE);

    if ($is_folder) {
        # It's a folder, just display its contents.
        $scrolled_win_single_view->hide;
        $vpaned_right->show;
        display_folder_contents($iter);
    } else {
        # It's a single feed. Mark all its entries as read.
        my $feed_id = $model->get($iter, 4);
        if (defined $feed_id) {
            my $sth = $dbh->prepare('UPDATE entries SET is_new = 0 WHERE feed_id = ?');
            $sth->execute($feed_id);
            $sth->finish;
            
            # After marking as read in the DB, update the UI counts immediately.
            update_tree_view_labels();
        }
        
        # Now, display the single feed's contents.
        $vpaned_right->hide;
        $scrolled_win_single_view->show;
        display_feed_contents($iter);
    }
}

sub display_folder_contents {
    my ($folder_iter) = @_;
    
    # REFACTOR-FIX: This function's ONLY job is to display the current DB state.
    display_folder_feeds($folder_iter);

    # COL-WIDTH-MOD: This logic is now handled by initial_resize_panes, so it's removed from here
    # to prevent overriding the user's settings on every selection change.
}

sub display_feed_contents {
    my ($iter) = @_;
    my $feed_id = $tree_store->get($iter, 4);
    return unless $feed_id;

    # Always clear the buffer first.
    $text_buffer->set_text('');

    my $sth = $dbh->prepare('SELECT content FROM entries WHERE feed_id = ? ORDER BY published_date DESC');
    $sth->execute($feed_id);

    my @rows = @{$sth->fetchall_arrayref};
    $sth->finish;

    if (@rows) {
        my $feed_name = $tree_store->get($iter, 0);
        my $end_iter = $text_buffer->get_end_iter;
        $text_buffer->insert($end_iter, "Feed: " . ($feed_name || 'No Title') . "\n\n");

        for my $row (@rows) {
            populate_text_view_with_links($text_buffer, $row->[0]);
            $end_iter = $text_buffer->get_end_iter;
            $text_buffer->insert($end_iter, "\n");
        }
    } else {
        # If there are no entries in the DB, just say so. No complex fallbacks.
        $text_buffer->set_text("No entries found in the database for this feed.");
    }
}


# --- MODIFICATION: Updated to correctly preserve multi-selection without crashing ---
sub display_folder_feeds {
    my ($folder_iter) = @_;
    return unless $folder_iter;

    $last_clicked_headline_path = undef;
    
    my $selection = $headline_view->get_selection;
    my $model = $headline_view->get_model; # Get the model correctly
    my @paths = $selection->get_selected_rows;
    my %selected_urls;
    for my $path (@paths) {
        my $iter = $model->get_iter($path);
        if ($iter) {
            my $url = $model->get($iter, 4);
            $selected_urls{$url} = 1 if $url;
        }
    }

    $headline_store->clear;
    if (!%selected_urls) {
        $folder_text_view->get_buffer->set_text('');
    }

    my $folder_id = $tree_store->get($folder_iter, 4);
    my @feed_ids_to_display = get_all_child_feed_ids($folder_id);

    if (@feed_ids_to_display) {
        my $placeholders = join(',', ('?') x @feed_ids_to_display);
        my $sql = qq{
            SELECT f.name, e.title, e.link, e.published_date, e.content, e.date_added, e.is_new
            FROM entries e
            JOIN feeds f ON e.feed_id = f.id
            WHERE e.feed_id IN ($placeholders)
        };
        
        if ($hide_unparseable_check->get_active()) {
            $sql .= " AND e.title != 'Unparseable Feed Content'";
        }
        
        $sql .= " ORDER BY e.date_added DESC LIMIT $query_limit";
        
        my $sth = $dbh->prepare($sql);
        $sth->execute(@feed_ids_to_display);

        while (my $row = $sth->fetchrow_arrayref) {
            my ($feed_name, $headline, $url, $sortable_date, $full_content, $date_added, $is_new) = @$row;
            
            $feed_name ||= 'Untitled Feed';
            $headline ||= 'No Title';
            $headline =~ s/[\r\n]+/ /g; $headline =~ s/^\s+|\s+$//g;
            
            my $date_str = $sortable_date ? strftime('%Y-%m-%d %H:%M', localtime($sortable_date)) : '';
            my $date_added_str = $date_added ? strftime('%Y-%m-%d %H:%M', localtime($date_added)) : '';
            my $weight = $is_new ? 700 : 400;

            my $iter = $headline_store->append;
            $headline_store->set($iter,
                0, $feed_name, 1, $headline, 2, $date_str, 3, $full_content, 4, $url,
                5, $sortable_date, 6, $date_added_str, 7, $date_added, 8, $weight, 9, $is_new
            );
        }
        $sth->finish;
    }

    if (%selected_urls) {
        my $iter = $headline_store->get_iter_first;
        while ($iter) {
            my $current_url = $headline_store->get($iter, 4);
            if ($current_url and exists $selected_urls{$current_url}) {
                $selection->select_iter($iter);
            }
            $iter = $headline_store->iter_next($iter);
        }
    }
    
    $headline_store->set_sort_column_id(7, 'descending');
    $headline_view->queue_draw();
}

# DB-MOD: New helper function to recursively find all feed IDs under a folder ID.
sub get_all_child_feed_ids {
    my ($parent_id) = @_;
    my @all_ids;
    
    my @queue = ();
    
    my $sth_check = $dbh->prepare('SELECT is_folder FROM feeds WHERE id = ?');
    $sth_check->execute($parent_id);
    my ($is_folder) = $sth_check->fetchrow_array;
    $sth_check->finish;

    if (defined $is_folder) {
        if ($is_folder) {
            push @queue, $parent_id;
        } else {
            push @all_ids, $parent_id;
            return @all_ids;
        }
    } else {
        return @all_ids;
    }

    my $sth_children = $dbh->prepare('SELECT id, is_folder FROM feeds WHERE parent_id = ?');
    
    while (my $current_id = shift @queue) {
        $sth_children->execute($current_id);
        while (my ($child_id, $child_is_folder) = $sth_children->fetchrow_array) {
            if ($child_is_folder) {
                push @queue, $child_id;
            } else {
                push @all_ids, $child_id;
            }
        }
    }
    
    $sth_children->finish;
    return @all_ids;
}

# FALLBACK-MOD: New subroutine to brute-force parse feed content with regex.
sub regex_fallback_parser {
    my ($content) = @_;
    my @entries;

    while ($content =~ m{<(item|entry)[\s>].*?</\1>}gis) {
        my $item_xml = $&;

        my ($title) = $item_xml =~ m{<title.*?>(?:<!\[CDATA\[)?(.*?)(?:\]\]>)?</title>}is;
        $title ||= 'No Title';

        my ($link_href) = $item_xml =~ m{<link[^>]*?href=['"]([^'"]+)['"]}is;
        my ($link_text) = $item_xml =~ m{<link.*?>(?:<!\[CDATA\[)?(.*?)(?:\]\]>)?</link>}is;
        my $link = $link_href || $link_text;
        next unless $link;

        my ($desc) = $item_xml =~ m{<(?:description|summary|content:encoded|content)(?:[\s>].*?)>(?:<!\[CDATA\[)?(.*?)(?:\]\]>)?</\1>}is;
        $desc ||= '';
        
        my ($date_str) = $item_xml =~ m{<(?:pubDate|published|updated|dc:date)>(.*?)</\1>}is;
        $date_str ||= '';

        for my $text ($title, $desc) {
            $text =~ s/<[^>]*>//g;
            $text =~ s/^\s+|\s+$//g;
        }
        $link =~ s/^\s+|\s+$//g;

        my $full_content = "Title: $title\nLink: $link\nDate: $date_str\nSummary: $desc";
        
        push @entries, {
            title   => $title,
            link    => $link,
            date    => 0,
            content => $full_content,
        };
    }
    
    return @entries;
}

sub find_all_feeds_under_iter {
    my ($iter, $feed_list_ref) = @_;
    my $is_folder = $tree_store->get($iter, 2);
    if ($is_folder) {
        my $child_iter = $tree_store->iter_children($iter);
        while ($child_iter) {
            find_all_feeds_under_iter($child_iter, $feed_list_ref);
            $child_iter = $tree_store->iter_next($child_iter);
        }
    } else {
        push @$feed_list_ref, $iter;
    }
}

sub format_post_content_string {
    my ($entry, $fallback_summaries) = @_;
    
    my $title = $entry->title || 'No Title';
    $title =~ s/[\r\n]+/ /g;
    $title =~ s/^\s+|\s+$//g;
    
    my $content = "Title: " . $title . "\n";
    
    my $link = $entry->link || '';
    if ($link) {
        $content .= "Link: $link\n";
    }
    my $date_obj = $entry->issued || $entry->modified;
    if ($date_obj) {
        $content .= "Date: " . $date_obj->strftime('%Y-%m-%d %H:%M:%S') . "\n";
    }
    my $summary_obj = $entry->summary;
    my $summary_text = '';
    if ($summary_obj) {
        $summary_text = $summary_obj->body // '';
    }
    if ($summary_text eq '' && $link ne '' && $fallback_summaries && exists $fallback_summaries->{$link}) {
        $summary_text = $fallback_summaries->{$link};
    }

    # HYPHEN-FIX: Clean up the summary text *before* further processing.
    # Replace the specific malformed byte sequence for a hyphen with a standard hyphen.
    # \xE2\x80\x9D is the byte sequence for a RIGHT DOUBLE QUOTATION MARK, often misused.
    # A common malformed hyphen is actually \xE2\x80\x93 (EN DASH). We'll catch both.
    $summary_text =~ s/\xE2\x80\x9D/-/g;
    $summary_text =~ s/\xE2\x80\x93/-/g;

    $summary_text =~ s/<[^>]*>//g; 
    $summary_text =~ s/\s+/ /g; 
    $summary_text =~ s/^\s+|\s+$//g;
    
    $content .= "Summary: $summary_text\n";
    return $content;
}

sub regexfallbackforparsingoutdescriptionsummary {
    my ($feed_content) = @_;
    my %summaries;
    while ($feed_content =~ m|<item>(.*?)</item>|gis) {
        my $item_xml = $1;
        my $link = '';
        my $description = '';
        if ($item_xml =~ m|<link>(?:<!\[CDATA\[)?(.*?)(?:\]\]>)?</link>|is) {
            $link = $1;
        }
        if ($item_xml =~ m|<description>(?:<!\[CDATA\[)?(.*?)(?:\]\]>)?</description>|is) {
            $description = $1;
        }
        if ($link ne '' && $description ne '') {
            $summaries{$link} = $description;
        }
    }
    return \%summaries;
}

sub on_headline_view_click {
    my ($widget, $event) = @_;
    
    # --- MODIFICATION: Handle Shift+Click for Range Selection ---
    if ($event->type eq 'button-press' and $event->button == 1) {
        my $selection = $widget->get_selection;
        my ($path) = $widget->get_path_at_pos($event->x, $event->y);
        return FALSE unless $path;

        my $state = $event->state;
        if ($state & 'shift-mask' and $last_clicked_headline_path) {
            $selection->unselect_all();
            $selection->select_range($last_clicked_headline_path, $path);
            return TRUE; # Consume the event
        } else {
            # This is a normal click, so set the anchor.
            $last_clicked_headline_path = $path;
            
            # --- And now perform the single-click actions ---
            my $model = $widget->get_model;
            my $iter = $model->get_iter($path);
            return FALSE unless $iter;

            # Check if the selected item is new.
            my ($is_new, $url) = $model->get($iter, 9, 4);
            if ($is_new) {
                # Mark as read in the database.
                my $sth = $dbh->prepare('UPDATE entries SET is_new = 0 WHERE link = ?');
                $sth->execute($url);
                $sth->finish;

                # Update the UI for immediate feedback.
                $model->set($iter, 8, 400, 9, FALSE); # Set weight to normal, is_new to false
                
                # Recalculate and update the counts in the left pane.
                update_tree_view_labels();
            }

            my $full_content = $model->get($iter, 3);
            my $buffer = $folder_text_view->get_buffer;
            $buffer->set_text('');
            populate_text_view_with_links($buffer, $full_content);
        }
    }

    # --- Handle Left-Click for Opening URL ---
    if ($event->type eq 'button-press' and $event->button == 1) {
        my ($path, $column) = $widget->get_path_at_pos($event->x, $event->y);
        return FALSE unless $path;
        if ($column == $headline_view->get_column(1)) { # Headline column
            my $iter = $headline_store->get_iter($path);
            my $url = $headline_store->get($iter, 4);
            system("xdg-open", $url) if $url;
            return TRUE;
        }
    }

    # --- MODIFICATION: New Right-Click Context Menu Logic ---
    if ($event->type eq 'button-press' and $event->button == 3) {
        my ($path, $column) = $widget->get_path_at_pos($event->x, $event->y);
        return FALSE unless $path;
        
        my $selection = $widget->get_selection;
        my $clicked_is_selected = $selection->path_is_selected($path);
        
        # If we didn't click on an already selected row in a multi-selection,
        # then just select the clicked row and treat it as a single selection.
        if (!$clicked_is_selected) {
             $selection->unselect_all();
             $selection->select_path($path);
        }
        
        my $count = $selection->count_selected_rows;
        my $menu = Gtk2::Menu->new;

        # --- Multi-select context menu ---
        if ($count > 1) {
            my $mark_read_item = Gtk2::MenuItem->new("Mark Read");
            $mark_read_item->signal_connect(activate => \&on_mark_headlines_read);
            $menu->append($mark_read_item);

            my $mark_new_item = Gtk2::MenuItem->new("Mark New");
            $mark_new_item->signal_connect(activate => \&on_mark_headlines_new);
            $menu->append($mark_new_item);

            my $delete_item = Gtk2::MenuItem->new("Delete");
            $delete_item->signal_connect(activate => \&on_delete_headline_entries);
            $menu->append($delete_item);
        } 
        # --- Single-select context menu ---
        else {
            # Original 'Show Feed' logic (only for the 'Feed' column)
            if ($column == $headline_view->get_column(0)) {
                my $headline_iter = $headline_store->get_iter($path);
                my ($feed_name) = $headline_store->get($headline_iter, 0);
                my $feed_tree_iter = find_feed_iter_by_name($feed_name);
                if ($feed_tree_iter) {
                    my $show_feed_item = Gtk2::MenuItem->new("Show Feed");
                    $show_feed_item->signal_connect(activate => sub {
                        my $feed_path = $tree_store->get_path($feed_tree_iter);
                        $tree_view->expand_to_path($feed_path);
                        $tree_view->get_selection->select_path($feed_path);
                        $tree_view->scroll_to_cell($feed_path, undef, TRUE, 0.5, 0.0);
                    });
                    $menu->append($show_feed_item);
                }
            }

            # Add 'Delete' for single-select as well
            my $delete_item = Gtk2::MenuItem->new("Delete");
            $delete_item->signal_connect(activate => \&on_delete_headline_entries);
            $menu->append($delete_item);
        }

        # Don't show an empty menu
        return FALSE unless scalar($menu->get_children);

        $menu->show_all;
        $menu->popup(undef, undef, undef, undef, $event->button, $event->time);
        return TRUE;
    }
    
    return FALSE; # Event not handled.
}

# --- MODIFICATION: New subroutines for multi-select actions ---
sub on_mark_headlines_read {
    my $selection = $headline_view->get_selection;
    my @paths = $selection->get_selected_rows;
    return unless @paths;
    my $model = $headline_view->get_model;
    
    my @links_to_update;
    my @iters_to_update;
    for my $path (@paths) {
        my $iter = $model->get_iter($path);
        next unless $iter;
        my ($link, $is_new) = $model->get($iter, 4, 9);
        if ($link and $is_new) {
            push @links_to_update, $link;
            push @iters_to_update, $iter->copy;
        }
    }
    return unless @links_to_update;

    my $placeholders = join(',', ('?') x @links_to_update);
    my $sth = $dbh->prepare("UPDATE entries SET is_new = 0 WHERE link IN ($placeholders)");
    $sth->execute(@links_to_update);
    $sth->finish;

    for my $iter (@iters_to_update) {
        $model->set($iter, 8, 400, 9, FALSE); # weight=normal, is_new=false
    }
    update_tree_view_labels();
}

sub on_mark_headlines_new {
    my $selection = $headline_view->get_selection;
    my @paths = $selection->get_selected_rows;
    return unless @paths;
    my $model = $headline_view->get_model;
    
    my @links_to_update;
    my @iters_to_update;
    for my $path (@paths) {
        my $iter = $model->get_iter($path);
        next unless $iter;
        my ($link, $is_new) = $model->get($iter, 4, 9);
        if ($link and !$is_new) { # Only update if it's currently read
            push @links_to_update, $link;
            push @iters_to_update, $iter->copy;
        }
    }
    return unless @links_to_update;

    my $placeholders = join(',', ('?') x @links_to_update);
    my $sth = $dbh->prepare("UPDATE entries SET is_new = 1 WHERE link IN ($placeholders)");
    $sth->execute(@links_to_update);
    $sth->finish;

    for my $iter (@iters_to_update) {
        $model->set($iter, 8, 700, 9, TRUE); # weight=bold, is_new=true
    }
    update_tree_view_labels();
}

sub on_delete_headline_entries {
    my $selection = $headline_view->get_selection;
    my @paths = $selection->get_selected_rows;
    return unless @paths;
    my $model = $headline_view->get_model;

    my @links_to_delete;
    my @row_refs;
    for my $path (@paths) {
        my $iter = $model->get_iter($path);
        next unless $iter;
        my ($link) = $model->get($iter, 4);
        push @links_to_delete, $link if $link;
        push @row_refs, Gtk2::TreeRowReference->new($model, $path);
    }
    return unless @links_to_delete;

    my $placeholders = join(',', ('?') x @links_to_delete);
    my $sth = $dbh->prepare("DELETE FROM entries WHERE link IN ($placeholders)");
    $sth->execute(@links_to_delete);
    $sth->finish;

    # Remove from UI model using stable references
    for my $ref (reverse @row_refs) {
        my $path = $ref->get_path;
        if ($path) {
            my $iter = $model->get_iter($path);
            $model->remove($iter) if $iter;
        }
    }
    update_tree_view_labels();
}

sub on_headline_selected {} # This is now intentionally blank. All logic moved to the click handler.

sub populate_text_view_with_links {
    # REFACTOR-MOD: This now accepts the target buffer as an argument.
    my ($buffer, $content) = @_;
    my $iter = $buffer->get_end_iter;
    my $url_to_open;
    if ($content =~ m/Link:\s*(https?:\S+)/) {
        $url_to_open = $1;
    }
    my $tag = $buffer->get_tag_table->lookup('hyperlink');
    for my $line (split /\n/, $content) {
        my $is_link_line = ($line =~ m/^Link:\s*(https?:\S+)/);
        my $is_title_line = ($line =~ m/^Title:/);
        my $start_mark = $buffer->create_mark(undef, $iter, TRUE);
        $buffer->insert($iter, $line);
        my $end_mark = $buffer->create_mark(undef, $iter, TRUE);
        if (($is_title_line || $is_link_line) && $url_to_open) {
            $buffer->apply_tag($tag,
                $buffer->get_iter_at_mark($start_mark),
                $buffer->get_iter_at_mark($end_mark)
            );
        }
        $buffer->delete_mark($_) for ($start_mark, $end_mark);
        $buffer->insert($iter, "\n");
    }
}

sub check_for_link_hover {
    my ($widget, $event) = @_;
    my ($buffer_x, $buffer_y) = $widget->window_to_buffer_coords('widget', $event->x, $event->y);
    my $iter = $widget->get_iter_at_location($buffer_x, $buffer_y);
    if ($iter->has_tag($widget->get_buffer->get_tag_table->lookup('hyperlink'))) {
        $widget->get_window('widget')->set_cursor(Gtk2::Gdk::Cursor->new('hand2'));
    } else {
        $widget->get_window('widget')->set_cursor(undef);
    }
    return FALSE;
}

sub check_for_link_click {
    my ($widget, $event) = @_;
    return TRUE unless ($event->type eq 'button-release' and $event->button == 1);
    
    my $buffer = $widget->get_buffer;

    # --- DRAG-FIX: Check if there is an active text selection. ---
    # get_selection_bounds returns false in a list context if there's no selection.
    if (my ($start, $end) = $buffer->get_selection_bounds) {
        # If the start and end iterators of the selection are different, it means the
        # user has dragged to select a range of text. We should NOT treat this as a link click.
        if ($start->compare($end) != 0) {
            return TRUE; # Consume the event to prevent the link from firing.
        }
    }

    my ($buffer_x, $buffer_y) = $widget->window_to_buffer_coords('widget', $event->x, $event->y);
    my $iter = $widget->get_iter_at_location($buffer_x, $buffer_y);
    if ($iter->has_tag($buffer->get_tag_table->lookup('hyperlink'))) {
        my $start_of_line = $iter->copy;
        $start_of_line->set_line_offset(0);
        my $end_of_buffer = $buffer->get_end_iter;
        my $text_from_line_onward = $buffer->get_text($start_of_line, $end_of_buffer, 1);
        if ($text_from_line_onward =~ m/Link:\s*(https?:\S+)/) {
            my $url = $1;
            system("xdg-open", $url);
            return TRUE;
        }
    }
    return FALSE;
}

# PRUNE-QUERY-MOD: New subroutines for Pruning and Querying functionality.

sub create_pruning_popup {
    my $popup = Gtk2::Window->new('toplevel');
    $popup->set_title("Age Pruning Settings");
    $popup->set_position('center-on-parent');
    $popup->set_transient_for($window);
    $popup->set_destroy_with_parent(1);
    $popup->set_border_width(10);

    my $vbox = Gtk2::VBox->new(FALSE, 10);
    $popup->add($vbox);

    # --- Row 1: Enable and Prune Now ---
    my $hbox1 = Gtk2::HBox->new(FALSE, 5);
    my $enable_checkbox = Gtk2::CheckButton->new("Enable automatic age pruning");
    $enable_checkbox->set_active($pruning_enabled);
    $enable_checkbox->signal_connect(toggled => sub {
        $pruning_enabled = $enable_checkbox->get_active();
        save_config_to_storable();
    });
    my $prune_now_button = Gtk2::Button->new("[prune now]");
    $hbox1->pack_start($enable_checkbox, TRUE, TRUE, 0);
    $hbox1->pack_start($prune_now_button, FALSE, FALSE, 0);

    # --- Row 2: Set Prune Time ---
    my $hbox2 = Gtk2::HBox->new(FALSE, 5);
    my $label = Gtk2::Label->new("Feed element prune time (days):");
    my $entry = Gtk2::Entry->new();
    $entry->set_text($prune_days);
    $entry->set_width_chars(5);
    my $set_button = Gtk2::Button->new("set");
    $hbox2->pack_start($label, FALSE, FALSE, 0);
    $hbox2->pack_start($entry, TRUE, TRUE, 0);
    $hbox2->pack_start($set_button, FALSE, FALSE, 0);

    # --- Row 4: Destructive Actions ---
    my $hbox4 = Gtk2::HBox->new(FALSE, 5);
    my $wipe_button = Gtk2::Button->new("wipe feed entries and vacuum");
    my $newdb_button = Gtk2::Button->new("start new db");
    $hbox4->pack_start($wipe_button, TRUE, TRUE, 0);
    $hbox4->pack_start($newdb_button, TRUE, TRUE, 0);

    $vbox->pack_start($hbox1, FALSE, FALSE, 0);
    $vbox->pack_start($hbox2, FALSE, FALSE, 0);
    $vbox->pack_start(Gtk2::HSeparator->new, FALSE, FALSE, 10);
    $vbox->pack_start($hbox4, FALSE, FALSE, 0);

    # --- Signal Handlers ---
    $prune_now_button->signal_connect(clicked => sub {
        prune_feed_entries_now(1); # Pass a flag to indicate manual trigger
        $popup->destroy();
    });

    $set_button->signal_connect(clicked => sub {
        my $text = $entry->get_text();
        if ($text =~ /^\d+$/ && $text > 0) {
            $prune_days = $text;
            save_config_to_storable();
            $popup->destroy();
        } else {
            warn "Invalid input. Please enter a positive integer for days.";
        }
    });

    $wipe_button->signal_connect(clicked => sub {
        my $dialog = Gtk2::MessageDialog->new($popup, ['modal', 'destroy-with-parent'], 'question',
            'ok-cancel', "Are you sure you want to delete ALL feed entries and VACUUM the database? This cannot be undone.");
        if ($dialog->run eq 'ok') {
            $dbh->do("DELETE FROM entries");
            $dbh->do("VACUUM");
            # Refresh current view if it's a folder
            my (undef, $selected_iter) = $tree_view->get_selection->get_selected;
            if ($selected_iter and $tree_store->get($selected_iter, 2)) {
                display_folder_contents($selected_iter);
            }
        }
        $dialog->destroy();
        $popup->destroy();
    });

    $newdb_button->signal_connect(clicked => sub {
        my $dialog = Gtk2::MessageDialog->new($popup, ['modal', 'destroy-with-parent'], 'question',
            'ok-cancel', "Are you sure you want to delete the ENTIRE database and start over? All feeds and folders will be lost.");
        if ($dialog->run eq 'ok') {
            $dbh->disconnect();
            unlink $db_file, "$db_file-wal", "$db_file-shm";
            init_database();
            $tree_store->clear();
            $headline_store->clear();
            load_broken_feed_status_from_db(); # Repopulate from the now empty db
            update_all_row_colors();
        }
        $dialog->destroy();
        $popup->destroy();
    });

    $popup->show_all;
}

# AUTO-PRUNE-MOD: Function updated to only run if enabled and provide feedback.
sub prune_feed_entries_now {
    my ($manual_run) = @_;
    return unless $pruning_enabled or $manual_run;

    my $cutoff = time() - ($prune_days * 86400); # 86400 seconds in a day
    my $pruned_count = 0;
    eval {
        my $sth = $dbh->prepare("DELETE FROM entries WHERE date_added < ?");
        $pruned_count = $sth->execute($cutoff);
        $sth->finish();
    };
    if ($@) {
        warn "DB pruning failed: $@";
        return;
    }
    
    if ($pruned_count > 0) {
        my $message = "Pruned $pruned_count old entries.";
        warn "$message\n";
        # Refresh current view if it's a folder to show the result
        my (undef, $selected_iter) = $tree_view->get_selection->get_selected;
        if ($selected_iter and $tree_store->get($selected_iter, 2)) {
            display_folder_contents($selected_iter);
        }
    }
}

sub show_query_results_in_main_view {
    return unless $last_successful_select_query;

    # --- Step 1: Execute the user's query to get the list of entry IDs ---
    my @entry_ids;
    eval {
        my $sth = $dbh->prepare($last_successful_select_query);
        $sth->execute();
        # Assume the first column of the result is the entry ID.
        while (my $row = $sth->fetchrow_arrayref) {
            push @entry_ids, $row->[0] if (defined $row->[0]);
        }
        $sth->finish();
    };
    if ($@) {
        warn "Could not re-run query to show in main view: $@";
        return;
    }

    if (!@entry_ids) {
        warn "Query returned no entry IDs to display.";
        return;
    }

    # --- Step 2: Ensure the main pane is in the correct two-pane folder view mode ---
    $scrolled_win_single_view->hide;
    $vpaned_right->show;

    # --- Step 3: Clear the UI and deselect the left pane ---
    $headline_store->clear;
    $folder_text_view->get_buffer->set_text('');
    $tree_view->get_selection->unselect_all();

    # PAGINATION-MOD: Enable the "Next Page" button now that we have results.
    $next_page_button->set_sensitive(TRUE);

    # --- Step 4: Run a known-good query to get full data and populate the view ---
    my $placeholders = join(',', ('?') x @entry_ids);
    my $sql = qq{
        SELECT f.name, e.title, e.link, e.published_date, e.content, e.date_added, e.is_new
        FROM entries e
        JOIN feeds f ON e.feed_id = f.id
        WHERE e.id IN ($placeholders)
    };
    
    if ($hide_unparseable_check->get_active()) {
        $sql .= " AND e.title != 'Unparseable Feed Content'";
    }
    
    my $sth = $dbh->prepare($sql);
    $sth->execute(@entry_ids);

    while (my $row = $sth->fetchrow_arrayref) {
        my ($feed_name, $headline, $url, $sortable_date, $full_content, $date_added, $is_new) = @$row;
        
        $feed_name ||= 'Untitled Feed';
        $headline ||= 'No Title';
        $headline =~ s/[\r\n]+/ /g; $headline =~ s/^\s+|\s+$//g;
        
        my $date_str = $sortable_date ? strftime('%Y-%m-%d %H:%M', localtime($sortable_date)) : '';
        my $date_added_str = $date_added ? strftime('%Y-%m-%d %H:%M', localtime($date_added)) : '';
        my $weight = $is_new ? 700 : 400;

        my $iter = $headline_store->append;
        $headline_store->set($iter,
            0, $feed_name, 1, $headline, 2, $date_str, 3, $full_content, 4, $url,
            5, $sortable_date, 6, $date_added_str, 7, $date_added, 8, $weight, 9, $is_new
        );
    }
    $sth->finish;

    # Set a default sort order
    $headline_store->set_sort_column_id(7, 'descending');
}

sub create_sql_query_popup {
    my $popup = Gtk2::Window->new('toplevel');
    $popup->set_title("Raw SQLite Query Interface");
    $popup->set_default_size(700, 650);
    $popup->set_position('center-on-parent');
    $popup->set_transient_for($window);
    $popup->set_destroy_with_parent(1);
    $popup->set_border_width(10);

    my $vbox = Gtk2::VBox->new(FALSE, 6);
    $popup->add($vbox);

    # --- Input Area ---
    my $input_frame = Gtk2::Frame->new("SQL Query (must SELECT entry id as first column)");
    my $input_sw = Gtk2::ScrolledWindow->new(undef, undef);
    $input_sw->set_policy('automatic', 'automatic');
    $input_sw->set_shadow_type('in');
    # RESIZE-FIX: Give the input box a smaller, fixed default height.
    $input_sw->set_size_request(-1, 100); 
    my $input_tv = Gtk2::TextView->new();
    $input_tv->set_wrap_mode('word');
    $input_sw->add($input_tv);
    $input_frame->add($input_sw);
    
    $input_tv->get_buffer->set_text("SELECT id, title FROM entries WHERE title LIKE '%space%' ORDER BY date_added DESC");

    # --- EXAMPLE-MOD: Add a collapsible expander with SQL examples ---
    my $expander = Gtk2::Expander->new_with_mnemonic("_Show SQL Examples");
    my $example_sw = Gtk2::ScrolledWindow->new(undef, undef);
    $example_sw->set_policy('automatic', 'automatic');
    $example_sw->set_shadow_type('in');
    # RESIZE-FIX: Give the examples box a default height.
    $example_sw->set_size_request(-1, 150);
    
    my $example_tv = Gtk2::TextView->new();
    $example_tv->set_editable(FALSE);
    $example_tv->set_cursor_visible(FALSE);
    $example_tv->set_wrap_mode('word');
    $example_sw->add($example_tv);
    $expander->add($example_sw);

    my $example_buffer = $example_tv->get_buffer();
    my $example_sql_text = <<'EXAMPLES';
-- Find entries by title (case-insensitive)
SELECT id, title FROM entries WHERE title LIKE '%search_term%' ORDER BY date_added DESC;

-- Find entries by content (case-insensitive)
SELECT id, title, content FROM entries WHERE content LIKE '%search_term%';

-- Find all "new" (unread) entries
SELECT id, title FROM entries WHERE is_new = 1 ORDER BY date_added DESC;

-- Find entries from a specific feed by its name
SELECT e.id, e.title FROM entries e JOIN feeds f ON e.feed_id = f.id WHERE f.name = 'Example Feed Name';

-- Find entries within a specific date range (using YYYY-MM-DD format)
SELECT id, title FROM entries WHERE date_added BETWEEN strftime('%s', '2025-09-01') AND strftime('%s', '2025-09-15');

-- Pagination: Get the FIRST 1000 entries (Page 1)
SELECT id, title FROM entries ORDER BY date_added DESC LIMIT 1000 OFFSET 0;

-- Pagination: Get the NEXT 1000 entries (Page 2: entries 1001-2000)
SELECT id, title FROM entries ORDER BY date_added DESC LIMIT 1000 OFFSET 1000;

-- Mark all entries from currently broken feeds as "read" (not new)
UPDATE entries SET is_new = 0 WHERE feed_id IN (SELECT id FROM feeds WHERE broken = 1);
EXAMPLES
    $example_buffer->set_text($example_sql_text);

    # --- Results Area ---
    my $results_frame = Gtk2::Frame->new("Results");
    my $results_sw = Gtk2::ScrolledWindow->new(undef, undef);
    $results_sw->set_policy('automatic', 'automatic');
    $results_sw->set_shadow_type('in');
    my $results_tv = Gtk2::TextView->new();
    $results_tv->set_editable(FALSE);
    $results_tv->set_cursor_visible(FALSE);
    $results_tv->set_wrap_mode('word');
    $results_sw->add($results_tv);
    $results_frame->add($results_sw);
    
    # --- Buttons Area ---
    my $button_hbox = Gtk2::HBox->new(FALSE, 10);
    my $run_button = Gtk2::Button->new("Run Query");
    my $show_in_main_button = Gtk2::Button->new("Show in Main");
    my $db_details_button = Gtk2::Button->new("DB Details");

    $show_in_main_button->set_sensitive(FALSE);
    
    $button_hbox->pack_start($run_button, TRUE, TRUE, 0);
    $button_hbox->pack_start($show_in_main_button, TRUE, TRUE, 0);
    $button_hbox->pack_start($db_details_button, FALSE, FALSE, 0);

    # --- Packing Order ---
    # RESIZE-FIX: Change the 'expand' and 'fill' properties to control sizing.
    $vbox->pack_start($input_frame, FALSE, FALSE, 0); # Don't expand the input frame
    $vbox->pack_start($expander, FALSE, FALSE, 0);    # Don't expand the expander label
    $vbox->pack_start($button_hbox, FALSE, FALSE, 0); # Don't expand the buttons
    $vbox->pack_start($results_frame, TRUE, TRUE, 0); # DO expand the results frame to fill remaining space
    
    # --- Button Logic ---
    $run_button->signal_connect(clicked => sub {
        my $results_buffer = $results_tv->get_buffer();
        my $input_buffer = $input_tv->get_buffer();
        my $sql = $input_buffer->get_text($input_buffer->get_start_iter, $input_buffer->get_end_iter, FALSE);
        
        $results_buffer->set_text("");
        # On every new run, disable the button until we confirm it's a valid SELECT.
        $show_in_main_button->set_sensitive(FALSE);
        $last_successful_select_query = '';

        return unless $sql =~ /\S/;

        eval {
            my $sth = $dbh->prepare($sql);
            $sth->execute();
            
            if ($sql !~ /^\s*SELECT/i) {
                my $rows_affected = $sth->rows;
                $results_buffer->set_text("Query executed successfully. Rows affected: $rows_affected\n");
                $sth->finish();
                return;
            }

            # If we get here, it was a successful SELECT query.
            $last_successful_select_query = $sql;
            $show_in_main_button->set_sensitive(TRUE);

            my @col_names = @{$sth->{NAME}};
            my $output = join("\t|\t", @col_names) . "\n";
            $output .= join("", map { "-" x length($_) . "\t-\t" } @col_names) . "\n";
            
            my $rows = 0;
            while (my @row = $sth->fetchrow_array) {
                $output .= join("\t|\t", map { defined $_ ? $_ : "NULL" } @row) . "\n";
                $output .= "\n";
                $rows++;
            }
            $sth->finish();
            
            $output .= "\nQuery returned $rows rows.";
            $results_buffer->set_text($output);
        };
        if ($@) {
            $results_buffer->set_text("SQL Error:\n$@");
            # On error, clear the last query and disable the button.
            $last_successful_select_query = '';
            $show_in_main_button->set_sensitive(FALSE);
        }
    });

    $show_in_main_button->signal_connect(clicked => sub {
        # PAGINATION-MOD: Reset the offset whenever a new query is shown.
        $current_query_offset = 0;
        show_query_results_in_main_view();
    });

    $db_details_button->signal_connect(clicked => sub { create_db_details_popup($popup); });

    $popup->show_all;
}

sub create_db_details_popup {
    my ($parent) = @_;
    my $popup = Gtk2::Window->new('toplevel');
    $popup->set_title("Database Schema Details");
    $popup->set_default_size(500, 400);
    $popup->set_position('center-on-parent');
    $popup->set_transient_for($parent);
    $popup->set_destroy_with_parent(1);
    $popup->set_border_width(5);

    my $sw = Gtk2::ScrolledWindow->new(undef, undef);
    $sw->set_policy('automatic', 'automatic');
    $sw->set_shadow_type('in');

    my $tv = Gtk2::TextView->new();
    $tv->set_editable(FALSE);
    $tv->set_wrap_mode('word');
    
    my $buffer = $tv->get_buffer();
    my $details_text = <<"DETAILS";
Database: $db_file

You can use the column names below in your SQL WHERE clauses.

TABLE: feeds
  id INTEGER PRIMARY KEY,
  parent_id INTEGER,
  name TEXT NOT NULL,
  url TEXT,
  is_folder BOOLEAN NOT NULL,
  broken BOOLEAN NOT NULL DEFAULT 0

TABLE: entries
  id INTEGER PRIMARY KEY,
  feed_id INTEGER NOT NULL,
  title TEXT,
  link TEXT UNIQUE,
  published_date INTEGER,
  content TEXT,
  date_added INTEGER NOT NULL,
  is_new BOOLEAN NOT NULL DEFAULT 1

INDEXES:
- feeds(parent_id)
- entries(feed_id)
- entries(published_date)
- entries(date_added)
- entries(feed_id, is_new)
DETAILS

    $buffer->set_text($details_text);

    $sw->add($tv);
    $popup->add($sw);
    $popup->show_all;
}


################################################################################
# Application Start
################################################################################

# AUTO-RESIZE-MOD: New function to handle initial pane sizing.
# COL-WIDTH-MOD: This function now sets widths from saved config values.
# TIMING-FIX: Re-introduced the looping timer to ensure widths are set only after
#             the headline view has been allocated a size by the layout engine.
sub initial_resize_panes {
    # 1. Resize the main horizontal pane
    $hpaned->set_position(300);

    # 2. Resize the right-side vertical pane
    my $right_pane_height = $right_pane_container->get_allocation->height;
    if ($right_pane_height > 1) {
        $vpaned_right->set_position(int($right_pane_height / 2));
    }
    
    # 3. Check if the headline view has been drawn yet. If not, try again soon.
    my $width = $headline_view->get_allocation->width;
    return TRUE if $width < 50; # Not ready yet, tell timer to run again.
    
    # 4. If it's ready, resize the columns using the saved/default values.
    $feed_column->set_fixed_width($column_widths{feed});
    $headline_column->set_fixed_width($column_widths{headline});
    $date_column->set_fixed_width($column_widths{date});
    $date_added_column->set_fixed_width($column_widths{date_added});

    # SQUISHED-FIX: The initial layout is now complete. We can now safely enable the
    # resize handler to listen for user-initiated changes.
    $initial_layout_complete = TRUE;
    
    return FALSE; # All done, stop the timer.
}

$progress_item->hide;
$pause_button->hide;
$cancel_button->hide;
$skip_button->hide;
# PAGINATION-MOD: The "Next Page" button should be disabled on startup.
$next_page_button->set_sensitive(FALSE);

# REFACTOR-MOD: Hide one of the right-pane views on startup.
$vpaned_right->hide;

load_config_from_storable();
# NEW-PERSIST-MOD: Update counts from the database on startup.
update_tree_view_labels();

# AUTO-PRUNE-MOD: Add a one-shot timer to prune on startup if enabled.
# Fires 30 seconds after launch to avoid interfering with initial setup.
Glib::Timeout->add(30000, sub {
    prune_feed_entries_now(0); # Pass 0 to indicate it's an automatic run
    return FALSE; # Returning FALSE makes the timer run only once.
});

$window->show_all;

# AUTO-RESIZE-MOD: Programmatically select the first item on startup to trigger sizing.
Glib::Timeout->add(200, sub {
    my $first_iter = $tree_store->get_iter_first;
    if ($first_iter) {
        my $path = $tree_store->get_path($first_iter);
        $tree_view->get_selection->select_iter($first_iter);
        # Briefly expand the row to trigger GTK's column sizing logic...
        $tree_view->expand_row($path, TRUE);
        # ...and then immediately collapse it again for the user.
        $tree_view->collapse_row($path);
    }
    # Also set the main h-pane position here, after the window is shown.
    $hpaned->set_position(300);
    return FALSE;
});

# AUTO-RESIZE-MOD: Use a timeout to set initial pane sizes after the window is shown.
# TIMING-FIX: The initial_resize_panes function will now self-repeat until it succeeds.
Glib::Timeout->add(100, \&initial_resize_panes);

Gtk2->main;

exit 0;
