From f735992a3263680d7b0cc98c2859bfd57656c800 Mon Sep 17 00:00:00 2001 From: AlexL Date: Fri, 23 Oct 2026 19:32:30 +0200 Subject: [PATCH] =?UTF-8?q?=D0=BC=D0=BD=D0=BE=D0=B3=D0=BE=20=D0=BF=D1=80?= =?UTF-8?q?=D0=B0=D0=B2=D0=BE=D0=BA=20=D0=B8=20=D0=BF=D0=B5=D1=80=D0=B5?= =?UTF-8?q?=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=BA=D0=B0=20=D0=B1=D0=B8=D0=B1?= =?UTF-8?q?=D0=BB=D0=B8=D0=BE=D1=82=D0=B5=D0=BA=D0=B8=20=D1=82=D1=80=D0=B5?= =?UTF-8?q?=D0=B2=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 +- Cargo.lock | 1 + Cargo.toml | 3 +- src/app.rs | 221 ++++++++++++++++++++++++++++++--------------------- src/main.rs | 8 -- src/state.rs | 7 +- src/track.rs | 110 ++++++++++++++++++++++++- src/ui.rs | 102 ++++++++++++------------ 8 files changed, 297 insertions(+), 158 deletions(-) diff --git a/.gitignore b/.gitignore index 9add9f3..40d9aca 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,2 @@ /target -/.idea -src.zip \ No newline at end of file +/.idea \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 5b269ed..d6ce65b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -986,6 +986,7 @@ dependencies = [ "bincode", "crossterm", "dirs", + "encoding_rs", "lofty", "ratatui", "rodio", diff --git a/Cargo.toml b/Cargo.toml index 6ac1b55..506443b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,4 +13,5 @@ lofty = "0.22" serde = { version = "1", features = ["derive"] } toml = "0.8" rustfft = "6" -bincode = "1.3" \ No newline at end of file +bincode = "1.3" +encoding_rs = "0.8" \ No newline at end of file diff --git a/src/app.rs b/src/app.rs index 53f71dd..73e0b52 100644 --- a/src/app.rs +++ b/src/app.rs @@ -5,7 +5,7 @@ use std::time::{Duration, Instant}; use crate::audio::AudioPlayer; use crate::library::{Library, RightRow, SortMode}; use crate::state::{RepeatMode, SavedState}; -use crate::track::{scan_dir, Track}; +use crate::track::{scan_source, Track}; #[derive(Clone, Copy, PartialEq)] pub enum Panel { @@ -28,20 +28,16 @@ pub struct App { pub active_panel: Panel, pub input_mode: InputMode, pub search_buffer: String, - /// Буфер командного режима (после ":") pub command_buffer: String, - /// Флаг запроса выхода (выставляется командой :q) pub request_quit: bool, raw_tracks: Vec, - music_dir: String, - /// Дополнительные директории, добавленные через :add - extra_dirs: Vec, - /// Пути, удалённые из списка (не с диска) + default_dir: String, + /// Единый список источников: папки и плейлисты. + sources: Vec, deleted_paths: HashSet, pub repeat: RepeatMode, pub shuffle: bool, pub show_help: bool, - /// Показывать окно управления директориями pub show_dirs: bool, status_set_at: Instant, } @@ -51,21 +47,9 @@ const STATUS_RESET_AFTER: Duration = Duration::from_secs(2); impl App { pub fn new(music_dir: &str) -> Result> { let saved = crate::state::load(); - // Загружаем кэш - 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); - crate::state::save_cache(&scanned); - scanned - } - }; - - let library = Library::build(tracks.clone(), SortMode::ByArtist); let mut app = Self { - library, + library: Library::build(Vec::new(), SortMode::ByArtist), player: AudioPlayer::new()?, status: "F1 - помощь, : - команда".into(), playing_index: None, @@ -74,9 +58,9 @@ impl App { search_buffer: String::new(), command_buffer: String::new(), request_quit: false, - raw_tracks: tracks, - music_dir: music_dir.to_string(), - extra_dirs: saved.extra_dirs.clone(), + raw_tracks: Vec::new(), + default_dir: music_dir.to_string(), + sources: saved.sources.clone(), deleted_paths: HashSet::new(), repeat: RepeatMode::Off, shuffle: false, @@ -84,18 +68,29 @@ impl App { 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(); - } + + // Если sources пуст — первый запуск, добавляем базовую директорию + if app.sources.is_empty() { + app.sources.push(music_dir.to_string()); } + // Пробуем загрузить кэш + let cached = crate::state::load_cache(); + app.raw_tracks = match cached { + Some(c) if !c.is_empty() => c, + _ => { + let mut tracks = Vec::new(); + for src in &app.sources { + let path = PathBuf::from(src); + tracks.extend(scan_source(&path)); + } + crate::state::save_cache(&tracks); + tracks + } + }; + + app.rebuild_library(); + if let Some(vol) = saved.volume { app.player.set_volume(vol); } @@ -209,15 +204,17 @@ impl App { let args = parts.next().unwrap_or("").trim(); match name { - "add" => self.add_dir(args), + "add" => self.add_source(args), + "load" => self.load_source(args), "reload" => self.reload_library(), "save" => self.save_now(), + "savepl" => self.save_playlist(args), "dirs" => { self.show_dirs = true; - self.set_status("Список директорий".into()); + self.set_status("Источники библиотеки".into()); } - "rmdir" => self.rmdir(args), - "clear" => self.clear_extra_dirs(), + "rm" => self.remove_source(args), + "clear" => self.clear_sources(), "help" => { self.show_help = true; self.set_status("Справка".into()); @@ -231,7 +228,7 @@ impl App { } } - fn add_dir(&mut self, raw_path: &str) { + fn add_source(&mut self, raw_path: &str) { if raw_path.is_empty() { self.set_status("Использование: :add <путь>".into()); return; @@ -245,13 +242,8 @@ impl App { return; } - if !path.is_dir() { - self.set_status(format!("Не директория: {}", expanded)); - return; - } - self.set_status(format!("Сканирование {}...", expanded)); - let new_tracks = scan_dir(&expanded); + let new_tracks = scan_source(&path); if new_tracks.is_empty() { self.set_status(format!("Нет треков в {}", expanded)); @@ -267,8 +259,8 @@ impl App { } } - if !self.extra_dirs.contains(&expanded) { - self.extra_dirs.push(expanded.clone()); + if !self.sources.contains(&expanded) { + self.sources.push(expanded.clone()); } self.rebuild_library(); @@ -276,50 +268,77 @@ impl App { self.set_status(format!("Добавлено {} треков из {}", added, expanded)); } - fn rmdir(&mut self, arg: &str) { + fn load_source(&mut self, raw_path: &str) { + if raw_path.is_empty() { + self.set_status("Использование: :load <путь>".into()); + return; + } + + let expanded = expand_tilde(raw_path); + let path = PathBuf::from(&expanded); + + if !path.exists() { + self.set_status(format!("Путь не существует: {}", expanded)); + return; + } + + self.set_status(format!("Загрузка {}...", expanded)); + let new_tracks = scan_source(&path); + + if new_tracks.is_empty() { + self.set_status(format!("Нет треков в {}", expanded)); + return; + } + + // Очищаем текущую сессию + self.raw_tracks = new_tracks; + self.sources = vec![expanded.clone()]; + self.deleted_paths.clear(); + + self.rebuild_library(); + crate::state::save_cache(&self.raw_tracks); + self.set_status(format!("Загружено {} треков из {}", self.raw_tracks.len(), expanded)); + } + + fn remove_source(&mut self, arg: &str) { let idx: usize = match arg.parse() { Ok(i) => i, Err(_) => { - self.set_status("Использование: :rmdir <индекс>".into()); + self.set_status("Использование: :rm <индекс>".into()); return; } }; - if idx == 0 { - self.set_status("Нельзя удалить базовую директорию".into()); + if idx >= self.sources.len() { + self.set_status(format!("Нет источника с индексом {}", idx)); 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(); + let removed = self.sources.remove(idx); + self.set_status(format!("Источник удалён: {}", removed)); + self.reload_library(); } - fn clear_extra_dirs(&mut self) { - if self.extra_dirs.is_empty() { - self.set_status("extra_dirs уже пуст".into()); + fn clear_sources(&mut self) { + if self.sources.is_empty() { + self.set_status("Список источников уже пуст".into()); return; } - self.extra_dirs.clear(); - self.rebuild_from_dirs(); - self.set_status("Все дополнительные директории удалены".into()); + self.sources.clear(); + self.raw_tracks.clear(); + self.deleted_paths.clear(); + self.playing_index = None; + self.player.stop(); + self.rebuild_library(); + crate::state::save_cache(&self.raw_tracks); + self.set_status("Все источники удалены".into()); } - /// Пересканировать только оставшиеся директории и пересобрать библиотеку - fn rebuild_from_dirs(&mut self) { - let mut dirs = vec![self.music_dir.clone()]; - dirs.extend(self.extra_dirs.clone()); - + fn rebuild_from_sources(&mut self) { let mut all = Vec::new(); - for dir in &dirs { - let expanded = expand_tilde(dir); - all.extend(scan_dir(&expanded)); + for src in &self.sources { + let path = PathBuf::from(expand_tilde(src)); + all.extend(scan_source(&path)); } let mut seen = HashSet::new(); @@ -329,23 +348,52 @@ impl App { 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.rebuild_from_sources(); self.set_status(format!("Пересканировано {} треков", self.raw_tracks.len())); } - - /// Принудительно сохранить кэш библиотеки и настройки. - /// Тот же `persist()`, но с подтверждением в статусе. + pub fn save_now(&mut self) { self.persist(); let msg = format!("Сохранено {} треков", self.raw_tracks.len()); self.set_status(msg); } + fn save_playlist(&mut self, raw_path: &str) { + if raw_path.is_empty() { + self.set_status("Использование: :savepl <файл.m3u|.pls>".into()); + return; + } + + let path = PathBuf::from(expand_tilde(raw_path)); + let is_pls = path + .extension() + .and_then(|e| e.to_str()) + .map(|s| s.eq_ignore_ascii_case("pls")) + .unwrap_or(false); + + let tracks: Vec = self + .raw_tracks + .iter() + .filter(|t| !self.deleted_paths.contains(&t.path)) + .cloned() + .collect(); + + let res = if is_pls { + crate::track::save_playlist_pls(&path, &tracks) + } else { + crate::track::save_playlist_m3u(&path, &tracks) + }; + + match res { + Ok(_) => self.set_status(format!("Плейлист сохранён: {}", path.display())), + Err(e) => self.set_status(format!("Ошибка сохранения: {}", e)), + } + } + // ── Удаление из списка (клавиша Del) ─────────────────────── pub fn delete_selected(&mut self) { match self.active_panel { @@ -496,8 +544,6 @@ impl App { } fn persist(&self) { - // Сохраняем кэш треков - чтобы при следующем запуске - // не сканировать директории заново. crate::state::save_cache(&self.raw_tracks); let playing_path = self @@ -512,14 +558,14 @@ impl App { }; let state = SavedState { - last_path: Some(self.music_dir.clone()), + last_path: Some(self.default_dir.clone()), volume: Some(self.player.volume()), sort_mode: Some(self.library.sort_mode), repeat: Some(self.repeat), shuffle: Some(self.shuffle), last_playing_path: playing_path, last_playing_pos_secs: playing_pos, - extra_dirs: self.extra_dirs.clone(), + sources: self.sources.clone(), }; crate::state::save(&state); } @@ -859,16 +905,11 @@ impl App { // ── Геттеры для UI ───────────────────────────────────────── - pub fn base_dir(&self) -> &str { - &self.music_dir - } - - pub fn extra_dirs(&self) -> &[String] { - &self.extra_dirs + pub fn sources(&self) -> &[String] { + &self.sources } } -/// Развернуть `~/...` в путь домашней директории. fn expand_tilde(path: &str) -> String { if let Some(rest) = path.strip_prefix("~/") { if let Some(home) = dirs::home_dir() { diff --git a/src/main.rs b/src/main.rs index 7069781..7738453 100644 --- a/src/main.rs +++ b/src/main.rs @@ -56,13 +56,11 @@ 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(); @@ -70,7 +68,6 @@ 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; @@ -78,7 +75,6 @@ fn handle_key(key: KeyEvent, app: &mut App) -> bool { return false; } - // Ввод команды if app.input_mode == InputMode::Command { match key.code { KeyCode::Esc => app.cancel_command(), @@ -90,7 +86,6 @@ fn handle_key(key: KeyEvent, app: &mut App) -> bool { return false; } - // Ctrl-комбинации if ctrl { match key.code { KeyCode::Right => { @@ -116,7 +111,6 @@ fn handle_key(key: KeyEvent, app: &mut App) -> bool { } } - // Ввод поиска if app.input_mode == InputMode::Search { match key.code { KeyCode::Esc => app.cancel_search(), @@ -128,13 +122,11 @@ 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/state.rs b/src/state.rs index cb2f2fd..47565cc 100644 --- a/src/state.rs +++ b/src/state.rs @@ -15,9 +15,9 @@ pub struct SavedState { pub shuffle: Option, pub last_playing_path: Option, pub last_playing_pos_secs: Option, - /// Дополнительные директории, добавленные через :add + /// Единый список источников: папки и плейлисты. #[serde(default)] - pub extra_dirs: Vec, + pub sources: Vec, } #[derive(Serialize, Deserialize, Clone, Copy, PartialEq, Debug, Default)] @@ -58,9 +58,6 @@ pub fn save(state: &SavedState) { } } -// ── Кэш плейлиста (bincode) ──────────────────────────────────── - -/// Кэш просто хранит все известные треки. Без привязки к конкретной директории. pub fn load_cache() -> Option> { let path = cache_path()?; let bytes = fs::read(&path).ok()?; diff --git a/src/track.rs b/src/track.rs index 53a250c..2b08e45 100644 --- a/src/track.rs +++ b/src/track.rs @@ -1,4 +1,4 @@ -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::time::Duration; use lofty::file::{AudioFile, TaggedFileExt}; @@ -91,4 +91,112 @@ pub fn scan_dir(dir: &str) -> Vec { } } tracks +} + +/// Универсальный сканер источника: папка или файл плейлиста. +pub fn scan_source(path: &Path) -> Vec { + if path.is_dir() { + scan_dir(&path.to_string_lossy()) + } else if path.is_file() { + read_playlist(path) + .into_iter() + .map(|p| Track::from_path(&p)) + .collect() + } else { + Vec::new() + } +} + +// ── Плейлисты (M3U / M3U8 / PLS) ─────────────────────────────── + +pub fn read_playlist(path: &Path) -> Vec { + let bytes = match std::fs::read(path) { + Ok(b) => b, + Err(_) => return Vec::new(), + }; + + let content = match std::str::from_utf8(&bytes) { + Ok(s) => s.to_string(), + Err(_) => { + let (decoded, _, _) = encoding_rs::WINDOWS_1251.decode(&bytes); + decoded.into_owned() + } + }; + + let content = content.trim_start_matches('\u{FEFF}'); + + let parent = path.parent().unwrap_or(Path::new("")); + let is_pls = path + .extension() + .and_then(|e| e.to_str()) + .map(|s| s.eq_ignore_ascii_case("pls")) + .unwrap_or(false); + + if is_pls { + parse_pls(content, parent) + } else { + parse_m3u(content, parent) + } +} + +fn parse_m3u(content: &str, parent: &Path) -> Vec { + content + .lines() + .map(|l| l.trim()) + .filter(|l| !l.is_empty() && !l.starts_with('#')) + .map(|l| resolve_path(l, parent)) + .filter(|p| p.exists()) + .collect() +} + +fn parse_pls(content: &str, parent: &Path) -> Vec { + let mut files = Vec::new(); + for line in content.lines() { + let line = line.trim(); + if line.to_lowercase().starts_with("file") { + if let Some((_, val)) = line.split_once('=') { + let p = resolve_path(val.trim(), parent); + if p.exists() { + files.push(p); + } + } + } + } + files +} + +fn resolve_path(s: &str, parent: &Path) -> PathBuf { + let p = PathBuf::from(s); + if p.is_relative() { + parent.join(p) + } else { + p + } +} + +pub fn save_playlist_m3u(path: &Path, tracks: &[Track]) -> std::io::Result<()> { + let mut out = String::from("#EXTM3U\n"); + for t in tracks { + let dur = t.duration.as_secs(); + out.push_str(&format!( + "#EXTINF:{},{} - {}\n{}\n", + dur, + t.artist, + t.title, + t.path.display() + )); + } + std::fs::write(path, out) +} + +pub fn save_playlist_pls(path: &Path, tracks: &[Track]) -> std::io::Result<()> { + let mut out = String::from("[playlist]\n"); + for (i, t) in tracks.iter().enumerate() { + let num = i + 1; + out.push_str(&format!("File{}={}\n", num, t.path.display())); + out.push_str(&format!("Title{}={} - {}\n", num, t.artist, t.title)); + out.push_str(&format!("Length{}={}\n", num, t.duration.as_secs())); + } + out.push_str(&format!("NumberOfEntries={}\nVersion=2\n", tracks.len())); + std::fs::write(path, out) } \ No newline at end of file diff --git a/src/ui.rs b/src/ui.rs index 260a60c..55be0e5 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1,6 +1,6 @@ use ratatui::prelude::*; use ratatui::widgets::{ - Bar, BarChart, BarGroup, Block, Borders, Clear, List, ListItem, Paragraph, + Bar, BarChart, BarGroup, Block, Borders, Clear, List, ListItem, Paragraph, Wrap, }; use crate::app::{App, InputMode, Panel}; @@ -70,8 +70,8 @@ fn draw_left_panel(f: &mut Frame, app: &mut App, area: Rect) { SortMode::ByAlbum => " Альбомы ", SortMode::ByFilename => " Источник ", }; - let album_color = Color::Rgb(100, 149, 237); - let title = Span::styled(title_text, Style::default().fg(album_color)); + 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)) @@ -133,7 +133,7 @@ fn draw_right_panel(f: &mut Frame, app: &mut App, area: Rect) { } RightRow::Track { index_in_library } => { let is_playing = Some(*index_in_library) == app.playing_index; - let marker = if is_playing { "* " } else { " " }; + let marker = if is_playing { "♪ " } else { " " }; let (title, num, dur) = match app.library.track(*index_in_library) { Some(t) => ( @@ -151,15 +151,15 @@ fn draw_right_panel(f: &mut Frame, app: &mut App, area: Rect) { } else { Style::default() }; - + let left = format!(" {}{}{}", marker, num, title); let inner_width = area.width.saturating_sub(4) as usize; - + let left_len = left.chars().count(); let dur_len = dur.chars().count(); let pad = inner_width - .saturating_sub(left_len + dur_len) - .max(1); + .saturating_sub(left_len + dur_len) + .max(1); ListItem::new(Line::from(vec![ Span::styled(left, style), @@ -206,8 +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 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) @@ -300,11 +300,11 @@ 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) - .title_style(Style::default().fg(Color::Rgb(100, 149, 237))) - ); + 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(); @@ -318,11 +318,11 @@ 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(" Время ") - .title_style(Style::default().fg(Color::Rgb(100, 149, 237))) - ); + 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) { @@ -338,7 +338,7 @@ fn draw_status(f: &mut Frame, app: &App, area: Rect) { .block( Block::default() .borders(Borders::ALL) - .title(" Pежим ") + .title(" Режим ") .title_style(Style::default().fg(Color::Rgb(100, 149, 237))) ); f.render_widget(mode, inner[2]); @@ -356,13 +356,15 @@ fn draw_status(f: &mut Frame, app: &App, area: Rect) { } 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 count = app.sources().len(); - // Ограничиваем высотой экрана - let max_h = screen.height.saturating_sub(2); - let height = desired_height.min(max_h).max(5); // минимум 5 строк + // Строки: заголовок (1) + пустая (1) + "Список" (1) + N + пустая (1) + подсказка (1) + let content_lines = 5 + count; + let desired_height = (content_lines + 2) as u16; + + let width = 70u16.min(screen.width.saturating_sub(4)); + let max_h = screen.height.saturating_sub(4); + let height = desired_height.min(max_h).max(5); let x = (screen.width.saturating_sub(width)) / 2; let y = (screen.height.saturating_sub(height)) / 2; @@ -378,40 +380,34 @@ fn draw_dirs(f: &mut Frame, app: &App, screen: Rect) { 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()), - ]), + Line::from(Span::styled(" Список", header)), ]; - 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()), - ])); - } + for (i, src) in app.sources().iter().enumerate() { + lines.push(Line::from(vec![ + Span::styled(format!(" [{}] ", i), dim), + Span::raw(src.clone()), + ])); } lines.push(Line::from("")); lines.push(Line::from(Span::styled( - " :rmdir — удалить, :clear — очистить всё, Esc — закрыть", + " :rm — удалить, Esc — закрыть", dim, ))); - let para = Paragraph::new(lines).block( - Block::default() - .borders(Borders::ALL) - .border_style(title_color) - .title(" Библиотека "), - ); + let para = Paragraph::new(lines) + .block( + Block::default() + .borders(Borders::ALL) + .border_style(title_color) + .title(" Библиотека "), + ) + .wrap(Wrap { trim: false }); f.render_widget(para, area); } @@ -459,10 +455,14 @@ fn draw_help(f: &mut Frame, screen: Rect) { Line::from(" 1..9 перейти к 10%..90% трека"), Line::from(""), Line::from(Span::styled(" Команды", header)), - Line::from(" :add <путь> добавить директорию"), + Line::from(" :add <путь> добавить в сессию"), + Line::from(" :load <путь> заменить сессию"), Line::from(" :reload пересканировать всё"), Line::from(" :save сохранить библиотеку"), - Line::from(" :dirs управление директориями"), + Line::from(" :savepl <файл> экспорт в M3U/PLS"), + Line::from(" :dirs список источников"), + Line::from(" :rm удалить источник"), + Line::from(" :clear очистить источники"), Line::from(" :help эта справка"), Line::from(" :q выход"), Line::from(""),