Skip to content

Commit d37de22

Browse files
committed
feat: suggest to choose folder when category doesn't have one assigned
1 parent f49389c commit d37de22

3 files changed

Lines changed: 119 additions & 26 deletions

File tree

src/library.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,12 @@ impl Library {
151151
self.settings.category_folder(category)
152152
}
153153

154+
pub fn category_needs_folder(&self, category: Category) -> bool {
155+
self.settings
156+
.category_folder(category)
157+
.is_none_or(|path| !path.is_dir())
158+
}
159+
154160
pub fn filters_open(&self) -> bool {
155161
self.filters_open
156162
}
@@ -1232,6 +1238,9 @@ mod tests {
12321238

12331239
let count = library.read_with(cx, |lib, _| lib.active_state().results.len());
12341240
assert_eq!(count, 0);
1241+
let needs_folder =
1242+
library.read_with(cx, |lib, _| lib.category_needs_folder(Category::Music));
1243+
assert!(needs_folder);
12351244
}
12361245

12371246
#[gpui::test]

src/ui/table.rs

Lines changed: 106 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@ use std::rc::Rc;
77

88
use futures::{StreamExt as _, channel::mpsc};
99
use gpui::{
10-
AnyElement, App, AppContext as _, ClickEvent, Context, DismissEvent, Entity, FocusHandle,
11-
Focusable, InteractiveElement as _, IntoElement, KeyDownEvent, Keystroke, MouseButton,
12-
MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, Point, Render,
13-
SharedString, Size, StatefulInteractiveElement as _, Styled, Window, div, hsla,
14-
prelude::FluentBuilder as _, px, red, size,
10+
AnyElement, App, AppContext as _, AsyncApp, ClickEvent, Context, DismissEvent, Entity,
11+
FocusHandle, Focusable, InteractiveElement as _, IntoElement, KeyDownEvent, Keystroke,
12+
MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, PathPromptOptions,
13+
Pixels, Point, Render, SharedString, Size, StatefulInteractiveElement as _, Styled, Window,
14+
div, hsla, prelude::FluentBuilder as _, px, red, size,
1515
};
1616
use gpui_component::{
1717
ActiveTheme as _, Sizable, StyledExt, VirtualListScrollHandle,
@@ -25,7 +25,7 @@ use gpui_component::{
2525
use crate::ui::CONTENT_PX;
2626
use crate::{
2727
library::{Library, LibraryEvent},
28-
model::{AudioFormat, FileRecord},
28+
model::{AudioFormat, Category, FileRecord},
2929
};
3030

3131
const TAG_CELL_X_PADDING_WIDTH: f32 = 24.;
@@ -107,6 +107,7 @@ pub struct FileTable {
107107
row_sizes: Rc<Vec<Size<Pixels>>>,
108108
row_sizes_len: usize,
109109
tag_width_cache: Option<TagWidthCache>,
110+
folder_prompt_active: bool,
110111
}
111112

112113
impl FileTable {
@@ -202,9 +203,54 @@ impl FileTable {
202203
row_sizes: Rc::new(Vec::new()),
203204
row_sizes_len: 0,
204205
tag_width_cache: None,
206+
folder_prompt_active: false,
205207
}
206208
}
207209

210+
fn choose_category_folder(&mut self, category: Category, cx: &mut Context<Self>) {
211+
if self.folder_prompt_active {
212+
return;
213+
}
214+
215+
self.folder_prompt_active = true;
216+
cx.notify();
217+
218+
let paths = cx.prompt_for_paths(PathPromptOptions {
219+
files: false,
220+
directories: true,
221+
multiple: false,
222+
prompt: Some(format!("Select {} folder", category.label()).into()),
223+
});
224+
let library = self.library.downgrade();
225+
226+
cx.spawn(async move |this, cx: &mut AsyncApp| {
227+
let path = paths
228+
.await
229+
.ok()
230+
.and_then(|paths| paths.ok())
231+
.flatten()
232+
.and_then(|paths| paths.into_iter().next());
233+
234+
this.update(cx, |this, cx| {
235+
this.folder_prompt_active = false;
236+
cx.notify();
237+
})
238+
.ok()?;
239+
240+
let Some(path) = path else {
241+
return Some(());
242+
};
243+
244+
library
245+
.update(cx, |lib, cx| {
246+
let _ = lib.set_category_folder(category, path, cx);
247+
})
248+
.ok()?;
249+
Some(())
250+
})
251+
.detach();
252+
}
253+
208254
fn table_columns(&mut self, cx: &mut Context<Self>) -> (Vec<String>, Vec<Pixels>, usize) {
209255
let editing = self.editing.clone();
210256
let (keys, row_count, widths) = {
@@ -1345,7 +1391,16 @@ impl Render for FileTable {
13451391
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
13461392
crate::perf::sample("table.render.rate");
13471393
let render_start = crate::perf::start();
1348-
let (keys, tag_widths, row_count) = self.table_columns(cx);
1394+
let missing_folder_category = {
1395+
let library = self.library.read(cx);
1396+
let active = library.active();
1397+
library.category_needs_folder(active).then_some(active)
1398+
};
1399+
let (keys, tag_widths, row_count) = if missing_folder_category.is_some() {
1400+
(Vec::new(), Vec::new(), 0)
1401+
} else {
1402+
self.table_columns(cx)
1403+
};
13491404

13501405
let mut header_row = TableRow::new().child(
13511406
TableHead::new()
@@ -1369,25 +1424,51 @@ impl Render for FileTable {
13691424
let virtual_tag_widths = tag_widths.clone();
13701425
let row_sizes = self.row_sizes(row_count);
13711426
let row_scroll_handle = self.row_scroll_handle.clone();
1372-
let rows = div()
1373-
.size_full()
1374-
.child(
1375-
v_virtual_list(
1376-
cx.entity().clone(),
1377-
"file-table-rows",
1378-
row_sizes,
1379-
move |this, range, _, cx| {
1380-
this.render_rows(
1381-
range,
1382-
virtual_keys.clone(),
1383-
virtual_tag_widths.clone(),
1384-
cx,
1385-
)
1386-
},
1427+
let rows: AnyElement = if let Some(category) = missing_folder_category {
1428+
div()
1429+
.id("missing-category-folder")
1430+
.size_full()
1431+
.flex()
1432+
.flex_col()
1433+
.items_center()
1434+
.justify_center()
1435+
.gap_2()
1436+
.px(CONTENT_PX)
1437+
.text_sm()
1438+
.text_color(cx.theme().muted_foreground)
1439+
.cursor_pointer()
1440+
.child(SharedString::from(format!(
1441+
"Click this area to choose the {} folder.",
1442+
category.label()
1443+
)))
1444+
.child("You can always change it via the category buttons above.")
1445+
.on_click(cx.listener(move |this, _: &ClickEvent, _, cx| {
1446+
this.choose_category_folder(category, cx);
1447+
cx.stop_propagation();
1448+
}))
1449+
.into_any_element()
1450+
} else {
1451+
div()
1452+
.size_full()
1453+
.child(
1454+
v_virtual_list(
1455+
cx.entity().clone(),
1456+
"file-table-rows",
1457+
row_sizes,
1458+
move |this, range, _, cx| {
1459+
this.render_rows(
1460+
range,
1461+
virtual_keys.clone(),
1462+
virtual_tag_widths.clone(),
1463+
cx,
1464+
)
1465+
},
1466+
)
1467+
.track_scroll(&row_scroll_handle),
13871468
)
1388-
.track_scroll(&row_scroll_handle),
1389-
)
1390-
.scrollbar(&row_scroll_handle, ScrollbarAxis::Vertical);
1469+
.scrollbar(&row_scroll_handle, ScrollbarAxis::Vertical)
1470+
.into_any_element()
1471+
};
13911472

13921473
let table = div()
13931474
.track_focus(&self.focus_handle)

src/ui/titlebar.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@ impl Render for AppTitleBar {
180180
for category in Category::ALL {
181181
let selected = category == active;
182182
let hovered = self.hovered_category == Some(category);
183+
let missing_folder = self.library.read(cx).category_needs_folder(category);
183184
let drag_hovered = self.drag_hovered_category == Some(category);
184185
let bg = if selected {
185186
selected_bg
@@ -221,6 +222,8 @@ impl Render for AppTitleBar {
221222
this.choose_category_folder(category, event, window, cx);
222223
}));
223224

225+
let show_folder_button = can_hover && (hovered || missing_folder);
226+
224227
categories = categories.child(
225228
div()
226229
.id(SharedString::from(category.label()))
@@ -243,7 +246,7 @@ impl Render for AppTitleBar {
243246
this.bg(drag_bg)
244247
})
245248
.when(can_hover, |this| this.hover(move |this| this.bg(hover_bg)))
246-
.when(can_hover && hovered, |this| {
249+
.when(show_folder_button, |this| {
247250
this.child(div().absolute().right(px(6.)).child(folder_button))
248251
})
249252
.on_drag_move::<ExternalPaths>(cx.listener(

0 commit comments

Comments
 (0)