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
|
/target
|
||||||
|
Cargo.lock
|
||||||
|
|
|
||||||
|
|
@ -135,8 +135,9 @@ If no path is given, it defaults to `~/Music`.
|
||||||
|---|---|
|
|---|---|
|
||||||
| `j`/`k`, `↓`/`↑` | navigate |
|
| `j`/`k`, `↓`/`↑` | navigate |
|
||||||
| `Enter` | expand album / play |
|
| `Enter` | expand album / play |
|
||||||
| `Esc` | collapse album |
|
| `Esc` | collapse album / clear search and show the full library |
|
||||||
| `/` | search |
|
| `/` | search |
|
||||||
|
| `t` | toggle view: by album ↔ flat list of all tracks (Enter on any track plays everything from there) |
|
||||||
| `a` | add to playlist |
|
| `a` | add to playlist |
|
||||||
| `c` | connect an SMB share |
|
| `c` | connect an SMB share |
|
||||||
| `L` | add another folder to the library |
|
| `L` | add another folder to the library |
|
||||||
|
|
|
||||||
|
|
@ -148,8 +148,9 @@ cargo build --release --target x86_64-pc-windows-gnu
|
||||||
|---|---|
|
|---|---|
|
||||||
| `j`/`k`, `↓`/`↑` | навигация |
|
| `j`/`k`, `↓`/`↑` | навигация |
|
||||||
| `Enter` | раскрыть альбом / сыграть |
|
| `Enter` | раскрыть альбом / сыграть |
|
||||||
| `Esc` | свернуть альбом |
|
| `Esc` | свернуть альбом / сбросить поиск и показать всю библиотеку |
|
||||||
| `/` | поиск |
|
| `/` | поиск |
|
||||||
|
| `t` | переключить вид: по альбомам ↔ плоский список всех треков (Enter на любом треке — играть всё оттуда) |
|
||||||
| `a` | добавить в плейлист |
|
| `a` | добавить в плейлист |
|
||||||
| `c` | подключить SMB-шару |
|
| `c` | подключить SMB-шару |
|
||||||
| `L` | добавить ещё одну папку к библиотеке |
|
| `L` | добавить ещё одну папку к библиотеке |
|
||||||
|
|
|
||||||
|
|
@ -80,8 +80,35 @@ pub struct SearchRow {
|
||||||
pub album: String,
|
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 {
|
pub enum LibraryView {
|
||||||
Albums(Vec<AlbumRow>),
|
Albums(Vec<AlbumRow>),
|
||||||
|
Tracks(Vec<TrackRow>),
|
||||||
Search(Vec<SearchRow>),
|
Search(Vec<SearchRow>),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -169,6 +196,7 @@ pub struct App {
|
||||||
// --- Library tab ---
|
// --- Library tab ---
|
||||||
pub search_query: String,
|
pub search_query: String,
|
||||||
pub library_view: LibraryView,
|
pub library_view: LibraryView,
|
||||||
|
pub library_mode: LibraryMode,
|
||||||
pub library_selected: usize,
|
pub library_selected: usize,
|
||||||
/// Текущий путь трека/альбома, для которого показывается список треков (None = список альбомов).
|
/// Текущий путь трека/альбома, для которого показывается список треков (None = список альбомов).
|
||||||
pub expanded_album: Option<usize>,
|
pub expanded_album: Option<usize>,
|
||||||
|
|
@ -301,6 +329,7 @@ impl App {
|
||||||
should_quit: false,
|
should_quit: false,
|
||||||
search_query: String::new(),
|
search_query: String::new(),
|
||||||
library_view: LibraryView::Albums(Vec::new()),
|
library_view: LibraryView::Albums(Vec::new()),
|
||||||
|
library_mode: LibraryMode::Albums,
|
||||||
library_selected: 0,
|
library_selected: 0,
|
||||||
expanded_album: None,
|
expanded_album: None,
|
||||||
smb_server: String::new(),
|
smb_server: String::new(),
|
||||||
|
|
@ -358,6 +387,8 @@ impl App {
|
||||||
|
|
||||||
pub fn rebuild_library_view(&mut self) {
|
pub fn rebuild_library_view(&mut self) {
|
||||||
if self.search_query.trim().is_empty() {
|
if self.search_query.trim().is_empty() {
|
||||||
|
match self.library_mode {
|
||||||
|
LibraryMode::Albums => {
|
||||||
let rows: Vec<AlbumRow> = self
|
let rows: Vec<AlbumRow> = self
|
||||||
.library
|
.library
|
||||||
.albums()
|
.albums()
|
||||||
|
|
@ -369,6 +400,25 @@ impl App {
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
self.library_view = LibraryView::Albums(rows);
|
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 {
|
} else {
|
||||||
let rows: Vec<SearchRow> = self
|
let rows: Vec<SearchRow> = self
|
||||||
.library
|
.library
|
||||||
|
|
@ -388,6 +438,15 @@ impl App {
|
||||||
self.expanded_album = None;
|
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) и добавляет её содержимое к библиотеке.
|
/// Монтирует введённую 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) {
|
pub fn play_expanded_track(&mut self, track_idx: usize) {
|
||||||
self.current_station = None;
|
self.current_station = None;
|
||||||
if let Some(album_idx) = self.expanded_album {
|
if let Some(album_idx) = self.expanded_album {
|
||||||
|
|
@ -655,6 +732,7 @@ impl App {
|
||||||
.map(|t| t.path.clone())
|
.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()),
|
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') => {
|
KeyCode::Char('C') => {
|
||||||
app.clear_library();
|
app.clear_library();
|
||||||
}
|
}
|
||||||
|
KeyCode::Char('t') => {
|
||||||
|
app.toggle_library_mode();
|
||||||
|
}
|
||||||
KeyCode::Down | KeyCode::Char('j') => move_selection(app, 1),
|
KeyCode::Down | KeyCode::Char('j') => move_selection(app, 1),
|
||||||
KeyCode::Up | KeyCode::Char('k') => move_selection(app, -1),
|
KeyCode::Up | KeyCode::Char('k') => move_selection(app, -1),
|
||||||
KeyCode::Esc => {
|
KeyCode::Esc => {
|
||||||
if app.expanded_album.is_some() {
|
if app.expanded_album.is_some() {
|
||||||
app.expanded_album = None;
|
app.expanded_album = None;
|
||||||
app.library_selected = 0;
|
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 {
|
KeyCode::Enter => match &app.library_view {
|
||||||
|
|
@ -515,6 +521,9 @@ fn handle_library_key(app: &mut App, code: KeyCode) {
|
||||||
app.library_selected = 0;
|
app.library_selected = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
LibraryView::Tracks(_) => {
|
||||||
|
app.play_all_tracks_from_selected();
|
||||||
|
}
|
||||||
LibraryView::Search(_) => {
|
LibraryView::Search(_) => {
|
||||||
app.play_album_from_selected();
|
app.play_album_from_selected();
|
||||||
}
|
}
|
||||||
|
|
@ -532,6 +541,7 @@ fn move_selection(app: &mut App, delta: i32) {
|
||||||
rows.len()
|
rows.len()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
LibraryView::Tracks(rows) => rows.len(),
|
||||||
LibraryView::Search(rows) => rows.len(),
|
LibraryView::Search(rows) => rows.len(),
|
||||||
};
|
};
|
||||||
if len == 0 {
|
if len == 0 {
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ use ratatui::widgets::{Bar, BarChart, BarGroup, Block, Borders, Gauge, List, Lis
|
||||||
use ratatui::Frame;
|
use ratatui::Frame;
|
||||||
use ratatui_image::StatefulImage;
|
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) {
|
pub fn draw(frame: &mut Frame, app: &mut App) {
|
||||||
if app.fullscreen {
|
if app.fullscreen {
|
||||||
|
|
@ -220,6 +220,10 @@ fn draw_library(frame: &mut Frame, app: &App, area: Rect) {
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
LibraryView::Tracks(rows) => rows
|
||||||
|
.iter()
|
||||||
|
.map(|r| ListItem::new(format!("{} — {} [{}]", r.artist, r.title, r.album)))
|
||||||
|
.collect(),
|
||||||
LibraryView::Search(rows) => rows
|
LibraryView::Search(rows) => rows
|
||||||
.iter()
|
.iter()
|
||||||
.map(|r| ListItem::new(format!("{} — {} [{}]", r.artist, r.title, r.album)))
|
.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() {
|
let title = if app.expanded_album.is_some() {
|
||||||
" Album (Enter: play from here, Esc: back) "
|
" Album (Enter: play from here, Esc: back) "
|
||||||
|
} else if !app.search_query.is_empty() {
|
||||||
|
" Search results (Enter: play, Esc: clear search & show all) "
|
||||||
} else {
|
} 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)
|
let list = List::new(items)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue