Skip to content

Commit 685fcac

Browse files
committed
Favorites: wire the editable-favorites frontend (Phase 2)
Lets users curate the volume switcher's Favorites section end to end, on top of the Phase 1 backend store. Resolves issue #27 (no way to change the favorite folders). - **Add to favorites** on three surfaces: an `favorites.add` command (palette, the Go menu's "Add to favorites", default ⌘D) favoriting the focused pane's current folder; plus context-menu items on folder rows and the `..` parent row. The context-menu add is handled in Rust (`FAVORITES_ADD_CONTEXT_ID` intercepted in `on_menu_event`, favoriting `MenuState.context.path`) so a folder row favorites the right-clicked folder and `..` favorites the parent dir, never the focused dir. `..` gets a dedicated one-item native menu via `show_parent_row_context_menu`. - **Per-item Remove / Rename** on the switcher's existing dropdown row menu (right-click a favorite). Rename swaps the label for an inline input (Enter commits, Escape/blur cancels). - **Reorder** by drag within the section AND keyboard (Alt+Up / Alt+Down on a focused favorite, since Cmdr is keyboard-first). Both persist the full order via `reorderFavorites`. Pure index math in `favorites-reorder.ts`. Drag cue respects `prefers-reduced-motion` (no animated transition). - **Empty state**: the `favorite` group now always renders (it's a real state — users can clear every favorite); shows a single disabled, non-focusable placeholder "(Your favorites will show here)". - All mutations go through typed `commands.*` wrappers in `tauri-commands/favorites.ts`; the `fav-` prefix on the switcher's `LocationInfo.id` is stripped via `stripFavoritePrefix` before remove/rename/reorder. Live updates ride the backend's `volumes-changed` re-emit (no manual refresh). - Tests: command registration + handler, the empty-state placeholder, drag/keyboard reorder calling `reorderFavorites` with the right bare-id order, rename, remove, id-prefix stripping, the reorder math, and an a11y pass. Updated the dispatchable-count and menu-roundtrip pins. Note: `VolumeBreadcrumb.svelte` now exceeds its file-length allowlist (1701 vs 1414, warn-only); left for a follow-up split rather than bumping the allowlist.
1 parent c660d6f commit 685fcac

29 files changed

Lines changed: 828 additions & 51 deletions

apps/desktop/src-tauri/src/commands/ui.rs

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
use crate::ignore_poison::IgnorePoison;
22
use crate::menu::{
33
CLOSE_TAB_ID, CommandScope, FileContextInfo, MenuState, REOPEN_CLOSED_TAB_ID, SettingsChanged, ViewMode,
4-
build_breadcrumb_context_menu, build_context_menu, build_network_host_context_menu, build_tab_context_menu,
5-
frontend_shortcut_to_accelerator, menu_id_to_command, rebuild_view_mode_items, sync_view_mode_check_states,
4+
build_breadcrumb_context_menu, build_context_menu, build_network_host_context_menu, build_parent_row_context_menu,
5+
build_tab_context_menu, frontend_shortcut_to_accelerator, menu_id_to_command, rebuild_view_mode_items,
6+
sync_view_mode_check_states,
67
};
78
#[cfg(any(target_os = "macos", target_os = "linux"))]
89
use std::process::Command;
@@ -155,6 +156,26 @@ pub fn show_breadcrumb_context_menu<R: Runtime>(
155156
Ok(())
156157
}
157158

159+
/// Shows the minimal `..` parent-row context menu (just "Add to favorites").
160+
///
161+
/// `parent_path` is the directory the `..` row points at; we stash it in `MenuState.context.path`
162+
/// so `on_menu_event` favorites it when the user clicks the item. The full file context menu makes
163+
/// no sense on `..`, hence this dedicated one-item menu.
164+
#[tauri::command]
165+
#[specta::specta]
166+
pub fn show_parent_row_context_menu<R: Runtime>(window: Window<R>, parent_path: String) -> Result<(), String> {
167+
let app = window.app_handle();
168+
{
169+
let state = app.state::<MenuState<R>>();
170+
let mut context = state.context.lock_ignore_poison();
171+
context.path = parent_path;
172+
context.filename = "..".to_string();
173+
}
174+
let menu = build_parent_row_context_menu(app).map_err(|e| e.to_string())?;
175+
menu.popup(window).map_err(|e| e.to_string())?;
176+
Ok(())
177+
}
178+
158179
#[tauri::command]
159180
#[specta::specta]
160181
pub fn show_main_window<R: Runtime>(window: Window<R>) -> Result<(), String> {

apps/desktop/src-tauri/src/ipc.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@ pub fn builder() -> Builder<tauri::Wry> {
195195
crate::commands::icons::clear_directory_icon_cache,
196196
crate::commands::ui::show_file_context_menu,
197197
crate::commands::ui::show_breadcrumb_context_menu,
198+
crate::commands::ui::show_parent_row_context_menu,
198199
crate::commands::ui::show_tab_context_menu,
199200
crate::commands::ui::show_network_host_context_menu,
200201
crate::commands::ui::update_pin_tab_menu,

apps/desktop/src-tauri/src/ipc_collectors.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,8 @@ pub(crate) fn collect_cross_platform_types(types: &mut Types) -> Vec<Function> {
9393
crate::commands::icons::refresh_directory_icons,
9494
crate::commands::icons::clear_extension_icon_cache,
9595
crate::commands::icons::clear_directory_icon_cache,
96-
// show_file_context_menu, show_breadcrumb_context_menu, update_pin_tab_menu,
96+
// show_file_context_menu, show_breadcrumb_context_menu, show_parent_row_context_menu,
97+
// update_pin_tab_menu,
9798
// set_reopen_closed_tab_enabled, show_main_window, update_menu_context,
9899
// set_menu_context, toggle_hidden_files, sync_menu_show_hidden,
99100
// update_view_mode_menu, copy_to_clipboard are generic (<R: Runtime>): excluded

apps/desktop/src-tauri/src/menu/linux.rs

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,13 @@ use super::menu_items::{
1212
use super::{
1313
ABOUT_ID, CHECK_FOR_UPDATES_ID, CLOSE_OTHER_TABS_ID, CLOSE_TAB_ID, COMMAND_PALETTE_ID, COPY_FILENAME_ID,
1414
COPY_PATH_ID, DESELECT_ALL_ID, DESELECT_FILES_ID, EDIT_COPY_ID, EDIT_CUT_ID, EDIT_ID, EDIT_PASTE_ID,
15-
EDIT_PASTE_MOVE_ID, ENTER_LICENSE_KEY_ID, FILE_COPY_ID, FILE_DELETE_ID, FILE_DELETE_PERMANENTLY_ID, FILE_MOVE_ID,
16-
FILE_NEW_FOLDER_ID, FILE_VIEW_ID, GET_INFO_ID, GO_BACK_ID, GO_FORWARD_ID, GO_LATEST_DOWNLOAD_ID, GO_PARENT_ID,
17-
GO_TO_PATH_ID, HELP_SEND_ERROR_REPORT_ID, HELP_SEND_FEEDBACK_ID, MenuItems, NEW_TAB_ID, NEXT_TAB_ID, OPEN_ID,
18-
PIN_TAB_MENU_ID, PREV_TAB_ID, QUICK_LOOK_ID, RENAME_ID, REOPEN_CLOSED_TAB_ID, SEARCH_FILES_ID, SELECT_ALL_ID,
19-
SELECT_FILES_ID, SETTINGS_ID, SHOW_HIDDEN_FILES_ID, SHOW_IN_FINDER_ID, SORT_BY_EXTENSION_ID, SORT_BY_MODIFIED_ID,
20-
SORT_BY_NAME_ID, SORT_BY_SIZE_ID, SWAP_PANES_ID, SWITCH_PANE_ID, VIEW_MODE_BRIEF_LEFT_ID, VIEW_MODE_BRIEF_RIGHT_ID,
21-
VIEW_MODE_FULL_LEFT_ID, VIEW_MODE_FULL_RIGHT_ID, ViewMode,
15+
EDIT_PASTE_MOVE_ID, ENTER_LICENSE_KEY_ID, FAVORITES_ADD_ID, FILE_COPY_ID, FILE_DELETE_ID,
16+
FILE_DELETE_PERMANENTLY_ID, FILE_MOVE_ID, FILE_NEW_FOLDER_ID, FILE_VIEW_ID, GET_INFO_ID, GO_BACK_ID, GO_FORWARD_ID,
17+
GO_LATEST_DOWNLOAD_ID, GO_PARENT_ID, GO_TO_PATH_ID, HELP_SEND_ERROR_REPORT_ID, HELP_SEND_FEEDBACK_ID, MenuItems,
18+
NEW_TAB_ID, NEXT_TAB_ID, OPEN_ID, PIN_TAB_MENU_ID, PREV_TAB_ID, QUICK_LOOK_ID, RENAME_ID, REOPEN_CLOSED_TAB_ID,
19+
SEARCH_FILES_ID, SELECT_ALL_ID, SELECT_FILES_ID, SETTINGS_ID, SHOW_HIDDEN_FILES_ID, SHOW_IN_FINDER_ID,
20+
SORT_BY_EXTENSION_ID, SORT_BY_MODIFIED_ID, SORT_BY_NAME_ID, SORT_BY_SIZE_ID, SWAP_PANES_ID, SWITCH_PANE_ID,
21+
VIEW_MODE_BRIEF_LEFT_ID, VIEW_MODE_BRIEF_RIGHT_ID, VIEW_MODE_FULL_LEFT_ID, VIEW_MODE_FULL_RIGHT_ID, ViewMode,
2222
};
2323

2424
/// Linux menu: builds all menus from scratch, matching the macOS menu structure.
@@ -273,6 +273,7 @@ pub(crate) fn build_menu_linux<R: Runtime>(
273273
true,
274274
Some("Cmd+J"),
275275
)?;
276+
let favorites_add_item = MenuItem::with_id(app, FAVORITES_ADD_ID, "&Add to favorites", true, Some("Cmd+D"))?;
276277

277278
let go_menu = Submenu::with_items(
278279
app,
@@ -286,6 +287,8 @@ pub(crate) fn build_menu_linux<R: Runtime>(
286287
&PredefinedMenuItem::separator(app)?,
287288
&go_to_path_item,
288289
&go_latest_download_item,
290+
&PredefinedMenuItem::separator(app)?,
291+
&favorites_add_item,
289292
],
290293
)?;
291294
menu.append(&go_menu)?;
@@ -427,12 +430,13 @@ pub(crate) fn build_menu_linux<R: Runtime>(
427430
register_item(&mut items, SORT_BY_SIZE_ID, &sort_items.by_size, &sort_submenu, 3);
428431

429432
// Go menu positions: back(0), forward(1), sep(2), parent(3), sep(4), go_to_path(5),
430-
// go_latest_download(6)
433+
// go_latest_download(6), sep(7), favorites_add(8)
431434
register_item(&mut items, GO_BACK_ID, &go_back_item, &go_menu, 0);
432435
register_item(&mut items, GO_FORWARD_ID, &go_forward_item, &go_menu, 1);
433436
register_item(&mut items, GO_PARENT_ID, &go_parent_item, &go_menu, 3);
434437
register_item(&mut items, GO_TO_PATH_ID, &go_to_path_item, &go_menu, 5);
435438
register_item(&mut items, GO_LATEST_DOWNLOAD_ID, &go_latest_download_item, &go_menu, 6);
439+
register_item(&mut items, FAVORITES_ADD_ID, &favorites_add_item, &go_menu, 8);
436440

437441
// Tab menu positions: new(0), close(1), reopen(2), sep(3), next(4), prev(5), sep(6), pin(7),
438442
// close_others(8)

apps/desktop/src-tauri/src/menu/macos.rs

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,14 @@ use super::menu_items::{
1515
use super::{
1616
ABOUT_ID, CHECK_FOR_UPDATES_ID, CLOSE_OTHER_TABS_ID, CLOSE_TAB_ID, COMMAND_PALETTE_ID, COPY_FILENAME_ID,
1717
COPY_PATH_ID, DESELECT_ALL_ID, DESELECT_FILES_ID, EDIT_COPY_ID, EDIT_CUT_ID, EDIT_ID, EDIT_PASTE_ID,
18-
EDIT_PASTE_MOVE_ID, ENTER_LICENSE_KEY_ID, FILE_COPY_ID, FILE_DELETE_ID, FILE_DELETE_PERMANENTLY_ID, FILE_MOVE_ID,
19-
FILE_NEW_FOLDER_ID, FILE_VIEW_ID, GET_INFO_ID, GO_BACK_ID, GO_FORWARD_ID, GO_LATEST_DOWNLOAD_ID, GO_PARENT_ID,
20-
GO_TO_PATH_ID, HELP_SEND_ERROR_REPORT_ID, HELP_SEND_FEEDBACK_ID, MenuItems, NEW_TAB_ID, NEXT_TAB_ID, OPEN_ID,
21-
OPEN_ONBOARDING_ID, PIN_TAB_MENU_ID, PREV_TAB_ID, QUICK_LOOK_ID, RENAME_ID, REOPEN_CLOSED_TAB_ID, SEARCH_FILES_ID,
22-
SELECT_ALL_ID, SELECT_FILES_ID, SETTINGS_ID, SHOW_HIDDEN_FILES_ID, SHOW_IN_FINDER_ID, SORT_BY_EXTENSION_ID,
23-
SORT_BY_MODIFIED_ID, SORT_BY_NAME_ID, SORT_BY_SIZE_ID, SWAP_PANES_ID, SWITCH_PANE_ID, VIEW_MODE_BRIEF_LEFT_ID,
24-
VIEW_MODE_BRIEF_RIGHT_ID, VIEW_MODE_FULL_LEFT_ID, VIEW_MODE_FULL_RIGHT_ID, ViewMode,
18+
EDIT_PASTE_MOVE_ID, ENTER_LICENSE_KEY_ID, FAVORITES_ADD_ID, FILE_COPY_ID, FILE_DELETE_ID,
19+
FILE_DELETE_PERMANENTLY_ID, FILE_MOVE_ID, FILE_NEW_FOLDER_ID, FILE_VIEW_ID, GET_INFO_ID, GO_BACK_ID, GO_FORWARD_ID,
20+
GO_LATEST_DOWNLOAD_ID, GO_PARENT_ID, GO_TO_PATH_ID, HELP_SEND_ERROR_REPORT_ID, HELP_SEND_FEEDBACK_ID, MenuItems,
21+
NEW_TAB_ID, NEXT_TAB_ID, OPEN_ID, OPEN_ONBOARDING_ID, PIN_TAB_MENU_ID, PREV_TAB_ID, QUICK_LOOK_ID, RENAME_ID,
22+
REOPEN_CLOSED_TAB_ID, SEARCH_FILES_ID, SELECT_ALL_ID, SELECT_FILES_ID, SETTINGS_ID, SHOW_HIDDEN_FILES_ID,
23+
SHOW_IN_FINDER_ID, SORT_BY_EXTENSION_ID, SORT_BY_MODIFIED_ID, SORT_BY_NAME_ID, SORT_BY_SIZE_ID, SWAP_PANES_ID,
24+
SWITCH_PANE_ID, VIEW_MODE_BRIEF_LEFT_ID, VIEW_MODE_BRIEF_RIGHT_ID, VIEW_MODE_FULL_LEFT_ID, VIEW_MODE_FULL_RIGHT_ID,
25+
ViewMode,
2526
};
2627

2728
pub(crate) fn build_menu_macos<R: Runtime>(
@@ -295,6 +296,7 @@ pub(crate) fn build_menu_macos<R: Runtime>(
295296
let go_to_path_item = MenuItem::with_id(app, GO_TO_PATH_ID, "Go to path\u{2026}", true, Some("Cmd+G"))?;
296297
let go_latest_download_item =
297298
MenuItem::with_id(app, GO_LATEST_DOWNLOAD_ID, "Go to latest download", true, Some("Cmd+J"))?;
299+
let favorites_add_item = MenuItem::with_id(app, FAVORITES_ADD_ID, "Add to favorites", true, Some("Cmd+D"))?;
298300

299301
let go_menu = Submenu::with_items(
300302
app,
@@ -308,6 +310,8 @@ pub(crate) fn build_menu_macos<R: Runtime>(
308310
&PredefinedMenuItem::separator(app)?,
309311
&go_to_path_item,
310312
&go_latest_download_item,
313+
&PredefinedMenuItem::separator(app)?,
314+
&favorites_add_item,
311315
],
312316
)?;
313317
menu.append(&go_menu)?;
@@ -446,12 +450,13 @@ pub(crate) fn build_menu_macos<R: Runtime>(
446450
register_item(&mut items, SORT_BY_SIZE_ID, &sort_items.by_size, &sort_submenu, 3);
447451

448452
// Go menu positions: back(0), forward(1), sep(2), parent(3), sep(4), go_to_path(5),
449-
// go_latest_download(6)
453+
// go_latest_download(6), sep(7), favorites_add(8)
450454
register_item(&mut items, GO_BACK_ID, &go_back_item, &go_menu, 0);
451455
register_item(&mut items, GO_FORWARD_ID, &go_forward_item, &go_menu, 1);
452456
register_item(&mut items, GO_PARENT_ID, &go_parent_item, &go_menu, 3);
453457
register_item(&mut items, GO_TO_PATH_ID, &go_to_path_item, &go_menu, 5);
454458
register_item(&mut items, GO_LATEST_DOWNLOAD_ID, &go_latest_download_item, &go_menu, 6);
459+
register_item(&mut items, FAVORITES_ADD_ID, &favorites_add_item, &go_menu, 8);
455460

456461
// Tab menu positions: new(0), close(1), reopen(2), sep(3), next(4), prev(5), sep(6), pin(7),
457462
// close_others(8)

apps/desktop/src-tauri/src/menu/menu_handlers.rs

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,13 @@ use crate::ignore_poison::IgnorePoison;
1717

1818
use super::menu_items::{brief_view_label, full_view_label};
1919
use super::{
20-
CLOSE_TAB_ID, CommandScope, EDIT_COPY_ID, EDIT_CUT_ID, EDIT_PASTE_ID, EJECT_VOLUME_ID, MenuItemEntry, MenuSort,
21-
MenuState, NETWORK_HOST_DISCONNECT_ID, NETWORK_HOST_FORGET_PASSWORD_ID, NETWORK_HOST_FORGET_SERVER_ID,
22-
SELECT_ALL_ID, SHOW_HIDDEN_FILES_ID, SORT_ASCENDING_ID, SORT_BY_CREATED_ID, SORT_BY_EXTENSION_ID,
23-
SORT_BY_MODIFIED_ID, SORT_BY_NAME_ID, SORT_BY_SIZE_ID, SORT_DESCENDING_ID, SettingsChanged, TAB_CLOSE_ID,
24-
TAB_CLOSE_OTHERS_ID, TAB_PIN_ID, VIEW_MODE_BRIEF_LEFT_ID, VIEW_MODE_BRIEF_RIGHT_ID, VIEW_MODE_FULL_LEFT_ID,
25-
VIEW_MODE_FULL_RIGHT_ID, VIEWER_WORD_WRAP_ID, ViewMode, ViewModeChanged, menu_id_to_command,
20+
CLOSE_TAB_ID, CommandScope, EDIT_COPY_ID, EDIT_CUT_ID, EDIT_PASTE_ID, EJECT_VOLUME_ID, FAVORITES_ADD_CONTEXT_ID,
21+
MenuItemEntry, MenuSort, MenuState, NETWORK_HOST_DISCONNECT_ID, NETWORK_HOST_FORGET_PASSWORD_ID,
22+
NETWORK_HOST_FORGET_SERVER_ID, SELECT_ALL_ID, SHOW_HIDDEN_FILES_ID, SORT_ASCENDING_ID, SORT_BY_CREATED_ID,
23+
SORT_BY_EXTENSION_ID, SORT_BY_MODIFIED_ID, SORT_BY_NAME_ID, SORT_BY_SIZE_ID, SORT_DESCENDING_ID, SettingsChanged,
24+
TAB_CLOSE_ID, TAB_CLOSE_OTHERS_ID, TAB_PIN_ID, VIEW_MODE_BRIEF_LEFT_ID, VIEW_MODE_BRIEF_RIGHT_ID,
25+
VIEW_MODE_FULL_LEFT_ID, VIEW_MODE_FULL_RIGHT_ID, VIEWER_WORD_WRAP_ID, ViewMode, ViewModeChanged,
26+
menu_id_to_command,
2627
};
2728

2829
/// Removes macOS system-injected items from the Edit menu and registers the Help menu.
@@ -412,6 +413,29 @@ pub fn handle_menu_event(app: &AppHandle<tauri::Wry>, event: tauri::menu::MenuEv
412413
return;
413414
}
414415

416+
// === Add to favorites (folder-row + parent-row context menus) ===
417+
// Favorites the right-clicked path stashed in `MenuState.context.path` (the folder for a folder
418+
// row, the parent dir for `..`). Intercepted here so it never routes through `favorites.add`
419+
// (which favorites the focused-pane dir instead). The store write touches the filesystem, so it
420+
// runs on the blocking pool, never on this menu thread; the command re-emits `volumes-changed`.
421+
if id == FAVORITES_ADD_CONTEXT_ID {
422+
let menu_state = app.state::<MenuState<tauri::Wry>>();
423+
let path = menu_state.context.lock_ignore_poison().path.clone();
424+
if path.is_empty() {
425+
log::warn!(target: "favorites", "Add to favorites: empty context path, ignoring");
426+
return;
427+
}
428+
tauri::async_runtime::spawn(async move {
429+
let write = tauri::async_runtime::spawn_blocking(move || crate::favorites::store::add(&path, None)).await;
430+
if let Err(e) = write {
431+
log::warn!(target: "favorites", "Add to favorites: store write failed: {e}");
432+
return;
433+
}
434+
crate::volume_broadcast::emit_volumes_changed();
435+
});
436+
return;
437+
}
438+
415439
// === Viewer word wrap: emit to the focused viewer window ===
416440
if id == VIEWER_WORD_WRAP_ID {
417441
for (label, window) in app.webview_windows() {

apps/desktop/src-tauri/src/menu/menu_structure.rs

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,11 @@ use super::menu_items::{
2727
#[cfg(target_os = "macos")]
2828
use super::{CLOUD_MAKE_OFFLINE_ID, CLOUD_REMOVE_DOWNLOAD_ID, GET_INFO_ID, QUICK_LOOK_ID};
2929
use super::{
30-
COPY_CURRENT_DIR_PATH_ID, COPY_FILENAME_ID, COPY_PATH_ID, EDIT_ID, EJECT_VOLUME_ID, FILE_COPY_ID, FILE_DELETE_ID,
31-
FILE_MOVE_ID, FILE_NEW_FOLDER_ID, FILE_VIEW_ID, MenuItems, NETWORK_HOST_DISCONNECT_ID,
32-
NETWORK_HOST_FORGET_PASSWORD_ID, NETWORK_HOST_FORGET_SERVER_ID, OPEN_ID, RENAME_ID, SHOW_IN_FINDER_ID,
33-
TAB_CLOSE_ID, TAB_CLOSE_OTHERS_ID, TAB_PIN_ID, TOGGLE_SELECTION_ID, VIEWER_WORD_WRAP_ID, ViewMode,
30+
COPY_CURRENT_DIR_PATH_ID, COPY_FILENAME_ID, COPY_PATH_ID, EDIT_ID, EJECT_VOLUME_ID, FAVORITES_ADD_CONTEXT_ID,
31+
FILE_COPY_ID, FILE_DELETE_ID, FILE_MOVE_ID, FILE_NEW_FOLDER_ID, FILE_VIEW_ID, MenuItems,
32+
NETWORK_HOST_DISCONNECT_ID, NETWORK_HOST_FORGET_PASSWORD_ID, NETWORK_HOST_FORGET_SERVER_ID, OPEN_ID, RENAME_ID,
33+
SHOW_IN_FINDER_ID, TAB_CLOSE_ID, TAB_CLOSE_OTHERS_ID, TAB_PIN_ID, TOGGLE_SELECTION_ID, VIEWER_WORD_WRAP_ID,
34+
ViewMode,
3435
};
3536

3637
/// Per-file information needed to build a fully-populated context menu.
@@ -180,6 +181,16 @@ pub fn build_context_menu<R: Runtime>(
180181
menu.append(&copy_filename_item)?;
181182
menu.append(&copy_path_item)?;
182183

184+
// Add to favorites — directories only (favorites are folders), and not on the search-results
185+
// snapshot pane (its rows aren't a stable folder to favorite). Favorites the right-clicked
186+
// folder's path, which `on_menu_event` reads from `MenuState.context.path`.
187+
if is_directory && !restrict_destination_actions {
188+
let add_favorite_item =
189+
MenuItem::with_id(app, FAVORITES_ADD_CONTEXT_ID, "Add to favorites", true, None::<&str>)?;
190+
menu.append(&PredefinedMenuItem::separator(app)?)?;
191+
menu.append(&add_favorite_item)?;
192+
}
193+
183194
// Cloud actions (macOS File Provider): only show when the file is in a
184195
// cloud-managed folder, gated by sync status.
185196
#[cfg(target_os = "macos")]
@@ -226,6 +237,17 @@ pub fn build_context_menu<R: Runtime>(
226237
})
227238
}
228239

240+
/// Builds the minimal context menu for the `..` parent row: a single "Add to favorites" item that
241+
/// favorites the parent directory. The full file context menu (Copy / Move / Delete, etc.) makes no
242+
/// sense on `..`, so this is its own one-item menu. The caller stashes the parent dir in
243+
/// `MenuState.context.path`; `on_menu_event` reads it back for the `FAVORITES_ADD_CONTEXT_ID` click.
244+
pub fn build_parent_row_context_menu<R: Runtime>(app: &AppHandle<R>) -> tauri::Result<Menu<R>> {
245+
let menu = Menu::new(app)?;
246+
let add_favorite_item = MenuItem::with_id(app, FAVORITES_ADD_CONTEXT_ID, "Add to favorites", true, None::<&str>)?;
247+
menu.append(&add_favorite_item)?;
248+
Ok(menu)
249+
}
250+
229251
/// Builds a context menu for the breadcrumb path bar.
230252
///
231253
/// `accelerator` is the user's configured shortcut for the "Copy path" command (in

apps/desktop/src-tauri/src/menu/mod.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ pub use menu_handlers::{
4545
};
4646
pub use menu_structure::{
4747
FileContextInfo, build_breadcrumb_context_menu, build_context_menu, build_menu, build_network_host_context_menu,
48-
build_tab_context_menu, build_viewer_menu,
48+
build_parent_row_context_menu, build_tab_context_menu, build_viewer_menu,
4949
};
5050

5151
/// `settings-changed`: a CheckMenuItem toggle (currently only "Show hidden
@@ -153,6 +153,17 @@ pub const GO_TO_PATH_ID: &str = "go_to_path";
153153
/// "Go to latest download" (⌘J): jumps the focused pane to the most recent download.
154154
pub const GO_LATEST_DOWNLOAD_ID: &str = "go_latest_download";
155155

156+
/// "Add to favorites" (⌘D), menu bar + palette: maps to the `favorites.add` command, which favorites
157+
/// the focused pane's current folder.
158+
pub const FAVORITES_ADD_ID: &str = "favorites_add";
159+
160+
/// "Add to favorites", folder-row + parent-row CONTEXT menus: favorites `MenuState.context.path`
161+
/// directly in `on_menu_event` (the right-clicked folder, or the parent dir for `..`). A separate id
162+
/// from `FAVORITES_ADD_ID` so the menu-bar item (focused-pane dir) and the context item
163+
/// (right-clicked path) can't be confused; intercepted before the unified `menu_id_to_command`
164+
/// lookup, so it never routes through a command.
165+
pub const FAVORITES_ADD_CONTEXT_ID: &str = "favorites_add_context";
166+
156167
/// Menu item IDs for sorting.
157168
pub const SORT_BY_NAME_ID: &str = "sort_by_name";
158169
pub const SORT_BY_EXTENSION_ID: &str = "sort_by_extension";
@@ -258,6 +269,7 @@ pub fn menu_id_to_command(menu_id: &str) -> Option<(&'static str, CommandScope)>
258269
GO_PARENT_ID => Some(("nav.parent", CommandScope::FileScoped)),
259270
GO_TO_PATH_ID => Some(("nav.goToPath", CommandScope::FileScoped)),
260271
GO_LATEST_DOWNLOAD_ID => Some(("downloads.goToLatest", CommandScope::FileScoped)),
272+
FAVORITES_ADD_ID => Some(("favorites.add", CommandScope::FileScoped)),
261273

262274
// Tab commands (file-scoped)
263275
NEW_TAB_ID => Some(("tab.new", CommandScope::FileScoped)),
@@ -350,6 +362,7 @@ pub fn command_id_to_menu_id(command_id: &str) -> Option<&'static str> {
350362
"nav.parent" => Some(GO_PARENT_ID),
351363
"nav.goToPath" => Some(GO_TO_PATH_ID),
352364
"downloads.goToLatest" => Some(GO_LATEST_DOWNLOAD_ID),
365+
"favorites.add" => Some(FAVORITES_ADD_ID),
353366
"tab.new" => Some(NEW_TAB_ID),
354367
"tab.close" => Some(CLOSE_TAB_ID),
355368
"tab.reopen" => Some(REOPEN_CLOSED_TAB_ID),
@@ -642,6 +655,7 @@ mod tests {
642655
"nav.parent",
643656
"nav.goToPath",
644657
"downloads.goToLatest",
658+
"favorites.add",
645659
"tab.new",
646660
"tab.close",
647661
"tab.reopen",

apps/desktop/src/lib/commands/command-ids.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ export const COMMAND_IDS = [
4848
// Navigation (Go to path)
4949
'nav.goToPath',
5050

51+
// Favorites
52+
'favorites.add',
53+
5154
// Downloads
5255
'downloads.goToLatest',
5356

apps/desktop/src/lib/commands/command-registry.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ const EXPECTED_PALETTE_IDS: readonly CommandId[] = [
2828
'feedback.send',
2929
'search.open',
3030
'nav.goToPath',
31+
'favorites.add',
3132
'downloads.goToLatest',
3233
'view.showHidden',
3334
'view.briefMode',

0 commit comments

Comments
 (0)