diff --git a/Cargo.lock b/Cargo.lock index ecf34ab..510ed96 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3317,6 +3317,7 @@ dependencies = [ "handsome_logger", "home", "humansize", + "image", "log", "open", "rand", diff --git a/czkawka_core/src/common.rs b/czkawka_core/src/common.rs index a3f4618..6613f86 100644 --- a/czkawka_core/src/common.rs +++ b/czkawka_core/src/common.rs @@ -44,7 +44,7 @@ pub fn get_number_of_threads() -> usize { fn filtering_messages(record: &Record) -> bool { if let Some(module_path) = record.module_path() { - module_path.starts_with("czkawka") + module_path.starts_with("czkawka") || module_path.starts_with("krokiet") } else { true } diff --git a/krokiet/Cargo.toml b/krokiet/Cargo.toml index 98010d1..a6213e1 100644 --- a/krokiet/Cargo.toml +++ b/krokiet/Cargo.toml @@ -32,6 +32,7 @@ log = "0.4.20" serde = "1.0" serde_json = "1.0" humansize = "2.1.3" +image = "0.24.7" [build-dependencies] slint-build = "1.3.0" diff --git a/krokiet/src/connect_scan.rs b/krokiet/src/connect_scan.rs index 9f05946..8aa66f5 100644 --- a/krokiet/src/connect_scan.rs +++ b/krokiet/src/connect_scan.rs @@ -1,6 +1,6 @@ use crate::common::create_vec_model_from_vec_string; use crate::settings::{collect_settings, SettingsCustom}; -use crate::Settings; +use crate::GuiState; use crate::{CurrentTab, MainListModel, MainWindow, ProgressToSend}; use chrono::NaiveDateTime; use crossbeam_channel::{Receiver, Sender}; @@ -98,7 +98,7 @@ fn scan_similar_images(a: Weak, progress_sender: Sender().set_info_text(messages.into()); + app.global::().set_info_text(messages.into()); }) }); } @@ -139,7 +139,7 @@ fn scan_empty_files(a: Weak, progress_sender: Sender, } app.set_empty_files_model(items.into()); app.invoke_scan_ended(format!("Found {} empty files", number_of_empty_files).into()); - app.global::().set_info_text(messages.into()); + app.global::().set_info_text(messages.into()); }) }); } @@ -180,7 +180,7 @@ fn scan_empty_folders(a: Weak, progress_sender: Sender } app.set_empty_folder_model(items.into()); app.invoke_scan_ended(format!("Found {} empty folders", folder_map.len()).into()); - app.global::().set_info_text(messages.into()); + app.global::().set_info_text(messages.into()); }) }); } diff --git a/krokiet/src/connect_show_preview.rs b/krokiet/src/connect_show_preview.rs new file mode 100644 index 0000000..bcd6e6a --- /dev/null +++ b/krokiet/src/connect_show_preview.rs @@ -0,0 +1,66 @@ +use crate::{Callabler, GuiState, MainWindow}; +use czkawka_core::common::IMAGE_RS_EXTENSIONS; +use image::DynamicImage; +use log::{debug, error}; +use slint::ComponentHandle; +use std::path::Path; +use std::time::{Duration, Instant}; + +pub type ImageBufferRgba = image::ImageBuffer, Vec>; + +pub fn connect_show_preview(app: &MainWindow) { + let a = app.as_weak(); + app.global::().on_load_image_preview(move |image_path| { + let app = a.upgrade().unwrap(); + + let path = Path::new(image_path.as_str()); + + let res = load_image(path); + if let Some((load_time, img)) = res { + let start_timer_convert_time = Instant::now(); + let slint_image = convert_into_slint_image(img); + let convert_time = start_timer_convert_time.elapsed(); + + let start_set_time = Instant::now(); + app.global::().set_preview_image(slint_image); + let set_time = start_set_time.elapsed(); + + debug!( + "Loading image took: {:?}, converting image took: {:?}, setting image took: {:?}", + load_time, convert_time, set_time + ); + app.global::().set_preview_visible(true); + } else { + app.global::().set_preview_visible(false); + } + }); +} + +fn convert_into_slint_image(img: DynamicImage) -> slint::Image { + let image_buffer: ImageBufferRgba = img.to_rgba8(); + let buffer = slint::SharedPixelBuffer::::clone_from_slice(image_buffer.as_raw(), image_buffer.width(), image_buffer.height()); + slint::Image::from_rgba8(buffer) +} + +fn load_image(image_path: &Path) -> Option<(Duration, image::DynamicImage)> { + if !image_path.is_file() { + return None; + } + let image_name = image_path.to_string_lossy().to_string(); + let image_extension = image_path.extension()?.to_string_lossy().to_lowercase(); + let extension_with_dot = format!(".{}", image_extension); + + if !IMAGE_RS_EXTENSIONS.contains(&extension_with_dot.as_str()) { + return None; + } + let load_img_start_timer = Instant::now(); + let img = image::open(image_name); + + match img { + Ok(img) => Some((load_img_start_timer.elapsed(), img)), + Err(e) => { + error!("Error while loading image: {}", e); + return None; + } + } +} diff --git a/krokiet/src/main.rs b/krokiet/src/main.rs index 15ae34f..102faa6 100644 --- a/krokiet/src/main.rs +++ b/krokiet/src/main.rs @@ -18,6 +18,7 @@ mod connect_directories_changes; mod connect_open; mod connect_progress_receiver; mod connect_scan; +mod connect_show_preview; mod connect_stop; mod settings; @@ -30,6 +31,7 @@ use crate::connect_scan::connect_scan_button; use crate::connect_directories_changes::connect_add_remove_directories; use crate::connect_progress_receiver::connect_progress_gathering; +use crate::connect_show_preview::connect_show_preview; use crate::connect_stop::connect_stop_button; use crate::settings::reset_settings; use czkawka_core::common::setup_logger; @@ -53,14 +55,21 @@ fn main() { connect_open_items(&app); connect_progress_gathering(&app, progress_receiver); connect_add_remove_directories(&app); + connect_show_preview(&app); reset_settings(&app); app.run().unwrap(); } -// TODO remove this after trying +// TODO remove this after debugging - or leave commented pub fn to_remove_debug(app: &MainWindow) { + app.set_empty_folder_model(to_remove_create_without_header("@@").into()); + app.set_empty_files_model(to_remove_create_without_header("%%").into()); + app.set_similar_images_model(to_remove_create_with_header().into()); +} + +fn to_remove_create_with_header() -> Rc> { let header_row_data: Rc> = Rc::new(VecModel::default()); for r in 0..100_000 { let items = VecModel::default(); @@ -81,12 +90,15 @@ pub fn to_remove_debug(app: &MainWindow) { header_row_data.push(item); } + header_row_data +} +fn to_remove_create_without_header(s: &str) -> Rc> { let non_header_row_data: Rc> = Rc::new(VecModel::default()); for r in 0..100_000 { let items = VecModel::default(); for c in 0..3 { - items.push(slint::format!("Item {r}.{c}")); + items.push(slint::format!("Item {r}.{c}.{s}")); } let is_checked = r % 2 == 0; @@ -100,8 +112,5 @@ pub fn to_remove_debug(app: &MainWindow) { non_header_row_data.push(item); } - - app.set_empty_folder_model(non_header_row_data.clone().into()); - app.set_empty_files_model(non_header_row_data.into()); - app.set_similar_images_model(header_row_data.into()); + non_header_row_data } diff --git a/krokiet/src/settings.rs b/krokiet/src/settings.rs index 9fdb834..f329c2c 100644 --- a/krokiet/src/settings.rs +++ b/krokiet/src/settings.rs @@ -3,6 +3,7 @@ use std::env; use std::path::PathBuf; use crate::common::create_string_standard_list_view_from_pathbuf; +use crate::GuiState; use crate::Settings; use home::home_dir; use slint::{ComponentHandle, Model}; @@ -33,7 +34,7 @@ pub fn set_settings_to_gui(app: &MainWindow, custom_settings: &SettingsCustom) { settings.set_excluded_directories(excluded_items); // Clear text - app.global::().set_info_text("".into()); + app.global::().set_info_text("".into()); } impl Default for SettingsCustom { diff --git a/krokiet/ui/bottom_panel.slint b/krokiet/ui/bottom_panel.slint index a8c8b51..f4872f5 100644 --- a/krokiet/ui/bottom_panel.slint +++ b/krokiet/ui/bottom_panel.slint @@ -3,6 +3,7 @@ import {Button, StandardListView, VerticalBox, ScrollView, TextEdit} from "std-w import {Settings} from "settings.slint"; import {BottomPanelVisibility} from "common.slint"; import {Callabler} from "callabler.slint"; +import {GuiState} from "gui_state.slint"; component DirectoriesPanel inherits HorizontalLayout { callback folder_choose_requested(bool); @@ -98,7 +99,7 @@ component TextErrorsPanel inherits TextEdit { height: 20px; read-only: true; wrap: TextWrap.no-wrap; - text <=> Settings.info_text; + text <=> GuiState.info_text; } export component BottomPanel { diff --git a/krokiet/ui/callabler.slint b/krokiet/ui/callabler.slint index 8029aa2..0d35e74 100644 --- a/krokiet/ui/callabler.slint +++ b/krokiet/ui/callabler.slint @@ -7,4 +7,7 @@ export global Callabler { callback item_opened(string); callback delete_selected_items(); + + // Preview + callback load_image_preview(string); } diff --git a/krokiet/ui/gui_state.slint b/krokiet/ui/gui_state.slint new file mode 100644 index 0000000..30d1187 --- /dev/null +++ b/krokiet/ui/gui_state.slint @@ -0,0 +1,5 @@ +export global GuiState { + in-out property info_text: "Nothing to report"; + in-out property preview_visible; + in-out property preview_image; +} diff --git a/krokiet/ui/main_lists.slint b/krokiet/ui/main_lists.slint index ae802c4..15fd1aa 100644 --- a/krokiet/ui/main_lists.slint +++ b/krokiet/ui/main_lists.slint @@ -9,19 +9,23 @@ export component MainList { in-out property <[MainListModel]> empty_folder_model; in-out property <[MainListModel]> empty_files_model; in-out property <[MainListModel]> similar_images_model; - // TODO - using root.active-tab in visible property will not clear model - if root.active-tab == CurrentTab.EmptyFolders: SelectableTableView { + + SelectableTableView { + visible: root.active-tab == CurrentTab.EmptyFolders; min-width: 200px; + height: parent.height; columns: ["Selection", "Folder Name", "Path"]; last-column: "Modification Date"; - column-sizes: [35px, 100px, 350px, 300px]; + column-sizes: [35px, 100px, 350px, 100px]; values <=> empty-folder-model; parentPathIdx: 2; fileNameIdx: 1; } - if root.active-tab == CurrentTab.EmptyFiles: SelectableTableView { + SelectableTableView { + visible: root.active-tab == CurrentTab.EmptyFiles; min-width: 200px; + height: parent.height; columns: ["Selection", "File Name", "Path"]; last-column: "Modification Date"; column-sizes: [35px, 100px, 350px, 100px]; @@ -30,8 +34,10 @@ export component MainList { fileNameIdx: 1; } - if root.active-tab == CurrentTab.SimilarImages: SelectableTableView { + SelectableTableView { + visible: root.active-tab == CurrentTab.SimilarImages; min-width: 200px; + height: parent.height; columns: ["Selection", "Similarity", "Size", "Dimensions", "File Name", "Path"]; last-column: "Modification Date"; column-sizes: [35px, 80px, 80px, 80px, 350px, 100px, 100px]; diff --git a/krokiet/ui/main_window.slint b/krokiet/ui/main_window.slint index 513ef72..5668fbe 100644 --- a/krokiet/ui/main_window.slint +++ b/krokiet/ui/main_window.slint @@ -10,8 +10,10 @@ import {Settings} from "settings.slint"; import {Callabler} from "callabler.slint"; import { BottomPanel } from "bottom_panel.slint"; import {ColorPalette} from "color_palette.slint"; +import {GuiState} from "gui_state.slint"; +import { Preview } from "preview.slint"; -export {Settings, Callabler} +export {Settings, Callabler, GuiState} export component MainWindow inherits Window { callback scan_stopping; @@ -36,7 +38,12 @@ export component MainWindow inherits Window { {checked: false, selected_row: false, header_row: false, val: ["witasphere", "/Xd1/Imagerren2", "25.11.1991"]} , {checked: true, selected_row: false, header_row: false, val: ["lokkaler", "/Xd1/Vide2", "01.23.1911"]} ]; - in-out property <[MainListModel]> empty_files_model: []; + in-out property <[MainListModel]> empty_files_model: [ + {checked: false, selected_row: false, header_row: true, val: ["kropkarz", "/Xd1", "24.10.2023"]} , + {checked: false, selected_row: false, header_row: false, val: ["witasphere", "/Xd1/Imagerren2", "25.11.1991"]} , + {checked: false, selected_row: false, header_row: false, val: ["witasphere", "/Xd1/Imagerren2", "25.11.1991"]} , + {checked: true, selected_row: false, header_row: false, val: ["lokkaler", "/Xd1/Vide2", "01.23.1911"]} + ]; in-out property <[MainListModel]> similar_images_model: []; VerticalBox { HorizontalBox { @@ -50,12 +57,24 @@ export component MainWindow inherits Window { VerticalLayout { horizontal-stretch: 1.0; - MainList { + Rectangle { vertical-stretch: 1.0; - active-tab <=> root.active-tab; - empty_folder_model <=> root.empty_folder_model; - empty_files_model <=> root.empty_files_model; - similar_images_model <=> root.similar_images_model; + MainList { + width: GuiState.preview_visible ? parent.width / 2 : parent.width; + height: parent.height; + horizontal-stretch: 0.5; + active-tab <=> root.active-tab; + empty_folder_model <=> root.empty_folder_model; + empty_files_model <=> root.empty_files_model; + similar_images_model <=> root.similar_images_model; + } + Preview { + height: parent.height; + x: parent.width / 2; + width: GuiState.preview_visible ? parent.width / 2 : 0; + visible: GuiState.preview_visible; + source: GuiState.preview_image; + } } if root.scanning: Progress { diff --git a/krokiet/ui/preview.slint b/krokiet/ui/preview.slint new file mode 100644 index 0000000..a7b4ead --- /dev/null +++ b/krokiet/ui/preview.slint @@ -0,0 +1,3 @@ +export component Preview inherits Image { + +} \ No newline at end of file diff --git a/krokiet/ui/selectable_tree_view.slint b/krokiet/ui/selectable_tree_view.slint index ca9c09e..b4a4efe 100644 --- a/krokiet/ui/selectable_tree_view.slint +++ b/krokiet/ui/selectable_tree_view.slint @@ -1,8 +1,9 @@ -import { Button, VerticalBox , HorizontalBox, TabWidget, ListView, StandardListView, StandardTableView, CheckBox} from "std-widgets.slint"; +import { Button, VerticalBox , HorizontalBox, TabWidget, ListView, StandardListView, StandardTableView, CheckBox, ScrollView} from "std-widgets.slint"; import {TypeOfOpenedItem} from "common.slint"; import {ColorPalette} from "color_palette.slint"; import {MainListModel} from "common.slint"; import {Callabler} from "callabler.slint"; +import {GuiState} from "gui_state.slint"; export component SelectableTableView inherits Rectangle { callback item_opened(string); @@ -10,7 +11,6 @@ export component SelectableTableView inherits Rectangle { in property last_column; in-out property <[MainListModel]> values; in-out property <[length]> column_sizes; - private property <[length]> real_sizes: [0px, 0px, 0px, 0px, 0px, 0px, 0px, 0px, 0px, 0px, 0px, 0px, 0px, 0px, 0px, 0px, 0px, 0px]; private property column_number: column-sizes.length + 1; // This idx, starts from zero, but since first is always a checkbox, and is not in model.val values, remove 1 from idx in-out property parentPathIdx; @@ -95,6 +95,12 @@ export component SelectableTableView inherits Rectangle { root.selected-item = -1; } } + + if (root.selected_item != -1) { + Callabler.load_image_preview(r.val[root.parentPathIdx - 1] + "/" + r.val[root.fileNameIdx - 1]); + } else { + GuiState.preview-visible = false; + } } } pointer-event(event) => { diff --git a/krokiet/ui/settings.slint b/krokiet/ui/settings.slint index c9d8660..95414c5 100644 --- a/krokiet/ui/settings.slint +++ b/krokiet/ui/settings.slint @@ -1,5 +1,4 @@ export global Settings { in-out property <[StandardListViewItem]> included_directories: [{text: "ABCD"}, {text: "BCDA"}]; in-out property <[StandardListViewItem]> excluded_directories: [{text: "ABCD"}, {text: "BCDA"}, {text: "CDFFF"}]; - in-out property info_text: "Nothing to report"; }