обновление

This commit is contained in:
2026-10-13 17:38:18 +02:00
parent 255a99ed0f
commit 08a8a7cce5
6 changed files with 502 additions and 87 deletions

View File

@@ -1,68 +1,80 @@
use std::time::Duration; use std::time::Duration;
use ratatui::widgets::ListState;
use crate::audio::AudioPlayer; use crate::audio::AudioPlayer;
use crate::track::{scan_dir, Track}; use crate::library::{Library, SortMode};
use crate::track::scan_dir;
/// Какая панель активна
#[derive(Clone, Copy, PartialEq)]
pub enum Panel {
Left,
Right,
}
pub struct App { pub struct App {
pub tracks: Vec<Track>, pub library: Library,
pub list_state: ListState,
pub player: AudioPlayer, pub player: AudioPlayer,
pub status: String, pub status: String,
pub playing_index: Option<usize>, pub playing_index: Option<usize>,
pub active_panel: Panel,
} }
impl App { impl App {
pub fn new(music_dir: &str) -> Result<Self, Box<dyn std::error::Error>> { pub fn new(music_dir: &str) -> Result<Self, Box<dyn std::error::Error>> {
let tracks = scan_dir(music_dir); let tracks = scan_dir(music_dir);
let mut list_state = ListState::default(); let library = Library::build(tracks, SortMode::ByArtist);
if !tracks.is_empty() {
list_state.select(Some(0));
}
Ok(Self { Ok(Self {
tracks, library,
list_state,
player: AudioPlayer::new()?, player: AudioPlayer::new()?,
status: "Нажмите Enter для воспроизведения".into(), status: "Enter — play, t — сортировка, Tab — панель".into(),
playing_index: None, playing_index: None,
active_panel: Panel::Left,
}) })
} }
pub fn next(&mut self) { /// Навигация вверх с учётом активной панели
if self.tracks.is_empty() { return; } pub fn up(&mut self) {
let i = match self.list_state.selected() { match self.active_panel {
Some(i) if i >= self.tracks.len() - 1 => 0, Panel::Left => self.library.left_move(-1),
Some(i) => i + 1, Panel::Right => self.library.right_move(-1),
None => 0, }
};
self.list_state.select(Some(i));
} }
pub fn prev(&mut self) { pub fn down(&mut self) {
if self.tracks.is_empty() { return; } match self.active_panel {
let i = match self.list_state.selected() { Panel::Left => self.library.left_move(1),
Some(0) => self.tracks.len() - 1, Panel::Right => self.library.right_move(1),
Some(i) => i - 1, }
None => 0,
};
self.list_state.select(Some(i));
} }
pub fn play_index(&mut self, index: usize) { pub fn toggle_panel(&mut self) {
let Some(track) = self.tracks.get(index) else { return; }; self.active_panel = match self.active_panel {
Panel::Left => Panel::Right,
Panel::Right => Panel::Left,
};
self.status = match self.active_panel {
Panel::Left => "Активна: левая панель".into(),
Panel::Right => "Активна: правая панель".into(),
};
}
pub fn play_index(&mut self, index_in_library: usize) {
let Some(track) = self.library.track(index_in_library).cloned() else {
return;
};
match self.player.play(&track.path) { match self.player.play(&track.path) {
Ok(_) => { Ok(_) => {
self.status = format!("▶ {} — {}", track.artist, track.title); self.status = format!("▶ {} — {}", track.artist, track.title);
self.playing_index = Some(index); self.playing_index = Some(index_in_library);
} }
Err(e) => self.status = format!("Ошибка: {}", e), Err(e) => self.status = format!("Ошибка: {}", e),
} }
} }
pub fn play_selected(&mut self) { pub fn play_selected(&mut self) {
if let Some(i) = self.list_state.selected() { if let Some(idx) = self.library.right_track_index() {
self.play_index(i); self.play_index(idx);
} }
} }
@@ -81,11 +93,11 @@ impl App {
return; return;
} }
let next = match self.playing_index { let next = match self.playing_index {
Some(i) if i + 1 < self.tracks.len() => i + 1, Some(i) if i + 1 < self.library.all_tracks.len() => i + 1,
_ => 0, _ => 0,
}; };
self.play_index(next); self.play_index(next);
self.list_state.select(Some(next)); self.library.sync_with_playing(next);
} }
pub fn volume_up(&mut self) { pub fn volume_up(&mut self) {
@@ -105,7 +117,7 @@ impl App {
} }
let total = self let total = self
.playing_index .playing_index
.and_then(|i| self.tracks.get(i)) .and_then(|i| self.library.track(i))
.map(|t| t.duration) .map(|t| t.duration)
.unwrap_or(Duration::ZERO); .unwrap_or(Duration::ZERO);
@@ -114,20 +126,47 @@ impl App {
let sign = if delta_secs >= 0 { "+" } else { "" }; let sign = if delta_secs >= 0 { "+" } else { "" };
self.status = format!( self.status = format!(
"Перемотка {}{}с → {:02}:{:02}", "Перемотка {}{}с → {:02}:{:02}",
sign, delta_secs, sign,
delta_secs,
new_pos.as_secs() / 60, new_pos.as_secs() / 60,
new_pos.as_secs() % 60, new_pos.as_secs() % 60,
); );
} }
Err(_) => self.status = "Перемотка недоступна для этого формата".into(), Err(_) => self.status = "Перемотка недоступна".into(),
} }
} }
/// Текущая позиция и длина играющего трека (для прогресс-бара)
pub fn progress(&self) -> (Duration, Duration) { pub fn progress(&self) -> (Duration, Duration) {
match self.playing_index.and_then(|i| self.tracks.get(i)) { match self.playing_index.and_then(|i| self.library.track(i)) {
Some(t) => (self.player.position(), t.duration), Some(t) => (self.player.position(), t.duration),
None => (Duration::ZERO, Duration::ZERO), None => (Duration::ZERO, Duration::ZERO),
} }
} }
pub fn toggle_sort(&mut self) {
let new_mode = match self.library.sort_mode {
SortMode::ByArtist => SortMode::ByFilename,
SortMode::ByFilename => SortMode::ByArtist,
};
let playing_path = self
.playing_index
.and_then(|i| self.library.track(i))
.map(|t| t.path.clone());
let all = self.library.all_tracks.clone();
self.library = Library::build(all, new_mode);
self.playing_index = playing_path
.and_then(|p| self.library.all_tracks.iter().position(|t| t.path == p));
if let Some(i) = self.playing_index {
self.library.sync_with_playing(i);
}
self.status = match new_mode {
SortMode::ByArtist => "Сортировка: по артисту/альбому".into(),
SortMode::ByFilename => "Сортировка: по имени файла".into(),
};
}
} }

