обновление
This commit is contained in:
257
src/app.rs
257
src/app.rs
@@ -2,48 +2,149 @@ use std::time::Duration;
|
||||
|
||||
use crate::audio::AudioPlayer;
|
||||
use crate::library::{Library, SortMode};
|
||||
use crate::state::{RepeatMode, SavedState};
|
||||
use crate::track::scan_dir;
|
||||
|
||||
/// Какая панель активна
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
pub enum Panel {
|
||||
Left,
|
||||
Right,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
pub enum InputMode {
|
||||
Normal,
|
||||
Search,
|
||||
}
|
||||
|
||||
pub struct App {
|
||||
pub library: Library,
|
||||
pub player: AudioPlayer,
|
||||
pub status: String,
|
||||
pub playing_index: Option<usize>,
|
||||
pub active_panel: Panel,
|
||||
pub input_mode: InputMode,
|
||||
pub search_buffer: String,
|
||||
/// Исходный (неотфильтрованный) список треков — для пересборки библиотеки
|
||||
raw_tracks: Vec<crate::track::Track>,
|
||||
/// Директория с музыкой — чтобы сохранить в state
|
||||
music_dir: String,
|
||||
pub repeat: RepeatMode,
|
||||
pub shuffle: bool,
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub fn new(music_dir: &str) -> Result<Self, Box<dyn std::error::Error>> {
|
||||
let tracks = scan_dir(music_dir);
|
||||
let library = Library::build(tracks, SortMode::ByArtist);
|
||||
let library = Library::build(tracks.clone(), SortMode::ByArtist);
|
||||
|
||||
Ok(Self {
|
||||
let mut app = Self {
|
||||
library,
|
||||
player: AudioPlayer::new()?,
|
||||
status: "Enter — play, t — сортировка, Tab — панель".into(),
|
||||
status: "? — help".into(),
|
||||
playing_index: None,
|
||||
active_panel: Panel::Left,
|
||||
})
|
||||
input_mode: InputMode::Normal,
|
||||
search_buffer: String::new(),
|
||||
raw_tracks: tracks,
|
||||
music_dir: music_dir.to_string(),
|
||||
repeat: RepeatMode::Off,
|
||||
shuffle: false,
|
||||
};
|
||||
|
||||
// Восстанавливаем сохранённое состояние
|
||||
let saved = crate::state::load();
|
||||
if let Some(vol) = saved.volume {
|
||||
app.player.set_volume(vol);
|
||||
}
|
||||
if let Some(rep) = saved.repeat {
|
||||
app.repeat = rep;
|
||||
}
|
||||
if let Some(sh) = saved.shuffle {
|
||||
app.shuffle = sh;
|
||||
}
|
||||
if saved.sort_by_filename == Some(true) {
|
||||
app.library = Library::build(app.raw_tracks.clone(), SortMode::ByFilename);
|
||||
}
|
||||
|
||||
Ok(app)
|
||||
}
|
||||
|
||||
/// Навигация вверх с учётом активной панели
|
||||
fn persist(&self) {
|
||||
let state = SavedState {
|
||||
last_path: Some(self.music_dir.clone()),
|
||||
volume: Some(self.player.volume()),
|
||||
sort_by_filename: Some(self.library.sort_mode == SortMode::ByFilename),
|
||||
repeat: Some(self.repeat),
|
||||
shuffle: Some(self.shuffle),
|
||||
};
|
||||
crate::state::save(&state);
|
||||
}
|
||||
|
||||
pub fn on_exit(&self) {
|
||||
self.persist();
|
||||
}
|
||||
|
||||
// ── Поиск ──────────────────────────────────────────────────
|
||||
|
||||
pub fn start_search(&mut self) {
|
||||
self.input_mode = InputMode::Search;
|
||||
self.search_buffer.clear();
|
||||
self.status = "Поиск: ".into();
|
||||
}
|
||||
|
||||
pub fn cancel_search(&mut self) {
|
||||
self.input_mode = InputMode::Normal;
|
||||
self.search_buffer.clear();
|
||||
self.rebuild_library();
|
||||
self.status = "Поиск отменён".into();
|
||||
}
|
||||
|
||||
pub fn commit_search(&mut self) {
|
||||
self.input_mode = InputMode::Normal;
|
||||
self.status = if self.search_buffer.is_empty() {
|
||||
"Фильтр сброшен".into()
|
||||
} else {
|
||||
format!("Фильтр: {}", self.search_buffer)
|
||||
};
|
||||
}
|
||||
|
||||
pub fn search_push(&mut self, c: char) {
|
||||
self.search_buffer.push(c);
|
||||
self.rebuild_library();
|
||||
self.status = format!("Поиск: {}", self.search_buffer);
|
||||
}
|
||||
|
||||
pub fn search_pop(&mut self) {
|
||||
self.search_buffer.pop();
|
||||
self.rebuild_library();
|
||||
self.status = format!("Поиск: {}", self.search_buffer);
|
||||
}
|
||||
|
||||
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(),
|
||||
);
|
||||
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.left_move(-1),
|
||||
Panel::Left => self.library.artist_move(-1),
|
||||
Panel::Right => self.library.right_move(-1),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn down(&mut self) {
|
||||
match self.active_panel {
|
||||
Panel::Left => self.library.left_move(1),
|
||||
Panel::Left => self.library.artist_move(1),
|
||||
Panel::Right => self.library.right_move(1),
|
||||
}
|
||||
}
|
||||
@@ -54,11 +155,43 @@ impl App {
|
||||
Panel::Right => Panel::Left,
|
||||
};
|
||||
self.status = match self.active_panel {
|
||||
Panel::Left => "Активна: левая панель".into(),
|
||||
Panel::Right => "Активна: правая панель".into(),
|
||||
Panel::Left => "Активна: исполнители".into(),
|
||||
Panel::Right => "Активна: альбомы/треки".into(),
|
||||
};
|
||||
}
|
||||
|
||||
/// f — прыжок к играющему треку
|
||||
pub fn jump_to_playing(&mut self) {
|
||||
if let Some(i) = self.playing_index {
|
||||
self.library.sync_with_playing(i);
|
||||
self.active_panel = Panel::Right;
|
||||
self.status = "→ к играющему треку".into();
|
||||
} else {
|
||||
self.status = "Ничего не играет".into();
|
||||
}
|
||||
}
|
||||
|
||||
/// g — в начало, G — в конец
|
||||
pub fn goto_start(&mut self) {
|
||||
match self.active_panel {
|
||||
Panel::Left => self.library.artist_selected = 0,
|
||||
Panel::Right => self.library.right_goto_start(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn goto_end(&mut self) {
|
||||
match self.active_panel {
|
||||
Panel::Left => {
|
||||
if !self.library.artists.is_empty() {
|
||||
self.library.artist_selected = self.library.artists.len() - 1;
|
||||
}
|
||||
}
|
||||
Panel::Right => self.library.right_goto_end(),
|
||||
}
|
||||
}
|
||||
|
||||
// ── Playback ───────────────────────────────────────────────
|
||||
|
||||
pub fn play_index(&mut self, index_in_library: usize) {
|
||||
let Some(track) = self.library.track(index_in_library).cloned() else {
|
||||
return;
|
||||
@@ -75,6 +208,8 @@ impl App {
|
||||
pub fn play_selected(&mut self) {
|
||||
if let Some(idx) = self.library.right_track_index() {
|
||||
self.play_index(idx);
|
||||
} else {
|
||||
self.status = "Выделен заголовок альбома".into();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,18 +223,76 @@ impl App {
|
||||
self.player.toggle_pause();
|
||||
}
|
||||
|
||||
/// Автопереход с учётом repeat и shuffle
|
||||
pub fn maybe_advance(&mut self) {
|
||||
if self.playing_index.is_none() || !self.player.is_finished() {
|
||||
return;
|
||||
}
|
||||
let next = match self.playing_index {
|
||||
Some(i) if i + 1 < self.library.all_tracks.len() => i + 1,
|
||||
_ => 0,
|
||||
|
||||
// Repeat One — играем тот же трек
|
||||
if self.repeat == RepeatMode::One {
|
||||
if let Some(i) = self.playing_index {
|
||||
self.play_index(i);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let total = self.library.all_tracks.len();
|
||||
if total == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let next = if self.shuffle {
|
||||
// Псевдослучайный выбор без rand-крейта
|
||||
let seed = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_nanos() as usize)
|
||||
.unwrap_or(0);
|
||||
seed % total
|
||||
} else {
|
||||
match self.playing_index {
|
||||
Some(i) if i + 1 < total => i + 1,
|
||||
_ => {
|
||||
if self.repeat == RepeatMode::All {
|
||||
0
|
||||
} else {
|
||||
// Off — останавливаемся
|
||||
self.status = "Конец плейлиста".into();
|
||||
self.playing_index = None;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
self.play_index(next);
|
||||
self.library.sync_with_playing(next);
|
||||
}
|
||||
|
||||
pub fn cycle_repeat(&mut self) {
|
||||
self.repeat = match self.repeat {
|
||||
RepeatMode::Off => RepeatMode::All,
|
||||
RepeatMode::All => RepeatMode::One,
|
||||
RepeatMode::One => RepeatMode::Off,
|
||||
};
|
||||
self.status = match self.repeat {
|
||||
RepeatMode::Off => "Repeat: off".into(),
|
||||
RepeatMode::All => "Repeat: all".into(),
|
||||
RepeatMode::One => "Repeat: one".into(),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn toggle_shuffle(&mut self) {
|
||||
self.shuffle = !self.shuffle;
|
||||
self.status = if self.shuffle {
|
||||
"Shuffle: on".into()
|
||||
} else {
|
||||
"Shuffle: off".into()
|
||||
};
|
||||
}
|
||||
|
||||
// ── Громкость, перемотка ───────────────────────────────────
|
||||
|
||||
pub fn volume_up(&mut self) {
|
||||
self.player.change_volume(0.05);
|
||||
self.status = format!("Громкость: {}%", (self.player.volume() * 100.0) as u32);
|
||||
@@ -136,6 +329,37 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
/// Перемотка по цифрам 1–9 (10%..90%)
|
||||
pub fn seek_percent(&mut self, percent: u8) {
|
||||
if self.playing_index.is_none() {
|
||||
self.status = "Ничего не воспроизводится".into();
|
||||
return;
|
||||
}
|
||||
let total = match self
|
||||
.playing_index
|
||||
.and_then(|i| self.library.track(i))
|
||||
.map(|t| t.duration)
|
||||
{
|
||||
Some(d) if !d.is_zero() => d,
|
||||
_ => {
|
||||
self.status = "Длительность неизвестна".into();
|
||||
return;
|
||||
}
|
||||
};
|
||||
let target = total.mul_f32(percent as f32 / 100.0);
|
||||
match self.player.seek_to(target) {
|
||||
Ok(_) => {
|
||||
self.status = format!(
|
||||
"→ {}% ({:02}:{:02})",
|
||||
percent,
|
||||
target.as_secs() / 60,
|
||||
target.as_secs() % 60,
|
||||
);
|
||||
}
|
||||
Err(_) => self.status = "Перемотка недоступна".into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn progress(&self) -> (Duration, Duration) {
|
||||
match self.playing_index.and_then(|i| self.library.track(i)) {
|
||||
Some(t) => (self.player.position(), t.duration),
|
||||
@@ -154,8 +378,11 @@ impl App {
|
||||
.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.library = Library::build_with_filter(
|
||||
self.raw_tracks.clone(),
|
||||
new_mode,
|
||||
self.search_buffer.clone(),
|
||||
);
|
||||
|
||||
self.playing_index = playing_path
|
||||
.and_then(|p| self.library.all_tracks.iter().position(|t| t.path == p));
|
||||
|
||||
Reference in New Issue
Block a user