Files
rumus/src/app.rs
2026-10-15 15:54:31 +02:00

530 lines
16 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<usize>,
pub active_panel: Panel,
pub input_mode: InputMode,
pub search_buffer: String,
raw_tracks: Vec<Track>,
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<Self, Box<dyn std::error::Error>> {
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);
}
}