feat(slint): A-Z jump strip for favorites tracks + albums grouping
Add an AlphaStrip overlaid on the right edge when grouping alphabetically: tracks "Group: Name" (jumps the virtualized ListView to the first row of a letter — accurate, uniform 50px rows) and albums "Group: A-Z" (jumps the grid Flickable proportionally by section ordinal — approximate, always lands the section in view, per the agreed ArtistView-style behavior). derive_tracks/derive_albums build the per-letter jump model (tracks-alpha / albums-alpha). cc
This commit is contained in:
3 files changed
+97
-1
No files matched your search
@@ -21,7 +21,7 @@ use slint::{ComponentHandle, Model, ModelRc, VecModel};
|
||||
use crate::album_map::{self, map_album, to_item, AlbumCard};
|
||||
use crate::artwork::{ArtworkJob, ArtworkTarget};
|
||||
use crate::{
|
||||
AlbumCardItem, AppWindow, DiscoverSection, FavoriteArtistItem, FavoriteLabelItem,
|
||||
AlbumCardItem, AlphaJump, AppWindow, DiscoverSection, FavoriteArtistItem, FavoriteLabelItem,
|
||||
FavoritePlaylistItem, FavoritesState, TrackItem,
|
||||
};
|
||||
|
||||
@@ -512,6 +512,7 @@ pub fn derive_tracks(window: &AppWindow) {
|
||||
let group = state.get_tracks_group_mode().to_string();
|
||||
let genre_names = crate::genre_filter::selected_names("favorites");
|
||||
let all = state.get_tracks();
|
||||
state.set_tracks_alpha(ModelRc::new(VecModel::from(Vec::<AlphaJump>::new())));
|
||||
// Fast path: no search + no grouping + no genre filter -> share model.
|
||||
if query.is_empty() && group == "off" && genre_names.is_empty() {
|
||||
state.set_tracks_visible(all);
|
||||
@@ -544,6 +545,22 @@ pub fn derive_tracks(window: &AppWindow) {
|
||||
"name" => filtered.sort_by(|a, b| lc(&a.title).cmp(&lc(&b.title))),
|
||||
_ => {}
|
||||
}
|
||||
// A-Z jump strip for name grouping: first row index per distinct initial.
|
||||
if group == "name" {
|
||||
let mut jumps: Vec<AlphaJump> = Vec::new();
|
||||
let mut last = String::new();
|
||||
for (i, t) in filtered.iter().enumerate() {
|
||||
let key = album_alpha_key(t.title.as_str());
|
||||
if key != last {
|
||||
jumps.push(AlphaJump {
|
||||
letter: key.clone().into(),
|
||||
index: i as i32,
|
||||
});
|
||||
last = key;
|
||||
}
|
||||
}
|
||||
state.set_tracks_alpha(ModelRc::new(VecModel::from(jumps)));
|
||||
}
|
||||
state.set_tracks_visible(ModelRc::new(VecModel::from(filtered)));
|
||||
}
|
||||
|
||||
@@ -583,6 +600,7 @@ pub fn derive_albums(window: &AppWindow) {
|
||||
let group = state.get_albums_group_mode().to_string();
|
||||
let genre_names = crate::genre_filter::selected_names("favorites");
|
||||
let all = state.get_albums();
|
||||
state.set_albums_alpha(ModelRc::new(VecModel::from(Vec::<AlphaJump>::new())));
|
||||
let empty_sections = || ModelRc::new(VecModel::from(Vec::<DiscoverSection>::new()));
|
||||
|
||||
// Fast path: no filter, default order, no grouping, no genre -> share.
|
||||
@@ -638,6 +656,18 @@ pub fn derive_albums(window: &AppWindow) {
|
||||
(false, true) => std::cmp::Ordering::Greater,
|
||||
_ => a.to_lowercase().cmp(&b.to_lowercase()),
|
||||
});
|
||||
// A-Z jump strip for alpha grouping: the section letters in order.
|
||||
if group == "alpha" {
|
||||
let alpha: Vec<AlphaJump> = map
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, (k, _))| AlphaJump {
|
||||
letter: k.clone().into(),
|
||||
index: i as i32,
|
||||
})
|
||||
.collect();
|
||||
state.set_albums_alpha(ModelRc::new(VecModel::from(alpha)));
|
||||
}
|
||||
let sections: Vec<DiscoverSection> = map
|
||||
.into_iter()
|
||||
.map(|(key, items)| DiscoverSection {
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
DragState,
|
||||
GenreFilterState,
|
||||
GenreFilterActions,
|
||||
AlphaJump,
|
||||
} from "../state.slint";
|
||||
import { AlbumCollectionView } from "../discover/AlbumCollectionView.slint";
|
||||
import { GenreFilterPopup } from "../discover/GenreFilterPopup.slint";
|
||||
@@ -366,6 +367,37 @@ component ViewToggle inherits Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
// Vertical A-Z jump strip overlaid on the right edge of a grouped list /
|
||||
// grid. Emits jump(ordinal, index); the host scrolls its list/flickable.
|
||||
component AlphaStrip inherits Rectangle {
|
||||
in property <[AlphaJump]> jumps;
|
||||
callback jump(int /* ordinal */, int /* index */);
|
||||
width: 18px;
|
||||
|
||||
VerticalLayout {
|
||||
alignment: center;
|
||||
spacing: 0px;
|
||||
for j[k] in root.jumps: Rectangle {
|
||||
height: 15px;
|
||||
width: 18px;
|
||||
Text {
|
||||
text: j.letter;
|
||||
color: la.has-hover ? Theme.accent : Theme.text-muted;
|
||||
font-size: 9px;
|
||||
font-weight: Typography.semibold;
|
||||
horizontal-alignment: center;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
la := TouchArea {
|
||||
mouse-cursor: pointer;
|
||||
clicked => {
|
||||
root.jump(k, j.index);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export component FavoritesView inherits Rectangle {
|
||||
callback open-album(string);
|
||||
callback open-artist(string);
|
||||
@@ -740,6 +772,18 @@ export component FavoritesView inherits Rectangle {
|
||||
visible-height: track-list.visible-height;
|
||||
viewport-y <=> track-list.viewport-y;
|
||||
}
|
||||
// A-Z jump strip (name grouping) — accurate (uniform rows).
|
||||
if FavoritesState.tracks-group-mode == "name"
|
||||
&& FavoritesState.tracks-alpha.length > 0: AlphaStrip {
|
||||
x: parent.width - self.width - 20px;
|
||||
height: 100%;
|
||||
jumps: FavoritesState.tracks-alpha;
|
||||
jump(ord, idx) => {
|
||||
track-list.viewport-y = -Math.min(
|
||||
idx * 50px / 1px,
|
||||
Math.max(0, (track-list.viewport-height - track-list.visible-height) / 1px)) * 1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -866,6 +910,18 @@ export component FavoritesView inherits Rectangle {
|
||||
visible-height: other-flick.height;
|
||||
viewport-y <=> other-flick.viewport-y;
|
||||
}
|
||||
// A-Z jump strip (albums alpha grouping) — proportional jump.
|
||||
if FavoritesState.active-tab == "albums"
|
||||
&& FavoritesState.albums-group-mode == "alpha"
|
||||
&& FavoritesState.albums-alpha.length > 0: AlphaStrip {
|
||||
x: parent.width - self.width - 20px;
|
||||
height: 100%;
|
||||
jumps: FavoritesState.albums-alpha;
|
||||
jump(ord, idx) => {
|
||||
other-flick.viewport-y = -(ord / Math.max(1, FavoritesState.albums-alpha.length - 1))
|
||||
* Math.max(0, (other-flick.viewport-height - other-flick.height) / 1px) * 1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,14 @@ export struct AlbumCardItem {
|
||||
plain-year: string, // "1973" — bare year for the list-row YEAR column
|
||||
}
|
||||
|
||||
// One entry in an A-Z jump strip (favorites tracks name-group / albums
|
||||
// A-Z group). `index` is the first row index (tracks); albums jump
|
||||
// proportionally by the entry's ordinal so it needs no exact offset.
|
||||
export struct AlphaJump {
|
||||
letter: string,
|
||||
index: int,
|
||||
}
|
||||
|
||||
// One button in the shared multi-select bulk bar (MultiSelectBar).
|
||||
export struct BulkAction {
|
||||
id: string, // emitted verbatim via MultiSelectBar.action(id)
|
||||
@@ -1463,6 +1471,7 @@ export global FavoritesState {
|
||||
in property <[TrackItem]> tracks-visible: [];
|
||||
in-out property <string> tracks-search: "";
|
||||
in-out property <string> tracks-group-mode: "off"; // off|album|artist|name
|
||||
in property <[AlphaJump]> tracks-alpha: []; // A-Z jumps (name grouping)
|
||||
in-out property <bool> tracks-multi-select: false;
|
||||
in property <int> tracks-selected-count: 0;
|
||||
in property <[AlbumCardItem]> albums: [];
|
||||
@@ -1472,6 +1481,7 @@ export global FavoritesState {
|
||||
in-out property <string> albums-search: "";
|
||||
in-out property <string> albums-sort-by: "default"; // default|title-asc|title-desc|artist-asc
|
||||
in-out property <string> albums-group-mode: "off"; // off|alpha|artist
|
||||
in property <[AlphaJump]> albums-alpha: []; // A-Z jumps (alpha grouping)
|
||||
in property <int> albums-shown: 0; // count after filter (grouped or flat)
|
||||
in property <[FavoriteArtistItem]> artists: [];
|
||||
in property <[FavoritePlaylistItem]> playlists: [];
|
||||
|
||||
Reference in new issue
Block a user