From 359cd63834a0ceb4507be782e94d1f887b73df13 Mon Sep 17 00:00:00 2001 From: Magnus Root Date: Tue, 14 Jul 2026 09:57:16 +0300 Subject: [PATCH] Fix library and search --- .gitignore | 1 + README.en.md | 3 +- README.md | 3 +- crates/tui/src/app.rs | 100 +++++++++++++++++++++++++++++++++++++----- crates/tui/src/lib.rs | 10 +++++ crates/tui/src/ui.rs | 17 ++++++- 6 files changed, 119 insertions(+), 15 deletions(-) diff --git a/.gitignore b/.gitignore index ea8c4bf..96ef6c0 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +Cargo.lock diff --git a/README.en.md b/README.en.md index 415e9d3..7707d83 100644 --- a/README.en.md +++ b/README.en.md @@ -135,8 +135,9 @@ If no path is given, it defaults to `~/Music`. |---|---| | `j`/`k`, `↓`/`↑` | navigate | | `Enter` | expand album / play | -| `Esc` | collapse album | +| `Esc` | collapse album / clear search and show the full library | | `/` | search | +| `t` | toggle view: by album ↔ flat list of all tracks (Enter on any track plays everything from there) | | `a` | add to playlist | | `c` | connect an SMB share | | `L` | add another folder to the library | diff --git a/README.md b/README.md index 5087bca..6ae65ad 100644 --- a/README.md +++ b/README.md @@ -148,8 +148,9 @@ cargo build --release --target x86_64-pc-windows-gnu |---|---| | `j`/`k`, `↓`/`↑` | навигация | | `Enter` | раскрыть альбом / сыграть | -| `Esc` | свернуть альбом | +| `Esc` | свернуть альбом / сбросить поиск и показать всю библиотеку | | `/` | поиск | +| `t` | переключить вид: по альбомам ↔ плоский список всех треков (Enter на любом треке — играть всё оттуда) | | `a` | добавить в плейлист | | `c` | подключить SMB-шару | | `L` | добавить ещё одну папку к библиотеке | diff --git a/crates/tui/src/app.rs b/crates/tui/src/app.rs index 10837d5..8e55bc5 100644 --- a/crates/tui/src/app.rs +++ b/crates/tui/src/app.rs @@ -80,8 +80,35 @@ pub struct SearchRow { pub album: String, } +/// Строка в плоском списке "все треки" (режим Tracks) — в отличие от SearchRow, сразу +/// хранит готовый QueueItem, чтобы не искать Track по пути повторно при проигрывании. +pub struct TrackRow { + pub item: QueueItem, + pub title: String, + pub artist: String, + pub album: String, +} + +/// Что показывать, когда не идёт поиск: альбомами (сгруппировано) или плоским списком +/// всех треков библиотеки — переключается клавишей `t`. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum LibraryMode { + Albums, + Tracks, +} + +impl LibraryMode { + pub fn toggle(self) -> Self { + match self { + LibraryMode::Albums => LibraryMode::Tracks, + LibraryMode::Tracks => LibraryMode::Albums, + } + } +} + pub enum LibraryView { Albums(Vec), + Tracks(Vec), Search(Vec), } @@ -169,6 +196,7 @@ pub struct App { // --- Library tab --- pub search_query: String, pub library_view: LibraryView, + pub library_mode: LibraryMode, pub library_selected: usize, /// Текущий путь трека/альбома, для которого показывается список треков (None = список альбомов). pub expanded_album: Option, @@ -301,6 +329,7 @@ impl App { should_quit: false, search_query: String::new(), library_view: LibraryView::Albums(Vec::new()), + library_mode: LibraryMode::Albums, library_selected: 0, expanded_album: None, smb_server: String::new(), @@ -358,17 +387,38 @@ impl App { pub fn rebuild_library_view(&mut self) { if self.search_query.trim().is_empty() { - let rows: Vec = self - .library - .albums() - .into_iter() - .map(|a| AlbumRow { - artist: a.artist, - album: a.album, - tracks: a.tracks.iter().map(|t| track_to_queue_item(t)).collect(), - }) - .collect(); - self.library_view = LibraryView::Albums(rows); + match self.library_mode { + LibraryMode::Albums => { + let rows: Vec = self + .library + .albums() + .into_iter() + .map(|a| AlbumRow { + artist: a.artist, + album: a.album, + tracks: a.tracks.iter().map(|t| track_to_queue_item(t)).collect(), + }) + .collect(); + self.library_view = LibraryView::Albums(rows); + } + LibraryMode::Tracks => { + let mut tracks: Vec<&library::Track> = self.library.tracks().iter().collect(); + tracks.sort_by(|a, b| { + (&a.artist, &a.album, a.track_no.unwrap_or(0), a.file_name()) + .cmp(&(&b.artist, &b.album, b.track_no.unwrap_or(0), b.file_name())) + }); + let rows: Vec = tracks + .into_iter() + .map(|t| TrackRow { + item: track_to_queue_item(t), + title: t.title.clone(), + artist: t.artist.clone(), + album: t.album.clone(), + }) + .collect(); + self.library_view = LibraryView::Tracks(rows); + } + } } else { let rows: Vec = self .library @@ -388,6 +438,15 @@ impl App { self.expanded_album = None; } + pub fn toggle_library_mode(&mut self) { + self.library_mode = self.library_mode.toggle(); + self.rebuild_library_view(); + self.status_message = match self.library_mode { + LibraryMode::Albums => "View: Albums".to_string(), + LibraryMode::Tracks => "View: All Tracks".to_string(), + }; + } + /// Монтирует введённую SMB-шару (через gvfs) и добавляет её содержимое к библиотеке. /// Сначала проверяет, не смонтирована ли она уже (например, вручную через файловый /// менеджер) — тогда пароль вообще не понадобится. @@ -610,6 +669,24 @@ impl App { } } + /// Ставит в очередь ВЕСЬ плоский список треков (режим Tracks), начиная с выбранного — + /// то самое "просто включить всю библиотеку", без необходимости проваливаться в альбом. + pub fn play_all_tracks_from_selected(&mut self) { + self.current_station = None; + if let LibraryView::Tracks(rows) = &self.library_view { + if rows.is_empty() { + return; + } + self.current_queue = rows.iter().map(|r| r.item.clone()).collect(); + let start = self.library_selected.min(self.current_queue.len() - 1); + if self.player.set_queue(self.current_queue.clone(), start) { + self.status_message = format!("Playing all {} tracks", self.current_queue.len()); + } else { + self.status_message = ENGINE_DEAD_MSG.to_string(); + } + } + } + pub fn play_expanded_track(&mut self, track_idx: usize) { self.current_station = None; if let Some(album_idx) = self.expanded_album { @@ -655,6 +732,7 @@ impl App { .map(|t| t.path.clone()) } } + LibraryView::Tracks(rows) => rows.get(self.library_selected).map(|r| r.item.path.clone()), LibraryView::Search(rows) => rows.get(self.library_selected).map(|r| r.path.clone()), } } diff --git a/crates/tui/src/lib.rs b/crates/tui/src/lib.rs index 538be6d..5b2dd6e 100644 --- a/crates/tui/src/lib.rs +++ b/crates/tui/src/lib.rs @@ -498,12 +498,18 @@ fn handle_library_key(app: &mut App, code: KeyCode) { KeyCode::Char('C') => { app.clear_library(); } + KeyCode::Char('t') => { + app.toggle_library_mode(); + } KeyCode::Down | KeyCode::Char('j') => move_selection(app, 1), KeyCode::Up | KeyCode::Char('k') => move_selection(app, -1), KeyCode::Esc => { if app.expanded_album.is_some() { app.expanded_album = None; app.library_selected = 0; + } else if !app.search_query.is_empty() { + app.search_query.clear(); + app.rebuild_library_view(); } } KeyCode::Enter => match &app.library_view { @@ -515,6 +521,9 @@ fn handle_library_key(app: &mut App, code: KeyCode) { app.library_selected = 0; } } + LibraryView::Tracks(_) => { + app.play_all_tracks_from_selected(); + } LibraryView::Search(_) => { app.play_album_from_selected(); } @@ -532,6 +541,7 @@ fn move_selection(app: &mut App, delta: i32) { rows.len() } } + LibraryView::Tracks(rows) => rows.len(), LibraryView::Search(rows) => rows.len(), }; if len == 0 { diff --git a/crates/tui/src/ui.rs b/crates/tui/src/ui.rs index 3ca2bb5..d2e0014 100644 --- a/crates/tui/src/ui.rs +++ b/crates/tui/src/ui.rs @@ -5,7 +5,7 @@ use ratatui::widgets::{Bar, BarChart, BarGroup, Block, Borders, Gauge, List, Lis use ratatui::Frame; use ratatui_image::StatefulImage; -use crate::app::{protocol_to_str, App, Focus, LibraryView, RightPanelView, Tab}; +use crate::app::{protocol_to_str, App, Focus, LibraryMode, LibraryView, RightPanelView, Tab}; pub fn draw(frame: &mut Frame, app: &mut App) { if app.fullscreen { @@ -220,6 +220,10 @@ fn draw_library(frame: &mut Frame, app: &App, area: Rect) { .collect() } } + LibraryView::Tracks(rows) => rows + .iter() + .map(|r| ListItem::new(format!("{} — {} [{}]", r.artist, r.title, r.album))) + .collect(), LibraryView::Search(rows) => rows .iter() .map(|r| ListItem::new(format!("{} — {} [{}]", r.artist, r.title, r.album))) @@ -228,8 +232,17 @@ fn draw_library(frame: &mut Frame, app: &App, area: Rect) { let title = if app.expanded_album.is_some() { " Album (Enter: play from here, Esc: back) " + } else if !app.search_query.is_empty() { + " Search results (Enter: play, Esc: clear search & show all) " } else { - " Albums (Enter: expand/play, /: search, a: add to playlist, c: connect SMB, L: add folder, C: clear library) " + match app.library_mode { + LibraryMode::Albums => { + " Albums (Enter: expand/play, /: search, a: add to playlist, t: all tracks, c: connect SMB, L: add folder, C: clear library) " + } + LibraryMode::Tracks => { + " All Tracks (Enter: play all from here, /: search, a: add to playlist, t: by album) " + } + } }; let list = List::new(items)