Rockbox open source high quality audio player as a Music Player Daemon
mpris rockbox mpd libadwaita audio rust zig deno
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

gpui: implement file navigation

+560 -22
+1
gpui/assets/icons/chevron-left.svg
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"/></svg>
+77 -5
gpui/src/client.rs
··· 1 1 use crate::api::v1alpha1::{ 2 + browse_service_client::BrowseServiceClient, 2 3 library_service_client::LibraryServiceClient, playback_service_client::PlaybackServiceClient, 3 4 playlist_service_client::PlaylistServiceClient, 4 5 settings_service_client::SettingsServiceClient, sound_service_client::SoundServiceClient, 5 6 system_service_client::SystemServiceClient, AdjustVolumeRequest, GetArtistsRequest, 6 7 GetCurrentRequest, GetGlobalSettingsRequest, GetGlobalStatusRequest, GetLikedTracksRequest, 7 - GetTracksRequest, InsertTracksRequest, LikeTrackRequest, NextRequest, PauseRequest, 8 - PlayAlbumRequest, PlayAllTracksRequest, PlayArtistTracksRequest, PlayTrackRequest, 9 - FastForwardRewindRequest, PlaylistResumeRequest, PreviousRequest, RemoveTracksRequest, 10 - ResumeRequest, ResumeTrackRequest, 8 + GetTracksRequest, InsertDirectoryRequest, InsertTracksRequest, LikeTrackRequest, NextRequest, 9 + PauseRequest, PlayAlbumRequest, PlayAllTracksRequest, PlayArtistTracksRequest, 10 + PlayDirectoryRequest, PlayTrackRequest, FastForwardRewindRequest, PlaylistResumeRequest, 11 + PreviousRequest, RemoveTracksRequest, ResumeRequest, ResumeTrackRequest, 11 12 SaveSettingsRequest, SearchRequest, StartRequest, StreamCurrentTrackRequest, 12 - StreamPlaylistRequest, StreamStatusRequest, UnlikeTrackRequest, 13 + StreamPlaylistRequest, StreamStatusRequest, TreeGetEntriesRequest, UnlikeTrackRequest, 13 14 }; 14 15 use crate::state::{SearchAlbum, SearchArtist, SearchResults}; 15 16 ··· 534 535 } 535 536 Ok(()) 536 537 } 538 + 539 + // ── File browser ────────────────────────────────────────────────────────────── 540 + 541 + #[derive(Clone, Debug)] 542 + pub struct FileEntry { 543 + pub name: String, 544 + pub path: String, 545 + pub is_dir: bool, 546 + } 547 + 548 + pub async fn tree_get_entries(path: Option<String>) -> Result<Vec<FileEntry>> { 549 + let mut c = BrowseServiceClient::connect(URL).await?; 550 + let resp = c.tree_get_entries(TreeGetEntriesRequest { path }).await?; 551 + let mut entries: Vec<FileEntry> = resp 552 + .into_inner() 553 + .entries 554 + .into_iter() 555 + .map(|e| { 556 + let is_dir = e.attr == 0x10; 557 + let name = std::path::Path::new(&e.name) 558 + .file_name() 559 + .and_then(|n| n.to_str()) 560 + .unwrap_or(&e.name) 561 + .to_string(); 562 + FileEntry { name, path: e.name, is_dir } 563 + }) 564 + .collect(); 565 + entries.sort_by(|a, b| match (a.is_dir, b.is_dir) { 566 + (true, false) => std::cmp::Ordering::Less, 567 + (false, true) => std::cmp::Ordering::Greater, 568 + _ => a.name.to_lowercase().cmp(&b.name.to_lowercase()), 569 + }); 570 + Ok(entries) 571 + } 572 + 573 + pub async fn play_directory(path: String, shuffle: bool) -> Result<()> { 574 + let mut c = PlaybackServiceClient::connect(URL).await?; 575 + c.play_directory(PlayDirectoryRequest { 576 + path, 577 + shuffle: Some(shuffle), 578 + recurse: Some(true), 579 + position: None, 580 + }) 581 + .await?; 582 + Ok(()) 583 + } 584 + 585 + pub async fn play_directory_at(path: String, position: i32) -> Result<()> { 586 + let mut c = PlaybackServiceClient::connect(URL).await?; 587 + c.play_directory(PlayDirectoryRequest { 588 + path, 589 + shuffle: Some(false), 590 + recurse: Some(true), 591 + position: Some(position), 592 + }) 593 + .await?; 594 + Ok(()) 595 + } 596 + 597 + pub async fn insert_directory(path: String, position: i32) -> Result<()> { 598 + let mut c = PlaylistServiceClient::connect(URL).await?; 599 + c.insert_directory(InsertDirectoryRequest { 600 + directory: path, 601 + position, 602 + recurse: Some(true), 603 + shuffle: None, 604 + playlist_id: None, 605 + }) 606 + .await?; 607 + Ok(()) 608 + }
+6
gpui/src/ui/components/icons.rs
··· 168 168 Options, 169 169 Heart, 170 170 HeartOutline, 171 + HardDrive, 172 + Directory, 173 + ChevronLeft, 171 174 } 172 175 173 176 impl IconNamed for Icons { ··· 194 197 Icons::Options => "icons/options.svg", 195 198 Icons::Heart => "icons/heart.svg", 196 199 Icons::HeartOutline => "icons/heart-outline.svg", 200 + Icons::HardDrive => "icons/harddrive.svg", 201 + Icons::Directory => "icons/directory.svg", 202 + Icons::ChevronLeft => "icons/chevron-left.svg", 197 203 } 198 204 .into() 199 205 }
+22
gpui/src/ui/components/mod.rs
··· 23 23 AlbumDetail, 24 24 ArtistDetail, 25 25 Likes, 26 + Files, 26 27 } 27 28 impl gpui::Global for LibrarySection {} 29 + 30 + #[derive(Clone, Default)] 31 + pub struct FilesBrowseState { 32 + pub current_path: Option<String>, 33 + pub path_history: Vec<Option<String>>, 34 + } 35 + impl gpui::Global for FilesBrowseState {} 36 + 37 + #[derive(Clone)] 38 + pub struct FileContextMenu { 39 + pub pos: gpui::Point<gpui::Pixels>, 40 + pub path: String, 41 + pub name: String, 42 + pub is_dir: bool, 43 + pub current_dir: String, 44 + pub dir_idx: usize, 45 + } 46 + 47 + #[derive(Clone, Default)] 48 + pub struct FileContextMenuState(pub Option<FileContextMenu>); 49 + impl gpui::Global for FileContextMenuState {} 28 50 29 51 #[derive(Clone, Default)] 30 52 pub struct LikedSongs(pub std::collections::HashSet<String>);
+286
gpui/src/ui/components/pages/files.rs
··· 1 + use crate::client::{play_directory, play_directory_at, FileEntry}; 2 + use crate::controller::Controller; 3 + use crate::ui::components::icons::{Icon, Icons}; 4 + use crate::ui::components::{FileContextMenu, FileContextMenuState, FilesBrowseState}; 5 + use crate::ui::theme::Theme; 6 + use gpui::prelude::FluentBuilder; 7 + use gpui::{ 8 + div, px, uniform_list, App, ClickEvent, Context, FontWeight, InteractiveElement, IntoElement, 9 + ParentElement, Render, StatefulInteractiveElement, Styled, WeakEntity, Window, 10 + }; 11 + 12 + pub struct FilesView { 13 + entries: Vec<FileEntry>, 14 + last_requested_path: Option<Option<String>>, 15 + } 16 + 17 + impl FilesView { 18 + pub fn new(cx: &mut App) -> Self { 19 + cx.set_global(FilesBrowseState::default()); 20 + cx.set_global(FileContextMenuState::default()); 21 + FilesView { entries: Vec::new(), last_requested_path: None } 22 + } 23 + 24 + fn load_if_needed(&mut self, cx: &mut Context<Self>) { 25 + let current_path = cx.global::<FilesBrowseState>().current_path.clone(); 26 + let needs_load = self 27 + .last_requested_path 28 + .as_ref() 29 + .map(|p| p != &current_path) 30 + .unwrap_or(true); 31 + if !needs_load { 32 + return; 33 + } 34 + self.last_requested_path = Some(current_path.clone()); 35 + 36 + // Run the gRPC fetch on the Tokio runtime (requires a Tokio reactor). 37 + let (tx, rx) = tokio::sync::oneshot::channel::<Vec<FileEntry>>(); 38 + cx.global::<Controller>().rt().spawn(async move { 39 + let entries = 40 + crate::client::tree_get_entries(current_path).await.unwrap_or_default(); 41 + let _ = tx.send(entries); 42 + }); 43 + 44 + // Await the oneshot in GPUI's executor (no Tokio reactor needed for the 45 + // oneshot receiver itself), then push the result back into our entity. 46 + cx.spawn(async move |this: WeakEntity<FilesView>, cx| { 47 + if let Ok(entries) = rx.await { 48 + let _ = this.update(cx, |this, cx| { 49 + this.entries = entries; 50 + cx.notify(); 51 + }); 52 + } 53 + }) 54 + .detach(); 55 + } 56 + } 57 + 58 + impl Render for FilesView { 59 + fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { 60 + self.load_if_needed(cx); 61 + 62 + let theme = *cx.global::<Theme>(); 63 + let browse = cx.global::<FilesBrowseState>().clone(); 64 + let entries = self.entries.clone(); 65 + let can_go_back = !browse.path_history.is_empty(); 66 + 67 + let path_display: String = browse 68 + .current_path 69 + .as_deref() 70 + .and_then(|p| std::path::Path::new(p).file_name()) 71 + .and_then(|n| n.to_str()) 72 + .unwrap_or("Files") 73 + .to_string(); 74 + 75 + let current_dir = browse.current_path.clone().unwrap_or_default(); 76 + 77 + div() 78 + .size_full() 79 + .flex() 80 + .flex_col() 81 + // ── Header: back button + current path ──────────────────────────── 82 + .child( 83 + div() 84 + .flex() 85 + .items_center() 86 + .gap_x_2() 87 + .px_4() 88 + .py_3() 89 + .border_b_1() 90 + .border_color(theme.library_table_border) 91 + .child( 92 + div() 93 + .id("files_back_btn") 94 + .p_1p5() 95 + .rounded_md() 96 + .flex() 97 + .items_center() 98 + .justify_center() 99 + .cursor_pointer() 100 + .text_color(theme.library_text) 101 + .opacity(if can_go_back { 1.0 } else { 0.3 }) 102 + .when(can_go_back, |this| { 103 + this.hover(|t| t.bg(theme.library_track_bg_hover)) 104 + .on_click(|_, _, cx: &mut App| { 105 + let state = cx.global_mut::<FilesBrowseState>(); 106 + if let Some(prev) = state.path_history.pop() { 107 + state.current_path = prev; 108 + } 109 + }) 110 + }) 111 + .child(Icon::new(Icons::ChevronLeft).size_4()), 112 + ) 113 + .child( 114 + div() 115 + .text_sm() 116 + .font_weight(FontWeight(600.0)) 117 + .text_color(theme.library_text) 118 + .child(path_display), 119 + ), 120 + ) 121 + // ── File list ───────────────────────────────────────────────────── 122 + .child( 123 + uniform_list("files_list", entries.len(), move |range, _, _cx| { 124 + range 125 + .map(|idx| { 126 + let entry = entries[idx].clone(); 127 + let group_name: gpui::SharedString = 128 + format!("file_row_{}", idx).into(); 129 + let gn_icon = group_name.clone(); 130 + let gn_play = group_name.clone(); 131 + let gn_opts = group_name.clone(); 132 + let path_nav = entry.path.clone(); 133 + let path_play = entry.path.clone(); 134 + let path_opts = entry.path.clone(); 135 + let name_opts = entry.name.clone(); 136 + let cur_dir_play = current_dir.clone(); 137 + let cur_dir_opts = current_dir.clone(); 138 + let is_dir = entry.is_dir; 139 + 140 + div() 141 + .id(("file_row", idx)) 142 + .group(group_name) 143 + .w_full() 144 + .flex() 145 + .items_center() 146 + .gap_x_3() 147 + .px_4() 148 + .py_2p5() 149 + .cursor_pointer() 150 + .hover(|t| t.bg(theme.library_track_bg_hover)) 151 + // Directory: click row to navigate in 152 + .when(is_dir, |this| { 153 + this.on_click(move |_, _, cx: &mut App| { 154 + let state = cx.global_mut::<FilesBrowseState>(); 155 + state.path_history.push(state.current_path.clone()); 156 + state.current_path = Some(path_nav.clone()); 157 + }) 158 + }) 159 + // ── Icon column (dir/music icon → play on hover) ── 160 + .child( 161 + div() 162 + .w(px(22.0)) 163 + .h(px(22.0)) 164 + .flex_shrink_0() 165 + .relative() 166 + // Default icon (hidden on hover) 167 + .child( 168 + div() 169 + .absolute() 170 + .top_0() 171 + .left_0() 172 + .w_full() 173 + .h_full() 174 + .flex() 175 + .items_center() 176 + .group_hover(gn_icon, |s| s.opacity(0.0)) 177 + .text_color(if is_dir { 178 + theme.library_text 179 + } else { 180 + theme.library_header_text 181 + }) 182 + .child( 183 + Icon::new(if is_dir { 184 + Icons::Directory 185 + } else { 186 + Icons::Music 187 + }) 188 + .size_5(), 189 + ), 190 + ) 191 + // Play icon (visible on hover) 192 + .child( 193 + div() 194 + .id(("file_play_icon", idx)) 195 + .absolute() 196 + .top_0() 197 + .left(px(-3.0)) 198 + .w_full() 199 + .h_full() 200 + .flex() 201 + .items_center() 202 + .opacity(0.0) 203 + .group_hover(gn_play, |s| s.opacity(1.0)) 204 + .cursor_pointer() 205 + .text_color(theme.library_text) 206 + .on_click(move |_, _, cx: &mut App| { 207 + cx.stop_propagation(); 208 + let rt = cx.global::<Controller>().rt(); 209 + if is_dir { 210 + rt.spawn(play_directory( 211 + path_play.clone(), 212 + false, 213 + )); 214 + } else { 215 + rt.spawn(play_directory_at( 216 + cur_dir_play.clone(), 217 + idx as i32, 218 + )); 219 + } 220 + }) 221 + .child(Icon::new(Icons::Play).size_5()), 222 + ), 223 + ) 224 + // ── Name ───────────────────────────────────── 225 + .child( 226 + div() 227 + .flex_1() 228 + .min_w_0() 229 + .text_sm() 230 + .truncate() 231 + .text_color(theme.library_text) 232 + .child(entry.name.clone()), 233 + ) 234 + // ── Context menu button (⋮) ────────────────── 235 + .child( 236 + div() 237 + .id(("file_opts_btn", idx)) 238 + .w(px(28.0)) 239 + .flex_shrink_0() 240 + .flex() 241 + .items_center() 242 + .justify_center() 243 + .opacity(0.0) 244 + .group_hover(gn_opts, |s| s.opacity(1.0)) 245 + .cursor_pointer() 246 + .text_color(theme.library_header_text) 247 + .on_click(move |event: &ClickEvent, _, cx: &mut App| { 248 + cx.stop_propagation(); 249 + cx.global_mut::<FileContextMenuState>().0 = 250 + Some(FileContextMenu { 251 + pos: event.position(), 252 + path: path_opts.clone(), 253 + name: name_opts.clone(), 254 + is_dir, 255 + current_dir: cur_dir_opts.clone(), 256 + dir_idx: idx, 257 + }); 258 + }) 259 + .child(Icon::new(Icons::Options).size_4()), 260 + ) 261 + }) 262 + .collect() 263 + }) 264 + .flex_1() 265 + .min_h_0(), 266 + ) 267 + } 268 + } 269 + 270 + pub fn menu_item( 271 + id: &'static str, 272 + label: &'static str, 273 + theme: Theme, 274 + on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, 275 + ) -> impl IntoElement { 276 + div() 277 + .id(id) 278 + .px_4() 279 + .py_2() 280 + .text_sm() 281 + .cursor_pointer() 282 + .text_color(theme.library_text) 283 + .hover(|t| t.bg(theme.library_track_bg_hover)) 284 + .on_click(on_click) 285 + .child(label) 286 + }
+167 -17
gpui/src/ui/components/pages/library.rs
··· 1 + use crate::client::{ 2 + insert_directory, insert_track_last, insert_track_next, insert_tracks, play_directory, 3 + INSERT_FIRST, INSERT_LAST, INSERT_LAST_SHUFFLED, INSERT_SHUFFLED, 4 + }; 1 5 use crate::controller::Controller; 2 6 use crate::state::{format_duration, PlaybackStatus}; 3 7 use crate::ui::animations::equalizer_bars; 4 8 use crate::ui::components::icons::{Icon, Icons}; 5 9 use crate::ui::components::miniplayer::MiniPlayer; 6 10 use crate::ui::components::search_input::SearchInput; 11 + use crate::ui::components::pages::files::{menu_item, FilesView}; 7 12 use crate::ui::components::{ 8 - AlbumContextMenu, AlbumContextMenuState, BackSection, HoveredAlbumIdx, LibraryContextMenu, 9 - LibraryContextMenuState, LibrarySection, LikedSongs, SelectedAlbum, SelectedArtist, 13 + AlbumContextMenu, AlbumContextMenuState, BackSection, FileContextMenuState, 14 + HoveredAlbumIdx, LibraryContextMenu, LibraryContextMenuState, LibrarySection, LikedSongs, 15 + SelectedAlbum, SelectedArtist, 10 16 }; 11 17 use crate::ui::theme::Theme; 12 18 use gpui::prelude::FluentBuilder; ··· 100 106 detail_scroll_handle: UniformListScrollHandle, 101 107 miniplayer: Entity<MiniPlayer>, 102 108 search_input: Entity<SearchInput>, 109 + files_view: Entity<FilesView>, 103 110 _search_sub: Option<Subscription>, 104 111 } 105 112 ··· 118 125 detail_scroll_handle: UniformListScrollHandle::new(), 119 126 miniplayer: cx.new(|_| MiniPlayer), 120 127 search_input: cx.new(|cx| SearchInput::new(cx)), 128 + files_view: cx.new(|cx| FilesView::new(cx)), 121 129 _search_sub: None, 122 130 } 123 131 } ··· 344 352 345 353 let context_menu = cx.global::<LibraryContextMenuState>().0.clone(); 346 354 let album_context_menu = cx.global::<AlbumContextMenuState>().0.clone(); 355 + let file_context_menu = cx.global::<FileContextMenuState>().0.clone(); 347 356 let n_album_tracks = album_tracks.len(); 348 357 let n_artist_tracks = artist_tracks.len(); 349 358 let scroll_handle = self.scroll_handle.clone(); 350 359 let _detail_scroll_handle = self.detail_scroll_handle.clone(); 351 360 352 361 // Sidebar nav item — Albums/Artists stay active while in their detail view 353 - let make_nav_item = move |icon: Icons, label: &'static str, target: LibrarySection| { 362 + let make_nav_item = move |icon: Icons, icon_size: u8, label: &'static str, target: LibrarySection| { 354 363 let is_active = section == target 355 364 || (section == LibrarySection::AlbumDetail && target == LibrarySection::Albums) 356 365 || (section == LibrarySection::ArtistDetail && target == LibrarySection::Artists) ··· 386 395 .flex() 387 396 .items_center() 388 397 .gap_x_2() 389 - .child(Icon::new(icon).size_5()) 398 + .child(icon_sized(icon, icon_size)) 390 399 .child(label), 391 400 ) 392 401 }; ··· 599 608 let show_search = !query.is_empty() 600 609 && !matches!( 601 610 section, 602 - LibrarySection::AlbumDetail | LibrarySection::ArtistDetail 611 + LibrarySection::AlbumDetail | LibrarySection::ArtistDetail | LibrarySection::Files 603 612 ); 604 613 let content: AnyElement = if show_search { 605 614 // ── Search results ──────────────────────────────────────────────────── ··· 1962 1971 ) 1963 1972 .into_any_element() 1964 1973 } 1974 + 1975 + // ── Files ───────────────────────────────────────────────────────────── 1976 + LibrarySection::Files => self.files_view.clone().into_any_element(), 1965 1977 }; 1966 1978 content_inner 1967 1979 }; // end if/else search ··· 1992 2004 .pt_4() 1993 2005 .child(self.search_input.clone()) 1994 2006 .gap_y_1() 1995 - .child(make_nav_item(Icons::Music, "Songs", LibrarySection::Songs)) 1996 - .child(make_nav_item(Icons::Disc, "Albums", LibrarySection::Albums)) 1997 - .child(make_nav_item( 1998 - Icons::Artist, 1999 - "Artists", 2000 - LibrarySection::Artists, 2001 - )) 2002 - .child(make_nav_item( 2003 - Icons::HeartOutline, 2004 - "Likes", 2005 - LibrarySection::Likes, 2006 - )), 2007 + .child(make_nav_item(Icons::Music, 5, "Songs", LibrarySection::Songs)) 2008 + .child(make_nav_item(Icons::Disc, 5, "Albums", LibrarySection::Albums)) 2009 + .child(make_nav_item(Icons::Artist, 5, "Artists", LibrarySection::Artists)) 2010 + .child(make_nav_item(Icons::HeartOutline, 5, "Likes", LibrarySection::Likes)) 2011 + .child(make_nav_item(Icons::HardDrive, 4, "Files", LibrarySection::Files)), 2007 2012 ) 2008 2013 .child(content), 2009 2014 ) ··· 2422 2427 }) 2423 2428 .child("Go to Artist"), 2424 2429 ), 2430 + ) 2431 + }) 2432 + .when_some(file_context_menu, |this, menu| { 2433 + let path_next = menu.path.clone(); 2434 + let path_last = menu.path.clone(); 2435 + let path_shuffled = menu.path.clone(); 2436 + let path_last_shuffled = menu.path.clone(); 2437 + let path_play_shuffled = menu.path.clone(); 2438 + let is_dir = menu.is_dir; 2439 + 2440 + let menu_w = px(220.0); 2441 + let menu_h = if is_dir { px(230.0) } else { px(140.0) }; 2442 + let margin = px(8.0); 2443 + let max_x = viewport.width - menu_w - margin; 2444 + let menu_x = if menu.pos.x > max_x { max_x } else { menu.pos.x }; 2445 + let menu_x = if menu_x < margin { margin } else { menu_x }; 2446 + let overflows_bottom = (menu.pos.y + menu_h + margin) > viewport.height; 2447 + let menu_y = if overflows_bottom { menu.pos.y - menu_h } else { menu.pos.y }; 2448 + let menu_y = if menu_y < margin { margin } else { menu_y }; 2449 + 2450 + this.child( 2451 + div() 2452 + .id("file_ctx_backdrop") 2453 + .absolute() 2454 + .top_0() 2455 + .left_0() 2456 + .size_full() 2457 + .occlude() 2458 + .on_click(|_, _, cx: &mut App| { 2459 + cx.stop_propagation(); 2460 + cx.global_mut::<FileContextMenuState>().0 = None; 2461 + }), 2462 + ) 2463 + .child( 2464 + div() 2465 + .absolute() 2466 + .left(menu_x) 2467 + .top(menu_y) 2468 + .w(menu_w) 2469 + .bg(theme.titlebar_bg) 2470 + .border_1() 2471 + .border_color(theme.library_table_border) 2472 + .rounded_md() 2473 + .overflow_hidden() 2474 + .flex() 2475 + .flex_col() 2476 + .child( 2477 + div() 2478 + .px_3() 2479 + .py_2p5() 2480 + .border_b_1() 2481 + .border_color(theme.library_table_border) 2482 + .flex() 2483 + .items_center() 2484 + .gap_x_2() 2485 + .child( 2486 + div().text_color(theme.library_header_text).child( 2487 + Icon::new(if is_dir { Icons::Directory } else { Icons::Music }) 2488 + .size_4(), 2489 + ), 2490 + ) 2491 + .child( 2492 + div() 2493 + .flex_1() 2494 + .min_w_0() 2495 + .text_sm() 2496 + .font_weight(FontWeight(600.0)) 2497 + .text_color(theme.library_text) 2498 + .truncate() 2499 + .child(menu.name.clone()), 2500 + ), 2501 + ) 2502 + .child(menu_item( 2503 + "file_ctx_next", 2504 + "Play Next", 2505 + theme, 2506 + move |_, _, cx: &mut App| { 2507 + let rt = cx.global::<Controller>().rt(); 2508 + if is_dir { 2509 + rt.spawn(insert_directory(path_next.clone(), INSERT_FIRST)); 2510 + } else { 2511 + rt.spawn(insert_track_next(path_next.clone())); 2512 + } 2513 + cx.global_mut::<FileContextMenuState>().0 = None; 2514 + }, 2515 + )) 2516 + .child(menu_item( 2517 + "file_ctx_last", 2518 + "Play Last", 2519 + theme, 2520 + move |_, _, cx: &mut App| { 2521 + let rt = cx.global::<Controller>().rt(); 2522 + if is_dir { 2523 + rt.spawn(insert_directory(path_last.clone(), INSERT_LAST)); 2524 + } else { 2525 + rt.spawn(insert_track_last(path_last.clone())); 2526 + } 2527 + cx.global_mut::<FileContextMenuState>().0 = None; 2528 + }, 2529 + )) 2530 + .child(menu_item( 2531 + "file_ctx_shuffled", 2532 + "Add Shuffled", 2533 + theme, 2534 + move |_, _, cx: &mut App| { 2535 + let rt = cx.global::<Controller>().rt(); 2536 + if is_dir { 2537 + rt.spawn(insert_directory(path_shuffled.clone(), INSERT_SHUFFLED)); 2538 + } else { 2539 + rt.spawn(insert_tracks( 2540 + vec![path_shuffled.clone()], 2541 + INSERT_SHUFFLED, 2542 + false, 2543 + )); 2544 + } 2545 + cx.global_mut::<FileContextMenuState>().0 = None; 2546 + }, 2547 + )) 2548 + .when(is_dir, |this| { 2549 + this.child(div().h(px(1.0)).bg(theme.library_table_border).mx_2()) 2550 + .child(menu_item( 2551 + "file_ctx_last_shuffled", 2552 + "Play Last Shuffled", 2553 + theme, 2554 + move |_, _, cx: &mut App| { 2555 + cx.global::<Controller>().rt().spawn(insert_directory( 2556 + path_last_shuffled.clone(), 2557 + INSERT_LAST_SHUFFLED, 2558 + )); 2559 + cx.global_mut::<FileContextMenuState>().0 = None; 2560 + }, 2561 + )) 2562 + .child(menu_item( 2563 + "file_ctx_play_shuffled", 2564 + "Play Shuffled", 2565 + theme, 2566 + move |_, _, cx: &mut App| { 2567 + cx.global::<Controller>().rt().spawn(play_directory( 2568 + path_play_shuffled.clone(), 2569 + true, 2570 + )); 2571 + cx.global_mut::<FileContextMenuState>().0 = None; 2572 + }, 2573 + )) 2574 + }), 2425 2575 ) 2426 2576 }) 2427 2577 }
+1
gpui/src/ui/components/pages/mod.rs
··· 1 + pub mod files; 1 2 pub mod library; 2 3 pub mod player; 3 4 pub mod queue;