обновление

This commit is contained in:
2026-10-15 15:54:31 +02:00
parent 4c7f46e105
commit 07baff09c4
9 changed files with 571 additions and 196 deletions

View File

@@ -1,16 +1,20 @@
use ratatui::widgets::ListState;
use serde::{Deserialize, Serialize};
use crate::track::Track;
#[derive(Clone)]
pub enum RightRow {
/// Заголовок группы: в режиме ByArtist — название альбома,
/// в режиме ByAlbum — имя исполнителя.
AlbumHeader { name: String },
Track { index_in_library: usize },
}
#[derive(Clone, Copy, PartialEq)]
#[derive(Clone, Copy, PartialEq, Serialize, Deserialize)]
pub enum SortMode {
ByArtist,
ByAlbum,
ByFilename,
}
@@ -25,11 +29,8 @@ pub struct Library {
pub artist_selected: usize,
pub right_selected: usize,
pub sort_mode: SortMode,
/// Активный фильтр поиска (пусто — без фильтра)
pub filter: String,
/// Состояние скролла левой панели (сохраняется между кадрами)
pub left_state: ListState,
/// Состояние скролла правой панели
pub right_state: ListState,
}
@@ -43,7 +44,6 @@ impl Library {
mode: SortMode,
filter: String,
) -> Self {
// Применяем фильтр
if !filter.is_empty() {
let f = filter.to_lowercase();
tracks.retain(|t| {
@@ -54,108 +54,165 @@ impl Library {
}
match mode {
SortMode::ByArtist => {
use std::collections::BTreeMap;
SortMode::ByArtist => Self::build_by_artist(tracks, mode, filter),
SortMode::ByAlbum => Self::build_by_album(tracks, mode, filter),
SortMode::ByFilename => Self::build_by_filename(tracks, mode, filter),
}
}
let mut map: BTreeMap<String, BTreeMap<String, Vec<Track>>> = BTreeMap::new();
for t in tracks.drain(..) {
map.entry(t.artist.clone())
.or_default()
.entry(t.album.clone())
.or_default()
.push(t);
}
fn build_by_artist(
mut tracks: Vec<Track>,
mode: SortMode,
filter: String,
) -> Self {
use std::collections::BTreeMap;
let mut all_tracks = Vec::new();
let mut artists = Vec::new();
let mut map: BTreeMap<String, BTreeMap<String, Vec<Track>>> = BTreeMap::new();
for t in tracks.drain(..) {
map.entry(t.artist.clone())
.or_default()
.entry(t.album.clone())
.or_default()
.push(t);
}
for (artist_name, albums_map) in map {
let mut rows = Vec::new();
let mut all_tracks = Vec::new();
let mut artists = Vec::new();
for (album_name, mut album_tracks) in albums_map {
album_tracks.sort_by(|a, b| {
a.track_number
.unwrap_or(u32::MAX)
.cmp(&b.track_number.unwrap_or(u32::MAX))
.then(a.title.cmp(&b.title))
});
for (artist_name, albums_map) in map {
let mut rows = Vec::new();
rows.push(RightRow::AlbumHeader { name: album_name });
for t in album_tracks {
let idx = all_tracks.len();
all_tracks.push(t);
rows.push(RightRow::Track {
index_in_library: idx,
});
}
}
artists.push(ArtistView {
name: artist_name,
rows,
});
}
// Вычисляем right_selected ДО перемещения artists
let right_selected = first_track_row_in(artists.first());
let mut left_state = ListState::default();
let mut right_state = ListState::default();
if !artists.is_empty() {
left_state.select(Some(0));
}
right_state.select(Some(right_selected));
Library {
all_tracks,
artists,
artist_selected: 0,
right_selected,
sort_mode: mode,
filter,
left_state,
right_state,
}
}
SortMode::ByFilename => {
tracks.sort_by(|a, b| a.path.cmp(&b.path));
let all_tracks = tracks;
let mut rows = Vec::new();
rows.push(RightRow::AlbumHeader {
name: "Все файлы".into(),
for (album_name, mut album_tracks) in albums_map {
album_tracks.sort_by(|a, b| {
a.track_number
.unwrap_or(u32::MAX)
.cmp(&b.track_number.unwrap_or(u32::MAX))
.then(a.title.cmp(&b.title))
});
for i in 0..all_tracks.len() {
rows.push(RightRow::AlbumHeader { name: album_name });
for t in album_tracks {
let idx = all_tracks.len();
all_tracks.push(t);
rows.push(RightRow::Track {
index_in_library: i,
index_in_library: idx,
});
}
}
let artists = vec![ArtistView {
name: "Все файлы".into(),
rows,
}];
artists.push(ArtistView {
name: artist_name,
rows,
});
}
let right_selected = first_track_row_in(artists.first());
Self::finalize(all_tracks, artists, mode, filter)
}
let mut left_state = ListState::default();
left_state.select(Some(0));
let mut right_state = ListState::default();
right_state.select(Some(right_selected));
fn build_by_album(
mut tracks: Vec<Track>,
mode: SortMode,
filter: String,
) -> Self {
use std::collections::BTreeMap;
Library {
all_tracks,
artists,
artist_selected: 0,
right_selected,
sort_mode: mode,
filter,
left_state,
right_state,
// Album -> Artist -> Vec<Track>
let mut map: BTreeMap<String, BTreeMap<String, Vec<Track>>> = BTreeMap::new();
for t in tracks.drain(..) {
map.entry(t.album.clone())
.or_default()
.entry(t.artist.clone())
.or_default()
.push(t);
}
let mut all_tracks = Vec::new();
let mut albums = Vec::new();
for (album_name, artists_map) in map {
let mut rows = Vec::new();
for (artist_name, mut artist_tracks) in artists_map {
artist_tracks.sort_by(|a, b| {
a.track_number
.unwrap_or(u32::MAX)
.cmp(&b.track_number.unwrap_or(u32::MAX))
.then(a.title.cmp(&b.title))
});
// Заголовок группы = имя исполнителя
rows.push(RightRow::AlbumHeader { name: artist_name });
for t in artist_tracks {
let idx = all_tracks.len();
all_tracks.push(t);
rows.push(RightRow::Track {
index_in_library: idx,
});
}
}
albums.push(ArtistView {
name: album_name,
rows,
});
}
Self::finalize(all_tracks, albums, mode, filter)
}
fn build_by_filename(
mut tracks: Vec<Track>,
mode: SortMode,
filter: String,
) -> Self {
tracks.sort_by(|a, b| a.path.cmp(&b.path));
let all_tracks = tracks;
let mut rows = Vec::new();
rows.push(RightRow::AlbumHeader {
name: "Все файлы".into(),
});
for i in 0..all_tracks.len() {
rows.push(RightRow::Track {
index_in_library: i,
});
}
let groups = vec![ArtistView {
name: "Все файлы".into(),
rows,
}];
Self::finalize(all_tracks, groups, mode, filter)
}
/// Общая инициализация ListState и позиций курсора.
fn finalize(
all_tracks: Vec<Track>,
groups: Vec<ArtistView>,
mode: SortMode,
filter: String,
) -> Self {
let right_selected = first_track_row_in(groups.first());
let mut left_state = ListState::default();
let mut right_state = ListState::default();
if !groups.is_empty() {
left_state.select(Some(0));
}
right_state.select(Some(right_selected));
Library {
all_tracks,
artists: groups,
artist_selected: 0,
right_selected,
sort_mode: mode,
filter,
left_state,
right_state,
}
}
@@ -191,7 +248,6 @@ impl Library {
self.artist_selected = new as usize;
self.right_selected = self.first_track_row();
// Синхронизируем ListState — иначе ratatui не знает о смене позиции
self.left_state.select(Some(self.artist_selected));
self.right_state.select(Some(self.right_selected));
}
@@ -255,8 +311,8 @@ impl Library {
}
}
fn first_track_row_in(artist: Option<&ArtistView>) -> usize {
artist
fn first_track_row_in(group: Option<&ArtistView>) -> usize {
group
.and_then(|a| {
a.rows
.iter()