@@ -53,6 +53,7 @@ pub struct App {
5353 pub subsonic_client : Arc < SubsonicClient > ,
5454 pub player : Rc < Mutex < Player > > ,
5555 pub queue : Vec < Track > ,
56+ pub favorites : Vec < Track > ,
5657 pub tracks : Vec < Track > ,
5758 pub albums : Vec < Album > ,
5859 pub artists : Vec < Artist > ,
@@ -62,6 +63,7 @@ pub struct App {
6263 pub selected_artist_index : usize ,
6364 pub selected_album_index : usize ,
6465 pub selected_playlist_index : usize ,
66+ pub selected_favorite_index : usize ,
6567 pub is_playing : bool ,
6668 pub current_track : Option < Track > ,
6769 pub current_volume : f64 ,
@@ -77,6 +79,7 @@ pub struct App {
7779 pub album_state : ListState ,
7880 pub playlist_state : ListState ,
7981 pub search_state : ListState ,
82+ pub favorite_state : ListState ,
8083 pub active_tab : ActiveTab ,
8184 pub active_section : ActiveSection ,
8285 // Search fields
@@ -97,6 +100,7 @@ pub struct App {
97100pub enum InputMode {
98101 Normal ,
99102 Search ,
103+ InlineSearch , // search in current tab
100104}
101105#[ derive( Clone , Debug ) ]
102106pub struct Track {
@@ -143,6 +147,7 @@ pub enum ActiveTab {
143147 Artists ,
144148 Songs ,
145149 Search ,
150+ Favorites ,
146151}
147152impl App {
148153 pub async fn new ( ) -> Result < Self > {
@@ -201,12 +206,14 @@ impl App {
201206 artists : Vec :: new ( ) ,
202207 albums : Vec :: new ( ) ,
203208 playlists : Vec :: new ( ) ,
209+ favorites : Vec :: new ( ) ,
204210 metadata : Metadata :: default ( ) ,
205211 selected_queue_index : 0 ,
206212 selected_index : 0 ,
207213 selected_artist_index : 0 ,
208214 selected_album_index : 0 ,
209215 selected_playlist_index : 0 ,
216+ selected_favorite_index : 0 ,
210217 playing_index : 0 ,
211218 is_playing : false ,
212219 current_track : None ,
@@ -218,6 +225,7 @@ impl App {
218225 album_state : ListState :: default ( ) ,
219226 search_state : ListState :: default ( ) ,
220227 playlist_state : ListState :: default ( ) ,
228+ favorite_state : ListState :: default ( ) ,
221229 mpris : mprisserver,
222230 command_receiver : rx,
223231 active_tab : ActiveTab :: Songs ,
@@ -229,7 +237,7 @@ impl App {
229237 search_engine,
230238 is_searching : false ,
231239 on_repeat : false ,
232- _on_shuffle : true ,
240+ _on_shuffle : false ,
233241 cover_art_protocol : None ,
234242 } ;
235243
@@ -267,6 +275,109 @@ impl App {
267275 ] )
268276 . await ;
269277 }
278+ pub fn start_inline_search ( & mut self ) {
279+ self . input_mode = InputMode :: InlineSearch ;
280+ self . search_query . clear ( ) ;
281+ }
282+ pub fn exit_inline_search ( & mut self ) {
283+ self . input_mode = InputMode :: Normal ;
284+ self . search_query . clear ( ) ;
285+ }
286+ pub fn inline_search_input ( & mut self , c : char ) {
287+ if self . input_mode == InputMode :: InlineSearch {
288+ self . search_query . push ( c) ;
289+ self . jump_to_inline_match ( ) ;
290+ }
291+ }
292+ pub fn inline_search_backspace ( & mut self ) {
293+ if self . input_mode == InputMode :: InlineSearch {
294+ self . search_query . pop ( ) ;
295+ self . jump_to_inline_match ( ) ;
296+ }
297+ }
298+
299+ pub fn jump_to_inline_match ( & mut self ) {
300+ if self . search_query . is_empty ( ) {
301+ return ;
302+ }
303+ let query = self . search_query . to_lowercase ( ) ;
304+ match self . active_section {
305+ ActiveSection :: Queue => {
306+ if let Some ( idx) = self . queue . iter ( ) . position ( |t| {
307+ t. title . to_lowercase ( ) . contains ( & query)
308+ || t. artist . to_lowercase ( ) . contains ( & query)
309+ } ) {
310+ self . selected_queue_index = idx;
311+ self . queue_state . select ( Some ( idx) ) ;
312+ }
313+ }
314+ ActiveSection :: Others => match self . active_tab {
315+ ActiveTab :: Songs => {
316+ if let Some ( idx) = self . tracks . iter ( ) . position ( |t| {
317+ t. title . to_lowercase ( ) . contains ( & query)
318+ || t. artist . to_lowercase ( ) . contains ( & query)
319+ } ) {
320+ self . selected_index = idx;
321+ self . list_state . select ( Some ( idx) ) ;
322+ }
323+ }
324+ ActiveTab :: Favorites => {
325+ if let Some ( idx) = self . favorites . iter ( ) . position ( |a| {
326+ a. title . to_lowercase ( ) . contains ( & query)
327+ || a. artist . to_lowercase ( ) . contains ( & query)
328+ } ) {
329+ self . selected_favorite_index = idx;
330+ self . favorite_state . select ( Some ( idx) ) ;
331+ }
332+ }
333+ ActiveTab :: Artists => {
334+ if let Some ( idx) = self
335+ . artists
336+ . iter ( )
337+ . position ( |a| a. name . to_lowercase ( ) . contains ( & query) )
338+ {
339+ self . selected_artist_index = idx;
340+ self . artist_state . select ( Some ( idx) ) ;
341+ }
342+ }
343+ ActiveTab :: Albums => {
344+ if let Some ( idx) = self . albums . iter ( ) . position ( |a| {
345+ a. name . to_lowercase ( ) . contains ( & query)
346+ || a. artist . to_lowercase ( ) . contains ( & query)
347+ } ) {
348+ self . selected_album_index = idx;
349+ self . album_state . select ( Some ( idx) ) ;
350+ }
351+ }
352+ ActiveTab :: Playlist => {
353+ if let Some ( idx) = self
354+ . playlists
355+ . iter ( )
356+ . position ( |p| p. name . to_lowercase ( ) . contains ( & query) )
357+ {
358+ self . selected_playlist_index = idx;
359+ self . playlist_state . select ( Some ( idx) ) ;
360+ }
361+ }
362+ ActiveTab :: Search => { }
363+ } ,
364+ }
365+ }
366+ pub async fn handle_inline_search_input ( & mut self , key : KeyEvent ) -> Result < bool > {
367+ match key. code {
368+ KeyCode :: Esc | KeyCode :: Enter => {
369+ self . exit_inline_search ( ) ;
370+ }
371+ KeyCode :: Backspace => {
372+ self . inline_search_backspace ( ) ;
373+ }
374+ KeyCode :: Char ( c) => {
375+ self . inline_search_input ( c) ;
376+ }
377+ _ => { }
378+ }
379+ Ok ( false )
380+ }
270381 pub fn enter_search_mode ( & mut self ) {
271382 self . input_mode = InputMode :: Search ;
272383 self . active_tab = ActiveTab :: Search ;
@@ -322,20 +433,29 @@ impl App {
322433 }
323434 pub async fn load_cover_art_for_track ( & mut self , track : & Track ) {
324435 self . cover_art_protocol = None ;
436+ let album = track
437+ . album
438+ . replace ( |c : char | !c. is_alphabetic ( ) && c != '-' , "_" )
439+ . to_lowercase ( )
440+ . chars ( )
441+ . fold ( String :: new ( ) , |mut acc, c| {
442+ if c == '_' && acc. ends_with ( '_' ) {
443+ acc
444+ } else {
445+ acc. push ( c) ;
446+ acc
447+ }
448+ } ) ;
325449
326450 let url = match & track. cover_art {
327451 Some ( url) if !url. is_empty ( ) => url,
328452 _ => return ,
329453 } ;
330454 let mut cache_path = std:: env:: temp_dir ( ) ;
331455 cache_path. push ( "sonicrust" ) ;
332- cache_path. push ( format ! ( "cover_{}_{} .jpg" , track . album, track . artist ) ) ;
456+ cache_path. push ( format ! ( "cover_{}.jpg" , album) ) ;
333457 let img_result = if cache_path. exists ( ) {
334- log:: debug!(
335- "Using cached cover_art for {}_{}" ,
336- track. album,
337- track. artist
338- ) ;
458+ log:: debug!( "Using cached cover_art for {}" , album) ;
339459 image:: open ( & cache_path)
340460 . map_err ( |e| Box :: new ( e) as Box < dyn std:: error:: Error + Send + Sync > )
341461 } else {
@@ -354,30 +474,17 @@ impl App {
354474 Err ( e) => eprintln ! ( "failed to load cover art: {}" , e) ,
355475 }
356476 }
357- async fn fetch_and_cache_image (
358- & self ,
359- url : & str ,
360- track_album : & str ,
361- track_artist : & str ,
362- ) -> Result < String > {
363- log:: debug!(
364- "Currently fetching cover are for track_id: {}_{}" ,
365- track_album,
366- track_artist
367- ) ;
477+ async fn fetch_and_cache_image ( & self , url : & str , track_album : & str ) -> Result < String > {
478+ log:: debug!( "Currently fetching cover are for album: {}" , track_album, ) ;
368479 let mut path = std:: env:: temp_dir ( ) ;
369480 path. push ( "sonicrust" ) ;
370481 std:: fs:: create_dir_all ( & path) . map_err ( |e| anyhow:: anyhow!( e) ) ?;
371- path. push ( format ! ( "cover_{}_{} .jpg" , track_album, track_artist ) ) ;
482+ path. push ( format ! ( "cover_{}.jpg" , track_album) ) ;
372483 if path. exists ( ) {
373- log:: debug!(
374- "Using cached cover art for {}_{}" ,
375- track_album,
376- track_artist
377- ) ;
484+ log:: debug!( "Using cached cover art for {}" , track_album, ) ;
378485 return Ok ( path. to_string_lossy ( ) . to_string ( ) ) ;
379486 }
380- log:: debug!( "fetching cover art for {}_{} " , track_album, track_artist ) ;
487+ log:: debug!( "fetching cover art for {}" , track_album) ;
381488 let img = self
382489 . fetch_cover_art ( url)
383490 . await
@@ -462,6 +569,7 @@ impl App {
462569 self . artists = self . subsonic_client . get_all_artists ( ) . await ?;
463570 self . albums = self . subsonic_client . get_all_albums ( ) . await ?;
464571 self . playlists = self . subsonic_client . get_playlists ( ) . await ?;
572+ self . favorites = self . subsonic_client . get_all_favorites ( ) . await ?;
465573 Ok ( ( ) )
466574 }
467575 pub fn find_selected ( & self ) -> usize {
@@ -482,6 +590,7 @@ impl App {
482590 }
483591 }
484592 ActiveTab :: Artists => self . selected_queue_index ,
593+ ActiveTab :: Favorites => self . selected_queue_index ,
485594 ActiveTab :: Playlist => self . selected_queue_index ,
486595 ActiveTab :: Songs => {
487596 if !self . queue . is_empty ( ) {
@@ -546,6 +655,16 @@ impl App {
546655 track_to_play = Some ( track) ;
547656 }
548657 }
658+ ActiveTab :: Favorites => {
659+ if let Some ( track) =
660+ self . favorites . get ( self . selected_favorite_index ) . cloned ( )
661+ {
662+ self . queue = vec ! [ track. clone( ) ] ;
663+ self . selected_queue_index = 0 ;
664+ track_to_play = Some ( track) ;
665+ self . playing_index = self . selected_queue_index ;
666+ }
667+ }
549668 ActiveTab :: Songs => {
550669 if let Some ( track) = self . tracks . get ( self . selected_index ) . cloned ( ) {
551670 self . queue = vec ! [ track. clone( ) ] ;
@@ -757,10 +876,21 @@ impl App {
757876 if let Some ( url) = & track. cover_art
758877 && !url. is_empty ( )
759878 {
760- match self
761- . fetch_and_cache_image ( url, & track. album , & track. artist )
762- . await
763- {
879+ let album = & track
880+ . album
881+ . replace ( |c : char | !c. is_alphabetic ( ) && c != '-' , "_" )
882+ . to_lowercase ( )
883+ . chars ( )
884+ . fold ( String :: new ( ) , |mut acc, c| {
885+ if c == '_' && acc. ends_with ( '_' ) {
886+ acc
887+ } else {
888+ acc. push ( c) ;
889+ acc
890+ }
891+ } ) ;
892+
893+ match self . fetch_and_cache_image ( url, album) . await {
764894 Ok ( path) => {
765895 notif. hint ( Hint :: ImagePath ( path) ) ;
766896 }
@@ -883,6 +1013,7 @@ impl App {
8831013 ActiveTab :: Playlist => self . playlist_state . select ( None ) ,
8841014 ActiveTab :: Artists => self . artist_state . select ( None ) ,
8851015 ActiveTab :: Albums => self . album_state . select ( None ) ,
1016+ ActiveTab :: Favorites => self . favorite_state . select ( None ) ,
8861017 ActiveTab :: Search => {
8871018 self . search_state . select ( None ) ;
8881019 self . input_mode = InputMode :: Normal ;
@@ -903,6 +1034,10 @@ impl App {
9031034 ActiveTab :: Artists if !self . artists . is_empty ( ) => {
9041035 self . artist_state . select ( Some ( self . selected_artist_index ) ) ;
9051036 }
1037+ ActiveTab :: Favorites if !self . favorites . is_empty ( ) => {
1038+ self . favorite_state
1039+ . select ( Some ( self . selected_favorite_index ) ) ;
1040+ }
9061041 ActiveTab :: Albums if !self . albums . is_empty ( ) => {
9071042 self . album_state . select ( Some ( self . selected_album_index ) ) ;
9081043 }
@@ -932,8 +1067,13 @@ impl App {
9321067 ActiveTab :: Songs => {
9331068 self . artist_state . select ( Some ( self . selected_index ) ) ;
9341069 }
1070+ ActiveTab :: Favorites => {
1071+ self . favorite_state
1072+ . select ( Some ( self . selected_favorite_index ) ) ;
1073+ }
9351074 ActiveTab :: Playlist => {
936- self . playlist_state . select ( Some ( self . selected_index ) ) ;
1075+ self . playlist_state
1076+ . select ( Some ( self . selected_playlist_index ) ) ;
9371077 }
9381078 ActiveTab :: Artists => {
9391079 self . album_state . select ( Some ( self . selected_artist_index ) ) ;
@@ -1007,6 +1147,20 @@ impl App {
10071147 self . list_state . select ( None ) ;
10081148 }
10091149 }
1150+ ActiveTab :: Favorites => {
1151+ if !self . favorites . is_empty ( ) {
1152+ let i = if let Some ( selected) = self . favorite_state . selected ( ) {
1153+ ( selected + 1 ) % self . favorites . len ( )
1154+ } else {
1155+ 0
1156+ } ;
1157+ self . selected_favorite_index = i;
1158+ self . favorite_state
1159+ . select ( Some ( self . selected_favorite_index ) ) ;
1160+ } else {
1161+ self . favorite_state . select ( None ) ;
1162+ }
1163+ }
10101164 ActiveTab :: Artists => {
10111165 if !self . artists . is_empty ( ) {
10121166 let i = if let Some ( selected) = self . artist_state . selected ( ) {
@@ -1057,6 +1211,24 @@ impl App {
10571211 }
10581212 ActiveSection :: Others => {
10591213 match self . active_tab {
1214+ ActiveTab :: Favorites => {
1215+ if !self . favorites . is_empty ( ) {
1216+ let i = if let Some ( selected) = self . favorite_state . selected ( ) {
1217+ if selected == 0 {
1218+ self . favorites . len ( ) - 1
1219+ } else {
1220+ selected - 1
1221+ }
1222+ } else {
1223+ self . favorites . len ( ) . saturating_sub ( 1 )
1224+ } ;
1225+ self . selected_favorite_index = i;
1226+ self . favorite_state
1227+ . select ( Some ( self . selected_favorite_index ) ) ;
1228+ } else {
1229+ self . favorite_state . select ( None ) ;
1230+ }
1231+ }
10601232 ActiveTab :: Playlist => {
10611233 if !self . playlists . is_empty ( ) {
10621234 let i = if let Some ( selected) = self . playlist_state . selected ( ) {
0 commit comments