App-DrivePlayer

 view release on metacpan or  search on metacpan

lib/App/DrivePlayer/GUI.pm  view on Meta::CPAN

    $pb_item->set_submenu($pb_menu);
    $mb->append($pb_item);

    return $mb;
}

sub _add_menu_item {
    my ($self, $menu, $label, $cb) = @_;
    my $item = Gtk3::MenuItem->new_with_label($label);
    $item->signal_connect(activate => $cb);
    $menu->append($item);
    return $item;
}

sub _build_toolbar {
    my ($self) = @_;
    my $tb = Gtk3::Toolbar->new();
    $tb->set_style('both-horiz');

    my $scan_btn = Gtk3::ToolButton->new(
        Gtk3::Image->new_from_icon_name('view-refresh', 'small-toolbar'),
        'Sync'
    );
    $scan_btn->signal_connect(clicked => sub { $self->_sync_all() });
    $tb->insert($scan_btn, -1);

    my $add_btn = Gtk3::ToolButton->new(
        Gtk3::Image->new_from_icon_name('folder-new', 'small-toolbar'),
        'Add Folder'
    );
    $add_btn->signal_connect(clicked => sub { $self->_add_folder_dialog() });
    $tb->insert($add_btn, -1);

    $tb->insert(Gtk3::SeparatorToolItem->new(), -1);

    my $settings_btn = Gtk3::ToolButton->new(
        Gtk3::Image->new_from_icon_name('preferences-system', 'small-toolbar'),
        'Settings'
    );
    $settings_btn->signal_connect(clicked => sub { $self->_settings_dialog() });
    $tb->insert($settings_btn, -1);

    return $tb;
}

sub _build_sidebar {
    my ($self) = @_;
    my $sw = Gtk3::ScrolledWindow->new();
    $sw->set_policy('automatic', 'automatic');
    $sw->set_size_request(100, 1);
    $sw->set_propagate_natural_height(FALSE);

    # TreeStore: label (str), type (str: 'category'|'artist'|'album'|'folder'),
    #            value (str: artist name, album name, folder_id)
    my $store = Gtk3::TreeStore->new('Glib::String', 'Glib::String', 'Glib::String');
    $self->sidebar_store($store);

    my $view = Gtk3::TreeView->new($store);
    $view->set_headers_visible(FALSE);
    $view->get_selection()->set_mode('single');
    $view->signal_connect('cursor-changed'   => sub { $self->_sidebar_activated($view) });
    $view->signal_connect('button-press-event' => sub { $self->_sidebar_button_press($view, $_[1]) });
    $self->sidebar_view($view);

    $view->set_size_request(100, 1);

    my $renderer = Gtk3::CellRendererText->new();
    $renderer->set(ellipsize => 'end');
    my $col = Gtk3::TreeViewColumn->new_with_attributes('', $renderer, text => 0);
    $col->set_sizing('fixed');
    $col->set_expand(TRUE);
    $view->append_column($col);
    $view->set_fixed_height_mode(TRUE);

    $sw->add($view);

    my $hbox = Gtk3::Box->new('horizontal', 0);
    $hbox->pack_start($self->_build_alpha_strip(), FALSE, FALSE, 0);
    $hbox->pack_start($sw, TRUE, TRUE, 0);
    return $hbox;
}

sub _build_alpha_strip {
    my ($self) = @_;

    my $css = Gtk3::CssProvider->new();
    $css->load_from_data(
        'treeview.alpha-nav { font-size: 10px; padding: 0; }'
        . ' treeview.alpha-nav row { min-height: 0; padding: 1px 0; }'
    );

    my $store = Gtk3::ListStore->new('Glib::String');
    for my $letter ('#', 'A' .. 'Z') {
        my $iter = $store->append();
        $store->set($iter, 0, $letter);
    }

    my $view = Gtk3::TreeView->new($store);
    $view->set_headers_visible(FALSE);
    $view->set_fixed_height_mode(TRUE);
    $view->set_can_focus(FALSE);
    $view->set_activate_on_single_click(TRUE);
    $view->get_style_context()->add_class('alpha-nav');
    $view->get_style_context()->add_provider($css, 600);
    $self->alpha_view($view);

    my $renderer = Gtk3::CellRendererText->new();
    $renderer->set(xalign => 0.5);
    my $col = Gtk3::TreeViewColumn->new_with_attributes('', $renderer, text => 0);
    $col->set_sizing('fixed');
    $col->set_fixed_width(32);
    $view->append_column($col);

    $view->signal_connect('row-activated' => sub {
        my ($tv, $path, $col) = @_;
        my $iter = $store->get_iter($path) or return;
        my $letter = $store->get($iter, 0);
        $self->_sidebar_jump_to_letter($letter);
        $tv->get_selection()->unselect_all();
    });

    my $sw = Gtk3::ScrolledWindow->new();
    $sw->set_policy('never', 'automatic');
    $sw->set_size_request(32, 1);
    $sw->set_propagate_natural_height(FALSE);
    $sw->add($view);
    return $sw;
}

