обновление
This commit is contained in:
313
src/app.rs
313
src/app.rs
@@ -1,7 +1,9 @@
|
||||
use std::collections::HashSet;
|
||||
use std::path::PathBuf;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use crate::audio::AudioPlayer;
|
||||
use crate::library::{Library, SortMode};
|
||||
use crate::library::{Library, RightRow, SortMode};
|
||||
use crate::state::{RepeatMode, SavedState};
|
||||
use crate::track::{scan_dir, Track};
|
||||
|
||||
@@ -15,6 +17,7 @@ pub enum Panel {
|
||||
pub enum InputMode {
|
||||
Normal,
|
||||
Search,
|
||||
Command,
|
||||
}
|
||||
|
||||
pub struct App {
|
||||
@@ -25,8 +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<Track>,
|
||||
music_dir: String,
|
||||
/// Дополнительные директории, добавленные через :add
|
||||
extra_dirs: Vec<String>,
|
||||
/// Пути, удалённые из списка (не с диска)
|
||||
deleted_paths: HashSet<PathBuf>,
|
||||
pub repeat: RepeatMode,
|
||||
pub shuffle: bool,
|
||||
pub show_help: bool,
|
||||
@@ -37,11 +48,14 @@ 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 tracks = match crate::state::load_cache(music_dir) {
|
||||
Some(cached) => cached,
|
||||
None => {
|
||||
let saved = crate::state::load();
|
||||
|
||||
// Пробуем загрузить кэш. Если его нет или он пустой — сканируем папку.
|
||||
let tracks = match crate::state::load_cache() {
|
||||
Some(cached) if !cached.is_empty() => cached,
|
||||
_ => {
|
||||
let scanned = scan_dir(music_dir);
|
||||
crate::state::save_cache(music_dir, &scanned);
|
||||
crate::state::save_cache(&scanned);
|
||||
scanned
|
||||
}
|
||||
};
|
||||
@@ -51,20 +65,23 @@ impl App {
|
||||
let mut app = Self {
|
||||
library,
|
||||
player: AudioPlayer::new()?,
|
||||
status: "F1 — помощь".into(),
|
||||
status: "F1 — помощь, : — команда".into(),
|
||||
playing_index: None,
|
||||
active_panel: Panel::Left,
|
||||
input_mode: InputMode::Normal,
|
||||
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(),
|
||||
deleted_paths: HashSet::new(),
|
||||
repeat: RepeatMode::Off,
|
||||
shuffle: false,
|
||||
show_help: false,
|
||||
status_set_at: Instant::now(),
|
||||
};
|
||||
|
||||
let saved = crate::state::load();
|
||||
if let Some(vol) = saved.volume {
|
||||
app.player.set_volume(vol);
|
||||
}
|
||||
@@ -92,12 +109,12 @@ impl App {
|
||||
fn default_status(&self) -> String {
|
||||
match self.playing_index.and_then(|i| self.library.track(i)) {
|
||||
Some(t) => format!("{} — {}", t.artist, t.title),
|
||||
None => "F1 — помощь".into(),
|
||||
None => "F1 — помощь, : — команда".into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tick_status(&mut self) {
|
||||
if self.input_mode == InputMode::Search {
|
||||
if self.input_mode != InputMode::Normal {
|
||||
return;
|
||||
}
|
||||
if self.status_set_at.elapsed() < STATUS_RESET_AFTER {
|
||||
@@ -140,6 +157,237 @@ impl App {
|
||||
self.show_help = !self.show_help;
|
||||
}
|
||||
|
||||
// ── Командный режим (:) ────────────────────────────────────
|
||||
|
||||
pub fn start_command(&mut self) {
|
||||
self.input_mode = InputMode::Command;
|
||||
self.command_buffer.clear();
|
||||
self.status_set_at = Instant::now();
|
||||
}
|
||||
|
||||
pub fn cancel_command(&mut self) {
|
||||
self.input_mode = InputMode::Normal;
|
||||
self.command_buffer.clear();
|
||||
self.set_status("Команда отменена".into());
|
||||
}
|
||||
|
||||
pub fn command_push(&mut self, c: char) {
|
||||
self.command_buffer.push(c);
|
||||
self.status_set_at = Instant::now();
|
||||
}
|
||||
|
||||
pub fn command_pop(&mut self) {
|
||||
self.command_buffer.pop();
|
||||
self.status_set_at = Instant::now();
|
||||
}
|
||||
|
||||
pub fn commit_command(&mut self) {
|
||||
let cmd = self.command_buffer.trim().to_string();
|
||||
self.input_mode = InputMode::Normal;
|
||||
self.command_buffer.clear();
|
||||
|
||||
if cmd.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut parts = cmd.splitn(2, char::is_whitespace);
|
||||
let name = parts.next().unwrap_or("");
|
||||
let args = parts.next().unwrap_or("").trim();
|
||||
|
||||
match name {
|
||||
"add" => self.add_dir(args),
|
||||
"reload" => self.reload_library(),
|
||||
"save" => self.save_now(),
|
||||
"help" => {
|
||||
self.show_help = true;
|
||||
self.set_status("Справка".into());
|
||||
}
|
||||
"q" | "quit" => {
|
||||
self.request_quit = true;
|
||||
}
|
||||
_ => {
|
||||
self.set_status(format!("Неизвестная команда: {}", name));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn add_dir(&mut self, raw_path: &str) {
|
||||
if raw_path.is_empty() {
|
||||
self.set_status("Использование: :add <путь>".into());
|
||||
return;
|
||||
}
|
||||
|
||||
let expanded = expand_tilde(raw_path);
|
||||
let path = PathBuf::from(&expanded);
|
||||
|
||||
if !path.exists() {
|
||||
self.set_status(format!("Путь не существует: {}", expanded));
|
||||
return;
|
||||
}
|
||||
|
||||
if !path.is_dir() {
|
||||
self.set_status(format!("Не директория: {}", expanded));
|
||||
return;
|
||||
}
|
||||
|
||||
self.set_status(format!("Сканирование {}...", expanded));
|
||||
let new_tracks = scan_dir(&expanded);
|
||||
|
||||
if new_tracks.is_empty() {
|
||||
self.set_status(format!("Нет треков в {}", expanded));
|
||||
return;
|
||||
}
|
||||
|
||||
let existing: HashSet<PathBuf> = self.raw_tracks.iter().map(|t| t.path.clone()).collect();
|
||||
let mut added = 0;
|
||||
for track in new_tracks {
|
||||
if !existing.contains(&track.path) && !self.deleted_paths.contains(&track.path) {
|
||||
self.raw_tracks.push(track);
|
||||
added += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if !self.extra_dirs.contains(&expanded) {
|
||||
self.extra_dirs.push(expanded.clone());
|
||||
}
|
||||
|
||||
self.rebuild_library();
|
||||
crate::state::save_cache(&self.raw_tracks);
|
||||
self.set_status(format!("Добавлено {} треков из {}", added, expanded));
|
||||
}
|
||||
|
||||
pub fn reload_library(&mut self) {
|
||||
let mut dirs = vec![self.music_dir.clone()];
|
||||
dirs.extend(self.extra_dirs.clone());
|
||||
|
||||
let mut all = Vec::new();
|
||||
for dir in &dirs {
|
||||
let expanded = expand_tilde(dir);
|
||||
all.extend(scan_dir(&expanded));
|
||||
}
|
||||
|
||||
let mut seen = HashSet::new();
|
||||
all.retain(|t| seen.insert(t.path.clone()));
|
||||
|
||||
self.raw_tracks = all;
|
||||
self.deleted_paths.clear();
|
||||
crate::state::save_cache(&self.raw_tracks);
|
||||
|
||||
self.rebuild_library();
|
||||
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);
|
||||
}
|
||||
|
||||
// ── Удаление из списка (клавиша Del) ───────────────────────
|
||||
pub fn delete_selected(&mut self) {
|
||||
match self.active_panel {
|
||||
Panel::Left => self.delete_current_left_group(),
|
||||
Panel::Right => self.delete_current_right_item(),
|
||||
}
|
||||
self.rebuild_library();
|
||||
}
|
||||
|
||||
fn delete_current_left_group(&mut self) {
|
||||
let Some(group) = self.library.artists.get(self.library.artist_selected) else {
|
||||
return;
|
||||
};
|
||||
let group_name = group.name.clone();
|
||||
|
||||
let to_remove: Vec<PathBuf> = group
|
||||
.rows
|
||||
.iter()
|
||||
.filter_map(|r| match r {
|
||||
RightRow::Track { index_in_library } => {
|
||||
self.library.track(*index_in_library).map(|t| t.path.clone())
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
let count = to_remove.len();
|
||||
for p in &to_remove {
|
||||
self.deleted_paths.insert(p.clone());
|
||||
}
|
||||
self.raw_tracks
|
||||
.retain(|t| !self.deleted_paths.contains(&t.path));
|
||||
|
||||
if let Some(idx) = self.playing_index {
|
||||
if let Some(t) = self.library.track(idx) {
|
||||
if to_remove.contains(&t.path) {
|
||||
self.player.stop();
|
||||
self.playing_index = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.set_status(format!("Удалено: {} ({} треков)", group_name, count));
|
||||
}
|
||||
|
||||
fn delete_current_right_item(&mut self) {
|
||||
let rows = self.library.current_right_rows();
|
||||
let Some(row) = rows.get(self.library.right_selected).cloned() else {
|
||||
return;
|
||||
};
|
||||
|
||||
match row {
|
||||
RightRow::Track { index_in_library } => {
|
||||
if let Some(track) = self.library.track(index_in_library).cloned() {
|
||||
let name = format!("{} — {}", track.artist, track.title);
|
||||
self.deleted_paths.insert(track.path.clone());
|
||||
self.raw_tracks.retain(|t| t.path != track.path);
|
||||
|
||||
if Some(index_in_library) == self.playing_index {
|
||||
self.player.stop();
|
||||
self.playing_index = None;
|
||||
}
|
||||
|
||||
self.set_status(format!("Удалено: {}", name));
|
||||
}
|
||||
}
|
||||
RightRow::AlbumHeader { name } => {
|
||||
let start = self.library.right_selected + 1;
|
||||
let mut to_remove: Vec<PathBuf> = Vec::new();
|
||||
for r in rows.iter().skip(start) {
|
||||
match r {
|
||||
RightRow::Track { index_in_library } => {
|
||||
if let Some(t) = self.library.track(*index_in_library) {
|
||||
to_remove.push(t.path.clone());
|
||||
}
|
||||
}
|
||||
RightRow::AlbumHeader { .. } => break,
|
||||
}
|
||||
}
|
||||
|
||||
let count = to_remove.len();
|
||||
for p in &to_remove {
|
||||
self.deleted_paths.insert(p.clone());
|
||||
}
|
||||
self.raw_tracks
|
||||
.retain(|t| !self.deleted_paths.contains(&t.path));
|
||||
|
||||
if let Some(idx) = self.playing_index {
|
||||
if let Some(t) = self.library.track(idx) {
|
||||
if to_remove.contains(&t.path) {
|
||||
self.player.stop();
|
||||
self.playing_index = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.set_status(format!("Удалено: {} ({} треков)", name, count));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Переключение треков ────────────────────────────────────
|
||||
|
||||
pub fn next_track(&mut self) {
|
||||
let total = self.library.all_tracks.len();
|
||||
if total == 0 {
|
||||
@@ -187,6 +435,10 @@ impl App {
|
||||
}
|
||||
|
||||
fn persist(&self) {
|
||||
// Сохраняем кэш треков — чтобы при следующем запуске
|
||||
// не сканировать директории заново.
|
||||
crate::state::save_cache(&self.raw_tracks);
|
||||
|
||||
let playing_path = self
|
||||
.playing_index
|
||||
.and_then(|i| self.library.track(i))
|
||||
@@ -206,6 +458,7 @@ impl App {
|
||||
shuffle: Some(self.shuffle),
|
||||
last_playing_path: playing_path,
|
||||
last_playing_pos_secs: playing_pos,
|
||||
extra_dirs: self.extra_dirs.clone(),
|
||||
};
|
||||
crate::state::save(&state);
|
||||
}
|
||||
@@ -214,6 +467,8 @@ impl App {
|
||||
self.persist();
|
||||
}
|
||||
|
||||
// ── Поиск ──────────────────────────────────────────────────
|
||||
|
||||
pub fn start_search(&mut self) {
|
||||
self.input_mode = InputMode::Search;
|
||||
self.search_buffer.clear();
|
||||
@@ -251,16 +506,20 @@ impl App {
|
||||
|
||||
fn rebuild_library(&mut self) {
|
||||
let mode = self.library.sort_mode;
|
||||
self.library = Library::build_with_filter(
|
||||
self.raw_tracks.clone(),
|
||||
mode,
|
||||
self.search_buffer.clone(),
|
||||
);
|
||||
let filtered: Vec<Track> = self
|
||||
.raw_tracks
|
||||
.iter()
|
||||
.filter(|t| !self.deleted_paths.contains(&t.path))
|
||||
.cloned()
|
||||
.collect();
|
||||
self.library = Library::build_with_filter(filtered, mode, self.search_buffer.clone());
|
||||
if let Some(i) = self.playing_index {
|
||||
self.library.sync_with_playing(i);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Навигация ──────────────────────────────────────────────
|
||||
|
||||
pub fn up(&mut self) {
|
||||
match self.active_panel {
|
||||
Panel::Left => self.library.artist_move(-1),
|
||||
@@ -321,6 +580,8 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Playback ───────────────────────────────────────────────
|
||||
|
||||
pub fn play_index(&mut self, index_in_library: usize) {
|
||||
let Some(track) = self.library.track(index_in_library).cloned() else {
|
||||
return;
|
||||
@@ -507,8 +768,15 @@ impl App {
|
||||
.and_then(|i| self.library.track(i))
|
||||
.map(|t| t.path.clone());
|
||||
|
||||
let filtered: Vec<Track> = self
|
||||
.raw_tracks
|
||||
.iter()
|
||||
.filter(|t| !self.deleted_paths.contains(&t.path))
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
self.library = Library::build_with_filter(
|
||||
self.raw_tracks.clone(),
|
||||
filtered,
|
||||
new_mode,
|
||||
self.search_buffer.clone(),
|
||||
);
|
||||
@@ -527,4 +795,19 @@ impl App {
|
||||
};
|
||||
self.set_status(msg);
|
||||
}
|
||||
}
|
||||
|
||||
/// Развернуть `~/...` в путь домашней директории.
|
||||
fn expand_tilde(path: &str) -> String {
|
||||
if let Some(rest) = path.strip_prefix("~/") {
|
||||
if let Some(home) = dirs::home_dir() {
|
||||
return home.join(rest).to_string_lossy().to_string();
|
||||
}
|
||||
}
|
||||
if path == "~" {
|
||||
if let Some(home) = dirs::home_dir() {
|
||||
return home.to_string_lossy().to_string();
|
||||
}
|
||||
}
|
||||
path.to_string()
|
||||
}
|
||||
28
src/main.rs
28
src/main.rs
@@ -26,8 +26,14 @@ 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;
|
||||
}
|
||||
|
||||
app.maybe_advance();
|
||||
app.tick_status(); // ← автосброс статуса через 2 секунды
|
||||
app.tick_status();
|
||||
app.player.spectrum.recompute();
|
||||
terminal.draw(|f| ui::draw(f, &mut app))?;
|
||||
|
||||
@@ -63,6 +69,17 @@ 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(),
|
||||
KeyCode::Enter => app.commit_command(),
|
||||
KeyCode::Backspace => app.command_pop(),
|
||||
KeyCode::Char(c) => app.command_push(c),
|
||||
_ => {}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if ctrl {
|
||||
match key.code {
|
||||
KeyCode::Right => {
|
||||
@@ -99,7 +116,16 @@ fn handle_key(key: KeyEvent, app: &mut App) -> bool {
|
||||
return false;
|
||||
}
|
||||
|
||||
if key.code == KeyCode::Delete {
|
||||
app.delete_selected();
|
||||
return false;
|
||||
}
|
||||
|
||||
match key.code {
|
||||
KeyCode::Char(':') => {
|
||||
app.start_command();
|
||||
return false;
|
||||
}
|
||||
KeyCode::Char('+') | KeyCode::Char('=') => {
|
||||
app.volume_up();
|
||||
return false;
|
||||
|
||||
29
src/state.rs
29
src/state.rs
@@ -15,6 +15,9 @@ pub struct SavedState {
|
||||
pub shuffle: Option<bool>,
|
||||
pub last_playing_path: Option<String>,
|
||||
pub last_playing_pos_secs: Option<u64>,
|
||||
/// Дополнительные директории, добавленные через :add
|
||||
#[serde(default)]
|
||||
pub extra_dirs: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Copy, PartialEq, Debug, Default)]
|
||||
@@ -25,7 +28,7 @@ pub enum RepeatMode {
|
||||
One,
|
||||
}
|
||||
|
||||
fn config_path() -> Option<PathBuf> {
|
||||
pub fn config_path() -> Option<PathBuf> {
|
||||
dirs::config_dir().map(|p| p.join("rumus").join("state.toml"))
|
||||
}
|
||||
|
||||
@@ -57,35 +60,21 @@ pub fn save(state: &SavedState) {
|
||||
|
||||
// ── Кэш плейлиста (bincode) ────────────────────────────────────
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct PlaylistCache {
|
||||
pub source_dir: String,
|
||||
pub tracks: Vec<Track>,
|
||||
}
|
||||
|
||||
pub fn load_cache(expected_dir: &str) -> Option<Vec<Track>> {
|
||||
/// Кэш просто хранит все известные треки. Без привязки к конкретной директории.
|
||||
pub fn load_cache() -> Option<Vec<Track>> {
|
||||
let path = cache_path()?;
|
||||
let bytes = fs::read(&path).ok()?;
|
||||
let cache: PlaylistCache = bincode::deserialize(&bytes).ok()?;
|
||||
if cache.source_dir == expected_dir {
|
||||
Some(cache.tracks)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
bincode::deserialize(&bytes).ok()
|
||||
}
|
||||
|
||||
pub fn save_cache(source_dir: &str, tracks: &[Track]) {
|
||||
pub fn save_cache(tracks: &[Track]) {
|
||||
let Some(path) = cache_path() else {
|
||||
return;
|
||||
};
|
||||
if let Some(parent) = path.parent() {
|
||||
let _ = fs::create_dir_all(parent);
|
||||
}
|
||||
let cache = PlaylistCache {
|
||||
source_dir: source_dir.to_string(),
|
||||
tracks: tracks.to_vec(),
|
||||
};
|
||||
if let Ok(bytes) = bincode::serialize(&cache) {
|
||||
if let Ok(bytes) = bincode::serialize(&tracks.to_vec()) {
|
||||
let _ = fs::write(path, bytes);
|
||||
}
|
||||
}
|
||||
56
src/ui.rs
56
src/ui.rs
@@ -149,14 +149,20 @@ 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);
|
||||
|
||||
ListItem::new(Line::from(vec![
|
||||
Span::raw(" "),
|
||||
Span::raw(marker),
|
||||
Span::styled(format!("{}{}", num, title), style),
|
||||
Span::raw(" "),
|
||||
// цвет таймера
|
||||
Span::styled(dur, Style::default().fg(Color::DarkGray)),
|
||||
Span::styled(left, style),
|
||||
Span::raw(" ".repeat(pad)),
|
||||
Span::styled(dur, style),
|
||||
]))
|
||||
}
|
||||
})
|
||||
@@ -247,13 +253,13 @@ fn draw_equalizer(f: &mut Frame, app: &App, area: Rect) {
|
||||
}
|
||||
|
||||
let bars: Vec<Bar> = display
|
||||
.iter()
|
||||
.map(|&v| {
|
||||
Bar::default()
|
||||
.value((v * 100.0) as u64)
|
||||
.style(Style::default().fg(Color::Rgb(100, 149, 237)))
|
||||
})
|
||||
.collect();
|
||||
.iter()
|
||||
.map(|&v| {
|
||||
Bar::default()
|
||||
.value((v * 100.0) as u64)
|
||||
.style(Style::default().fg(Color::Rgb(100, 149, 237)))
|
||||
})
|
||||
.collect();
|
||||
|
||||
let chart = BarChart::default()
|
||||
.data(BarGroup::default().bars(&bars))
|
||||
@@ -282,11 +288,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 {
|
||||
let prefix = if app.player.is_paused() { "|| " } else { " " };
|
||||
(format!("{}{}", prefix, app.status), " Статус ")
|
||||
let (status_text, status_title) = match app.input_mode {
|
||||
InputMode::Search => (format!("> {}", app.search_buffer), " Поиск "),
|
||||
InputMode::Command => (format!(":{}", app.command_buffer), " Команда "),
|
||||
InputMode::Normal => {
|
||||
let prefix = if app.player.is_paused() { "|| " } else { " " };
|
||||
(format!("{}{}", prefix, app.status), " Статус ")
|
||||
}
|
||||
};
|
||||
|
||||
let status = Paragraph::new(status_text)
|
||||
@@ -327,8 +335,8 @@ fn draw_status(f: &mut Frame, app: &App, area: Rect) {
|
||||
}
|
||||
|
||||
fn draw_help(f: &mut Frame, screen: Rect) {
|
||||
let width = 60u16.min(screen.width.saturating_sub(4));
|
||||
let height = 34u16.min(screen.height.saturating_sub(4));
|
||||
let width = 62u16.min(screen.width.saturating_sub(4));
|
||||
let height = 40u16.min(screen.height.saturating_sub(4));
|
||||
|
||||
let x = (screen.width.saturating_sub(width)) / 2;
|
||||
let y = (screen.height.saturating_sub(height)) / 2;
|
||||
@@ -368,9 +376,17 @@ fn draw_help(f: &mut Frame, screen: Rect) {
|
||||
Line::from(" d / → +5 секунд"),
|
||||
Line::from(" 1..9 перейти к 10%..90% трека"),
|
||||
Line::from(""),
|
||||
Line::from(Span::styled(" Команды", header)),
|
||||
Line::from(" :add <путь> добавить директорию"),
|
||||
Line::from(" :reload пересканировать всё"),
|
||||
Line::from(" :save сохранить библиотеку"),
|
||||
Line::from(" :help эта справка"),
|
||||
Line::from(" :q выход"),
|
||||
Line::from(""),
|
||||
Line::from(Span::styled(" Звук и прочее", header)),
|
||||
Line::from(" + / - громкость"),
|
||||
Line::from(" / поиск"),
|
||||
Line::from(" Del удалить из списка"),
|
||||
Line::from(" t сортировка: артист → альбом → файл"),
|
||||
Line::from(" F1 эта справка"),
|
||||
Line::from(" q / Esc выход"),
|
||||
|
||||
Reference in New Issue
Block a user