diff --git a/src.7z b/src.7z new file mode 100644 index 0000000..ef9875c Binary files /dev/null and b/src.7z differ diff --git a/src/library.rs b/src/library.rs index 801215b..aecebbb 100644 --- a/src/library.rs +++ b/src/library.rs @@ -1,3 +1,5 @@ +use ratatui::widgets::ListState; + use crate::track::Track; #[derive(Clone)] @@ -25,6 +27,10 @@ pub struct Library { pub sort_mode: SortMode, /// Активный фильтр поиска (пусто — без фильтра) pub filter: String, + /// Состояние скролла левой панели (сохраняется между кадрами) + pub left_state: ListState, + /// Состояние скролла правой панели + pub right_state: ListState, } impl Library { @@ -91,13 +97,25 @@ impl Library { }); } + // Вычисляем 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: 0, + right_selected, sort_mode: mode, filter, + left_state, + right_state, } } @@ -120,13 +138,22 @@ impl Library { 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: 0, + right_selected, sort_mode: mode, filter, + left_state, + right_state, } } } @@ -163,6 +190,10 @@ 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)); } pub fn right_move(&mut self, delta: isize) { @@ -191,6 +222,7 @@ impl Library { } self.right_selected = track_rows[new_pos as usize]; + self.right_state.select(Some(self.right_selected)); } pub fn right_goto_start(&mut self) { @@ -215,8 +247,20 @@ impl 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) } \ No newline at end of file diff --git a/src/ui.rs b/src/ui.rs index 85f39fb..f3bb43c 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1,6 +1,6 @@ use ratatui::prelude::*; use ratatui::widgets::{ - Bar, BarChart, BarGroup, Block, Borders, Gauge, List, ListItem, ListState, Paragraph, + Bar, BarChart, BarGroup, Block, Borders, Gauge, List, ListItem, Paragraph, }; use crate::app::{App, InputMode, Panel}; @@ -30,7 +30,7 @@ pub fn draw(f: &mut Frame, app: &mut App) { draw_status(f, app, chunks[3]); } -fn draw_left_panel(f: &mut Frame, app: &App, area: Rect) { +fn draw_left_panel(f: &mut Frame, app: &mut App, area: Rect) { let items: Vec = app .library .artists @@ -44,17 +44,20 @@ fn draw_left_panel(f: &mut Frame, app: &App, area: Rect) { Style::default().add_modifier(Modifier::DIM) }; - let mut list_state = ListState::default(); - if !app.library.artists.is_empty() { - list_state.select(Some(app.library.artist_selected)); - } + // Обновляем selected в сохранённом состоянии (не создаём новый ListState) + let sel = if app.library.artists.is_empty() { + None + } else { + Some(app.library.artist_selected) + }; + app.library.left_state.select(sel); let list = List::new(items) .block(Block::default().borders(Borders::ALL).title(" Исполнители ")) .highlight_style(highlight) .highlight_symbol("> "); - f.render_stateful_widget(list, area, &mut list_state); + f.render_stateful_widget(list, area, &mut app.library.left_state); } fn fmt_dur(d: std::time::Duration) -> String { @@ -65,7 +68,7 @@ fn fmt_dur(d: std::time::Duration) -> String { } } -fn draw_right_panel(f: &mut Frame, app: &App, area: Rect) { +fn draw_right_panel(f: &mut Frame, app: &mut App, area: Rect) { let rows = app.library.current_right_rows(); let items: Vec = rows @@ -91,12 +94,12 @@ fn draw_right_panel(f: &mut Frame, app: &App, area: Rect) { let pad = inner_width .saturating_sub(name.len() + total_str.len() + 3) .max(1); - let line = format!("{} {} {}", name, "─".repeat(pad), total_str); + let line = format!("{} ] {} {}", name, "─".repeat(pad), total_str); let album_color = Color::Rgb(100, 149, 237); ListItem::new(Line::from(vec![ - Span::styled("▶ ", Style::default().fg(album_color)), + Span::styled("[ ", Style::default().fg(album_color)), Span::styled( line, Style::default() @@ -137,6 +140,7 @@ fn draw_right_panel(f: &mut Frame, app: &App, area: Rect) { }) .collect(); + // Заголовок с индикатором активного фильтра let title = match app.library.sort_mode { SortMode::ByArtist => { let artist_name = app @@ -145,9 +149,22 @@ fn draw_right_panel(f: &mut Frame, app: &App, area: Rect) { .get(app.library.artist_selected) .map(|a| a.name.as_str()) .unwrap_or("—"); - format!(" Альбомы и треки — {} ", artist_name) + if app.library.filter.is_empty() { + format!(" Альбомы и треки — {} ", artist_name) + } else { + format!( + " Альбомы и треки — {} [{}] ", + artist_name, app.library.filter + ) + } + } + SortMode::ByFilename => { + if app.library.filter.is_empty() { + " Альбомы и треки ".into() + } else { + format!(" Альбомы и треки [{}] ", app.library.filter) + } } - SortMode::ByFilename => " Альбомы и треки ".into(), }; let highlight = if app.active_panel == Panel::Right { @@ -156,24 +173,26 @@ fn draw_right_panel(f: &mut Frame, app: &App, area: Rect) { Style::default().add_modifier(Modifier::DIM) }; - let mut list_state = ListState::default(); - if !rows.is_empty() { - list_state.select(Some(app.library.right_selected)); - } + let sel = if rows.is_empty() { + None + } else { + Some(app.library.right_selected) + }; + app.library.right_state.select(sel); let list = List::new(items) .block(Block::default().borders(Borders::ALL).title(title)) .highlight_style(highlight) .highlight_symbol("> "); - f.render_stateful_widget(list, area, &mut list_state); + f.render_stateful_widget(list, area, &mut app.library.right_state); } /// Цветной эквалайзер: 32 столбика с градиентом от синего к красному fn draw_equalizer(f: &mut Frame, app: &App, area: Rect) { let bars_data = app.player.spectrum.bars(); let num_bars = 32; - + let mut display: Vec = Vec::with_capacity(num_bars); for i in 0..num_bars { let a = bars_data[i * 2]; @@ -230,7 +249,7 @@ fn draw_progress(f: &mut Frame, app: &App, area: Rect) { let gauge = Gauge::default() .block(Block::default().borders(Borders::ALL).title(" Прогресс ")) - .gauge_style(Style::default().fg(Color::Green)) + .gauge_style(Style::default().fg(Color::Rgb(70, 130, 180))) .ratio(ratio) .label(format!( "{:02}:{:02} / {:02}:{:02}", @@ -253,10 +272,13 @@ fn draw_status(f: &mut Frame, app: &App, area: Rect) { ]) .split(area); + // Если поиск активен — показываем строку ввода. + // Иначе — статус с индикатором паузы. let (status_text, status_title) = if app.input_mode == InputMode::Search { (format!("> {}", app.search_buffer), " Поиск ") } else { - (app.status.clone(), " Статус ") + let prefix = if app.player.is_paused() { "⏸ " } else { "" }; + (format!("{}{}", prefix, app.status), " Статус ") }; let status = Paragraph::new(status_text) @@ -276,9 +298,9 @@ fn draw_status(f: &mut Frame, app: &App, area: Rect) { .block(Block::default().borders(Borders::ALL).title(" Mode ")); f.render_widget(mode, inner[1]); - let vol_text = format!("vol: {}%", (app.player.volume() * 100.0) as u32); + let vol_text = format!("Vol: {}%", (app.player.volume() * 100.0) as u32); let vol = Paragraph::new(vol_text) .alignment(Alignment::Center) - .block(Block::default().borders(Borders::ALL).title(" Звук ")); + .block(Block::default().borders(Borders::ALL).title(" ")); f.render_widget(vol, inner[2]); } \ No newline at end of file