Added filters, scroll thumbnails

This commit is contained in:
Dawid Pietrykowski 2025-04-12 23:19:02 +02:00
parent b41554c608
commit b00bbbadc0
4 changed files with 122 additions and 56 deletions

View File

@ -1,11 +1,11 @@
use crate::egui_tools::EguiRenderer; use crate::egui_tools::EguiRenderer;
use egui::load::{ImageLoadResult, ImageLoader}; 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::wgpu::SurfaceError;
use egui_wgpu::{ScreenDescriptor, wgpu}; use egui_wgpu::{ScreenDescriptor, wgpu};
use image::metadata::Orientation; use image::metadata::Orientation;
use imflow::image::swap_wh; use imflow::image::{ImageFormat, swap_wh};
use imflow::store::ImageStore; use imflow::store::{FileFilters, ImageStore};
use std::cmp::{max, min}; use std::cmp::{max, min};
use std::collections::HashMap; use std::collections::HashMap;
use std::path::PathBuf; use std::path::PathBuf;
@ -223,6 +223,7 @@ pub struct AppState {
pub render_pipeline: wgpu::RenderPipeline, pub render_pipeline: wgpu::RenderPipeline,
pub transform_buffer: wgpu::Buffer, pub transform_buffer: wgpu::Buffer,
pub transform_data: TransformData, pub transform_data: TransformData,
pub filters: FileFilters,
} }
impl AppState { impl AppState {
@ -315,6 +316,7 @@ impl AppState {
render_pipeline, render_pipeline,
transform_buffer, transform_buffer,
transform_data, transform_data,
filters: FileFilters::default(),
} }
} }
@ -629,18 +631,23 @@ impl App {
render_pass.draw_indexed(0..6, 0, 0..1); render_pass.draw_indexed(0..6, 0, 0..1);
} }
let mut rating_filter = [false; 6];
// let mut file_filters;
let rating; let rating;
let path; let path;
let current_id; let current_id;
let image_count; let image_count;
let filename; let filename;
let window; let window;
let filtered_images;
{ {
let store = state.store.read().unwrap(); let store = state.store.read().unwrap();
rating = store.get_current_rating(); rating = store.get_current_rating();
path = store.current_image_path.clone(); path = store.current_image_path.clone();
current_id = store.current_image_id; current_id = store.current_image_id;
image_count = store.available_images.len(); image_count = store.available_images.len();
filtered_images = store.get_filtered_images(&state.filters);
filename = path.path.file_name().unwrap(); filename = path.path.file_name().unwrap();
window = self.window.as_ref().unwrap(); window = self.window.as_ref().unwrap();
} }
@ -665,59 +672,45 @@ impl App {
); );
}); });
}); });
egui::Window::new("Id")
.collapsible(false) egui::TopBottomPanel::bottom("Thumbnails")
.resizable(false) .exact_height(120.0)
.default_width(5.0) .show(state.egui_renderer.context(), |panel_ui| {
.anchor(Align2::RIGHT_TOP, [-5.0, 5.0]) egui::ScrollArea::horizontal().show(panel_ui, |ui| {
.pivot(Align2::RIGHT_TOP) ui.horizontal_centered(|horizontal| {
.show(state.egui_renderer.context(), |ui| { for image in filtered_images {
ui.vertical_centered(|ui| { let source = ImageSource::Bytes {
ui.label( uri: std::borrow::Cow::Owned(image.get_hash_str()),
egui::RichText::new(format!("{}/{}", current_id, image_count)) bytes: egui::load::Bytes::Static(&[]),
.size(22.0) };
.strong(),
); 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") egui::SidePanel::right("Filters").show(state.egui_renderer.context(), |ui| {
.collapsible(false) for (i, mut rating) in state.filters.rating.iter_mut().enumerate().rev() {
.resizable(false) ui.checkbox(&mut rating, format!("{} stars", i));
.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(),
// );
const NUM: i32 = 5; ui.text_edit_singleline(&mut state.filters.name);
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.add( for (format, mut value) in state.filters.file_format.iter_mut() {
egui::Image::new(source) ui.checkbox(&mut value, format!("{}", format));
// .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);
});
});
state.egui_renderer.end_frame_and_draw( state.egui_renderer.end_frame_and_draw(
&state.device, &state.device,
@ -845,7 +838,7 @@ impl ApplicationHandler for App {
pub struct ImflowEguiLoader { pub struct ImflowEguiLoader {
store: Arc<RwLock<ImageStore>>, store: Arc<RwLock<ImageStore>>,
// stored: Option<ImageLoadResult>, // stored: Option<ImageLoadResult>,
cache: egui::mutex::Mutex<HashMap<usize, ImageLoadResult>>, cache: egui::mutex::Mutex<HashMap<String, ImageLoadResult>>,
} }
impl ImflowEguiLoader { impl ImflowEguiLoader {
@ -870,13 +863,14 @@ impl ImageLoader for ImflowEguiLoader {
) -> egui::load::ImageLoadResult { ) -> egui::load::ImageLoadResult {
let mut cache = self.cache.lock(); let mut cache = self.cache.lock();
let id = uri.parse::<usize>().unwrap(); // let id = uri.parse::<usize>().unwrap();
let id = uri.to_string();
if let Some(handle) = cache.get(&id) { if let Some(handle) = cache.get(&id) {
handle.clone() handle.clone()
} else { } else {
let imbuf = { let imbuf = {
let binding = self.store.read().unwrap(); 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 mut image = ColorImage::new([imbuf.width, imbuf.height], Color32::BLACK);
let image_buffer = image.as_raw_mut(); let image_buffer = image.as_raw_mut();

View File

@ -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::wgpu::{CommandEncoder, Device, Queue, StoreOp, TextureFormat, TextureView};
use egui_wgpu::{Renderer, ScreenDescriptor, wgpu}; use egui_wgpu::{Renderer, ScreenDescriptor, wgpu};
use egui_winit::State; use egui_winit::State;
@ -24,6 +24,14 @@ impl EguiRenderer {
window: &Window, window: &Window,
) -> EguiRenderer { ) -> EguiRenderer {
let egui_context = Context::default(); 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( let egui_state = egui_winit::State::new(
egui_context, egui_context,

View File

@ -18,6 +18,8 @@ use zune_image::codecs::qoi::zune_core::colorspace::ColorSpace;
use zune_image::codecs::qoi::zune_core::options::DecoderOptions; use zune_image::codecs::qoi::zune_core::options::DecoderOptions;
use std::env; use std::env;
use std::fmt::Display;
// use std::fmt::Write;
use std::fs; use std::fs;
use std::fs::File; use std::fs::File;
use std::fs::read; use std::fs::read;
@ -37,6 +39,16 @@ pub enum ImageFormat {
Heif, 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)] #[derive(Clone)]
pub struct ImageData { pub struct ImageData {
pub path: PathBuf, pub path: PathBuf,
@ -44,6 +56,7 @@ pub struct ImageData {
pub embedded_thumbnail: bool, pub embedded_thumbnail: bool,
pub orientation: Orientation, pub orientation: Orientation,
pub hash: GenericArray<u8, U32>, pub hash: GenericArray<u8, U32>,
pub rating: i32,
} }
impl ImageData { impl ImageData {
@ -56,6 +69,10 @@ impl ImageData {
let hash_hex = format!("{:x}", self.hash); let hash_hex = format!("{:x}", self.hash);
return cache_dir.join(hash_hex).to_path_buf(); return cache_dir.join(hash_hex).to_path_buf();
} }
pub fn get_hash_str(&self) -> String {
format!("{:x}", self.hash)
}
} }
impl Hash for ImageData { impl Hash for ImageData {
@ -253,12 +270,14 @@ pub fn load_available_images(dir: PathBuf) -> Vec<ImageData> {
let orientation = Orientation::from_exif(meta.get_orientation() as u8) let orientation = Orientation::from_exif(meta.get_orientation() as u8)
.unwrap_or(Orientation::NoTransforms); .unwrap_or(Orientation::NoTransforms);
let hash = get_file_hash(&path); let hash = get_file_hash(&path);
let rating = meta.get_tag_numeric("Xmp.xmp.Rating");
Some(ImageData { Some(ImageData {
path, path,
format, format,
embedded_thumbnail, embedded_thumbnail,
orientation, orientation,
hash, hash,
rating,
}) })
} else { } else {
None None

View File

@ -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 crate::image::{ImflowImageBuffer, load_available_images, load_image};
use crossbeam_channel::{Receiver, Sender, unbounded}; use crossbeam_channel::{Receiver, Sender, unbounded};
use rayon::prelude::*; use rayon::prelude::*;
@ -11,6 +11,26 @@ use threadpool::ThreadPool;
const PRELOAD_NEXT_IMAGE_N: usize = 16; const PRELOAD_NEXT_IMAGE_N: usize = 16;
pub struct FileFilters {
pub rating: [bool; 6],
pub name: String,
pub file_format: HashMap<ImageFormat, bool>,
}
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 struct ImageStore {
pub current_image_id: usize, pub current_image_id: usize,
pub(crate) loaded_images: HashMap<ImageData, ImflowImageBuffer>, pub(crate) loaded_images: HashMap<ImageData, ImflowImageBuffer>,
@ -176,6 +196,14 @@ impl ImageStore {
self.loaded_images_thumbnails.get(path).unwrap() 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 { pub fn get_thumbnail(&mut self) -> &ImflowImageBuffer {
if self if self
.loaded_images_thumbnails .loaded_images_thumbnails
@ -196,4 +224,21 @@ impl ImageStore {
.get(&self.current_image_path) .get(&self.current_image_path)
.unwrap(); .unwrap();
} }
pub fn get_filtered_images(&self, filter: &FileFilters) -> Vec<ImageData> {
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::<Vec<ImageData>>()
}
} }