diff --git a/i18n/de/cosmic_files.ftl b/i18n/de/cosmic_files.ftl index 7abaca12..e83702d1 100644 --- a/i18n/de/cosmic_files.ftl +++ b/i18n/de/cosmic_files.ftl @@ -40,6 +40,15 @@ open-multiple-folders = Mehrere Ordner öffnen save = Speichern save-file = Datei speichern +## Dauerhaft Löschen Dialog +selected-items = {$items} gewählte {$items -> + [one] Objekt + *[other] Objekte + } +permanently-delete-question = {$target} dauerhaft löschen? +delete = Löschen +permanently-delete-warning = Dauerhaft gelöschte Objekte können nicht wiederhergestellt werden + # Umbenennen-Dialog rename-file = Datei umbenennen rename-folder = Ordner umbenennen @@ -100,6 +109,14 @@ restored = {$items} {$items -> } aus dem {trash} wiederhergestellt undo = Rückgängig unknown-folder = unbekannter Ordner +permanently-deleting = Lösche {$items} {$items -> + [one] Objekt + *[other] Objekte + } dauerhaft +permanently-deleted = {$items} {$items -> + [one] Objekt + *[other] Objekte + } dauerhaft gelöscht ## Öffnen mit open-with = Öffnen mit @@ -111,7 +128,10 @@ properties = Eigenschaften ## Einstellungen settings = Einstellungen settings-tab = Tab +settings-optional-context-menu-actions = Optionale Aktionen im Kontextmenü +settings-optional-context-menu-actions-description = Weitere Aktionen im Menü anzeigen. Tastenkürzel können auch für ausgeblendete Aktionen verwendet werden. settings-show-hidden = Versteckte Dateien anzeigen +settings-show-delete-permanently = Dauerhaft löschen default-view = Standardansicht icon-size-list = Symbolgröße (Liste) icon-size-grid = Symbolgröße (Raster) @@ -133,6 +153,7 @@ new-file = Neue Datei new-folder = Neuer Ordner open-in-terminal = Im Terminal öffnen move-to-trash = In den Papierkorb verschieben +permanently-delete = Dauerhaft löschen... restore-from-trash = Aus dem Papierkorb wiederherstellen remove-from-sidebar = Von der Seitenleiste entfernen sort-by-name = Nach Name sortieren diff --git a/i18n/en/cosmic_files.ftl b/i18n/en/cosmic_files.ftl index b150ffc1..c6471889 100644 --- a/i18n/en/cosmic_files.ftl +++ b/i18n/en/cosmic_files.ftl @@ -43,6 +43,15 @@ open-multiple-folders = Open multiple folders save = Save save-file = Save file +## Permanently delete Dialog +selected-items = {$items} selected {$items -> + [one] item + *[other] items + } +permanently-delete-question = Permanently delete {$target}? +delete = Delete +permanently-delete-warning = Permanently deleted items can not be restored + ## Rename Dialog rename-file = Rename file rename-folder = Rename folder @@ -108,6 +117,14 @@ moved = Moved {$items} {$items -> [one] item *[other] items } from {$from} to {$to} +permanently-deleting = Permanently deleting {$items} {$items -> + [one] item + *[other] items + } +permanently-deleted = Permanently deleted {$items} {$items -> + [one] item + *[other] items + } renaming = Renaming {$from} to {$to} renamed = Renamed {$from} to {$to} restoring = Restoring {$items} {$items -> @@ -131,7 +148,10 @@ show-details = Show details ## Settings settings = Settings settings-tab = Tab +settings-optional-context-menu-actions = Optional Context Menu Actions +settings-optional-context-menu-actions-description = Show more actions in the menus. Keyboard shortcuts can be used even if the actions are not shown. settings-show-hidden = Show hidden files +settings-show-delete-permanently = Permanently delete default-view = Default view icon-size-list = Icon size (list) icon-size-grid = Icon size (grid) @@ -154,6 +174,7 @@ new-file = New file... new-folder = New folder... open-in-terminal = Open in terminal move-to-trash = Move to trash +permanently-delete = Delete permanently... restore-from-trash = Restore from trash remove-from-sidebar = Remove from sidebar sort-by-name = Sort by name diff --git a/src/app.rs b/src/app.rs index 87cbab72..734d3f54 100644 --- a/src/app.rs +++ b/src/app.rs @@ -89,6 +89,7 @@ pub enum Action { OpenTerminal, OpenWith, Paste, + PermanentlyDelete, Properties, Rename, RestoreFromTrash, @@ -138,6 +139,7 @@ impl Action { Action::OpenTerminal => Message::OpenTerminal(entity_opt), Action::OpenWith => Message::ToggleContextPage(ContextPage::OpenWith), Action::Paste => Message::Paste(entity_opt), + Action::PermanentlyDelete => Message::PermanentlyDelete(entity_opt), Action::Properties => Message::ToggleContextPage(ContextPage::Properties(None)), Action::Rename => Message::Rename(entity_opt), Action::RestoreFromTrash => Message::RestoreFromTrash(entity_opt), @@ -243,6 +245,7 @@ pub enum Message { PendingComplete(u64), PendingError(u64, String), PendingProgress(u64, f32), + PermanentlyDelete(Option), RescanTrash, Rename(Option), ReplaceResult(ReplaceResult), @@ -306,6 +309,9 @@ pub enum DialogPage { name: String, dir: bool, }, + PermanentlyDelete { + paths: Vec, + }, RenameItem { from: PathBuf, parent: PathBuf, @@ -969,6 +975,24 @@ impl App { ) }) .into(), + widget::settings::view_section(fl!("settings-optional-context-menu-actions")) + .add(widget::text(fl!( + "settings-optional-context-menu-actions-description" + ))) + .add({ + let tab_config = self.config.tab.clone(); + widget::settings::item::builder(fl!("settings-show-delete-permanently")) + .toggler( + tab_config.show_delete_permanently, + move |show_delete_permanently| { + Message::TabConfig(TabConfig { + show_delete_permanently, + ..tab_config + }) + }, + ) + }) + .into(), ]) .into() } @@ -1305,6 +1329,9 @@ impl Application for App { Operation::NewFile { path } }); } + DialogPage::PermanentlyDelete { paths } => { + self.operation(Operation::PermanentlyDelete { paths }); + } DialogPage::RenameItem { from, parent, name, .. } => { @@ -1712,6 +1739,13 @@ impl Application for App { } return self.update_notification(); } + Message::PermanentlyDelete(entity_opt) => { + let paths = self.selected_paths(entity_opt); + if !paths.is_empty() { + self.dialog_pages + .push_back(DialogPage::PermanentlyDelete { paths }); + } + } Message::RescanTrash => { // Update trash icon if empty/full let maybe_entity = self.nav_model.iter().find(|&entity| { @@ -2401,6 +2435,28 @@ impl Application for App { .spacing(space_xxs), ) } + DialogPage::PermanentlyDelete { paths } => { + let target = if paths.len() == 1 { + format!( + "»{}«", + paths[0] + .file_name() + .map(std::ffi::OsStr::to_string_lossy) + .unwrap_or_else(|| paths[0].to_string_lossy()) + ) + } else { + fl!("selected-items", items = paths.len()) + }; + widget::dialog(fl!("permanently-delete-question", target = target)) + .primary_action( + widget::button::destructive(fl!("delete")) + .on_press(Message::DialogComplete), + ) + .secondary_action( + widget::button::standard(fl!("cancel")).on_press(Message::DialogCancel), + ) + .control(widget::text(fl!("permanently-delete-warning"))) + } DialogPage::RenameItem { from, parent, diff --git a/src/config.rs b/src/config.rs index 96c5ed3f..73fc837b 100644 --- a/src/config.rs +++ b/src/config.rs @@ -131,6 +131,8 @@ pub struct TabConfig { pub sort_direction: bool, /// Icon zoom pub icon_sizes: IconSizes, + /// Show delete permanently context menu item + pub show_delete_permanently: bool, } impl Default for TabConfig { @@ -142,6 +144,7 @@ impl Default for TabConfig { sort_name: HeadingOptions::Name, sort_direction: true, icon_sizes: IconSizes::default(), + show_delete_permanently: false, } } } diff --git a/src/key_bind.rs b/src/key_bind.rs index 255fb897..646f2603 100644 --- a/src/key_bind.rs +++ b/src/key_bind.rs @@ -41,6 +41,7 @@ pub fn key_binds() -> HashMap { bind!([Shift], Key::Named(Named::ArrowUp), ItemUp); bind!([Alt], Key::Named(Named::ArrowUp), LocationUp); bind!([], Key::Named(Named::Delete), MoveToTrash); + bind!([Shift], Key::Named(Named::Delete), PermanentlyDelete); bind!([Ctrl, Shift], Key::Character("n".into()), NewFolder); bind!([], Key::Named(Named::Enter), Open); bind!([Ctrl], Key::Named(Named::Enter), OpenInNewTab); diff --git a/src/menu.rs b/src/menu.rs index 68b08bab..4f91e0cd 100644 --- a/src/menu.rs +++ b/src/menu.rs @@ -60,6 +60,7 @@ pub fn context_menu<'a>( let TabConfig { sort_name, sort_direction, + show_delete_permanently, .. } = tab.config; let sort_item = |label, variant| { @@ -141,6 +142,11 @@ pub fn context_menu<'a>( children.push(menu_item(fl!("add-to-sidebar"), Action::AddToSidebar).into()); children.push(container(horizontal_rule(1)).padding([0, 8]).into()); children.push(menu_item(fl!("move-to-trash"), Action::MoveToTrash).into()); + if show_delete_permanently { + children.push( + menu_item(fl!("permanently-delete"), Action::PermanentlyDelete).into(), + ); + } } else { //TODO: need better designs for menu with no selection //TODO: have things like properties but they apply to the folder? diff --git a/src/operation.rs b/src/operation.rs index 37bdd425..b8bcfeec 100644 --- a/src/operation.rs +++ b/src/operation.rs @@ -157,6 +157,10 @@ pub enum Operation { NewFolder { path: PathBuf, }, + /// Permanently delete items, skipping the trash + PermanentlyDelete { + paths: Vec, + }, Rename { from: PathBuf, to: PathBuf, @@ -276,6 +280,7 @@ impl Operation { name = file_name(path), parent = parent_name(path) ), + Self::PermanentlyDelete { paths } => fl!("permanently-deleting", items = paths.len()), Self::Rename { from, to } => { fl!("renaming", from = file_name(from), to = file_name(to)) } @@ -320,6 +325,7 @@ impl Operation { name = file_name(path), parent = parent_name(path) ), + Self::PermanentlyDelete { paths } => fl!("permanently-deleted", items = paths.len()), Self::Rename { from, to } => fl!("renamed", from = file_name(from), to = file_name(to)), Self::Restore { paths } => fl!("restored", items = paths.len()), } @@ -583,6 +589,36 @@ impl Operation { .send(Message::PendingProgress(id, 100.0)) .await; } + Self::PermanentlyDelete { paths } => { + let total = paths.len(); + let mut count = 0; + for path in paths { + tokio::task::spawn_blocking(|| { + if path.is_symlink() || path.is_file() { + fs::remove_file(path) + } else if path.is_dir() { + fs::remove_dir_all(path) + } else { + Err(std::io::Error::new( + std::io::ErrorKind::InvalidData, + "File to delete is not symlink, file or directory", + )) + } + }) + .await + .map_err(err_str)? + .map_err(err_str)?; + count += 1; + let _ = msg_tx + .lock() + .await + .send(Message::PendingProgress( + id, + 100.0 * (count as f32) / (total as f32), + )) + .await; + } + } Self::Rename { from, to } => { tokio::task::spawn_blocking(|| fs::rename(from, to)) .await