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, Serialize, Deserialize)] pub enum SortMode { ByArtist, ByAlbum, ByFilename, } pub struct ArtistView { pub name: String, pub rows: Vec, } pub struct Library { pub all_tracks: Vec, pub artists: Vec, 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, mode: SortMode) -> Self { Self::build_with_filter(tracks, mode, String::new()) } pub fn build_with_filter( mut tracks: Vec, 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 => 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), } } fn build_by_artist( mut tracks: Vec, mode: SortMode, filter: String, ) -> Self { use std::collections::BTreeMap; let mut map: BTreeMap>> = 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, }); } Self::finalize(all_tracks, artists, mode, filter) } fn build_by_album( mut tracks: Vec, mode: SortMode, filter: String, ) -> Self { use std::collections::BTreeMap; // Album -> Artist -> Vec let mut map: BTreeMap>> = 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, 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, groups: Vec, 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, } } 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 { 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(); 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 = 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(group: Option<&ArtistView>) -> usize { group .and_then(|a| { a.rows .iter() .position(|r| matches!(r, RightRow::Track { .. })) }) .unwrap_or(0) }