Files
rumus/src/library.rs
2026-10-15 20:08:57 +02:00

322 lines
9.1 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 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<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 => 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<Track>,
mode: SortMode,
filter: String,
) -> Self {
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,
});
}
Self::finalize(all_tracks, artists, mode, filter)
}
fn build_by_album(
mut tracks: Vec<Track>,
mode: SortMode,
filter: String,
) -> Self {
use std::collections::BTreeMap;
// 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,
}
}
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();
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(group: Option<&ArtistView>) -> usize {
group
.and_then(|a| {
a.rows
.iter()
.position(|r| matches!(r, RightRow::Track { .. }))
})
.unwrap_or(0)
}