From 52ca495a6ef7183d76c007ccb82a50f1c837da58 Mon Sep 17 00:00:00 2001 From: AlexL Date: Sat, 17 Oct 2026 15:36:45 +0200 Subject: [PATCH] =?UTF-8?q?=D0=BF=D1=80=D0=B0=D0=B2=D0=BA=D0=B0=20=D1=80?= =?UTF-8?q?=D0=B0=D0=B1=D0=BE=D1=82=D1=8B=20=D1=81=20=D0=BA=D0=B5=D1=88?= =?UTF-8?q?=D0=B5=D0=BC,=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D1=8B=20=D0=BA=D0=BE=D0=BC=D0=B0=D0=BD=D0=B4=D1=8B=20?= =?UTF-8?q?=D0=B4=D0=BB=D1=8F=20=D1=80=D0=B5=D0=B4=D0=BE=D0=BA=D1=82=D0=B8?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5=20=D0=B1=D0=B8=D0=B1?= =?UTF-8?q?=D0=B8=D0=BB=D0=B8=D0=BE=D1=82=D0=B5=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + src.zip | Bin 0 -> 17909 bytes src/app.rs | 81 +++++++++++++++++++++++++++++++++--- src/main.rs | 16 +++++++- src/ui.rs | 115 ++++++++++++++++++++++++++++++++++++++++++++-------- 5 files changed, 191 insertions(+), 22 deletions(-) create mode 100644 src.zip diff --git a/.gitignore b/.gitignore index ea8c4bf..40d9aca 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +/.idea \ No newline at end of file diff --git a/src.zip b/src.zip new file mode 100644 index 0000000000000000000000000000000000000000..d700cefee04f65a33b11d8481039d61c8c72cc65 GIT binary patch literal 17909 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

LFR-?dp%7P*)oA=U5#MYC!{wdb&k+cySlbx87y z?sT!Wo@7;c=*J4A)|v8sdoZnCowm%xg~wq0JYx6%oBW&3Q@2mm_;j^YUAMQ|@xE3u zs`OrOg#6{*flfJ$I(x;^JAU%+FTMBN+DCcM!R@_~>yAd0T=!m47Wt^P!9z)%o3VZV z$4}X3?LR)~w%rhwztj4N)J&<9leav+$EmZit1Df@w}q$u4Od`Z#r4iv`L}QXDARwk z&xPy3rnPD!Tl=1T5YO-XwddYL-z)a5=e#^>XG}=HP*)q#RCsCQmx=Dz^Ah#`Z9IJO zhtRu<>=@qJSEoG;{$;da$%m~1Ql@h|qe`Q~iewkvn>zF6Tcbyz7fb*3B~KMnnm)

twsFo=Bwt{9B_)|a(}(LYzk`L2sj+$b5-ws%`P6eCh=kHdL=o@iSza{eU4P_p0CF6 zj%B-R%rZx*8(*9culf->_q*%b7L_{QEUmd;PPtB4#BsnjSGnlf>U-g-Pja3vk}5ad z@WIeO@3vk2p9_!8WCbQBa=e`)xP(Vq`G@6JZwGUanY)h!{cLX8I=Ql1KKO;jj&Fam zO#ehag8{o~zWY2)>@C)Dy zB`^?BfY6|$Ft`{X97YBS1`DaEJriT^GD2h#B*Ii|2WG%b1(~A+#2`~4$7dj${rTO8 ziUS19##|4MZ1#NM5glN&p{v7@O%G4}!f6zP-{qK#wUA9ukb{_xSh|I5e&)U4uZIb^ z9&<_;+5A~*5c8oEyvT+hnLkJ84gtqwPFElsF0BJG95#7@Y&y%|)jIbGn2tH}ifsBi zeTeC>aad&2V@wU^Y7;OWb2JIrbPHpM>EN*?WTSg0Ue45r!xpEkY@oEs0fbMO7#My# Gfp`FG Result> { 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 + } } /// Развернуть `~/...` в путь домашней директории. diff --git a/src/main.rs b/src/main.rs index 4830767..7069781 100644 --- a/src/main.rs +++ b/src/main.rs @@ -26,7 +26,6 @@ fn main() -> Result<(), Box> { 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> { 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(); diff --git a/src/ui.rs b/src/ui.rs index 6560a94..98f8dae 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -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 — удалить, :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(""),