Fix library and search

This commit is contained in:
Magnus Root 2026-07-14 09:57:16 +03:00
parent e6c694a740
commit 359cd63834
6 changed files with 119 additions and 15 deletions

1
.gitignore vendored
View file

@ -1 +1,2 @@
/target
Cargo.lock

View file

@ -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 |

View file

@ -148,8 +148,9 @@ cargo build --release --target x86_64-pc-windows-gnu
|---|---|
| `j`/`k`, `↓`/`↑` | навигация |
| `Enter` | раскрыть альбом / сыграть |
| `Esc` | свернуть альбом |
| `Esc` | свернуть альбом / сбросить поиск и показать всю библиотеку |
| `/` | поиск |
| `t` | переключить вид: по альбомам ↔ плоский список всех треков (Enter на любом треке — играть всё оттуда) |
| `a` | добавить в плейлист |
| `c` | подключить SMB-шару |
| `L` | добавить ещё одну папку к библиотеке |

View file

@ -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<AlbumRow>),
Tracks(Vec<TrackRow>),
Search(Vec<SearchRow>),
}
@ -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<usize>,
@ -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,6 +387,8 @@ impl App {
pub fn rebuild_library_view(&mut self) {
if self.search_query.trim().is_empty() {
match self.library_mode {
LibraryMode::Albums => {
let rows: Vec<AlbumRow> = self
.library
.albums()
@ -369,6 +400,25 @@ impl App {
})
.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<TrackRow> = 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<SearchRow> = 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()),
}
}

View file

@ -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 {

View file

@ -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)