use std::time::{Duration, Instant}; use crate::audio::AudioPlayer; use crate::library::{Library, SortMode}; use crate::state::{RepeatMode, SavedState}; use crate::track::{scan_dir, Track}; #[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, pub active_panel: Panel, pub input_mode: InputMode, pub search_buffer: String, raw_tracks: Vec, music_dir: String, pub repeat: RepeatMode, pub shuffle: bool, pub show_help: bool, status_set_at: Instant, } const STATUS_RESET_AFTER: Duration = Duration::from_secs(2); impl App { pub fn new(music_dir: &str) -> Result> { let tracks = match crate::state::load_cache(music_dir) { Some(cached) => cached, None => { let scanned = scan_dir(music_dir); crate::state::save_cache(music_dir, &scanned); scanned } }; let library = Library::build(tracks.clone(), SortMode::ByArtist); let mut app = Self { library, player: AudioPlayer::new()?, status: "F1 — помощь".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, show_help: false, status_set_at: Instant::now(), }; 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 let Some(mode) = saved.sort_mode { if mode != SortMode::ByArtist { app.library = Library::build(app.raw_tracks.clone(), mode); } } app.restore_playing(&saved); Ok(app) } fn set_status(&mut self, text: String) { self.status = text; self.status_set_at = Instant::now(); } 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(), } } pub fn tick_status(&mut self) { if self.input_mode == InputMode::Search { return; } if self.status_set_at.elapsed() < STATUS_RESET_AFTER { return; } let default = self.default_status(); if self.status != default { self.status = default; } } fn restore_playing(&mut self, saved: &SavedState) { let Some(path_str) = &saved.last_playing_path else { return; }; let target_path = std::path::PathBuf::from(path_str); let Some(idx) = self .library .all_tracks .iter() .position(|t| t.path == target_path) else { return; }; let start_pos = saved.last_playing_pos_secs.map(Duration::from_secs); if let Some(track) = self.library.track(idx).cloned() { if self.player.play(&track.path, start_pos).is_ok() { self.playing_index = Some(idx); self.library.sync_with_playing(idx); self.player.toggle_pause(); self.set_status(format!("Восстановлен: {} — {}", track.artist, track.title)); } } } pub fn toggle_help(&mut self) { self.show_help = !self.show_help; } pub fn next_track(&mut self) { let total = self.library.all_tracks.len(); if total == 0 { self.set_status("Библиотека пуста".into()); return; } let next = match self.playing_index { Some(i) if i + 1 < total => i + 1, Some(_) => { if self.repeat == RepeatMode::Off { self.set_status("Конец плейлиста".into()); return; } 0 } None => 0, }; self.play_index(next); self.library.sync_with_playing(next); } pub fn prev_track(&mut self) { let total = self.library.all_tracks.len(); if total == 0 { self.set_status("Библиотека пуста".into()); return; } let prev = match self.playing_index { Some(0) => { if self.repeat == RepeatMode::Off { self.set_status("Это первый трек".into()); return; } total - 1 } Some(i) => i - 1, None => 0, }; self.play_index(prev); self.library.sync_with_playing(prev); } fn persist(&self) { let playing_path = self .playing_index .and_then(|i| self.library.track(i)) .map(|t| t.path.to_string_lossy().to_string()); let playing_pos = if self.playing_index.is_some() { Some(self.player.position().as_secs()) } else { None }; let state = SavedState { last_path: Some(self.music_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, }; 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_set_at = Instant::now(); } pub fn cancel_search(&mut self) { self.input_mode = InputMode::Normal; self.search_buffer.clear(); self.rebuild_library(); self.set_status("Поиск отменён".into()); } pub fn commit_search(&mut self) { self.input_mode = InputMode::Normal; let msg = if self.search_buffer.is_empty() { "Фильтр сброшен".to_string() } else { format!("Фильтр: {}", self.search_buffer) }; self.set_status(msg); } pub fn search_push(&mut self, c: char) { self.search_buffer.push(c); self.rebuild_library(); self.status_set_at = Instant::now(); } pub fn search_pop(&mut self) { self.search_buffer.pop(); self.rebuild_library(); self.status_set_at = Instant::now(); } 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.artist_move(-1), Panel::Right => self.library.right_move(-1), } } pub fn down(&mut self) { match self.active_panel { Panel::Left => self.library.artist_move(1), Panel::Right => self.library.right_move(1), } } pub fn toggle_panel(&mut self) { self.active_panel = match self.active_panel { Panel::Left => Panel::Right, Panel::Right => Panel::Left, }; let msg = match self.active_panel { Panel::Left => "Активна: левая панель".to_string(), Panel::Right => "Активна: правая панель".to_string(), }; self.set_status(msg); } 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.set_status("→ к играющему треку".into()); } else { self.set_status("Ничего не играет".into()); } } pub fn goto_start(&mut self) { match self.active_panel { Panel::Left => { self.library.artist_selected = 0; self.library.left_state.select(Some(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; self.library .left_state .select(Some(self.library.artist_selected)); } } Panel::Right => self.library.right_goto_end(), } } 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, None) { Ok(_) => { self.playing_index = Some(index_in_library); self.set_status(format!("▶ {} — {}", track.artist, track.title)); } Err(e) => self.set_status(format!("Ошибка: {}", e)), } } pub fn play_selected(&mut self) { if let Some(idx) = self.library.right_track_index() { self.play_index(idx); } else { self.set_status("Выделен заголовок группы".into()); } } pub fn stop(&mut self) { self.player.stop(); self.playing_index = None; self.set_status("Остановлено".into()); } pub fn toggle_pause(&mut self) { self.player.toggle_pause(); } pub fn maybe_advance(&mut self) { if self.playing_index.is_none() || !self.player.is_finished() { return; } 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 { 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 { self.set_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, }; let msg = match self.repeat { RepeatMode::Off => "Repeat: off".to_string(), RepeatMode::All => "Repeat: all".to_string(), RepeatMode::One => "Repeat: one".to_string(), }; self.set_status(msg); } pub fn toggle_shuffle(&mut self) { self.shuffle = !self.shuffle; let msg = if self.shuffle { "Shuffle: on".to_string() } else { "Shuffle: off".to_string() }; self.set_status(msg); } pub fn volume_up(&mut self) { self.player.change_volume(0.05); let msg = format!("Громкость: {}%", (self.player.volume() * 100.0) as u32); self.set_status(msg); } pub fn volume_down(&mut self) { self.player.change_volume(-0.05); let msg = format!("Громкость: {}%", (self.player.volume() * 100.0) as u32); self.set_status(msg); } pub fn seek(&mut self, delta_secs: i64) { if self.playing_index.is_none() { self.set_status("Ничего не воспроизводится".into()); return; } let total = self .playing_index .and_then(|i| self.library.track(i)) .map(|t| t.duration) .unwrap_or(Duration::ZERO); match self.player.seek_by(delta_secs, total) { Ok(new_pos) => { let sign = if delta_secs >= 0 { "+" } else { "" }; let msg = format!( "Перемотка {}{}с → {:02}:{:02}", sign, delta_secs, new_pos.as_secs() / 60, new_pos.as_secs() % 60, ); self.set_status(msg); } Err(_) => self.set_status("Перемотка недоступна".into()), } } pub fn seek_percent(&mut self, percent: u8) { if self.playing_index.is_none() { self.set_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.set_status("Длительность неизвестна".into()); return; } }; let target = total.mul_f32(percent as f32 / 100.0); match self.player.seek_to(target) { Ok(_) => { let msg = format!( "→ {}% ({:02}:{:02})", percent, target.as_secs() / 60, target.as_secs() % 60, ); self.set_status(msg); } Err(_) => self.set_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), None => (Duration::ZERO, Duration::ZERO), } } pub fn toggle_sort(&mut self) { let new_mode = match self.library.sort_mode { SortMode::ByArtist => SortMode::ByAlbum, SortMode::ByAlbum => SortMode::ByFilename, SortMode::ByFilename => SortMode::ByArtist, }; let playing_path = self .playing_index .and_then(|i| self.library.track(i)) .map(|t| t.path.clone()); 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)); if let Some(i) = self.playing_index { self.library.sync_with_playing(i); } let msg = match new_mode { SortMode::ByArtist => "Сортировка: по артисту/альбому".to_string(), SortMode::ByAlbum => "Сортировка: по альбому/артисту".to_string(), SortMode::ByFilename => "Сортировка: по имени файла".to_string(), }; self.set_status(msg); } }