App-DrivePlayer

 view release on metacpan or  search on metacpan

Changes  view on Meta::CPAN

          against the new config, so a refreshed token takes
          effect without restarting the app.
        - Errors: HTTP 4xx / OAuth-grant failures from Google now
          pop the Settings dialog automatically after the error
          is dismissed, routing the user straight to the
          credentials.  Re-entry from errors fired inside the
          Settings dialog itself (e.g. Find-or-Create) is
          suppressed.
        - Tracklist: Artist, Album and Genre cells are now click
          targets that switch the sidebar to that group; the
          existing cursor-changed signal repopulates the
          tracklist.  A hand cursor on hover indicates the cell
          is clickable.
        - Edit metadata: when an artist / album / genre value
          changes and other tracks share the previous value, a
          single confirmation dialog offers to apply the rename
          across all matching tracks -- so one mojibake fix can
          clean up the lingering sidebar entry instead of leaving
          it stuck behind unrenamed siblings.
        - Edit metadata: small grey help label under the Fetch
          button states that Fetch only fills blank fields, and
          that clearing a populated field lets Fetch replace it.

Changes  view on Meta::CPAN

          only on sheet -> Drive is queried and the track is either
          added to the DB (exists) or removed from the sheet (gone).
          Drive API errors during the existence check preserve the row
          on both sides -- sync never destroys data on an API error.
        - Removed the separate File -> Sync from Sheet / Sync to Sheet
          menu items.  Auto-push after metadata edits/fetches is kept
          as a targeted merge-push, since running a full two-way sync
          after every field edit would be wasteful.
        - Fix: clicking a track in the list no longer auto-scrolls the
          viewport between the two clicks of a double-click, which
          could cause a different track to end up under the cursor and
          be played instead of the one clicked.  The tracklist now
          handles left-clicks manually so GTK's set_cursor auto-scroll
          never fires on click.

0.2.4 2026-04-17
        - Sheet push is now a merge instead of a full replace: local
          non-blank values overwrite the sheet, local blanks preserve
          existing sheet values, and drive_ids only present on the sheet
          are kept intact.  Prevents a device with partially-populated
          metadata from wiping data pushed from another device.
        - No longer auto-fetches metadata on startup.  If another device
          has already populated metadata and synced it to the sheet, a

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

    $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);

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

    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'

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

    # 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);

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

        $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;
            }

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

            # 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);

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

# 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) = @_;

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

        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;



( run in 1.948 second using v1.01-cache-2.11-cpan-cdf2f3d4e48 )