Krolyxon
Commit ad32349

rmptuisrcbrowser.rs

164 lines · 4.9 KB · Rust

1use std::path::Path;
2
3use crate::{
4 connection::{Connection, Song},
5 utils::is_audio_file,
6 AppResult,
7};
8
9#[derive(Debug, Default)]
10pub struct FileBrowser {
11 pub entries: Vec<BrowserEntry>,
12 pub selected: usize,
13 pub prev_selected: usize,
14 pub path: String,
15}
16
17#[derive(Clone, Debug, PartialEq, Eq)]
18pub enum BrowserEntry {
19 Directory(String),
20 Song(Song),
21}
22
23impl BrowserEntry {
24 pub fn name(&self) -> &str {
25 match self {
26 Self::Directory(path) => Path::new(path)
27 .file_name()
28 .and_then(|name| name.to_str())
29 .unwrap_or(path),
30 Self::Song(song) => Path::new(&song.file)
31 .file_name()
32 .and_then(|name| name.to_str())
33 .unwrap_or(&song.file),
34 }
35 }
36}
37
38impl FileBrowser {
39 pub fn new() -> Self {
40 Self {
41 path: ".".to_string(),
42 ..Self::default()
43 }
44 }
45
46 pub async fn update_directory(&mut self, conn: &mut Connection) -> AppResult<()> {
47 let query_path = self.path.trim_start_matches("./").trim_matches('/');
48 let (directories, songs) = conn.ls_info(query_path).await?;
49 let mut entries = Vec::new();
50 for directory in directories {
51 if let Some(name) = Path::new(&directory)
52 .file_name()
53 .and_then(|name| name.to_str())
54 {
55 if !name.starts_with('.') {
56 entries.push(BrowserEntry::Directory(directory));
57 }
58 }
59 }
60
61 for song in songs {
62 let Some(name) = Path::new(&song.file)
63 .file_name()
64 .and_then(|name| name.to_str())
65 else {
66 continue;
67 };
68 if is_audio_file(name) {
69 entries.push(BrowserEntry::Song(song));
70 }
71 }
72
73 entries.sort_by(|a, b| {
74 let kind_order = |entry: &BrowserEntry| match entry {
75 BrowserEntry::Directory(_) => 0,
76 BrowserEntry::Song(_) => 1,
77 };
78 kind_order(a).cmp(&kind_order(b)).then_with(|| {
79 let name_a = a.name();
80 let name_b = b.name();
81 let number_a = name_a.parse::<u32>().unwrap_or(u32::MAX);
82 let number_b = name_b.parse::<u32>().unwrap_or(u32::MAX);
83 number_a
84 .cmp(&number_b)
85 .then_with(|| name_a.to_lowercase().cmp(&name_b.to_lowercase()))
86 })
87 });
88
89 self.entries = entries;
90 self.selected = self.selected.min(self.entries.len().saturating_sub(1));
91 Ok(())
92 }
93
94 pub fn next(&mut self) {
95 if self.selected < self.entries.len().saturating_sub(1) {
96 self.selected += 1;
97 }
98 }
99
100 pub fn prev(&mut self) {
101 self.selected = self.selected.saturating_sub(1);
102 }
103
104 pub async fn handle_go_back(&mut self, conn: &mut Connection) -> AppResult<()> {
105 if self.path != "." {
106 self.path = Path::new(&self.path)
107 .parent()
108 .and_then(Path::to_str)
109 .filter(|path| !path.is_empty())
110 .unwrap_or(".")
111 .to_string();
112 self.update_directory(conn).await?;
113 }
114 self.selected = self.prev_selected.min(self.entries.len().saturating_sub(1));
115 Ok(())
116 }
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122 use tokio::{
123 io::{AsyncBufReadExt, AsyncWriteExt, BufReader},
124 net::TcpListener,
125 };
126
127 #[tokio::test]
128 async fn preserves_mpd_directory_uris_and_displays_their_basename() {
129 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
130 let address = listener.local_addr().unwrap();
131 let server = tokio::spawn(async move {
132 let (stream, _) = listener.accept().await.unwrap();
133 let mut stream = BufReader::new(stream);
134 stream
135 .get_mut()
136 .write_all(b"OK MPD 0.23.0\n")
137 .await
138 .unwrap();
139
140 for response in ["OK\n", "OK\n", "OK\n", "directory: library/Artist\nOK\n"] {
141 let mut command = String::new();
142 stream.read_line(&mut command).await.unwrap();
143 stream
144 .get_mut()
145 .write_all(response.as_bytes())
146 .await
147 .unwrap();
148 }
149 });
150
151 let mut conn = Connection::connect(&address.to_string(), None)
152 .await
153 .unwrap();
154 let mut browser = FileBrowser::new();
155 browser.update_directory(&mut conn).await.unwrap();
156
157 assert_eq!(
158 browser.entries,
159 vec![BrowserEntry::Directory("library/Artist".to_string())]
160 );
161 assert_eq!(browser.entries[0].name(), "Artist");
162 server.await.unwrap();
163 }
164}