266 lines
8.2 KiB
Rust
266 lines
8.2 KiB
Rust
use ratatui::widgets::ListState;
|
||
|
||
use crate::track::Track;
|
||
|
||
#[derive(Clone)]
|
||
pub enum RightRow {
|
||
AlbumHeader { name: String },
|
||
Track { index_in_library: usize },
|
||
}
|
||
|
||
#[derive(Clone, Copy, PartialEq)]
|
||
pub enum SortMode {
|
||
ByArtist,
|
||
ByFilename,
|
||
}
|
||
|
||
pub struct ArtistView {
|
||
pub name: String,
|
||
pub rows: Vec<RightRow>,
|
||
}
|
||
|
||
pub struct Library {
|
||
pub all_tracks: Vec<Track>,
|
||
pub artists: Vec<ArtistView>,
|
||
pub artist_selected: usize,
|
||
pub right_selected: usize,
|
||
pub sort_mode: SortMode,
|
||
/// Активный фильтр поиска (пусто — без фильтра)
|
||
pub filter: String,
|
||
/// Состояние скролла левой панели (сохраняется между кадрами)
|
||
pub left_state: ListState,
|
||
/// Состояние скролла правой панели
|
||
pub right_state: ListState,
|
||
}
|
||
|
||
impl Library {
|
||
pub fn build(tracks: Vec<Track>, mode: SortMode) -> Self {
|
||
Self::build_with_filter(tracks, mode, String::new())
|
||
}
|
||
|
||
pub fn build_with_filter(
|
||
mut tracks: Vec<Track>,
|
||
mode: SortMode,
|
||
filter: String,
|
||
) -> Self {
|
||
// Применяем фильтр
|
||
if !filter.is_empty() {
|
||
let f = filter.to_lowercase();
|
||
tracks.retain(|t| {
|
||
t.title.to_lowercase().contains(&f)
|
||
|| t.artist.to_lowercase().contains(&f)
|
||
|| t.album.to_lowercase().contains(&f)
|
||
});
|
||
}
|
||
|
||
match mode {
|
||
SortMode::ByArtist => {
|
||
use std::collections::BTreeMap;
|
||
|
||
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);
|
||
}
|
||
|
||
let mut all_tracks = Vec::new();
|
||
let mut artists = Vec::new();
|
||
|
||
for (artist_name, albums_map) in map {
|
||
let mut rows = 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))
|
||
});
|
||
|
||
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 i in 0..all_tracks.len() {
|
||
rows.push(RightRow::Track {
|
||
index_in_library: i,
|
||
});
|
||
}
|
||
|
||
let artists = vec![ArtistView {
|
||
name: "Все файлы".into(),
|
||
rows,
|
||
}];
|
||
|
||
let right_selected = first_track_row_in(artists.first());
|
||
|
||
let mut left_state = ListState::default();
|
||
left_state.select(Some(0));
|
||
let mut right_state = ListState::default();
|
||
right_state.select(Some(right_selected));
|
||
|
||
Library {
|
||
all_tracks,
|
||
artists,
|
||
artist_selected: 0,
|
||
right_selected,
|
||
sort_mode: mode,
|
||
filter,
|
||
left_state,
|
||
right_state,
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
pub fn track(&self, index: usize) -> Option<&Track> {
|
||
self.all_tracks.get(index)
|
||
}
|
||
|
||
pub fn current_right_rows(&self) -> &[RightRow] {
|
||
self.artists
|
||
.get(self.artist_selected)
|
||
.map(|a| a.rows.as_slice())
|
||
.unwrap_or(&[])
|
||
}
|
||
|
||
pub fn right_track_index(&self) -> Option<usize> {
|
||
match self.current_right_rows().get(self.right_selected)? {
|
||
RightRow::Track { index_in_library } => Some(*index_in_library),
|
||
RightRow::AlbumHeader { .. } => None,
|
||
}
|
||
}
|
||
|
||
pub fn artist_move(&mut self, delta: isize) {
|
||
if self.artists.is_empty() {
|
||
return;
|
||
}
|
||
let len = self.artists.len() as isize;
|
||
let mut new = self.artist_selected as isize + delta;
|
||
if new < 0 {
|
||
new = len - 1;
|
||
} else if new >= len {
|
||
new = 0;
|
||
}
|
||
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));
|
||
}
|
||
|
||
pub fn right_move(&mut self, delta: isize) {
|
||
let track_rows: Vec<usize> = self
|
||
.current_right_rows()
|
||
.iter()
|
||
.enumerate()
|
||
.filter_map(|(i, r)| matches!(r, RightRow::Track { .. }).then_some(i))
|
||
.collect();
|
||
|
||
if track_rows.is_empty() {
|
||
return;
|
||
}
|
||
|
||
let current_pos = track_rows
|
||
.iter()
|
||
.position(|&i| i == self.right_selected)
|
||
.unwrap_or(0) as isize;
|
||
|
||
let len = track_rows.len() as isize;
|
||
let mut new_pos = current_pos + delta;
|
||
if new_pos < 0 {
|
||
new_pos = len - 1;
|
||
} else if new_pos >= len {
|
||
new_pos = 0;
|
||
}
|
||
|
||
self.right_selected = track_rows[new_pos as usize];
|
||
self.right_state.select(Some(self.right_selected));
|
||
}
|
||
|
||
pub fn right_goto_start(&mut self) {
|
||
self.right_move(-1000000);
|
||
}
|
||
|
||
pub fn right_goto_end(&mut self) {
|
||
self.right_move(1000000);
|
||
}
|
||
|
||
fn first_track_row(&self) -> usize {
|
||
self.current_right_rows()
|
||
.iter()
|
||
.position(|r| matches!(r, RightRow::Track { .. }))
|
||
.unwrap_or(0)
|
||
}
|
||
|
||
pub fn sync_with_playing(&mut self, index_in_library: usize) {
|
||
for (ai, artist) in self.artists.iter().enumerate() {
|
||
if let Some(pos) = artist.rows.iter().position(|r| {
|
||
matches!(r, RightRow::Track { index_in_library: i } if *i == index_in_library)
|
||
}) {
|
||
self.artist_selected = ai;
|
||
self.right_selected = pos;
|
||
self.left_state.select(Some(ai));
|
||
self.right_state.select(Some(pos));
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
fn first_track_row_in(artist: Option<&ArtistView>) -> usize {
|
||
artist
|
||
.and_then(|a| {
|
||
a.rows
|
||
.iter()
|
||
.position(|r| matches!(r, RightRow::Track { .. }))
|
||
})
|
||
.unwrap_or(0)
|
||
} |