diff --git a/src/app.rs b/src/app.rs index 0baa5df..0325f6c 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,11 +1,11 @@ use crate::egui_tools::EguiRenderer; use egui::load::{ImageLoadResult, ImageLoader}; -use egui::{Align2, Color32, ColorImage, Event, ImageSource, Key, PointerButton}; +use egui::{Align2, Color32, ColorImage, Event, Image, ImageSource, Key, PointerButton, Sense}; use egui_wgpu::wgpu::SurfaceError; use egui_wgpu::{ScreenDescriptor, wgpu}; use image::metadata::Orientation; -use imflow::image::swap_wh; -use imflow::store::ImageStore; +use imflow::image::{ImageFormat, swap_wh}; +use imflow::store::{FileFilters, ImageStore}; use std::cmp::{max, min}; use std::collections::HashMap; use std::path::PathBuf; @@ -223,6 +223,7 @@ pub struct AppState { pub render_pipeline: wgpu::RenderPipeline, pub transform_buffer: wgpu::Buffer, pub transform_data: TransformData, + pub filters: FileFilters, } impl AppState { @@ -315,6 +316,7 @@ impl AppState { render_pipeline, transform_buffer, transform_data, + filters: FileFilters::default(), } } @@ -629,18 +631,23 @@ impl App { render_pass.draw_indexed(0..6, 0, 0..1); } + let mut rating_filter = [false; 6]; + + // let mut file_filters; let rating; let path; let current_id; let image_count; let filename; let window; + let filtered_images; { let store = state.store.read().unwrap(); rating = store.get_current_rating(); path = store.current_image_path.clone(); current_id = store.current_image_id; image_count = store.available_images.len(); + filtered_images = store.get_filtered_images(&state.filters); filename = path.path.file_name().unwrap(); window = self.window.as_ref().unwrap(); } @@ -665,59 +672,45 @@ impl App { ); }); }); - egui::Window::new("Id") - .collapsible(false) - .resizable(false) - .default_width(5.0) - .anchor(Align2::RIGHT_TOP, [-5.0, 5.0]) - .pivot(Align2::RIGHT_TOP) - .show(state.egui_renderer.context(), |ui| { - ui.vertical_centered(|ui| { - ui.label( - egui::RichText::new(format!("{}/{}", current_id, image_count)) - .size(22.0) - .strong(), - ); + + egui::TopBottomPanel::bottom("Thumbnails") + .exact_height(120.0) + .show(state.egui_renderer.context(), |panel_ui| { + egui::ScrollArea::horizontal().show(panel_ui, |ui| { + ui.horizontal_centered(|horizontal| { + for image in filtered_images { + let source = ImageSource::Bytes { + uri: std::borrow::Cow::Owned(image.get_hash_str()), + bytes: egui::load::Bytes::Static(&[]), + }; + + let image_widget = horizontal.add( + egui::Image::new(source) + .fit_to_original_size(0.8) + .corner_radius(10) + .sense(Sense::click()), + ); + if image_widget.clicked() { + image_widget.scroll_to_me(None); + // ui.scroll_to_rect(image_widget.rect, None); + println!("{}", image.get_hash_str()); + } + } + }); }); }); - egui::Window::new("Images") - .collapsible(false) - .resizable(false) - .default_width(500.0) - .default_height(300.0) - .anchor(Align2::CENTER_BOTTOM, [0.0, 10.0]) - .pivot(Align2::CENTER_BOTTOM) - .show(state.egui_renderer.context(), |ui| { - ui.horizontal(|ui| { - // ui.label( - // egui::RichText::new(format!("{}/{}", current_id, image_count)) - // .size(22.0) - // .strong(), - // ); + egui::SidePanel::right("Filters").show(state.egui_renderer.context(), |ui| { + for (i, mut rating) in state.filters.rating.iter_mut().enumerate().rev() { + ui.checkbox(&mut rating, format!("{} stars", i)); + } - const NUM: i32 = 5; - for i in max((current_id as i32) - NUM, 0) - ..min((current_id as i32) + NUM + 1, image_count as i32) - { - let source = ImageSource::Bytes { - uri: std::borrow::Cow::Owned(i.to_string()), - bytes: egui::load::Bytes::Static(&[]), - }; + ui.text_edit_singleline(&mut state.filters.name); - ui.add( - egui::Image::new(source) - // .sca - // .load_for_size(ctx, available_size) - // .fit_to_fraction(Vec2::new(10.0, 10.0)) - // .max_width(200.0) - .fit_to_original_size(1.0) - .corner_radius(10), - ); - } - // ui.image(source); - }); - }); + for (format, mut value) in state.filters.file_format.iter_mut() { + ui.checkbox(&mut value, format!("{}", format)); + } + }); state.egui_renderer.end_frame_and_draw( &state.device, @@ -845,7 +838,7 @@ impl ApplicationHandler for App { pub struct ImflowEguiLoader { store: Arc>, // stored: Option, - cache: egui::mutex::Mutex>, + cache: egui::mutex::Mutex>, } impl ImflowEguiLoader { @@ -870,13 +863,14 @@ impl ImageLoader for ImflowEguiLoader { ) -> egui::load::ImageLoadResult { let mut cache = self.cache.lock(); - let id = uri.parse::().unwrap(); + // let id = uri.parse::().unwrap(); + let id = uri.to_string(); if let Some(handle) = cache.get(&id) { handle.clone() } else { let imbuf = { let binding = self.store.read().unwrap(); - binding.get_thumbnail_id(id).clone() + binding.get_thumbnail_hash(id.clone()).clone() }; let mut image = ColorImage::new([imbuf.width, imbuf.height], Color32::BLACK); let image_buffer = image.as_raw_mut(); diff --git a/src/egui_tools.rs b/src/egui_tools.rs index 4dda2f3..06dbea0 100644 --- a/src/egui_tools.rs +++ b/src/egui_tools.rs @@ -1,4 +1,4 @@ -use egui::Context; +use egui::{Color32, Context, Visuals}; use egui_wgpu::wgpu::{CommandEncoder, Device, Queue, StoreOp, TextureFormat, TextureView}; use egui_wgpu::{Renderer, ScreenDescriptor, wgpu}; use egui_winit::State; @@ -24,6 +24,14 @@ impl EguiRenderer { window: &Window, ) -> EguiRenderer { let egui_context = Context::default(); + egui_context.options_mut(|o| o.line_scroll_speed = 200.0); + egui_context.set_visuals_of( + egui::Theme::Dark, + Visuals { + panel_fill: Color32::BLACK, + ..Default::default() + }, + ); let egui_state = egui_winit::State::new( egui_context, diff --git a/src/image.rs b/src/image.rs index 2a321c5..1d4813a 100644 --- a/src/image.rs +++ b/src/image.rs @@ -18,6 +18,8 @@ use zune_image::codecs::qoi::zune_core::colorspace::ColorSpace; use zune_image::codecs::qoi::zune_core::options::DecoderOptions; use std::env; +use std::fmt::Display; +// use std::fmt::Write; use std::fs; use std::fs::File; use std::fs::read; @@ -37,6 +39,16 @@ pub enum ImageFormat { Heif, } +impl Display for ImageFormat { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ImageFormat::Jpg => f.write_str("JPG"), + ImageFormat::Jxl => f.write_str("JXL"), + ImageFormat::Heif => f.write_str("HEIF"), + } + } +} + #[derive(Clone)] pub struct ImageData { pub path: PathBuf, @@ -44,6 +56,7 @@ pub struct ImageData { pub embedded_thumbnail: bool, pub orientation: Orientation, pub hash: GenericArray, + pub rating: i32, } impl ImageData { @@ -56,6 +69,10 @@ impl ImageData { let hash_hex = format!("{:x}", self.hash); return cache_dir.join(hash_hex).to_path_buf(); } + + pub fn get_hash_str(&self) -> String { + format!("{:x}", self.hash) + } } impl Hash for ImageData { @@ -253,12 +270,14 @@ pub fn load_available_images(dir: PathBuf) -> Vec { let orientation = Orientation::from_exif(meta.get_orientation() as u8) .unwrap_or(Orientation::NoTransforms); let hash = get_file_hash(&path); + let rating = meta.get_tag_numeric("Xmp.xmp.Rating"); Some(ImageData { path, format, embedded_thumbnail, orientation, hash, + rating, }) } else { None diff --git a/src/store.rs b/src/store.rs index c05f0a5..ec57adf 100644 --- a/src/store.rs +++ b/src/store.rs @@ -1,4 +1,4 @@ -use crate::image::{ImageData, load_thumbnail}; +use crate::image::{ImageData, ImageFormat, load_thumbnail}; use crate::image::{ImflowImageBuffer, load_available_images, load_image}; use crossbeam_channel::{Receiver, Sender, unbounded}; use rayon::prelude::*; @@ -11,6 +11,26 @@ use threadpool::ThreadPool; const PRELOAD_NEXT_IMAGE_N: usize = 16; +pub struct FileFilters { + pub rating: [bool; 6], + pub name: String, + pub file_format: HashMap, +} + +impl Default for FileFilters { + fn default() -> Self { + let mut formats = HashMap::new(); + formats.insert(ImageFormat::Jpg, true); + formats.insert(ImageFormat::Jxl, true); + formats.insert(ImageFormat::Heif, true); + FileFilters { + rating: [true; 6], + name: "".to_string(), + file_format: formats, + } + } +} + pub struct ImageStore { pub current_image_id: usize, pub(crate) loaded_images: HashMap, @@ -176,6 +196,14 @@ impl ImageStore { self.loaded_images_thumbnails.get(path).unwrap() } + pub fn get_thumbnail_hash(&self, hash: String) -> &ImflowImageBuffer { + self.loaded_images_thumbnails + .iter() + .find(|f| f.0.get_hash_str() == hash) + .unwrap() + .1 + } + pub fn get_thumbnail(&mut self) -> &ImflowImageBuffer { if self .loaded_images_thumbnails @@ -196,4 +224,21 @@ impl ImageStore { .get(&self.current_image_path) .unwrap(); } + + pub fn get_filtered_images(&self, filter: &FileFilters) -> Vec { + self.available_images + .iter() + .filter(|f| filter.rating[f.rating.clamp(0, 5) as usize]) + .filter(|f| filter.file_format[&f.format]) + .filter(|f| { + f.path + .file_name() + .unwrap() + .to_str() + .unwrap() + .contains(&filter.name) + }) + .map(|f| f.clone()) + .collect::>() + } }