diff --git a/Changelog.md b/Changelog.md index a581796..c223e64 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,10 +1,12 @@ ## Version 4.0.0 - ? +- Multithread support for collecting files to check(2/3x on 4 thread processor) - [#502](https://github.com/qarmin/czkawka/pull/502), [#504](https://github.com/qarmin/czkawka/pull/504) +- Add Polish and Italian translation - [#469](https://github.com/qarmin/czkawka/pull/469), [#508](https://github.com/qarmin/czkawka/pull/508) - Add support for finding similar videos - [#460](https://github.com/qarmin/czkawka/pull/460) - GUI code refactoring(could fix some bugs) - [#462](https://github.com/qarmin/czkawka/pull/462) - Fixed crash when trying to hard/symlink 0 files - [#462](https://github.com/qarmin/czkawka/pull/462) -- GTK 4 compatibility improvements for future change of toolkit - [#467](https://github.com/qarmin/czkawka/pull/467), [#468](https://github.com/qarmin/czkawka/pull/468), [#473](https://github.com/qarmin/czkawka/pull/473), [#474](https://github.com/qarmin/czkawka/pull/474) +- GTK 4 compatibility improvements for future change of toolkit - [#467](https://github.com/qarmin/czkawka/pull/467), [#468](https://github.com/qarmin/czkawka/pull/468), [#473](https://github.com/qarmin/czkawka/pull/473), [#474](https://github.com/qarmin/czkawka/pull/474), [#503](https://github.com/qarmin/czkawka/pull/503), [#505](https://github.com/qarmin/czkawka/pull/505) - Change minimal supported OS to Ubuntu 20.04(needed by GTK) - [#468](https://github.com/qarmin/czkawka/pull/468) -- Increased performance when using image previews - [#468](https://github.com/qarmin/czkawka/pull/468) +- Increased performance by avoiding creating unnecessary image previews - [#468](https://github.com/qarmin/czkawka/pull/468) - Improved performance due caching hash of broken/not supported images/videos = [#471](https://github.com/qarmin/czkawka/pull/471) - Option to not remove cache from non existent files(e.g. from unplugged pendrive) - [#472](https://github.com/qarmin/czkawka/pull/472) - Add multiple tooltips with helpful messages - [#472](https://github.com/qarmin/czkawka/pull/472) @@ -13,6 +15,9 @@ - Remove support for finding zeroed files - [#461](https://github.com/qarmin/czkawka/pull/461) - Remove HashMB mode - [#476](https://github.com/qarmin/czkawka/pull/476) - Approximate comparison of music - [#483](https://github.com/qarmin/czkawka/pull/483) +- Enable column sorting for simple treeview - [#487](https://github.com/qarmin/czkawka/pull/487) +- Allow to hide upper panel - [#491](https://github.com/qarmin/czkawka/pull/491) +- Make UI take less space - [#500](https://github.com/qarmin/czkawka/pull/500) ## Version 3.3.1 - 22.11.2021r - Fix crash when moving buttons [#457](https://github.com/qarmin/czkawka/pull/457) diff --git a/czkawka_gui/src/connect_button_delete.rs b/czkawka_gui/src/connect_button_delete.rs index 671b781..03919f1 100644 --- a/czkawka_gui/src/connect_button_delete.rs +++ b/czkawka_gui/src/connect_button_delete.rs @@ -41,31 +41,31 @@ pub async fn delete_things(gui_data: GuiData) { let preview_path = gui_data.preview_path.clone(); let text_view_errors = gui_data.text_view_errors.clone(); - if !check_if_can_delete_files(&check_button_settings_confirm_deletion, &window_main).await { - return; - } let nb_number = notebook_main.current_page().unwrap(); let tree_view = &main_tree_views[nb_number as usize]; let nb_object = &NOTEBOOKS_INFOS[nb_number as usize]; + let (number_of_selected_items, number_of_selected_groups) = check_how_much_elements_is_selected(tree_view, nb_object.column_color, nb_object.column_selection); + + // Nothing is selected + if number_of_selected_items == 0 { + return; + } + + if !check_if_can_delete_files(&check_button_settings_confirm_deletion, &window_main, number_of_selected_items, number_of_selected_groups).await { + return; + } + if let Some(column_color) = nb_object.column_color { if !check_button_settings_confirm_group_deletion.is_active() || !check_if_deleting_all_files_in_group(tree_view, column_color, nb_object.column_selection, &window_main, &check_button_settings_confirm_group_deletion).await { - tree_remove( - &tree_view.clone(), - nb_object.column_name, - nb_object.column_path, - column_color, - nb_object.column_selection, - &check_button_settings_use_trash, - &text_view_errors, - ); + tree_remove(tree_view, nb_object.column_name, nb_object.column_path, column_color, nb_object.column_selection, &check_button_settings_use_trash, &text_view_errors); } } else { if nb_number == NotebookMainEnum::EmptyDirectories as u32 { - empty_folder_remover(&tree_view.clone(), nb_object.column_name, nb_object.column_path, nb_object.column_selection, &check_button_settings_use_trash, &text_view_errors); + empty_folder_remover(tree_view, nb_object.column_name, nb_object.column_path, nb_object.column_selection, &check_button_settings_use_trash, &text_view_errors); } else { - basic_remove(&tree_view.clone(), nb_object.column_name, nb_object.column_path, nb_object.column_selection, &check_button_settings_use_trash, &text_view_errors); + basic_remove(tree_view, nb_object.column_name, nb_object.column_path, nb_object.column_selection, &check_button_settings_use_trash, &text_view_errors); } } @@ -82,9 +82,9 @@ pub async fn delete_things(gui_data: GuiData) { } } -pub async fn check_if_can_delete_files(check_button_settings_confirm_deletion: >k::CheckButton, window_main: >k::Window) -> bool { +pub async fn check_if_can_delete_files(check_button_settings_confirm_deletion: >k::CheckButton, window_main: >k::Window, number_of_selected_items: u64, number_of_selected_groups: u64) -> bool { if check_button_settings_confirm_deletion.is_active() { - let (confirmation_dialog_delete, check_button) = create_dialog_ask_for_deletion(window_main); + let (confirmation_dialog_delete, check_button) = create_dialog_ask_for_deletion(window_main, number_of_selected_items, number_of_selected_groups); let response_type = confirmation_dialog_delete.run_future().await; if response_type == gtk::ResponseType::Ok { @@ -102,12 +102,19 @@ pub async fn check_if_can_delete_files(check_button_settings_confirm_deletion: & true } -fn create_dialog_ask_for_deletion(window_main: >k::Window) -> (Dialog, CheckButton) { +fn create_dialog_ask_for_deletion(window_main: >k::Window, number_of_selected_items: u64, number_of_selected_groups: u64) -> (Dialog, CheckButton) { let dialog = gtk::Dialog::builder().title(&fl!("delete_title_dialog")).transient_for(window_main).modal(true).build(); let button_ok = dialog.add_button(&fl!("general_ok_button"), ResponseType::Ok); dialog.add_button(&fl!("general_close_button"), ResponseType::Cancel); let label: gtk::Label = gtk::Label::new(Some(&fl!("delete_question_label"))); + let label2: gtk::Label = match number_of_selected_groups { + 0 => gtk::Label::new(Some(&fl!("delete_items_label", generate_translation_hashmap(vec![("items", number_of_selected_items.to_string())])))), + _ => gtk::Label::new(Some(&fl!( + "delete_items_groups_label", + generate_translation_hashmap(vec![("items", number_of_selected_items.to_string()), ("groups", number_of_selected_groups.to_string())]) + ))), + }; let check_button: gtk::CheckButton = gtk::CheckButton::with_label(&fl!("dialogs_ask_next_time")); check_button.set_active(true); check_button.set_halign(Align::Center); @@ -116,7 +123,10 @@ fn create_dialog_ask_for_deletion(window_main: >k::Window) -> (Dialog, CheckBu let internal_box = get_dialog_box_child(&dialog); internal_box.add(&label); + internal_box.add(&label2); internal_box.add(&check_button); + internal_box.set_margin(5); + check_button.set_margin_top(5); dialog.show_all(); (dialog, check_button) @@ -227,7 +237,7 @@ pub fn empty_folder_remover(tree_view: >k::TreeView, column_file_name: i32, co // We must check if folder is really empty or contains only other empty folders let mut error_happened = false; - let mut folders_to_check: Vec = vec![format!("{}/{}", path, name)]; + let mut folders_to_check: Vec = vec![get_full_name_from_path_name(&path, &name)]; let mut current_folder: String; let mut next_folder: String; 'dir: while !folders_to_check.is_empty() { @@ -275,14 +285,14 @@ pub fn empty_folder_remover(tree_view: >k::TreeView, column_file_name: i32, co if !error_happened { if !use_trash { - match fs::remove_dir_all(format!("{}/{}", path, name)) { + match fs::remove_dir_all(get_full_name_from_path_name(&path, &name)) { Ok(_) => { model.remove(&iter); } Err(_inspected) => error_happened = true, } } else { - match trash::delete(format!("{}/{}", path, name)) { + match trash::delete(get_full_name_from_path_name(&path, &name)) { Ok(_) => { model.remove(&iter); } @@ -291,7 +301,7 @@ pub fn empty_folder_remover(tree_view: >k::TreeView, column_file_name: i32, co } } if error_happened { - messages += &fl!("delete_folder_failed", generate_translation_hashmap(vec![("dir", format!("{}/{}", path, name))])); + messages += &fl!("delete_folder_failed", generate_translation_hashmap(vec![("dir", get_full_name_from_path_name(&path, &name))])); messages += "\n"; } } @@ -332,23 +342,23 @@ pub fn basic_remove(tree_view: >k::TreeView, column_file_name: i32, column_pat let path = model.value(&iter, column_path).get::().unwrap(); if !use_trash { - match fs::remove_file(format!("{}/{}", path, name)) { + match fs::remove_file(get_full_name_from_path_name(&path, &name)) { Ok(_) => { model.remove(&iter); } Err(e) => { - messages += fl!("delete_file_failed", generate_translation_hashmap(vec![("name", format!("{}/{}", path, name)), ("reason", e.to_string())])).as_str(); + messages += fl!("delete_file_failed", generate_translation_hashmap(vec![("name", get_full_name_from_path_name(&path, &name)), ("reason", e.to_string())])).as_str(); messages += "\n"; } } } else { - match trash::delete(format!("{}/{}", path, name)) { + match trash::delete(get_full_name_from_path_name(&path, &name)) { Ok(_) => { model.remove(&iter); } Err(e) => { - messages += fl!("delete_file_failed", generate_translation_hashmap(vec![("name", format!("{}/{}", path, name)), ("reason", e.to_string())])).as_str(); + messages += fl!("delete_file_failed", generate_translation_hashmap(vec![("name", get_full_name_from_path_name(&path, &name)), ("reason", e.to_string())])).as_str(); messages += "\n"; } } @@ -410,12 +420,12 @@ pub fn tree_remove(tree_view: >k::TreeView, column_file_name: i32, column_path vec_file_name.dedup(); for file_name in vec_file_name { if !use_trash { - if let Err(e) = fs::remove_file(format!("{}/{}", path.clone(), file_name.clone())) { - messages += fl!("delete_file_failed", generate_translation_hashmap(vec![("name", format!("{}/{}", path, file_name)), ("reason", e.to_string())])).as_str(); + if let Err(e) = fs::remove_file(get_full_name_from_path_name(&path, &file_name)) { + messages += fl!("delete_file_failed", generate_translation_hashmap(vec![("name", get_full_name_from_path_name(&path, &file_name)), ("reason", e.to_string())])).as_str(); messages += "\n"; } - } else if let Err(e) = trash::delete(format!("{}/{}", path.clone(), file_name.clone())) { - messages += fl!("delete_file_failed", generate_translation_hashmap(vec![("name", format!("{}/{}", path, file_name)), ("reason", e.to_string())])).as_str(); + } else if let Err(e) = trash::delete(get_full_name_from_path_name(&path, &file_name)) { + messages += fl!("delete_file_failed", generate_translation_hashmap(vec![("name", get_full_name_from_path_name(&path, &file_name)), ("reason", e.to_string())])).as_str(); messages += "\n"; } diff --git a/czkawka_gui/src/connect_button_hardlink.rs b/czkawka_gui/src/connect_button_hardlink.rs index aabd9be..0bb0548 100644 --- a/czkawka_gui/src/connect_button_hardlink.rs +++ b/czkawka_gui/src/connect_button_hardlink.rs @@ -140,7 +140,7 @@ pub fn hardlink_symlink(tree_view: >k::TreeView, column_file_name: i32, column if model.path(¤t_iter).unwrap() == selected_rows[current_selected_index] { let file_name = model.value(¤t_iter, column_file_name).get::().unwrap(); let path = model.value(¤t_iter, column_path).get::().unwrap(); - let full_file_path = format!("{}/{}", path, file_name); + let full_file_path = get_full_name_from_path_name(&path, &file_name); if current_symhardlink_data.is_some() { vec_tree_path_to_remove.push(model.path(¤t_iter).unwrap()); diff --git a/czkawka_gui/src/connect_button_move.rs b/czkawka_gui/src/connect_button_move.rs index 1dba9e2..33e3c38 100644 --- a/czkawka_gui/src/connect_button_move.rs +++ b/czkawka_gui/src/connect_button_move.rs @@ -31,6 +31,12 @@ pub fn connect_button_move(gui_data: &GuiData) { let tree_view = &main_tree_views[nb_number as usize]; let nb_object = &NOTEBOOKS_INFOS[nb_number as usize]; + let (number_of_selected_items, _number_of_selected_groups) = check_how_much_elements_is_selected(tree_view, nb_object.column_color, nb_object.column_selection); + + // Nothing is selected + if number_of_selected_items == 0 { + return; + } move_things( tree_view, nb_object.column_name, @@ -174,7 +180,7 @@ fn move_files_common(selected_rows: &[TreePath], model: >k::ListStore, column_ let file_name = model.value(&iter, column_file_name).get::().unwrap(); let path = model.value(&iter, column_path).get::().unwrap(); - let thing = format!("{}/{}", path, file_name); + let thing = get_full_name_from_path_name(&path, &file_name); let destination_file = destination_folder.join(file_name); if Path::new(&thing).is_dir() { if let Err(e) = fs_extra::dir::move_dir(&thing, &destination_file, &fs_extra::dir::CopyOptions::new()) { diff --git a/czkawka_gui/src/connect_popovers.rs b/czkawka_gui/src/connect_popovers.rs index 5a748d7..9a62390 100644 --- a/czkawka_gui/src/connect_popovers.rs +++ b/czkawka_gui/src/connect_popovers.rs @@ -401,11 +401,8 @@ fn popover_custom_select_unselect(popover: >k::Popover, window_main: &Window, let is_selected = model.value(&iter, column_button_selection as i32).get::().unwrap(); let path = model.value(&iter, column_path).get::().unwrap(); let name = model.value(&iter, column_file_name).get::().unwrap(); - #[cfg(not(target_family = "windows"))] - let character = "/"; - #[cfg(target_family = "windows")] - let character = "\\"; - let path_and_name = format!("{}{}{}", path, character, name); + + let path_and_name = get_full_name_from_path_name(&path, &name); let mut need_to_change_thing: bool = false; diff --git a/czkawka_gui/src/help_functions.rs b/czkawka_gui/src/help_functions.rs index 385e987..f5910cd 100644 --- a/czkawka_gui/src/help_functions.rs +++ b/czkawka_gui/src/help_functions.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use std::path::{Path, PathBuf}; use gtk::prelude::*; -use gtk::{ListStore, TextView, Widget}; +use gtk::{ListStore, TextView, TreeView, Widget}; use czkawka_core::big_file::BigFile; use czkawka_core::broken_files::BrokenFiles; @@ -19,6 +19,11 @@ use czkawka_core::{fl, invalid_symlinks}; use crate::notebook_enums::{NotebookMainEnum, NUMBER_OF_NOTEBOOK_MAIN_TABS}; +#[cfg(not(target_family = "windows"))] +pub const CHARACTER: char = '/'; +#[cfg(target_family = "windows")] +pub const CHARACTER: char = '\\'; + pub const KEY_DELETE: u32 = 119; pub const KEY_ENTER: u32 = 36; pub const KEY_SPACE: u32 = 65; @@ -470,6 +475,14 @@ pub fn get_notebook_object_from_tree_view(tree_view: >k::TreeView) -> &Noteboo &NOTEBOOKS_INFOS[nb_enum as usize] } +pub fn get_full_name_from_path_name(path: &str, name: &str) -> String { + let mut string = String::with_capacity(path.len() + name.len() + 1); + string.push_str(path); + string.push(CHARACTER); + string.push_str(name); + string +} + // After e.g. deleting files, header may become orphan or have one child, so should be deleted in this case pub fn clean_invalid_headers(model: >k::ListStore, column_color: i32) { // Remove only child from header @@ -541,6 +554,52 @@ pub fn clean_invalid_headers(model: >k::ListStore, column_color: i32) { } } } +pub fn check_how_much_elements_is_selected(tree_view: &TreeView, column_color: Option, column_selection: i32) -> (u64, u64) { + let mut number_of_selected_items: u64 = 0; + let mut number_of_selected_groups: u64 = 0; + + let model = get_list_store(tree_view); + + let mut is_item_currently_selected_in_group: bool = false; + + // First iter + if let Some(iter) = model.iter_first() { + if let Some(column_color) = column_color { + assert_eq!(model.value(&iter, column_color).get::().unwrap(), HEADER_ROW_COLOR); // First element should be header + + loop { + if !model.iter_next(&iter) { + break; + } + + if model.value(&iter, column_color).get::().unwrap() == HEADER_ROW_COLOR { + is_item_currently_selected_in_group = false; + } else { + if model.value(&iter, column_selection).get::().unwrap() { + number_of_selected_items += 1; + + if !is_item_currently_selected_in_group { + number_of_selected_groups += 1; + } + is_item_currently_selected_in_group = true; + } + } + } + } else { + loop { + if !model.iter_next(&iter) { + break; + } + + if model.value(&iter, column_selection).get::().unwrap() { + number_of_selected_items += 1; + } + } + } + } + + (number_of_selected_items, number_of_selected_groups) +} pub fn get_custom_label_from_button_with_image(button: >k::Bin) -> gtk::Label { let internal_box = button.child().unwrap().downcast::().unwrap(); diff --git a/czkawka_gui/src/initialize_gui.rs b/czkawka_gui/src/initialize_gui.rs index 2f73b20..5831efd 100644 --- a/czkawka_gui/src/initialize_gui.rs +++ b/czkawka_gui/src/initialize_gui.rs @@ -673,7 +673,7 @@ fn show_preview(tree_view: &TreeView, text_view_errors: &TextView, check_button_ let path = tree_model.value(&tree_model.iter(&tree_path).unwrap(), column_path).get::().unwrap(); let name = tree_model.value(&tree_model.iter(&tree_path).unwrap(), column_name).get::().unwrap(); - let file_name = format!("{}/{}", path, name); + let file_name = get_full_name_from_path_name(&path, &name); let file_name = file_name.as_str(); if let Some(extension) = Path::new(file_name).extension() { diff --git a/czkawka_gui/src/opening_selecting_records.rs b/czkawka_gui/src/opening_selecting_records.rs index 83c7de2..4e60c09 100644 --- a/czkawka_gui/src/opening_selecting_records.rs +++ b/czkawka_gui/src/opening_selecting_records.rs @@ -80,9 +80,7 @@ fn common_open_function(tree_view: >k::TreeView, column_name: i32, column_path let end_path = match opening_mode { OpenMode::OnlyPath => path, - OpenMode::PathAndName => { - format!("{}/{}", path, name) - } + OpenMode::PathAndName => get_full_name_from_path_name(&path, &name), }; open::that_in_background(&end_path); @@ -96,11 +94,9 @@ fn common_open_function(tree_view: >k::TreeView, column_name: i32, column_path fn handle_tree_keypress(tree_view: >k::TreeView, key_code: u32, name_column: i32, path_column: i32, mark_column: i32) { match key_code { KEY_ENTER => { - // Enter common_open_function(tree_view, name_column, path_column, OpenMode::PathAndName); } KEY_SPACE => { - // Space common_mark_function(tree_view, mark_column); } _ => {} diff --git a/i18n/en/czkawka_gui.ftl b/i18n/en/czkawka_gui.ftl index 9b58cd7..9e379b7 100644 --- a/i18n/en/czkawka_gui.ftl +++ b/i18n/en/czkawka_gui.ftl @@ -424,6 +424,9 @@ delete_all_files_in_group_label1 = In some groups there are selected all records delete_all_files_in_group_label2 = Are you sure that you want to delete them? delete_folder_failed = Failed to remove folder {$dir} because folder doesn't exists, you don't have permissions or isn't empty. +delete_items_label = { $items } files will be removed. +delete_items_groups_label = { $items } files from { $groups } groups will be removed. + hardlink_failed = Failed to hardlink hard_sym_invalid_selection_title_dialog = Invalid selection with some groups hard_sym_invalid_selection_label_1 = In some groups there is only 1 record selected and it will be ignored. diff --git a/i18n/pl/czkawka_gui.ftl b/i18n/pl/czkawka_gui.ftl index a966e92..86e8501 100644 --- a/i18n/pl/czkawka_gui.ftl +++ b/i18n/pl/czkawka_gui.ftl @@ -367,6 +367,8 @@ delete_all_files_in_group_title = Potwierdzenie usunięcia wszystkich plików w delete_all_files_in_group_label1 = W niektórych grupach zaznaczono wszystkie rekordy. delete_all_files_in_group_label2 = Czy na pewno je usunąć? delete_folder_failed = Nie udało się usunąć folderu { $name } ponieważ nie istnieje, uprawnienia nie są wystarczające lub nie jest pusty. +delete_items_label = { $items } plików zostanie usuniętych. +delete_items_groups_label = { $items } plików z { $groups } grup będzie usuniętych. hardlink_failed = Nie udało się utworzyć twardego dowiązania hard_sym_invalid_selection_title_dialog = Niepoprawne zaznaczenie w niektórych grupach hard_sym_invalid_selection_label_1 = W niektórych grupach zaznaczono tylko 1 rekord który zostanie zignorowany. diff --git a/instructions/Translations.md b/instructions/Translations.md index f914cda..f882ba6 100644 --- a/instructions/Translations.md +++ b/instructions/Translations.md @@ -63,4 +63,31 @@ Next new record must be added to array. combo_box_text: "Polski (Polish)", short_text: "pl", }, -``` \ No newline at end of file +``` + +# Validating translation offline +When trying to translate objects offline, due renames, adding and removing elements, may happen that translations will contain outdated entries. + +To help find such keywords, special python script can be used. + +To be able to use it, be sure that you are directly inside main `czkawka` folder. +Next, be sure that your language is available in array/list and also in i18n folder and then run python script `python3 misc/translation_test.py`. +Then results should be visible in console: +``` +Checking pl language +Missing keyword - duplicate_mode_name_combo_box +Missing keyword - duplicate_mode_size_combo_box +Missing keyword - duplicate_mode_hash_combo_box +Missing keyword - settings_language_label_tooltip +Missing keyword - settings_language_label +Unused keyword - duplicate_mode_name_checkbox +Unused keyword - duplicate_mode_size_checkbox +Unused keyword - duplicate_mode_hash_checkbox +Unused keyword - duplicate_mode_name_checkbox_tooltip +Unused keyword - duplicate_mode_size_checkbox_tooltip +Unused keyword - duplicate_mode_hash_checkbox_tooltip +``` +`Missing keyword` means that some keywords exists in base translations and texts needs to be translated. +`Unused keyword` means that keyword is no longer used. It can be renamed or entirely removed from file. + +When script will not print anything except "Checking language", then this means that translation file have exactly same keys as base one. \ No newline at end of file