sub _sidebar_jump_to_letter {
    my ($self, $letter) = @_;
    my $cat_label = $self->_alpha_category or return;
    my $store = $self->sidebar_store;
    my $view  = $self->sidebar_view;

    my $cat_iter = $store->get_iter_first() or return;
    my $target;
    while (1) {
        if (($store->get($cat_iter, 0) // '') eq $cat_label) {
            $target = $store->get_path($cat_iter);  # snapshot path before iter moves
            last;
        }
        last unless $store->iter_next($cat_iter);
    }
    return unless $target;

    my $target_iter = $store->get_iter($target) or return;
    my $n = $store->iter_n_children($target_iter);
    for my $i (0 .. $n - 1) {
        my $child = $store->iter_nth_child($target_iter, $i) or next;
        my $label = $store->get($child, 0) // '';
        my $first = uc(substr($label, 0, 1));
        my $matches = $letter eq '#' ? ($first lt 'A' || $first gt 'Z')
                                     : $first eq $letter;
        next unless $matches;
        my $path = $store->get_path($child);
        $view->expand_to_path($path);
        $view->set_cursor($path, undef, FALSE);
        $view->scroll_to_cell($path, undef, TRUE, 0.0, 0.0);
        return;
    }
}

sub _navigate_sidebar_to {
    my ($self, $kind, $value) = @_;
    my $cat_label = $kind eq 'artist' ? 'Artists'
                  : $kind eq 'album'  ? 'Albums'
                  : $kind eq 'genre'  ? 'Genres'
                  :                     return;
    my $store = $self->sidebar_store;
    my $view  = $self->sidebar_view;

    # Locate the category header (top-level row whose label matches).
    my $cat_iter = $store->get_iter_first() or return;
    my $cat_path;
    while (1) {
        if (($store->get($cat_iter, 0) // '') eq $cat_label) {
            $cat_path = $store->get_path($cat_iter);
            last;
        }
        last unless $store->iter_next($cat_iter);
    }
    return unless $cat_path;

    # Match against the raw value stored in column 2, not the display label
    # in column 0 (which may carry LRM markers from _display_text/_album).
    my $cat_iter2 = $store->get_iter($cat_path) or return;
    my $n = $store->iter_n_children($cat_iter2);
    for my $i (0 .. $n - 1) {
        my $child  = $store->iter_nth_child($cat_iter2, $i) or next;
        my $stored = $store->get($child, 2);
        next unless defined $stored && $stored eq $value;
        my $path = $store->get_path($child);
        $view->expand_to_path($path);
        $view->set_cursor($path, undef, FALSE);
        $view->scroll_to_cell($path, undef, TRUE, 0.5, 0.0);
        return;
    }
}

sub _update_alpha_category {
    my ($self, $label) = @_;
    my $enabled = defined $label
               && $label =~ / \A (?: Artists | Albums | Genres ) \z /x;
    $self->_alpha_category($enabled ? $label : undef);
    $self->alpha_view->set_sensitive($enabled ? TRUE : FALSE)
        if $self->alpha_view;
}

sub _build_tracklist {
    my ($self) = @_;
    my $vbox = Gtk3::Box->new('vertical', 0);

    my $sw = Gtk3::ScrolledWindow->new();
    $sw->set_policy('automatic', 'automatic');
    $sw->set_size_request(-1, 1);
    $sw->set_propagate_natural_height(FALSE);
    $sw->set_kinetic_scrolling(FALSE);
    $sw->set_capture_button_press(FALSE);
    $sw->set_overlay_scrolling(FALSE);
    $vbox->pack_start($sw, TRUE, TRUE, 0);

    my $count_lbl = Gtk3::Label->new('');
    $count_lbl->set_xalign(1.0);
    $count_lbl->set_margin_end(6);
    $count_lbl->set_margin_top(2);
    $count_lbl->set_margin_bottom(2);
    $self->track_count_label($count_lbl);
    $vbox->pack_start($count_lbl, FALSE, FALSE, 0);

    # ListStore columns: id, track#, title, artist, album, genre, year, duration_str, drive_id
    my $store = Gtk3::ListStore->new(
        'Glib::Int',    # 0 db id
        'Glib::String', # 1 track#
        'Glib::String', # 2 title
        'Glib::String', # 3 artist
        'Glib::String', # 4 album
        'Glib::String', # 5 genre
        'Glib::String', # 6 year
        'Glib::String', # 7 duration
        'Glib::String', # 8 drive_id
    );
    $self->track_store($store);

    my $view = Gtk3::TreeView->new($store);
    $view->set_headers_visible(TRUE);
    $view->set_fixed_height_mode(TRUE);
    $view->get_selection()->set_mode('multiple');
    $view->signal_connect('row-activated' => sub { $self->_track_activated(@_) });
    $self->track_view($view);

    my @cols = (
        ['#',        1,  40],
        ['Title',    2, 220],
        ['Artist',   3, 160],
        ['Album',    4, 160],
        ['Genre',    5, 100],
        ['Year',     6,  60],
        ['Duration', 7,  65],
    );
    # Map: column-index → sidebar 'kind' for cells that act as navigation
    # links to the corresponding sidebar category.
    my %link_kind_by_col_idx = (3 => 'artist', 4 => 'album', 5 => 'genre');
    my %link_kind;   # TreeViewColumn ref → kind string
    for my $col_def (@cols) {
        my ($title, $idx, $width) = @$col_def;
        my $r = Gtk3::CellRendererText->new();
        my $c = Gtk3::TreeViewColumn->new_with_attributes($title, $r, text => $idx);
        $c->set_resizable(TRUE);
        $c->set_sort_column_id($idx);
        $c->set_sizing('fixed');
        $c->set_fixed_width($width);
        $view->append_column($c);
        $link_kind{$c} = $link_kind_by_col_idx{$idx}
            if exists $link_kind_by_col_idx{$idx};
    }

    # Take full control of left-click so GTK's default handler (which
    # calls set_cursor and auto-scrolls the clicked row into view) doesn't
    # shift the viewport between the clicks of a double-click.
    $view->signal_connect('button-press-event' => sub {
        my ($w, $event) = @_;

        if ($event->button == 3) {
            $self->_tracklist_context_menu($event);
            return TRUE;
        }

        return FALSE unless $event->button == 1;

        my ($path, $col) = $w->get_path_at_pos($event->x, $event->y);
        return FALSE unless $path;

        if ($event->type eq '2button-press') {
            $self->_play_at_path($path, no_scroll => 1);
            return TRUE;
        }

        # Single-click on a "link" cell (Artist / Album / Genre) jumps the
        # sidebar to that group. The sidebar's cursor-changed signal then
        # repopulates the tracklist, so no further work is needed here.
        if ($col && (my $kind = $link_kind{$col})) {
            my $iter  = $self->track_store->get_iter($path);
            my $id    = $iter ? $self->track_store->get($iter, 0) : undef;
            my $track = $id ? $self->_track_by_id->{$id} : undef;
            my $value = $track ? $track->{$kind} : undef;
            if (defined $value && length $value) {
                $self->_navigate_sidebar_to($kind, $value);
                return TRUE;
            }
        }

        my $sel   = $w->get_selection();
        my $state = $event->state;
        if ($state & 'control-mask') {
            $sel->path_is_selected($path)
                ? $sel->unselect_path($path)
                : $sel->select_path($path);
        }
        elsif ($state & 'shift-mask') {
            # Let GTK handle shift-click range extension.
            return FALSE;
        }
        else {
            $sel->unselect_all();
            $sel->select_path($path);
        }
        return TRUE;
    });

    # Hover affordance: show the link/hand cursor over Artist and Album
    # cells that hold a value, so users see those columns are clickable.
    my $is_link_cursor = 0;
    my $set_cursor = sub {
        my ($want_link) = @_;
        return if $want_link == $is_link_cursor;
        my $window = $view->get_bin_window() or return;
        if ($want_link) {
            my $display = $window->get_display();
            my $cursor  = Gtk3::Gdk::Cursor->new_for_display($display, 'hand2');
            $window->set_cursor($cursor);
        } else {
            $window->set_cursor(undef);
        }
        $is_link_cursor = $want_link;
    };

    $view->signal_connect('motion-notify-event' => sub {
        my ($w, $event) = @_;
        my ($path, $col) = $w->get_path_at_pos($event->x, $event->y);
        my $want_link = 0;
        if ($path && $col && (my $kind = $link_kind{$col})) {
            my $iter  = $self->track_store->get_iter($path);
            my $id    = $iter ? $self->track_store->get($iter, 0) : undef;
            my $track = $id ? $self->_track_by_id->{$id} : undef;
            my $value = $track ? $track->{$kind} : undef;
            $want_link = 1 if defined $value && length $value;
        }
        $set_cursor->($want_link);
        return FALSE;
    });

    $view->signal_connect('leave-notify-event' => sub {
        $set_cursor->(0);
        return FALSE;
    });

    $sw->add($view);
    return $vbox;
}

sub _build_searchbar {
    my ($self) = @_;
    my $hbox = Gtk3::Box->new('horizontal', 4);
    $hbox->set_border_width(2);

    my $label = Gtk3::Label->new('Search:');
    $hbox->pack_start($label, FALSE, FALSE, 4);

    my $entry = Gtk3::SearchEntry->new();
    $entry->set_placeholder_text('Artist, album or title…');
    $entry->signal_connect('search-changed' => sub { $self->_on_search($entry->get_text()) });
    $self->search_entry($entry);
    $hbox->pack_start($entry, TRUE, TRUE, 0);

    my $clear = Gtk3::Button->new_with_label('Clear');
    $clear->signal_connect(clicked => sub {
        $entry->set_text('');
        $self->_load_library();
    });
    $hbox->pack_start($clear, FALSE, FALSE, 0);

    return $hbox;
}

sub _build_controls {
    my ($self) = @_;
    my $frame = Gtk3::Frame->new();
    my $vbox  = Gtk3::Box->new('vertical', 2);
    $vbox->set_border_width(4);
    $frame->add($vbox);

    # Now-playing label
    $self->now_playing_label(Gtk3::Label->new('Not playing'));
    $self->now_playing_label->set_ellipsize('end');
    $self->now_playing_label->set_xalign(0.0);
    $vbox->pack_start($self->now_playing_label, FALSE, FALSE, 0);

    # Progress bar + time labels
    my $prog_hbox = Gtk3::Box->new('horizontal', 4);
    $self->time_label(Gtk3::Label->new('0:00'));
    $self->time_label->set_size_request(40, -1);
    $prog_hbox->pack_start($self->time_label, FALSE, FALSE, 0);

    $self->progress(Gtk3::Scale->new_with_range('horizontal', 0, 100, 1));
    $self->progress->set_draw_value(FALSE);
    $self->progress->set_range(0, 1);
    $self->progress->signal_connect('button-press-event' => sub {
        $self->_progress_dragging(1); return FALSE;
    });
    $self->progress->signal_connect('button-release-event' => sub {
        $self->_progress_dragging(0);
        $self->player->seek($self->progress->get_value()) if $self->player;
        return FALSE;

lib/App/DrivePlayer/GUI.pm  view on Meta::CPAN

    $self->dur_label(Gtk3::Label->new('0:00'));
    $self->dur_label->set_size_request(40, -1);
    $prog_hbox->pack_start($self->dur_label, FALSE, FALSE, 0);
    $vbox->pack_start($prog_hbox, FALSE, FALSE, 0);

    # Buttons + volume
    my $btn_hbox = Gtk3::Box->new('horizontal', 4);
    $vbox->pack_start($btn_hbox, FALSE, FALSE, 0);

    $self->prev_btn($self->_icon_button('media-skip-backward', sub { $self->_prev_track() }));
    $self->play_btn($self->_icon_button('media-playback-start', sub { $self->_toggle_play() }));
    $self->stop_btn($self->_icon_button('media-playback-stop',  sub { $self->_stop() }));
    $self->next_btn($self->_icon_button('media-skip-forward',   sub { $self->_next_track() }));

    $btn_hbox->pack_start($self->prev_btn, FALSE, FALSE, 0);
    $btn_hbox->pack_start($self->play_btn, FALSE, FALSE, 0);
    $btn_hbox->pack_start($self->stop_btn, FALSE, FALSE, 0);
    $btn_hbox->pack_start($self->next_btn, FALSE, FALSE, 0);

    $btn_hbox->pack_start(Gtk3::Label->new(' Vol:'), FALSE, FALSE, 8);
    $self->vol_scale(Gtk3::Scale->new_with_range('horizontal', 0, 100, 1));
    $self->vol_scale->set_value(80);
    $self->vol_scale->set_size_request(100, -1);
    $self->vol_scale->set_draw_value(FALSE);
    $self->vol_scale->signal_connect('value-changed' => sub {
        $self->player->set_volume($self->vol_scale->get_value()) if $self->player;
    });
    $btn_hbox->pack_start($self->vol_scale, FALSE, FALSE, 0);

    return $frame;
}

sub _icon_button {
    my ($self, $icon_name, $cb) = @_;
    my $btn = Gtk3::Button->new();
    $btn->set_image(Gtk3::Image->new_from_icon_name($icon_name, 'button'));
    $btn->signal_connect(clicked => $cb);
    return $btn;
}

# ---- Library loading ----

sub _load_library {
    my ($self) = @_;
    $self->_populate_sidebar();
    $self->_populate_tracklist($self->db->all_tracks());
    my $count = $self->db->track_count();
    $self->_set_status("$count tracks in library");
}

# Rebuild the sidebar tree (so renamed/added/dropped artists, albums, and
# genres are reflected) without losing the user's current selection or
# kicking off a tracklist reload.  Used after a metadata edit when an
# artist/album/genre changed.
sub _refresh_sidebar_keep_selection {
    my ($self) = @_;
    my $store = $self->sidebar_store;
    my $view  = $self->sidebar_view;

    my $sel;
    if (my ($path) = $view->get_cursor()) {
        if (my $iter = $store->get_iter($path)) {
            $sel = {
                type  => $store->get($iter, 1),
                value => $store->get($iter, 2),
            };
        }
    }

    $self->_suppress_sidebar_activated(1);
    $self->_populate_sidebar();

    if ($sel) {
        my $cat = $store->get_iter_first;
        TOP: while ($cat) {
            my $check = sub {
                my $iter = shift;
                return ($store->get($iter, 1) // '') eq ($sel->{type}  // '')
                    && ($store->get($iter, 2) // '') eq ($sel->{value} // '');
            };
            if ($check->($cat)) {
                $view->set_cursor($store->get_path($cat), undef, FALSE);
                last TOP;
            }
            my $n = $store->iter_n_children($cat);
            for my $i (0 .. $n - 1) {
                my $child = $store->iter_nth_child($cat, $i) or next;
                next unless $check->($child);
                my $cp = $store->get_path($child);
                $view->expand_to_path($cp);
                $view->set_cursor($cp, undef, FALSE);
                last TOP;
            }
            last unless $store->iter_next($cat);
        }
    }
    $self->_suppress_sidebar_activated(0);
}

sub _populate_sidebar {
    my ($self) = @_;
    my $store = $self->sidebar_store;
    $store->clear();

    # All Tracks
    my $all_iter = $store->append(undef);
    $store->set($all_iter, 0, 'All Tracks', 1, 'all', 2, '');

    # Artists
    my $art_iter = $store->append(undef);
    $store->set($art_iter, 0, 'Artists', 1, 'category', 2, '');
    for my $artist ($self->db->all_artists()) {
        my $iter = $store->append($art_iter);
        $store->set($iter, 0, _display_text($artist), 1, 'artist', 2, $artist);
    }

    # Albums
    my $alb_iter = $store->append(undef);
    $store->set($alb_iter, 0, 'Albums', 1, 'category', 2, '');
    for my $album ($self->db->all_albums()) {
        my $iter = $store->append($alb_iter);
        $store->set($iter, 0, _display_album($album), 1, 'album', 2, $album);
    }

    # Genres
    my $gen_iter = $store->append(undef);
    $store->set($gen_iter, 0, 'Genres', 1, 'category', 2, '');
    for my $genre ($self->db->all_genres()) {
        my $iter = $store->append($gen_iter);
        $store->set($iter, 0, $genre, 1, 'genre', 2, $genre);
    }

    # Folders
    my $fld_iter = $store->append(undef);
    $store->set($fld_iter, 0, 'Folders', 1, 'category', 2, '');
    for my $sf ($self->db->all_scan_folders()) {
        my $iter = $store->append($fld_iter);
        $store->set($iter, 0, $sf->{name}, 1, 'folder', 2, $sf->{drive_id});
    }

    $self->sidebar_view->expand_all();
}

sub _populate_tracklist {
    my ($self, @tracks) = @_;
    my $store = $self->track_store;
    $store->clear();
    $self->_track_iter_map({});
    $self->_track_by_id({});
    $self->_playlist(\@tracks);
    $self->_playing_row_ref(undef);

lib/App/DrivePlayer/GUI.pm  view on Meta::CPAN


sub _prev_track {
    my ($self) = @_;
    my $path = $self->_current_path() or return;
    return unless $path->prev();
    $self->_play_at_path($path);
}

# ---- Player callbacks ----

sub _on_track_end {
    my ($self) = @_;
    $self->_next_track();
}

sub _on_position {
    my ($self, $pos, $dur) = @_;
    return if $self->_progress_dragging;
    $self->progress->set_range(0, $dur) if $dur;
    $self->progress->set_value($pos)    if defined $pos;
    $self->time_label->set_text(_sec_str($pos));
    $self->dur_label->set_text(_sec_str($dur));

    # Persist duration when mpv reports it for a track that doesn't have one yet.
    if ($dur && $dur > 0) {
        my $id    = $self->_playing_track_id;
        my $track = $id ? $self->_track_by_id->{$id} : undef;
        if ($track && !$track->{duration_ms}) {
            my $ms = int($dur * 1000);
            $track->{duration_ms} = $ms;   # mutate to prevent firing again
            $self->db->update_track_metadata($track->{id}, duration_ms => $ms);
            $self->_refresh_track_row($track->{id});
        }
    }
}

sub _on_state_change {
    my ($self, $state) = @_;
    my $icon = $state eq 'play' ? 'media-playback-pause' : 'media-playback-start';
    $self->play_btn->set_image(Gtk3::Image->new_from_icon_name($icon, 'button'));
    if ($state eq 'stop') {
        $self->stop_btn->hide();
    } else {
        $self->stop_btn->show();
    }
}

sub _player_poll {
    my ($self) = @_;
    if ($self->player) {
        eval { $self->player->poll() };
        $log->warn("Player poll error: $@") if $@ && $log;
    }
}

# ---- Sidebar activation ----

sub _sidebar_activated {
    my ($self, $view) = @_;
    return if $self->_suppress_sidebar_activated;
    my ($path) = $view->get_cursor();
    return unless $path;
    my $store = $self->sidebar_store;
    my $iter  = $store->get_iter($path);
    my $type  = $store->get($iter, 1);
    my $value = $store->get($iter, 2);
    my $label = $store->get($iter, 0);

    # Resolve the alpha-nav category for the current selection. Leaf rows look
    # up their parent category's label; category headers use their own label.
    my $cat_label;
    if ($type eq 'category') {
        $cat_label = $label;
    } elsif ($type =~ / \A (?: artist | album | genre ) \z /x) {
        my $parent = $store->iter_parent($iter);
        $cat_label = $parent ? $store->get($parent, 0) : undef;
    }
    $self->_update_alpha_category($cat_label);

    if ($type eq 'all') {
        $self->_populate_tracklist($self->db->all_tracks());
    } elsif ($type eq 'artist') {
        $self->_populate_tracklist($self->db->tracks_by_artist($value));
    } elsif ($type eq 'album') {
        $self->_populate_tracklist($self->db->tracks_by_album($value));
    } elsif ($type eq 'genre') {
        $self->_populate_tracklist($self->db->tracks_by_genre($value));
    } elsif ($type eq 'folder') {
        my $sf = $self->db->get_scan_folder_by_drive_id($value) or return;
        $self->_populate_tracklist($self->db->tracks_by_scan_folder($sf->{id}));
    } else {
        return;  # category header selected — no tracklist change
    }

    $self->_set_status(scalar(@{ $self->_playlist }) . ' tracks');
}

sub _sidebar_button_press {
    my ($self, $view, $event) = @_;
    return FALSE unless $event->button == 3;

    my ($path) = $view->get_path_at_pos($event->x, $event->y);
    return FALSE unless $path;

    my $store = $self->sidebar_store;
    my $iter  = $store->get_iter($path);
    my $type  = $store->get($iter, 1);
    return FALSE unless $type eq 'folder';

    my $drive_id = $store->get($iter, 2);
    my $sf       = $self->db->get_scan_folder_by_drive_id($drive_id) or return FALSE;

    my $menu = Gtk3::Menu->new();
    $self->_add_menu_item($menu, 'Fetch Metadata for This Folder', sub {
        $self->_stop_metadata_fetch() if $self->_meta_watch_id;
        $self->_fetch_all_metadata($sf->{id});
    });
    $menu->show_all();
    $menu->popup_at_pointer($event);
    return TRUE;
}



( run in 2.042 seconds using v1.01-cache-2.11-cpan-cdf2f3d4e48 )