Add empty folder support for GUI

This commit is contained in:
Rafał Mikrut 2020-09-22 11:24:55 +02:00
parent 85de5b412d
commit aba4e65fb8
5 changed files with 296 additions and 92 deletions

View File

@ -1,4 +1,6 @@
## Version 0.1.2 dev
- Add basic search empty folders in GTK GUI
- Remember place where button are placed
## Version 0.1.1 - 20.09.2020r
- Added images to readme

View File

@ -6,6 +6,8 @@ This is my first ever project in Rust so probably a lot of things are written in
## Done
- Rich instruction with examples - CLI(`cargo run --bin czkawka_cli`)
- GTK Frontend(Still WIP) - (`cargo run --bin czkawka_gui`)
- Basic layout
- Remembering of buttons between different tabs
- Orbtk Frontend(Still very early WIP) - (`cargo run --bin czkawka_gui_orbtk`)
- Saving results to file
- Duplicated file finding
@ -20,12 +22,15 @@ This is my first ever project in Rust so probably a lot of things are written in
## TODO
- Comments - a lot of things should be described
- Github CI
- More unit tests
- Finding files with debug symbols
- Maybe windows support, but this will need some refactoring in code
- Translation support
- Add support for fast searching based on checking only first ~1MB of file.
- GTK Gui
- Selection of records(don't know how to do this)
- Popups
- Choosing directories(include, excluded)
## Usage and requirements
Rustc 1.46 works fine(not sure about a minimal version)
@ -82,7 +87,7 @@ I checked my home directory without any folder exceptions(I removed all director
First run reads file entry and save it to cache so this step is mostly limited by disk performance, and with second run cache helps it so searching is a lot of faster.
Duplicate Checker(Version 0.1)
Duplicate Checker(Version 0.1.0)
| App| Executing Time |
|:----------:|:-------------:|

View File

@ -1,5 +1,5 @@
use crate::common::{Common, Messages};
use std::collections::HashMap;
use std::collections::BTreeMap;
use std::fs::{File, Metadata};
use std::io::Write;
use std::path::Path;
@ -16,9 +16,10 @@ enum FolderEmptiness {
/// Struct assigned to each checked folder with parent path(used to ignore parent if children are not empty) and flag which shows if folder is empty
#[derive(Clone)]
struct FolderEntry {
parent_path: Option<String>,
pub struct FolderEntry {
pub parent_path: Option<String>,
is_empty: FolderEmptiness,
pub modified_date: SystemTime,
}
/// Struct to store most basics info about all folder
@ -26,14 +27,14 @@ pub struct EmptyFolder {
information: Info,
delete_folders: bool,
text_messages: Messages,
empty_folder_list: HashMap<String, FolderEntry>, // Path, FolderEntry
empty_folder_list: BTreeMap<String, FolderEntry>, // Path, FolderEntry
included_directories: Vec<String>,
}
/// Info struck with helpful information's about results
pub struct Info {
number_of_checked_folders: usize,
number_of_empty_folders: usize,
pub number_of_empty_folders: usize,
}
impl Info {
pub fn new() -> Info {
@ -62,9 +63,16 @@ impl EmptyFolder {
}
}
pub fn get_empty_folder_list(&self) -> &BTreeMap<String, FolderEntry> {
&self.empty_folder_list
}
pub fn get_text_messages(&self) -> &Messages {
&self.text_messages
}
pub fn get_information(&self) -> &Info {
&self.information
}
/// Public function used by CLI to search for empty folders
pub fn find_empty_folders(&mut self) {
@ -122,7 +130,7 @@ impl EmptyFolder {
/// Clean directory tree
/// If directory contains only 2 empty folders, then this directory should be removed instead two empty folders inside because it will produce another empty folder.
fn optimize_folders(&mut self) {
let mut new_directory_folders: HashMap<String, FolderEntry> = Default::default();
let mut new_directory_folders: BTreeMap<String, FolderEntry> = Default::default();
for entry in &self.empty_folder_list {
match &entry.1.parent_path {
@ -145,7 +153,7 @@ impl EmptyFolder {
fn check_for_empty_folders(&mut self, initial_checking: bool) {
let start_time: SystemTime = SystemTime::now();
let mut folders_to_check: Vec<String> = Vec::with_capacity(1024 * 2); // This should be small enough too not see to big difference and big enough to store most of paths without needing to resize vector
let mut folders_checked: HashMap<String, FolderEntry> = Default::default();
let mut folders_checked: BTreeMap<String, FolderEntry> = Default::default();
if initial_checking {
// Add root folders for finding
@ -155,21 +163,23 @@ impl EmptyFolder {
FolderEntry {
parent_path: None,
is_empty: FolderEmptiness::Maybe,
modified_date: SystemTime::now(),
},
);
folders_to_check.push(id.clone());
}
} else {
// Add folders searched before
for id in &self.empty_folder_list {
for (name, entry) in &self.empty_folder_list {
folders_checked.insert(
id.0.clone(),
name.clone(),
FolderEntry {
parent_path: None,
is_empty: FolderEmptiness::Maybe,
modified_date: entry.modified_date,
},
);
folders_to_check.push(id.0.clone());
folders_to_check.push(name.clone());
}
}
@ -206,6 +216,13 @@ impl EmptyFolder {
FolderEntry {
parent_path: Option::from(current_folder.clone()),
is_empty: FolderEmptiness::Maybe,
modified_date: match metadata.modified() {
Ok(t) => t,
Err(_) => {
self.text_messages.warnings.push(format!("Failed to read modification date of folder {}", current_folder));
SystemTime::now()
}
},
},
);
} else {
@ -234,7 +251,7 @@ impl EmptyFolder {
}
} else {
// We need to check if parent of folder isn't also empty, because we wan't to delete only parent with two empty folders except this folders and at the end parent folder
let mut new_folders_list: HashMap<String, FolderEntry> = Default::default();
let mut new_folders_list: BTreeMap<String, FolderEntry> = Default::default();
for entry in folders_checked {
if entry.1.is_empty != FolderEmptiness::No && self.empty_folder_list.contains_key(&entry.0) {
new_folders_list.insert(entry.0, entry.1);

View File

@ -32,11 +32,6 @@ Author: Rafał Mikrut
<!-- interface-name Czkawka -->
<!-- interface-description Czkawka is simple and fast app to find duplicates, empty folders etc. -->
<!-- interface-authors Rafa\305\202 Mikrut -->
<object class="GtkAdjustment" id="adjustment1">
<property name="upper">100</property>
<property name="step_increment">1</property>
<property name="page_increment">10</property>
</object>
<object class="GtkDialog" id="delete_confirmation_dialog">
<property name="can_focus">False</property>
<property name="icon_name">applications-engineering</property>
@ -272,11 +267,13 @@ Author: Rafał Mikrut
</packing>
</child>
<child>
<object class="GtkEntry">
<object class="GtkEntry" id="duplicate_minimal_size">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="max_length">3</property>
<property name="text" translatable="yes">1</property>
<property name="input_purpose">digits</property>
<property name="caps_lock_warning">False</property>
<property name="input_purpose">number</property>
</object>
<packing>
<property name="expand">False</property>
@ -319,20 +316,12 @@ Author: Rafał Mikrut
</packing>
</child>
<child>
<object class="GtkScrolledWindow" id="notebook_empty_folders_label">
<object class="GtkScrolledWindow" id="scrolled_window_empty_folder_finder">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkTreeView">
<property name="visible">True</property>
<property name="can_focus">True</property>
<child internal-child="selection">
<object class="GtkTreeSelection">
<property name="mode">none</property>
</object>
</child>
</object>
<placeholder/>
</child>
</object>
<packing>

View File

@ -5,10 +5,13 @@ use humansize::{file_size_opts as options, FileSize};
extern crate gtk;
use chrono::NaiveDateTime;
use czkawka_core::duplicate::CheckingMethod;
use czkawka_core::empty_folder::EmptyFolder;
use duplicate::DuplicateFinder;
use gtk::prelude::*;
use gtk::{Builder, TreeView, TreeViewColumn};
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use std::time::UNIX_EPOCH;
#[derive(Debug)]
@ -19,10 +22,6 @@ enum ColumnsDuplicate {
Modification,
}
thread_local! {
pub static CHECK_TYPE: duplicate::CheckingMethod = duplicate::CheckingMethod::NONE;
}
fn main() {
gtk::init().expect("Failed to initialize GTK.");
@ -34,17 +33,36 @@ fn main() {
let main_window: gtk::Window = builder.get_object("main_window").unwrap();
main_window.show_all();
////////////////////////////////////////////////////////////////////////////////////////////////
// State
// Buttons State
// let shared_buttons: Rc<RefCell<_>> = Rc::new(RefCell::new( HashMap::<&str, bool>::new()));
let shared_buttons: Rc<RefCell<_>> = Rc::new(RefCell::new(HashMap::<String, HashMap<String, bool>>::new()));
shared_buttons.borrow_mut().clear();
let mut hashmap_buttons: HashMap<&str, bool> = Default::default();
for i in ["duplicate", "empty_folder"].iter() {
hashmap_buttons.insert(i, false);
let mut temp_hashmap: HashMap<String, bool> = Default::default();
for j in ["search", "stop", "resume", "pause", "select", "delete", "save"].iter() {
if *j == "search" {
temp_hashmap.insert(j.to_string(), true);
} else {
temp_hashmap.insert(j.to_string(), false);
}
}
shared_buttons.borrow_mut().insert(i.to_string(), temp_hashmap);
}
// Duplicate Finder state
let shared_duplication_state: Rc<RefCell<_>> = Rc::new(RefCell::new(DuplicateFinder::new()));
let shared_empty_folders_state: Rc<RefCell<_>> = Rc::new(RefCell::new(EmptyFolder::new()));
////////////////////////////////////////////////////////////////////////////////////////////////
// GUI Notepad Buttons
// GUI Duplicate Entry
let minimal_size_entry: gtk::Entry = builder.get_object("duplicate_minimal_size").unwrap();
// GUI Buttons
let buttons_search: gtk::Button = builder.get_object("buttons_search").unwrap();
let buttons_stop: gtk::Button = builder.get_object("buttons_stop").unwrap();
@ -66,6 +84,7 @@ fn main() {
for i in notebook_chooser_tool.get_children() {
notebook_chooser_tool_children_names.push(i.get_buildable_name().unwrap().to_string());
println!("{}", i.get_buildable_name().unwrap().to_string());
}
// Entry
@ -73,28 +92,93 @@ fn main() {
// Scrolled window
let scrolled_window_duplicate_finder: gtk::ScrolledWindow = builder.get_object("scrolled_window_duplicate_finder").unwrap();
let scrolled_window_empty_folder_finder: gtk::ScrolledWindow = builder.get_object("scrolled_window_empty_folder_finder").unwrap();
{
// Set starting intro
// Duplicate Finder
// Set starting information in bottom panel
info_entry.set_text("Duplicated Files");
// // Disable all unused buttons
// Disable all unused buttons
buttons_search.show();
buttons_save.hide();
buttons_delete.hide();
}
{
// Connect Notebook Tabs
{
let shared_buttons = shared_buttons.clone();
let buttons_search = buttons_search.clone();
let buttons_stop = buttons_stop.clone();
let buttons_resume = buttons_resume.clone();
let buttons_pause = buttons_pause.clone();
let buttons_select = buttons_select.clone();
let buttons_delete = buttons_delete.clone();
let buttons_save = buttons_save.clone();
let notebook_chooser_tool_children_names = notebook_chooser_tool_children_names.clone();
notebook_chooser_tool.connect_switch_page(move |_, _, number| {
let page: &str;
match notebook_chooser_tool_children_names.get(number as usize).unwrap().as_str() {
"notebook_duplicate_finder_label" => {
page = "duplicate";
}
"scrolled_window_empty_folder_finder" => {
page = "empty_folder";
}
e => {
panic!("Not existent page {}", e);
}
};
if *shared_buttons.borrow_mut().get_mut(page).unwrap().get_mut("search").unwrap() == true {
buttons_search.show();
} else {
buttons_search.hide();
}
if *shared_buttons.borrow_mut().get_mut(page).unwrap().get_mut("stop").unwrap() == true {
buttons_stop.show();
} else {
buttons_stop.hide();
}
if *shared_buttons.borrow_mut().get_mut(page).unwrap().get_mut("resume").unwrap() == true {
buttons_resume.show();
} else {
buttons_resume.hide();
}
if *shared_buttons.borrow_mut().get_mut(page).unwrap().get_mut("pause").unwrap() == true {
buttons_pause.show();
} else {
buttons_pause.hide();
}
if *shared_buttons.borrow_mut().get_mut(page).unwrap().get_mut("select").unwrap() == true {
buttons_select.show();
} else {
buttons_select.hide();
}
if *shared_buttons.borrow_mut().get_mut(page).unwrap().get_mut("delete").unwrap() == true {
buttons_delete.show();
} else {
buttons_delete.hide();
}
if *shared_buttons.borrow_mut().get_mut(page).unwrap().get_mut("save").unwrap() == true {
buttons_save.show();
} else {
buttons_save.hide();
}
});
}
// Connect Buttons
let buttons_search_clone = buttons_search.clone();
assert!(notebook_chooser_tool_children_names.contains(&"notebook_duplicate_finder_label".to_string()));
assert!(notebook_chooser_tool_children_names.contains(&"scrolled_window_empty_folder_finder".to_string()));
buttons_search.connect_clicked(move |_| {
assert!(notebook_chooser_tool_children_names.contains(&"notebook_duplicate_finder_label".to_string()));
assert!(notebook_chooser_tool_children_names.contains(&"notebook_empty_folders_label".to_string()));
match notebook_chooser_tool_children_names.get(notebook_chooser_tool.get_current_page().unwrap() as usize).unwrap().as_str() {
"notebook_duplicate_finder_label" => {
// Find duplicates
// TODO Change to proper value
let mut df = DuplicateFinder::new();
let check_method = duplicate::CheckingMethod::HASH;
@ -130,29 +214,52 @@ fn main() {
info_entry.set_text(format!("Found {} duplicates files in {} groups which took {}.", duplicates_number, duplicates_group, duplicates_size.file_size(options::BINARY).unwrap()).as_str());
// Set Scrolled window
// Create GUI
{
// Remove scrolled window from before - BUG - when doing it when view is scrolled, then scroll button disappears
for i in &scrolled_window_duplicate_finder.get_children() {
scrolled_window_duplicate_finder.remove(i);
}
//let results =df.
let col_types: [glib::types::Type; 3] = [glib::types::Type::String, glib::types::Type::String, glib::types::Type::String];
let list_store: gtk::ListStore = gtk::ListStore::new(&col_types);
// Remove scrolled window from before - BUG - when doing it when view is scrolled, then scroll button disappears
for i in &scrolled_window_duplicate_finder.get_children() {
scrolled_window_duplicate_finder.remove(i);
}
let mut tree_view_duplicate_finder: gtk::TreeView = TreeView::with_model(&list_store);
let col_types: [glib::types::Type; 3] = [glib::types::Type::String, glib::types::Type::String, glib::types::Type::String];
let list_store: gtk::ListStore = gtk::ListStore::new(&col_types);
create_tree_view_duplicates(&mut tree_view_duplicate_finder);
let mut tree_view_duplicate_finder: gtk::TreeView = TreeView::with_model(&list_store);
let col_indices = [0, 1, 2];
create_tree_view_duplicates(&mut tree_view_duplicate_finder);
match check_method {
CheckingMethod::HASH => {
let hashmap = df.get_files_sorted_by_hash();
let col_indices = [0, 1, 2];
match check_method {
CheckingMethod::HASH => {
let hashmap = df.get_files_sorted_by_hash();
for (size, vectors_vector) in hashmap {
for vector in vectors_vector {
let values: [&dyn ToValue; 3] = [
&(vector.len().to_string() + " x " + size.to_string().as_str()),
&("(".to_string() + ((vector.len() - 1) as u64 * *size as u64).to_string().as_str() + ")"),
&"Bytes lost".to_string(),
];
list_store.set(&list_store.append(), &col_indices, &values);
for entry in vector {
let path = &entry.path;
let index = path.rfind('/').unwrap();
for (size, vectors_vector) in hashmap {
for vector in vectors_vector {
let values: [&dyn ToValue; 3] = [
&(path[index + 1..].to_string()),
&(path[..index].to_string()),
&(NaiveDateTime::from_timestamp(entry.modified_date.duration_since(UNIX_EPOCH).expect("Invalid file date").as_secs() as i64, 0).to_string()),
];
list_store.set(&list_store.append(), &col_indices, &values);
}
}
}
}
CheckingMethod::SIZE => {
let hashmap = df.get_files_sorted_by_size();
for (size, vector) in hashmap {
let values: [&dyn ToValue; 3] = [
&(vector.len().to_string() + " x " + size.to_string().as_str()),
&("(".to_string() + ((vector.len() - 1) as u64 * *size as u64).to_string().as_str() + ")"),
@ -172,44 +279,97 @@ fn main() {
}
}
}
}
CheckingMethod::SIZE => {
let hashmap = df.get_files_sorted_by_size();
for (size, vector) in hashmap {
let values: [&dyn ToValue; 3] = [
&(vector.len().to_string() + " x " + size.to_string().as_str()),
&("(".to_string() + ((vector.len() - 1) as u64 * *size as u64).to_string().as_str() + ")"),
&"Bytes lost".to_string(),
];
list_store.set(&list_store.append(), &col_indices, &values);
for entry in vector {
let path = &entry.path;
let index = path.rfind('/').unwrap();
let values: [&dyn ToValue; 3] = [
&(path[index + 1..].to_string()),
&(path[..index].to_string()),
&(NaiveDateTime::from_timestamp(entry.modified_date.duration_since(UNIX_EPOCH).expect("Invalid file date").as_secs() as i64, 0).to_string()),
];
list_store.set(&list_store.append(), &col_indices, &values);
}
CheckingMethod::NONE => {
panic!();
}
}
CheckingMethod::NONE => {
panic!();
}
scrolled_window_duplicate_finder.add(&tree_view_duplicate_finder);
scrolled_window_duplicate_finder.show_all();
}
scrolled_window_duplicate_finder.add(&tree_view_duplicate_finder);
scrolled_window_duplicate_finder.show_all();
// Set state
{
*shared_duplication_state.borrow_mut() = df;
// Buttons
buttons_search_clone.show();
buttons_save.hide();
buttons_delete.hide();
if duplicates_size > 0 {
buttons_save.show();
buttons_delete.show();
*shared_buttons.borrow_mut().get_mut("duplicate").unwrap().get_mut("save").unwrap() = true;
*shared_buttons.borrow_mut().get_mut("duplicate").unwrap().get_mut("delete").unwrap() = true;
} else {
buttons_save.hide();
buttons_delete.hide();
*shared_buttons.borrow_mut().get_mut("duplicate").unwrap().get_mut("save").unwrap() = false;
*shared_buttons.borrow_mut().get_mut("duplicate").unwrap().get_mut("delete").unwrap() = false;
}
}
}
"scrolled_window_empty_folder_finder" => {
// Find empty folders
// TODO Change to proper value
let mut ef = EmptyFolder::new();
ef.set_include_directory("/home/rafal/Pulpit".to_string());
ef.set_delete_folder(false);
ef.find_empty_folders();
let information = ef.get_information();
let empty_folder_number: usize = information.number_of_empty_folders;
info_entry.set_text(format!("Found {} empty folders.", empty_folder_number).as_str());
// Create GUI
{
// Remove scrolled window from before - BUG - when doing it when view is scrolled, then scroll button disappears
for i in &scrolled_window_empty_folder_finder.get_children() {
scrolled_window_empty_folder_finder.remove(i);
}
let col_types: [glib::types::Type; 3] = [glib::types::Type::String, glib::types::Type::String, glib::types::Type::String];
let list_store: gtk::ListStore = gtk::ListStore::new(&col_types);
let mut tree_view_empty_folder_finder: gtk::TreeView = TreeView::with_model(&list_store);
create_tree_view_empty_folders(&mut tree_view_empty_folder_finder);
let col_indices = [0, 1, 2];
let hashmap = ef.get_empty_folder_list();
for (name, entry) in hashmap {
let name: String = name[..(name.len() - 1)].to_string();
let index = name.rfind('/').unwrap();
let values: [&dyn ToValue; 3] = [
&(name[index + 1..].to_string()),
&(name[..index].to_string()),
&(NaiveDateTime::from_timestamp(entry.modified_date.duration_since(UNIX_EPOCH).expect("Invalid file date").as_secs() as i64, 0).to_string()),
];
list_store.set(&list_store.append(), &col_indices, &values);
}
scrolled_window_empty_folder_finder.add(&tree_view_empty_folder_finder);
scrolled_window_empty_folder_finder.show_all();
}
// Set state
{
*shared_empty_folders_state.borrow_mut() = ef;
if empty_folder_number > 0 {
buttons_save.show();
buttons_delete.show();
*shared_buttons.borrow_mut().get_mut("empty_folder").unwrap().get_mut("save").unwrap() = true;
*shared_buttons.borrow_mut().get_mut("empty_folder").unwrap().get_mut("delete").unwrap() = true;
} else {
buttons_save.hide();
buttons_delete.hide();
*shared_buttons.borrow_mut().get_mut("empty_folder").unwrap().get_mut("save").unwrap() = false;
*shared_buttons.borrow_mut().get_mut("empty_folder").unwrap().get_mut("delete").unwrap() = false;
}
}
}
"notebook_empty_folders_label" => {}
e => panic!("Not existent {}", e),
}
});
@ -254,3 +414,34 @@ pub fn create_tree_view_duplicates(tree_view_duplicate_finder: &mut gtk::TreeVie
tree_view_duplicate_finder.set_vexpand(true);
}
pub fn create_tree_view_empty_folders(tree_view_empty_folder_finder: &mut gtk::TreeView) {
let renderer = gtk::CellRendererText::new();
let name_column: gtk::TreeViewColumn = TreeViewColumn::new();
name_column.pack_start(&renderer, true);
name_column.set_title("Folder Name");
name_column.set_resizable(true);
name_column.set_min_width(50);
name_column.add_attribute(&renderer, "text", ColumnsDuplicate::Name as i32);
tree_view_empty_folder_finder.append_column(&name_column);
let renderer = gtk::CellRendererText::new();
let path_column: gtk::TreeViewColumn = TreeViewColumn::new();
path_column.pack_start(&renderer, true);
path_column.set_title("Path");
path_column.set_resizable(true);
path_column.set_min_width(100);
path_column.add_attribute(&renderer, "text", ColumnsDuplicate::Path as i32);
tree_view_empty_folder_finder.append_column(&path_column);
let renderer = gtk::CellRendererText::new();
let modification_date_column: gtk::TreeViewColumn = TreeViewColumn::new();
modification_date_column.pack_start(&renderer, true);
modification_date_column.set_title("Modification Date");
modification_date_column.set_resizable(true);
modification_date_column.set_min_width(100);
modification_date_column.add_attribute(&renderer, "text", ColumnsDuplicate::Modification as i32);
tree_view_empty_folder_finder.append_column(&modification_date_column);
tree_view_empty_folder_finder.set_vexpand(true);
}