View File

@@ -16,7 +16,11 @@ impl AudioPlayer {
let stream_handle = OutputStreamBuilder::open_default_stream()?; let stream_handle = OutputStreamBuilder::open_default_stream()?;
let sink = Sink::connect_new(&stream_handle.mixer()); let sink = Sink::connect_new(&stream_handle.mixer());
sink.set_volume(1.0); sink.set_volume(1.0);
Ok(Self { _stream: stream_handle, sink, paused_pos: None }) Ok(Self {
_stream: stream_handle,
sink,
paused_pos: None,
})
} }
pub fn play(&mut self, path: &PathBuf) -> Result<(), Box<dyn std::error::Error>> { pub fn play(&mut self, path: &PathBuf) -> Result<(), Box<dyn std::error::Error>> {
@@ -65,7 +69,11 @@ impl AudioPlayer {
self.sink.set_volume(v); self.sink.set_volume(v);
} }
pub fn seek_by(&mut self, delta: i64, total: Duration) -> Result<Duration, Box<dyn std::error::Error>> { pub fn seek_by(
&mut self,
delta: i64,
total: Duration,
) -> Result<Duration, Box<dyn std::error::Error>> {
let current = self.position(); let current = self.position();
let new_secs = (current.as_secs() as i64 + delta).max(0) as u64; let new_secs = (current.as_secs() as i64 + delta).max(0) as u64;
let mut new_pos = Duration::from_secs(new_secs); let mut new_pos = Duration::from_secs(new_secs);

202
src/library.rs Normal file
View File

@@ -0,0 +1,202 @@
use crate::track::Track;
#[derive(Clone)]
pub enum LeftRow {
Artist { name: String },
Album { name: String },
}
#[derive(Clone)]
pub enum RightRow {
Track { index_in_library: usize },
}
#[derive(Clone, Copy, PartialEq)]
pub enum SortMode {
ByArtist,
ByFilename,
}
pub struct Library {
pub all_tracks: Vec<Track>,
/// Левая панель: артисты и альбомы
pub left_rows: Vec<LeftRow>,
/// Для каждой строки левой панели — список индексов треков в all_tracks
pub left_to_tracks: Vec<Vec<usize>>,
/// Индекс выбранной строки левой панели
pub left_selected: usize,
/// Индекс выбранного трека в правой панели (внутри текущего набора)
pub right_selected: usize,
pub sort_mode: SortMode,
}
impl Library {
pub fn build(mut tracks: Vec<Track>, mode: SortMode) -> Self {
match mode {
SortMode::ByArtist => {
use std::collections::BTreeMap;
let mut map: BTreeMap<String, BTreeMap<String, Vec<Track>>> = BTreeMap::new();
for t in tracks.drain(..) {
map.entry(t.artist.clone())
.or_default()
.entry(t.album.clone())
.or_default()
.push(t);
}
let mut all_tracks = Vec::new();
let mut left_rows = Vec::new();
let mut left_to_tracks: Vec<Vec<usize>> = Vec::new();
for (artist_name, albums_map) in map {
// Строка артиста: содержит все треки всех его альбомов
let mut artist_track_indices = Vec::new();
let mut album_entries = Vec::new();
for (album_name, mut album_tracks) in albums_map {
album_tracks.sort_by(|a, b| {
a.track_number
.unwrap_or(u32::MAX)
.cmp(&b.track_number.unwrap_or(u32::MAX))
.then(a.title.cmp(&b.title))
});
let mut album_indices = Vec::new();
for t in album_tracks {
let idx = all_tracks.len();
all_tracks.push(t);
album_indices.push(idx);
artist_track_indices.push(idx);
}
album_entries.push((album_name, album_indices));
}
// 1) строка артиста
left_rows.push(LeftRow::Artist {
name: artist_name,
});
left_to_tracks.push(artist_track_indices);
// 2) строки альбомов
for (album_name, indices) in album_entries {
left_rows.push(LeftRow::Album { name: album_name });
left_to_tracks.push(indices);
}
}
Library {
all_tracks,
left_rows,
left_to_tracks,
left_selected: 0,
right_selected: 0,
sort_mode: mode,
}
}
SortMode::ByFilename => {
tracks.sort_by(|a, b| a.path.cmp(&b.path));
let all_tracks = tracks;
let left_rows = vec![LeftRow::Album {
name: "Все файлы".into(),
}];
let left_to_tracks = vec![(0..all_tracks.len()).collect()];
Library {
all_tracks,
left_rows,
left_to_tracks,
left_selected: 0,
right_selected: 0,
sort_mode: mode,
}
}
}
}
pub fn track(&self, index: usize) -> Option<&Track> {
self.all_tracks.get(index)
}
/// Индексы треков, относящихся к текущей выделенной строке левой панели
pub fn current_right_indices(&self) -> &[usize] {
self.left_to_tracks
.get(self.left_selected)
.map(|v| v.as_slice())
.unwrap_or(&[])
}
/// Индекс трека в all_tracks, выделенного в правой панели
pub fn right_track_index(&self) -> Option<usize> {
self.current_right_indices()
.get(self.right_selected)
.copied()
}
/// Найти строку левой панели, к которой относится трек (для синхронизации)
pub fn left_row_of_track(&self, index_in_library: usize) -> Option<usize> {
// Приоритет — альбом (более точное совпадение)
let mut album_match = None;
for (i, row) in self.left_rows.iter().enumerate() {
let contains = self
.left_to_tracks
.get(i)
.map(|v| v.contains(&index_in_library))
.unwrap_or(false);
if contains {
match row {
LeftRow::Album { .. } => return Some(i),
LeftRow::Artist { .. } => album_match = Some(i),
}
}
}
album_match
}
/// Передвинуть курсор левой панели вверх/вниз, синхронизировать правую
pub fn left_move(&mut self, delta: isize) {
if self.left_rows.is_empty() {
return;
}
let len = self.left_rows.len() as isize;
let mut new = self.left_selected as isize + delta;
if new < 0 {
new = len - 1;
} else if new >= len {
new = 0;
}
self.left_selected = new as usize;
self.right_selected = 0;
}
/// Передвинуть курсор правой панели вверх/вниз
pub fn right_move(&mut self, delta: isize) {
let len = self.current_right_indices().len() as isize;
if len == 0 {
return;
}
let mut new = self.right_selected as isize + delta;
if new < 0 {
new = len - 1;
} else if new >= len {
new = 0;
}
self.right_selected = new as usize;
}
/// Синхронизировать правую панель с играющим треком
pub fn sync_with_playing(&mut self, index_in_library: usize) {
if let Some(row) = self.left_row_of_track(index_in_library) {
self.left_selected = row;
if let Some(pos) = self
.left_to_tracks
.get(row)
.and_then(|v| v.iter().position(|&i| i == index_in_library))
{
self.right_selected = pos;
}
}
}
}

View File

@@ -1,5 +1,6 @@
mod app; mod app;
mod audio; mod audio;
mod library;
mod track; mod track;
mod ui; mod ui;
@@ -25,22 +26,11 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
if event::poll(Duration::from_millis(100))? { if event::poll(Duration::from_millis(100))? {
if let Event::Key(key) = event::read()? { if let Event::Key(key) = event::read()? {
if key.kind != KeyEventKind::Press { if key.kind == KeyEventKind::Release {
continue; continue;
} }
match key.code { if handle_key(key.code, &mut app) {
//навигация break;
KeyCode::Char('q') | KeyCode::Esc => break,
KeyCode::Up | KeyCode::Char('w') => app.prev(),
KeyCode::Down | KeyCode::Char('s') => app.next(),
KeyCode::Left | KeyCode::Char('a') => app.seek(-5),
KeyCode::Right | KeyCode::Char('d') => app.seek(5),
KeyCode::Enter => app.play_selected(),
KeyCode::Char(' ') => app.toggle_pause(),
KeyCode::Char('z') => app.stop(),
KeyCode::Char('+') | KeyCode::Char('=') => app.volume_up(),
KeyCode::Char('-') | KeyCode::Char('_') => app.volume_down(),
_ => {}
} }
} }
} }
@@ -49,3 +39,59 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
ratatui::restore(); ratatui::restore();
Ok(()) Ok(())
} }
fn handle_key(code: KeyCode, app: &mut App) -> bool {
match code {
KeyCode::Char('+') | KeyCode::Char('=') => {
app.volume_up();
return false;
}
KeyCode::Char('-') | KeyCode::Char('_') => {
app.volume_down();
return false;
}
KeyCode::Up => {
app.up();
return false;
}
KeyCode::Down => {
app.down();
return false;
}
KeyCode::Left => {
app.seek(-5);
return false;
}
KeyCode::Right => {
app.seek(5);
return false;
}
KeyCode::Enter => {
app.play_selected();
return false;
}
KeyCode::Char(' ') => {
app.toggle_pause();
return false;
}
KeyCode::Tab => {
app.toggle_panel();
return false;
}
_ => {}
}
if let KeyCode::Char(c) = code {
match c.to_ascii_lowercase() {
'q' => return true,
'w' => app.up(),
's' => app.down(),
'a' => app.seek(-5),
'd' => app.seek(5),
'z' => app.stop(),
't' => app.toggle_sort(),
_ => {}
}
}
false
}

View File

@@ -4,11 +4,13 @@ use std::time::Duration;
use lofty::file::{AudioFile, TaggedFileExt}; use lofty::file::{AudioFile, TaggedFileExt};
use lofty::tag::Accessor; use lofty::tag::Accessor;
#[derive(Clone)]
pub struct Track { pub struct Track {
pub path: PathBuf, pub path: PathBuf,
pub title: String, pub title: String,
pub artist: String, pub artist: String,
pub album: String, pub album: String,
pub track_number: Option<u32>,
pub duration: Duration, pub duration: Duration,
} }
@@ -23,48 +25,70 @@ impl Track {
match lofty::read_from_path(path) { match lofty::read_from_path(path) {
Ok(tagged) => { Ok(tagged) => {
let duration = tagged.properties().duration(); let duration = tagged.properties().duration();
let (title, artist, album) = match tagged let (title, artist, album, track_number) = match tagged
.primary_tag() .primary_tag()
.or_else(|| tagged.first_tag()) .or_else(|| tagged.first_tag())
{ {
Some(tag) => ( Some(tag) => (
tag.title().map(|s| s.to_string()).unwrap_or_else(|| fallback_name.clone()), tag.title()
tag.artist().map(|s| s.to_string()).unwrap_or_else(|| "Unknown Artist".into()), .map(|s| s.to_string())
tag.album().map(|s| s.to_string()).unwrap_or_else(|| "Unknown Album".into()), .unwrap_or_else(|| fallback_name.clone()),
tag.artist()
.map(|s| s.to_string())
.unwrap_or_else(|| "Unknown Artist".into()),
tag.album()
.map(|s| s.to_string())
.unwrap_or_else(|| "Unknown Album".into()),
tag.track(),
),
None => (
fallback_name,
"Unknown Artist".into(),
"Unknown Album".into(),
None,
), ),
None => (fallback_name, "Unknown Artist".into(), "Unknown Album".into()),
}; };
Track { path: path.clone(), title, artist, album, duration } Track {
path: path.clone(),
title,
artist,
album,
track_number,
duration,
}
} }
Err(_) => Track { Err(_) => Track {
path: path.clone(), path: path.clone(),
title: fallback_name, title: fallback_name,
artist: "Unknown Artist".into(), artist: "Unknown Artist".into(),
album: "Unknown Album".into(), album: "Unknown Album".into(),
track_number: None,
duration: Duration::ZERO, duration: Duration::ZERO,
}, },
} }
} }
} }
/// Рекурсивно собирает все аудиофайлы в директории /// Собирает все аудиофайлы в директории (без сортировки)
pub fn scan_dir(dir: &str) -> Vec<Track> { pub fn scan_dir(dir: &str) -> Vec<Track> {
use walkdir::WalkDir; use walkdir::WalkDir;
let mut tracks = Vec::new(); let mut tracks = Vec::new();
for entry in WalkDir::new(dir).follow_links(true).into_iter().filter_map(|e| e.ok()) { for entry in WalkDir::new(dir)
.follow_links(true)
.into_iter()
.filter_map(|e| e.ok())
{
let path = entry.path(); let path = entry.path();
if path.is_file() { if path.is_file() {
if let Some(ext) = path.extension().and_then(|e| e.to_str()) { if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
if matches!(ext.to_lowercase().as_str(), "mp3" | "flac" | "ogg" | "wav" | "m4a") { if matches!(
ext.to_lowercase().as_str(),
"mp3" | "flac" | "ogg" | "wav" | "m4a"
) {
tracks.push(Track::from_path(&path.to_path_buf())); tracks.push(Track::from_path(&path.to_path_buf()));
} }
} }
} }
} }
tracks.sort_by(|a, b| {
a.artist.cmp(&b.artist)
.then(a.album.cmp(&b.album))
.then(a.title.cmp(&b.title))
});
tracks tracks
} }

