Fix library and search
This commit is contained in:
parent
e6c694a740
commit
359cd63834
6 changed files with 119 additions and 15 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1 +1,2 @@
|
|||
/target
|
||||
Cargo.lock
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -148,8 +148,9 @@ cargo build --release --target x86_64-pc-windows-gnu
|
|||
|---|---|
|
||||
| `j`/`k`, `↓`/`↑` | навигация |
|
||||
| `Enter` | раскрыть альбом / сыграть |
|
||||
| `Esc` | свернуть альбом |
|
||||
| `Esc` | свернуть альбом / сбросить поиск и показать всю библиотеку |
|
||||
| `/` | поиск |
|
||||
| `t` | переключить вид: по альбомам ↔ плоский список всех треков (Enter на любом треке — играть всё оттуда) |
|
||||
| `a` | добавить в плейлист |
|
||||
| `c` | подключить SMB-шару |
|
||||
| `L` | добавить ещё одну папку к библиотеке |
|
||||
|
|
|
|||
|
|
@ -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,17 +387,38 @@ impl App {
|
|||
|
||||
pub fn rebuild_library_view(&mut self) {
|
||||
if self.search_query.trim().is_empty() {
|
||||
let rows: Vec<AlbumRow> = 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<AlbumRow> = 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<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()),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue