Files
rumus/src/library.rs
2026-10-14 01:41:43 +02:00

266 lines
8.2 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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)
}