130
src/ui.rs
View File

@@ -1,7 +1,8 @@
use ratatui::prelude::*; use ratatui::prelude::*;
use ratatui::widgets::{Block, Borders, Gauge, List, ListItem, Paragraph}; use ratatui::widgets::{Block, Borders, Gauge, List, ListItem, Paragraph};
use crate::app::App; use crate::app::{App, Panel};
use crate::library::{LeftRow, SortMode};
pub fn draw(f: &mut Frame, app: &mut App) { pub fn draw(f: &mut Frame, app: &mut App) {
let chunks = Layout::default() let chunks = Layout::default()
@@ -14,29 +15,114 @@ pub fn draw(f: &mut Frame, app: &mut App) {
]) ])
.split(f.area()); .split(f.area());
draw_track_list(f, app, chunks[0]); // Горизонтальное разделение верхней области: 40% слева, 60% справа
let top = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(40), Constraint::Percentage(60)])
.split(chunks[0]);
draw_left_panel(f, app, top[0]);
draw_right_panel(f, app, top[1]);
draw_progress(f, app, chunks[1]); draw_progress(f, app, chunks[1]);
draw_volume(f, app, chunks[2]); draw_volume(f, app, chunks[2]);
draw_status(f, app, chunks[3]); draw_status(f, app, chunks[3]);
} }
fn draw_track_list(f: &mut Frame, app: &mut App, area: Rect) { fn draw_left_panel(f: &mut Frame, app: &App, area: Rect) {
let items: Vec<ListItem> = app let items: Vec<ListItem> = app
.tracks .library
.left_rows
.iter() .iter()
.enumerate() .map(|row| match row {
.map(|(i, t)| { LeftRow::Artist { name } => ListItem::new(Line::from(vec![
let marker = if Some(i) == app.playing_index { "♪ " } else { " " }; Span::styled("▶ ", Style::default().fg(Color::Yellow)),
ListItem::new(format!("{}{} - {}", marker, t.artist, t.title)) Span::styled(
name.clone(),
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
),
])),
LeftRow::Album { name } => ListItem::new(Line::from(vec![
Span::raw(" "),
Span::styled(name.clone(), Style::default().fg(Color::Cyan)),
])),
}) })
.collect(); .collect();
let title = match app.library.sort_mode {
SortMode::ByArtist => " Артисты / Альбомы ",
SortMode::ByFilename => " Источник ",
};
let highlight = if app.active_panel == Panel::Left {
Style::default().add_modifier(Modifier::REVERSED)
} else {
Style::default().add_modifier(Modifier::DIM)
};
let mut list_state = ratatui::widgets::ListState::default();
if !app.library.left_rows.is_empty() {
list_state.select(Some(app.library.left_selected));
}
let list = List::new(items) let list = List::new(items)
.block(Block::default().borders(Borders::ALL).title(" Треки ")) .block(Block::default().borders(Borders::ALL).title(title))
.highlight_style(Style::default().add_modifier(Modifier::REVERSED)) .highlight_style(highlight)
.highlight_symbol("> "); .highlight_symbol("> ");
f.render_stateful_widget(list, area, &mut app.list_state); f.render_stateful_widget(list, area, &mut list_state);
}
fn draw_right_panel(f: &mut Frame, app: &App, area: Rect) {
let indices = app.library.current_right_indices();
let items: Vec<ListItem> = indices
.iter()
.map(|&idx| {
let is_playing = Some(idx) == app.playing_index;
let marker = if is_playing { "♪ " } else { " " };
let (title, num) = match app.library.track(idx) {
Some(t) => (
t.title.clone(),
t.track_number
.map(|n| format!("{:02}. ", n))
.unwrap_or_default(),
),
None => ("<unknown>".into(), String::new()),
};
let style = if is_playing {
Style::default().fg(Color::Green)
} else {
Style::default()
};
ListItem::new(Line::from(vec![
Span::raw(marker),
Span::styled(format!("{}{}", num, title), style),
]))
})
.collect();
let highlight = if app.active_panel == Panel::Right {
Style::default().add_modifier(Modifier::REVERSED)
} else {
Style::default().add_modifier(Modifier::DIM)
};
let mut list_state = ratatui::widgets::ListState::default();
if !indices.is_empty() {
list_state.select(Some(app.library.right_selected));
}
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);
} }
fn draw_progress(f: &mut Frame, app: &App, area: Rect) { fn draw_progress(f: &mut Frame, app: &App, area: Rect) {
@@ -53,8 +139,10 @@ fn draw_progress(f: &mut Frame, app: &App, area: Rect) {
.ratio(ratio) .ratio(ratio)
.label(format!( .label(format!(
"{:02}:{:02} / {:02}:{:02}", "{:02}:{:02} / {:02}:{:02}",
pos.as_secs() / 60, pos.as_secs() % 60, pos.as_secs() / 60,
total.as_secs() / 60, total.as_secs() % 60, pos.as_secs() % 60,
total.as_secs() / 60,
total.as_secs() % 60,
)); ));
f.render_widget(gauge, area); f.render_widget(gauge, area);
@@ -63,12 +151,20 @@ fn draw_progress(f: &mut Frame, app: &App, area: Rect) {
fn draw_volume(f: &mut Frame, app: &App, area: Rect) { fn draw_volume(f: &mut Frame, app: &App, area: Rect) {
let vol = app.player.volume(); let vol = app.player.volume();
let ratio = (vol as f64 / 2.0).clamp(0.0, 1.0); let ratio = (vol as f64 / 2.0).clamp(0.0, 1.0);
let color = if vol > 1.0 { Color::Yellow } let color = if vol > 1.0 {
else if vol > 0.5 { Color::Cyan } Color::Yellow
else { Color::Blue }; } else if vol > 0.5 {
Color::Cyan
} else {
Color::Blue
};
let gauge = Gauge::default() let gauge = Gauge::default()
.block(Block::default().borders(Borders::ALL).title(" Громкость (+/-) ")) .block(
Block::default()
.borders(Borders::ALL)
.title(" Громкость (+/-) "),
)
.gauge_style(Style::default().fg(color)) .gauge_style(Style::default().fg(color))
.ratio(ratio) .ratio(ratio)
.label(format!("{}%", (vol * 100.0) as u32)); .label(format!("{}%", (vol * 100.0) as u32));