правка работы с кешем, добавлены команды для редоктирование бибилиотеки

This commit is contained in:
2026-10-17 15:36:45 +02:00
parent 4c742ea210
commit 52ca495a6e
5 changed files with 191 additions and 22 deletions

View File

@@ -41,6 +41,8 @@ pub struct App {
pub repeat: RepeatMode,
pub shuffle: bool,
pub show_help: bool,
/// Показывать окно управления директориями
pub show_dirs: bool,
status_set_at: Instant,
}
@@ -49,9 +51,9 @@ const STATUS_RESET_AFTER: Duration = Duration::from_secs(2);
impl App {
pub fn new(music_dir: &str) -> Result<Self, Box<dyn std::error::Error>> {
let saved = crate::state::load();
// Пробуем загрузить кэш. Если его нет или он пустой - сканируем папку.
let tracks = match crate::state::load_cache() {
// Загружаем кэш
let cached_tracks = crate::state::load_cache();
let tracks = match cached_tracks {
Some(cached) if !cached.is_empty() => cached,
_ => {
let scanned = scan_dir(music_dir);
@@ -79,8 +81,20 @@ impl App {
repeat: RepeatMode::Off,
shuffle: false,
show_help: false,
show_dirs: false,
status_set_at: Instant::now(),
};
// ВАЖНО: Если есть extra_dirs, но в кэше нет треков из них,
// пересобираем библиотеку с нуля (сканируем все директории).
if !app.extra_dirs.is_empty() {
// Проверяем, есть ли в кэше треки не из базовой директории
let base_dir = std::path::Path::new(music_dir);
let has_extra_tracks = app.raw_tracks.iter().any(|t| !t.path.starts_with(base_dir));
if !has_extra_tracks {
// Кэш не содержит extra_dirs — пересканируем всё
app.rebuild_from_dirs();
}
}
if let Some(vol) = saved.volume {
app.player.set_volume(vol);
@@ -197,7 +211,13 @@ impl App {
match name {
"add" => self.add_dir(args),
"reload" => self.reload_library(),
"save" => self.save_now(),
"save" => self.save_now(),
"dirs" => {
self.show_dirs = true;
self.set_status("Список директорий".into());
}
"rmdir" => self.rmdir(args),
"clear" => self.clear_extra_dirs(),
"help" => {
self.show_help = true;
self.set_status("Справка".into());
@@ -256,7 +276,43 @@ impl App {
self.set_status(format!("Добавлено {} треков из {}", added, expanded));
}
pub fn reload_library(&mut self) {
fn rmdir(&mut self, arg: &str) {
let idx: usize = match arg.parse() {
Ok(i) => i,
Err(_) => {
self.set_status("Использование: :rmdir <индекс>".into());
return;
}
};
if idx == 0 {
self.set_status("Нельзя удалить базовую директорию".into());
return;
}
let extra_idx = idx - 1;
if extra_idx >= self.extra_dirs.len() {
self.set_status(format!("Нет директории с индексом {}", idx));
return;
}
let removed = self.extra_dirs.remove(extra_idx);
self.set_status(format!("Удалена директория: {}", removed));
self.rebuild_from_dirs();
}
fn clear_extra_dirs(&mut self) {
if self.extra_dirs.is_empty() {
self.set_status("extra_dirs уже пуст".into());
return;
}
self.extra_dirs.clear();
self.rebuild_from_dirs();
self.set_status("Все дополнительные директории удалены".into());
}
/// Пересканировать только оставшиеся директории и пересобрать библиотеку
fn rebuild_from_dirs(&mut self) {
let mut dirs = vec![self.music_dir.clone()];
dirs.extend(self.extra_dirs.clone());
@@ -271,9 +327,14 @@ impl App {
self.raw_tracks = all;
self.deleted_paths.clear();
crate::state::save_cache(&self.raw_tracks);
self.rebuild_library();
}
pub fn reload_library(&mut self) {
self.rebuild_from_dirs();
self.set_status(format!("Пересканировано {} треков", self.raw_tracks.len()));
}
@@ -795,6 +856,16 @@ impl App {
};
self.set_status(msg);
}
// ── Геттеры для UI ─────────────────────────────────────────
pub fn base_dir(&self) -> &str {
&self.music_dir
}
pub fn extra_dirs(&self) -> &[String] {
&self.extra_dirs
}
}
/// Развернуть `~/...` в путь домашней директории.

View File

@@ -26,7 +26,6 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut app = App::new(&music_dir)?;
loop {
// Проверяем флаг выхода, выставленный командой :q
if app.request_quit {
app.on_exit();
break;
@@ -57,11 +56,13 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
fn handle_key(key: KeyEvent, app: &mut App) -> bool {
let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
// F1 переключает справку в любом режиме
if key.code == KeyCode::F(1) {
app.toggle_help();
return false;
}
// Если открыта справка — только Esc её закрывает
if app.show_help {
if key.code == KeyCode::Esc {
app.toggle_help();
@@ -69,6 +70,15 @@ fn handle_key(key: KeyEvent, app: &mut App) -> bool {
return false;
}
// Если открыто окно директорий — Esc его закрывает
if app.show_dirs {
if key.code == KeyCode::Esc {
app.show_dirs = false;
}
return false;
}
// Ввод команды
if app.input_mode == InputMode::Command {
match key.code {
KeyCode::Esc => app.cancel_command(),
@@ -80,6 +90,7 @@ fn handle_key(key: KeyEvent, app: &mut App) -> bool {
return false;
}
// Ctrl-комбинации
if ctrl {
match key.code {
KeyCode::Right => {
@@ -105,6 +116,7 @@ fn handle_key(key: KeyEvent, app: &mut App) -> bool {
}
}
// Ввод поиска
if app.input_mode == InputMode::Search {
match key.code {
KeyCode::Esc => app.cancel_search(),
@@ -116,11 +128,13 @@ fn handle_key(key: KeyEvent, app: &mut App) -> bool {
return false;
}
// Delete
if key.code == KeyCode::Delete {
app.delete_selected();
return false;
}
// Обычные клавиши
match key.code {
KeyCode::Char(':') => {
app.start_command();

115
src/ui.rs
View File

@@ -8,27 +8,24 @@ use crate::library::{RightRow, SortMode};
use crate::state::RepeatMode;
pub fn draw(f: &mut Frame, app: &mut App) {
// Вертикально: верхняя часть (панели + эквалайзер) и статус
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Min(1),
Constraint::Length(3), // статус + время + mode + vol
Constraint::Length(3),
])
.split(f.area());
// Горизонтально: левая колонка (исполнители + эквалайзер) и правая (треки)
let top = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(30), Constraint::Percentage(70)])
.split(chunks[0]);
// Левая колонка делится вертикально: список сверху, эквалайзер снизу
let left_col = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Min(1),
Constraint::Length(8), // эквалайзер
Constraint::Length(8),
])
.split(top[0]);
@@ -40,6 +37,9 @@ pub fn draw(f: &mut Frame, app: &mut App) {
if app.show_help {
draw_help(f, f.area());
}
if app.show_dirs {
draw_dirs(f, app, f.area());
}
}
fn draw_left_panel(f: &mut Frame, app: &mut App, area: Rect) {
@@ -65,11 +65,13 @@ fn draw_left_panel(f: &mut Frame, app: &mut App, area: Rect) {
};
app.library.left_state.select(sel);
let title = match app.library.sort_mode {
let title_text = match app.library.sort_mode {
SortMode::ByArtist => " Исполнители ",
SortMode::ByAlbum => " Альбомы ",
SortMode::ByFilename => " Источник ",
};
let album_color = Color::Rgb(100, 149, 237);
let title = Span::styled(title_text, Style::default().fg(album_color));
let list = List::new(items)
.block(Block::default().borders(Borders::ALL).title(title))
@@ -175,7 +177,7 @@ fn draw_right_panel(f: &mut Frame, app: &mut App, area: Rect) {
.map(|a| a.name.as_str())
.unwrap_or("—");
let title = match app.library.sort_mode {
let title_text = match app.library.sort_mode {
SortMode::ByArtist => {
if app.library.filter.is_empty() {
format!(" Альбомы и треки - {} ", selected_name)
@@ -204,6 +206,8 @@ fn draw_right_panel(f: &mut Frame, app: &mut App, area: Rect) {
}
}
};
let album_color = Color::Rgb(100, 149, 237);
let title = Span::styled(title_text, Style::default().fg(album_color));
let highlight = if app.active_panel == Panel::Right {
Style::default().add_modifier(Modifier::REVERSED)
@@ -227,14 +231,11 @@ fn draw_right_panel(f: &mut Frame, app: &mut App, area: Rect) {
}
fn draw_equalizer(f: &mut Frame, app: &App, area: Rect) {
// Внутренняя ширина панели (минус 2 символа рамки)
let inner_width = area.width.saturating_sub(2) as usize;
let inner_width = area.width.saturating_sub(1) as usize;
if inner_width == 0 {
return;
}
// Каждый столбик занимает 2 ячейки: 1 символ + 1 gap.
// Значит число столбиков = ширина / 2.
let num_bars = (inner_width / 2).max(4);
let bars_data = app.player.spectrum.bars();
@@ -298,7 +299,12 @@ fn draw_status(f: &mut Frame, app: &App, area: Rect) {
};
let status = Paragraph::new(status_text)
.block(Block::default().borders(Borders::ALL).title(status_title));
.block(
Block::default()
.borders(Borders::ALL)
.title(status_title)
.title_style(Style::default().fg(Color::Rgb(100, 149, 237)))
);
f.render_widget(status, inner[0]);
let (pos, total) = app.progress();
@@ -311,7 +317,12 @@ fn draw_status(f: &mut Frame, app: &App, area: Rect) {
);
let time = Paragraph::new(time_text)
.alignment(Alignment::Center)
.block(Block::default().borders(Borders::ALL).title(" Время "));
.block(
Block::default()
.borders(Borders::ALL)
.title(" Время ")
.title_style(Style::default().fg(Color::Rgb(100, 149, 237)))
);
f.render_widget(time, inner[1]);
let mode_str = match (app.repeat, app.shuffle) {
@@ -324,19 +335,90 @@ fn draw_status(f: &mut Frame, app: &App, area: Rect) {
};
let mode = Paragraph::new(mode_str)
.alignment(Alignment::Center)
.block(Block::default().borders(Borders::ALL).title(" Mode "));
.block(
Block::default()
.borders(Borders::ALL)
.title(" Pежим ")
.title_style(Style::default().fg(Color::Rgb(100, 149, 237)))
);
f.render_widget(mode, inner[2]);
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(" Звук ")
.title_style(Style::default().fg(Color::Rgb(100, 149, 237)))
);
f.render_widget(vol, inner[3]);
}
fn draw_dirs(f: &mut Frame, app: &App, screen: Rect) {
let width = 70u16.min(screen.width.saturating_sub(4));
let total_lines = app.extra_dirs().len() + 8;
let desired_height = (total_lines + 2) as u16; // +2 на рамки
// Ограничиваем высотой экрана
let max_h = screen.height.saturating_sub(2);
let height = desired_height.min(max_h).max(5); // минимум 5 строк
let x = (screen.width.saturating_sub(width)) / 2;
let y = (screen.height.saturating_sub(height)) / 2;
let area = Rect::new(x, y, width, height);
f.render_widget(Clear, area);
let title_color = Style::default().fg(Color::Rgb(100, 149, 237));
let header = Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD);
let dim = Style::default().fg(Color::DarkGray);
let mut lines = vec![
Line::from(Span::styled(
" Директории библиотеки ",
title_color.add_modifier(Modifier::BOLD),
)),
Line::from(""),
Line::from(Span::styled(" Базовые", header)),
Line::from(vec![
Span::styled(" [0] ", dim),
Span::raw(app.base_dir()),
]),
];
if !app.extra_dirs().is_empty() {
lines.push(Line::from(""));
lines.push(Line::from(Span::styled(" Дополнительные (:add)", header)));
for (i, dir) in app.extra_dirs().iter().enumerate() {
lines.push(Line::from(vec![
Span::styled(format!(" [{}] ", i + 1), dim),
Span::raw(dir.clone()),
]));
}
}
lines.push(Line::from(""));
lines.push(Line::from(Span::styled(
" :rmdir <N> — удалить, :clear — очистить всё, Esc — закрыть",
dim,
)));
let para = Paragraph::new(lines).block(
Block::default()
.borders(Borders::ALL)
.border_style(title_color)
.title(" Библиотека "),
);
f.render_widget(para, area);
}
fn draw_help(f: &mut Frame, screen: Rect) {
let width = 62u16.min(screen.width.saturating_sub(4));
let height = 40u16.min(screen.height.saturating_sub(4));
let height = 44u16.min(screen.height.saturating_sub(4));
let x = (screen.width.saturating_sub(width)) / 2;
let y = (screen.height.saturating_sub(height)) / 2;
@@ -380,6 +462,7 @@ fn draw_help(f: &mut Frame, screen: Rect) {
Line::from(" :add <путь> добавить директорию"),
Line::from(" :reload пересканировать всё"),
Line::from(" :save сохранить библиотеку"),
Line::from(" :dirs управление директориями"),
Line::from(" :help эта справка"),
Line::from(" :q выход"),
Line::from(""),