From 48ef588ef096b5f4ab061cbe46ec1275ffda3b83 Mon Sep 17 00:00:00 2001 From: Ananas Date: Mon, 29 Dec 2025 02:24:02 +0100 Subject: [PATCH 01/10] refactor,feat: show location, use exif creation date --- app/src/windows/info.rs | 39 +++++++++++++++++++-------------------- core/src/metadata.rs | 4 ++-- 2 files changed, 21 insertions(+), 22 deletions(-) diff --git a/app/src/windows/info.rs b/app/src/windows/info.rs index 29f62e4..48200a0 100644 --- a/app/src/windows/info.rs +++ b/app/src/windows/info.rs @@ -1,10 +1,10 @@ use std::{alloc, fmt::{self, Formatter}, sync::Arc}; use cap::Cap; -use chrono::{DateTime, Local}; +use chrono::{DateTime, Local, NaiveDateTime}; use egui::{AtomExt, Color32, Label, Pos2, RichText, Stroke, TextureHandle, Ui, Vec2, WidgetText}; use eframe::egui::{self, Response}; -use roseate_core::{decoded_image::DecodedImageInfo}; +use roseate_core::{decoded_image::DecodedImageInfo, metadata::ImageMetadata}; use crate::{image::Image, image_handler::{optimization::ImageOptimizations, resource::ImageResource}}; @@ -30,7 +30,7 @@ struct ExpensiveData { } impl ExpensiveData { - pub fn new(image_resource: &ImageResource, image: &Image) -> Self { + pub fn new(image_resource: &ImageResource, image_metadata: &ImageMetadata, image: &Image) -> Self { let path = &image.path; let file_name = path.file_name().unwrap().to_string_lossy().to_string(); @@ -52,22 +52,21 @@ impl ExpensiveData { let mut image_created_time = None; let mut file_modified_time = None; - if let Some(metadata) = file_metadata { - let date_format = "%d/%m/%Y %H:%M %p"; - - // TODO: prioritize using time picture was taken from EXIF tag instead of file created date. - image_created_time = match metadata.created() { - Ok(time) => { - let datetime: DateTime = time.into(); - Some(datetime.format(date_format).to_string()) - }, - Err(error) => { - log::warn!("Failed to retrieve image file created date! Error: {}", error); + let date_format = "%d/%m/%Y %H:%M %p"; - None + if let Some(time) = &image_metadata.originally_created { + log::debug!("originally created: {}", time); + match NaiveDateTime::parse_from_str(time, "%Y-%m-%d %H:%M:%S") { + Ok(datetime) => { + image_created_time = Some(datetime.format(date_format).to_string()); }, - }; + Err(err) => { + log::warn!("Failed to retrieve image file created date! Error: {}", err); + } + } + } + if let Some(metadata) = file_metadata { file_modified_time = match metadata.modified() { Ok(time) => { let datetime: DateTime = time.into(); @@ -127,7 +126,7 @@ impl ImageInfoWindow { .max_col_width(120.0) .striped(false) .show(ui, |ui| { - // I'm using let Some() because in the future + // I'm using let Some() because in the future // I'll actually make use of the struct inside. if let Some(_) = image_optimizations.monitor_downsampling { @@ -301,7 +300,7 @@ impl ImageInfoWindow { show_extra: bool ) -> Response { let image_info_data = self.data.get_or_insert_with( - || ExpensiveData::new(image_resource, image) + || ExpensiveData::new(image_resource, &decoded_info_image.metadata, image) ); let main_frame = egui::Frame::group(&ui.style()) @@ -364,7 +363,7 @@ impl ImageInfoWindow { ui.add( egui::Image::from_texture(texture) - // 16 is the padding from + // 16 is the padding from // the image optimizations grid .max_size([200.0 + 16.0, 140.0].into()) .corner_radius(8) @@ -418,4 +417,4 @@ impl ImageInfoWindow { fn ui_non_select_label(ui: &mut Ui, text: impl Into) -> Response { ui.add(Label::new(text).selectable(false)) -} \ No newline at end of file +} diff --git a/core/src/metadata.rs b/core/src/metadata.rs index a556819..5d5d905 100644 --- a/core/src/metadata.rs +++ b/core/src/metadata.rs @@ -17,7 +17,7 @@ impl ImageMetadata { pub fn new(exif_chunk: Vec) -> Result { debug!("Reading and parsing exif chunk..."); - // TODO: use the same buf reader we use for the decoder with + // TODO: use the same buf reader we use for the decoder with // 'exif_reader.read_from_container' to save performance and reduce memory duplication. let exif_reader = Reader::new(); let exif = exif_reader.read_raw(exif_chunk) @@ -67,4 +67,4 @@ impl ImageMetadata { } ) } -} \ No newline at end of file +} From 628a33b896df98053f1bf22eb1414d224e6f89ab Mon Sep 17 00:00:00 2001 From: Ananas Date: Mon, 29 Dec 2025 02:30:55 +0100 Subject: [PATCH 02/10] fix: part 2? --- Cargo.lock | 234 +++++++++++++++++++++++++++++++++++++++- app/Cargo.toml | 13 ++- app/src/windows/info.rs | 55 +++++++++- core/src/metadata.rs | 20 +++- 4 files changed, 309 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0c99a06..39e8c66 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -599,6 +599,12 @@ dependencies = [ "arrayvec", ] +[[package]] +name = "az" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b7e4c2464d97fe331d41de9d5db0def0a96f4d823b8b32a2efd503578988973" + [[package]] name = "base64" version = "0.22.1" @@ -1076,6 +1082,17 @@ dependencies = [ "memchr", ] +[[package]] +name = "country-emoji" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9033f056ab6806a9406f405e7babb0c7c31e753495b81c64b8c3e9390cb16677" +dependencies = [ + "once_cell", + "regex", + "unidecode", +] + [[package]] name = "crc32fast" version = "1.5.0" @@ -1116,6 +1133,27 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" +[[package]] +name = "csv" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938" +dependencies = [ + "csv-core", + "itoa", + "ryu", + "serde_core", +] + +[[package]] +name = "csv-core" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782" +dependencies = [ + "memchr", +] + [[package]] name = "cursor-icon" version = "1.2.0" @@ -1201,6 +1239,12 @@ dependencies = [ "syn", ] +[[package]] +name = "divrem" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69dde51e8fef5e12c1d65e0929b03d66e4c0c18282bc30ed2ca050ad6f44dd82" + [[package]] name = "dlib" version = "0.5.2" @@ -1416,6 +1460,12 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "elapsed" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f4e5af126dafd0741c2ad62d47f68b28602550102e5f0dd45c8a97fc8b49c29" + [[package]] name = "emath" version = "0.33.3" @@ -1647,6 +1697,19 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" +[[package]] +name = "fixed" +version = "1.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707070ccf8c4173548210893a0186e29c266901b71ed20cd9e2ca0193dfe95c3" +dependencies = [ + "az", + "bytemuck", + "half", + "num-traits", + "typenum", +] + [[package]] name = "flate2" version = "1.1.5" @@ -1788,6 +1851,21 @@ dependencies = [ "slab", ] +[[package]] +name = "generator" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f04ae4152da20c76fe800fa48659201d5cf627c5149ca0b707b69d7eef6cf9" +dependencies = [ + "cc", + "cfg-if", + "libc", + "log", + "rustversion", + "windows-link 0.2.1", + "windows-result 0.4.1", +] + [[package]] name = "gethostname" version = "1.1.0" @@ -2222,6 +2300,12 @@ dependencies = [ "hashbrown 0.16.1", ] +[[package]] +name = "init_with" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0175f63815ce00183bf755155ad0cb48c65226c5d17a724e369c25418d2b7699" + [[package]] name = "interpolate_name" version = "0.2.4" @@ -2239,6 +2323,15 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.14.0" @@ -2480,6 +2573,29 @@ version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" +[[package]] +name = "kiddo" +version = "4.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60c5fcd3044b774e2c80a502b2387b75d1baa95e99b2bceeb5db00f2e2d27fe9" +dependencies = [ + "az", + "divrem", + "doc-comment", + "elapsed", + "fixed", + "generator", + "init_with", + "itertools 0.13.0", + "log", + "num-traits", + "ordered-float 4.6.0", + "sorted-vec", + "tracing", + "tracing-subscriber", + "ubyte", +] + [[package]] name = "kurbo" version = "0.11.3" @@ -2491,6 +2607,12 @@ dependencies = [ "smallvec", ] +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "lebe" version = "0.5.3" @@ -2786,6 +2908,15 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "num-bigint" version = "0.4.6" @@ -3167,6 +3298,15 @@ dependencies = [ "libredox", ] +[[package]] +name = "ordered-float" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" +dependencies = [ + "num-traits", +] + [[package]] name = "ordered-float" version = "5.1.0" @@ -3578,7 +3718,7 @@ dependencies = [ "built", "cfg-if", "interpolate_name", - "itertools", + "itertools 0.14.0", "libc", "libfuzzer-sys", "log", @@ -3645,7 +3785,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bca57bbe860f2bf690c985bca4f93d9a93450eb8500270f5ddc09f899fd5debc" dependencies = [ "half", - "itertools", + "itertools 0.14.0", "num-traits", ] @@ -3736,6 +3876,18 @@ dependencies = [ "usvg", ] +[[package]] +name = "reverse_geocoder" +version = "4.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c987d5006fe57c099370a219602d52da978376fa7bf3324e036ad647beafda2" +dependencies = [ + "csv", + "kiddo", + "serde", + "serde_derive", +] + [[package]] name = "rfd" version = "0.15.4" @@ -3795,6 +3947,7 @@ dependencies = [ "cirrus_softbinds", "cirrus_theming", "clap", + "country-emoji", "derive_more", "dirs", "eframe", @@ -3806,6 +3959,7 @@ dependencies = [ "fs2", "log", "re_format", + "reverse_geocoder", "rfd", "roseate-core", "serde", @@ -4033,6 +4187,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "shlex" version = "1.3.0" @@ -4183,6 +4346,12 @@ dependencies = [ "serde", ] +[[package]] +name = "sorted-vec" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f58d7b0190c7f12df7e8be6b79767a0836059159811b869d5ab55721fe14d0" + [[package]] name = "spirv" version = "0.3.0+sdk-1.3.268.0" @@ -4342,6 +4511,15 @@ dependencies = [ "syn", ] +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + [[package]] name = "tiff" version = "0.10.3" @@ -4474,6 +4652,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", ] [[package]] @@ -4491,6 +4695,18 @@ dependencies = [ "rustc-hash 2.1.1", ] +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "ubyte" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f720def6ce1ee2fc44d40ac9ed6d3a59c361c80a75a7aa8e75bb9baed31cf2ea" + [[package]] name = "uds_windows" version = "1.1.0" @@ -4538,6 +4754,12 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "unidecode" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402bb19d8e03f1d1a7450e2bd613980869438e0666331be3e073089124aa1adc" + [[package]] name = "untrusted" version = "0.9.0" @@ -4634,6 +4856,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + [[package]] name = "version_check" version = "0.9.5" @@ -5038,7 +5266,7 @@ dependencies = [ "ndk-sys", "objc", "once_cell", - "ordered-float", + "ordered-float 5.1.0", "parking_lot", "portable-atomic", "portable-atomic-util", diff --git a/app/Cargo.toml b/app/Cargo.toml index 2ad2821..7526391 100644 --- a/app/Cargo.toml +++ b/app/Cargo.toml @@ -40,12 +40,15 @@ serde = {version = "1.0", features = ["derive"]} chrono = "0.4.42" derive_more = { version = "2.1.0", features = ["from", "display", "debug"] } +reverse_geocoder = "4.1.1" +country-emoji = "0.3.2" + # I've now disabled compiling release builds of dependencies to speed up dev compile time. -# -# THIS MEANS IMAGE PROCESSING WILL BE MUCH MUCH +# +# THIS MEANS IMAGE PROCESSING WILL BE MUCH MUCH # SLOWER ON DEV BUILDS COMPARED TO RELEASE BUILDS -# +# # So with that said ALWAYS compile release unless you are developing Roseate. -# +# #[profile.dev.package."*"] -#opt-level = 3 \ No newline at end of file +#opt-level = 3 diff --git a/app/src/windows/info.rs b/app/src/windows/info.rs index 48200a0..ba8666a 100644 --- a/app/src/windows/info.rs +++ b/app/src/windows/info.rs @@ -2,7 +2,7 @@ use std::{alloc, fmt::{self, Formatter}, sync::Arc}; use cap::Cap; use chrono::{DateTime, Local, NaiveDateTime}; -use egui::{AtomExt, Color32, Label, Pos2, RichText, Stroke, TextureHandle, Ui, Vec2, WidgetText}; +use egui::{AtomExt, Color32, Label, OpenUrl, Pos2, RichText, Stroke, TextureHandle, Ui, Vec2, WidgetText}; use eframe::egui::{self, Response}; use roseate_core::{decoded_image::DecodedImageInfo, metadata::ImageMetadata}; @@ -20,6 +20,24 @@ macro_rules! rich_text_or_unknown { }; } +macro_rules! dms_to_decimal { + ($dms_str:expr) => {{ + let numbers: Vec = $dms_str + .split(|c: char| !c.is_ascii_digit() && c != '.' && c != '-') + .filter(|s| !s.is_empty()) + .filter_map(|s| s.parse::().ok()) + .collect(); + + match numbers.len() { + 3 => numbers[0] + numbers[1] / 60.0 + numbers[2] / 3600.0, + 2 => numbers[0] + numbers[1] / 60.0, + 1 => numbers[0], + _ => 0.0, + } + }}; +} + + struct ExpensiveData { pub file_name: String, pub file_size: Option, @@ -27,6 +45,8 @@ struct ExpensiveData { pub image_created_time: Option, pub file_modified_time: Option, pub memory_allocated_for_image: f64, + + pub location: Option<(String, String)> } impl ExpensiveData { @@ -51,11 +71,11 @@ impl ExpensiveData { let mut file_size = None; let mut image_created_time = None; let mut file_modified_time = None; + let mut location = None; let date_format = "%d/%m/%Y %H:%M %p"; if let Some(time) = &image_metadata.originally_created { - log::debug!("originally created: {}", time); match NaiveDateTime::parse_from_str(time, "%Y-%m-%d %H:%M:%S") { Ok(datetime) => { image_created_time = Some(datetime.format(date_format).to_string()); @@ -66,6 +86,25 @@ impl ExpensiveData { } } + if let Some(latitude) = &image_metadata.location.latitude + && let Some(longitude) = &image_metadata.location.longitude { + let geocoder = reverse_geocoder::ReverseGeocoder::new(); + + let latitude = dms_to_decimal!(latitude); + let longitude = dms_to_decimal!(longitude); + log::debug!("converted dms to decimal: {}, {}", latitude, longitude); + + let result = geocoder.search((latitude, longitude)); + + let country_name = country_emoji::name(&result.record.cc).unwrap(); // this should always exist ~ ananas + + let formatted_location = format!("{}, {}", result.record.name, country_name); + // TODO: add possiblity to change the default map to google maps or custom one by formatting. + let url = format!("https://www.openstreetmap.org?mlat={}&mlon={}#map=18/{}/{}", latitude, longitude, latitude, longitude); + + location = Some((formatted_location, url)); + } + if let Some(metadata) = file_metadata { file_modified_time = match metadata.modified() { Ok(time) => { @@ -99,7 +138,8 @@ impl ExpensiveData { size as f64 }, - } + }, + location } } } @@ -251,6 +291,15 @@ impl ImageInfoWindow { ui_non_select_label(ui, "Exposure Time:"); ui.label(rich_text_or_unknown!("{}s", &decoded_image_info.metadata.exposure_time)); ui.end_row(); + + if let Some(location) = &expensive_data.location { + ui_non_select_label(ui, "Location:"); + if ui.button(&location.0).clicked() { + ui.ctx().open_url( + OpenUrl::new_tab(&location.1) + ); + } + } } }); } diff --git a/core/src/metadata.rs b/core/src/metadata.rs index 5d5d905..e10a363 100644 --- a/core/src/metadata.rs +++ b/core/src/metadata.rs @@ -3,6 +3,13 @@ use log::debug; use crate::error::{Error, Result}; +#[derive(Default, Clone)] +pub struct Location { + pub longitude: Option, + pub latitude: Option, + pub altitude: Option +} + #[derive(Default, Clone)] pub struct ImageMetadata { pub model: Option, @@ -11,6 +18,8 @@ pub struct ImageMetadata { pub focal_length: Option, pub exposure_time: Option, pub originally_created: Option, + + pub location: Location, } impl ImageMetadata { @@ -55,6 +64,12 @@ impl ImageMetadata { None => None, }; + let location = Location { + longitude: exif.get_field(Tag::GPSLongitude, In::PRIMARY).and_then(to_option_fn), + latitude: exif.get_field(Tag::GPSLatitude, In::PRIMARY).and_then(to_option_fn), + altitude: exif.get_field(Tag::GPSAltitude, In::PRIMARY).and_then(to_option_fn) + }; + Ok( Self { model: model_and_maker, @@ -62,8 +77,9 @@ impl ImageMetadata { aperture: exif.get_field(Tag::ApertureValue, In::PRIMARY).and_then(to_option_fn), focal_length: exif.get_field(Tag::FocalLength, In::PRIMARY).and_then(to_option_fn), exposure_time: exposure_time, - // TODO: use this for date modifier field - originally_created: exif.get_field(Tag::DateTimeOriginal, In::PRIMARY).and_then(to_option_fn) + originally_created: exif.get_field(Tag::DateTimeOriginal, In::PRIMARY).and_then(to_option_fn), + + location: location, } ) } From 0d01c32953704a278792c27cd8430208991baecb Mon Sep 17 00:00:00 2001 From: Goldy <66202304+THEGOLDENPRO@users.noreply.github.com> Date: Mon, 29 Dec 2025 02:02:41 +0000 Subject: [PATCH 03/10] fix: unused deps --- core/src/backends/image_rs/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/backends/image_rs/mod.rs b/core/src/backends/image_rs/mod.rs index caa5a0a..3574e51 100644 --- a/core/src/backends/image_rs/mod.rs +++ b/core/src/backends/image_rs/mod.rs @@ -3,7 +3,7 @@ use std::{collections::HashSet, io::BufReader}; use image::{AnimationDecoder, ImageDecoder, ImageError, codecs::{gif::GifDecoder, jpeg::JpegDecoder, png::PngDecoder, webp::WebPDecoder}}; use log::debug; -use crate::{backends::{backend::DecodeBackend, image_rs::buffer_image::{BufferImage, BufferImageVariant}}, colour_type::{self, ImageColourType}, decoded_image::{DecodedImage, DecodedImageContent, ImageSize, Pixels}, error::{Error, Result}, format::ImageFormat, image_info::metadata::ImageMetadata, modifications::{ImageModification, ImageModifications}, reader::{ImageReader, ImageReaderData, ReadSeek}}; +use crate::{backends::{backend::DecodeBackend, image_rs::buffer_image::{BufferImage, BufferImageVariant}}, colour_type::{ImageColourType}, decoded_image::{DecodedImage, DecodedImageContent, ImageSize, Pixels}, error::{Error, Result}, format::ImageFormat, image_info::metadata::ImageMetadata, modifications::{ImageModification, ImageModifications}, reader::{ImageReader, ImageReaderData, ReadSeek}}; mod colour; mod buffer_image; From 744e2ff527ae6f714fdcc05c8f1bf5c583a10980 Mon Sep 17 00:00:00 2001 From: Ananas Date: Tue, 30 Dec 2025 15:46:33 +0100 Subject: [PATCH 04/10] feat: expensive data handled in a different thread, added another gps format --- app/src/windows/info.rs | 311 ++++++++++++++++++++++++++-------------- 1 file changed, 206 insertions(+), 105 deletions(-) diff --git a/app/src/windows/info.rs b/app/src/windows/info.rs index 0a97bf7..cfd0d52 100644 --- a/app/src/windows/info.rs +++ b/app/src/windows/info.rs @@ -1,4 +1,4 @@ -use std::{alloc, sync::Arc}; +use std::{alloc, sync::{Arc, Mutex}}; use cap::Cap; use chrono::{DateTime, Local, NaiveDateTime}; @@ -18,16 +18,49 @@ macro_rules! rich_text_or_unknown { None => RichText::new("Unknown").weak(), } }; + + ($opt:expr) => { + match &$opt { + Some(string) => RichText::new(string), + None => RichText::new("Unknown").weak(), + } + }; +} + +macro_rules! rich_text_or_init { + ($data:expr, $arg:literal) => { + match &$data { + Some(data) => rich_text_or_unknown!(data.get($arg)), + None => RichText::new("Initializing...").weak(), + } + }; } macro_rules! dms_to_decimal { ($dms_str:expr) => {{ - let numbers: Vec = $dms_str - .split(|c: char| !c.is_ascii_digit() && c != '.' && c != '-') + let parts: Vec<&str> = $dms_str + .split(|c: char| !c.is_ascii_digit() && c != '.' && c != '/') .filter(|s| !s.is_empty()) - .filter_map(|s| s.parse::().ok()) .collect(); + let mut numbers = Vec::new(); + + for part in parts { + if part.contains("/") { + let (first, second) = part.split_once("/").unwrap(); + if let (Ok(first), Ok(second)) = ( + first.parse::(), + second.parse::() + ) { + numbers.push(first / second); + } else { + log::warn!("GPS Data format is unknown: {}", $dms_str); + } + } else if let Ok(num) = part.parse::() { + numbers.push(num); + } + } + match numbers.len() { 3 => numbers[0] + numbers[1] / 60.0 + numbers[2] / 3600.0, 2 => numbers[0] + numbers[1] / 60.0, @@ -37,7 +70,7 @@ macro_rules! dms_to_decimal { }}; } - +#[derive(Debug, Clone)] struct ExpensiveData { pub file_name: String, pub file_size: Option, @@ -50,108 +83,150 @@ struct ExpensiveData { } impl ExpensiveData { - pub fn new(image_resource: &ImageResource, image_metadata: &ImageMetadata, image: &Image) -> Self { - let path = &image.path; + pub fn new(image_resource: &ImageResource, image_metadata: &ImageMetadata, image: &Image) -> Arc> { + let date_format = "%d/%m/%Y %H:%M %p"; - let file_name = path.file_name().unwrap().to_string_lossy().to_string(); - let file_relative_path = path.to_string_lossy().to_string(); + let path = image.path.clone(); + let image_metadata_clone = image_metadata.clone(); - let file_metadata = match path.metadata() { - Ok(metadata) => Some(metadata), + let mut image_created_time = if let Some(time) = &image_metadata_clone.originally_created { + match NaiveDateTime::parse_from_str(time, "%Y-%m-%d %H:%M:%S") { + Ok(datetime) => { + Some(datetime.format(date_format).to_string()) + }, + Err(err) => { + log::warn!("Failed to parse image created date! Error: {}", err); + + None + } + } + } else { + None + }; + + let (file_size, file_modified_time) = match path.metadata() { + Ok(metadata) => { + if image_created_time.is_none() { + image_created_time = match metadata.created() { + Ok(time) => { + let datetime: DateTime = time.into(); + Some(datetime.format(date_format).to_string()) + }, + Err(error) => { + log::warn!("Failed to retrieve image file creation date! Error: {}", error); + + None + }, + }; + } + + let file_modified_time = match metadata.modified() { + Ok(time) => { + let datetime: DateTime = time.into(); + Some(datetime.format(date_format).to_string()) + }, + Err(error) => { + log::warn!("Failed to retrieve image file modified date! Error: {}", error); + + None + }, + }; + + (Some(metadata.len() as f64), file_modified_time) + }, Err(error) => { log::error!( "Failed to retrive image file metadata from file system! Error: {}", error ); - None + (None, None) }, }; - let mut file_size = None; - let mut image_created_time = None; - let mut file_modified_time = None; - let mut location = None; - let date_format = "%d/%m/%Y %H:%M %p"; + let initial_data = Self { + file_name: path.file_name() + .unwrap_or_default() + .to_string_lossy() + .to_string(), + file_size, + file_relative_path: path.to_string_lossy().to_string(), + image_created_time, + file_modified_time, + memory_allocated_for_image: 0.0, + location: None, + }; - if let Some(time) = &image_metadata.originally_created { - match NaiveDateTime::parse_from_str(time, "%Y-%m-%d %H:%M:%S") { - Ok(datetime) => { - image_created_time = Some(datetime.format(date_format).to_string()); - }, - Err(err) => { - log::warn!("Failed to retrieve image file created date! Error: {}", err); - } - } - } + let mutex_data = Arc::new(Mutex::new(initial_data)); + let mutex_data_clone = mutex_data.clone(); - if let Some(latitude) = &image_metadata.location.latitude - && let Some(longitude) = &image_metadata.location.longitude { - let geocoder = reverse_geocoder::ReverseGeocoder::new(); + let resource = image_resource.clone(); - let latitude = dms_to_decimal!(latitude); - let longitude = dms_to_decimal!(longitude); - log::debug!("converted dms to decimal: {}, {}", latitude, longitude); + std::thread::spawn(move || { + let mut locked_data = mutex_data_clone.lock().unwrap(); - let result = geocoder.search((latitude, longitude)); + if let Some(latitude) = &image_metadata_clone.location.latitude + && let Some(longitude) = &image_metadata_clone.location.longitude { + log::debug!("original coords: {}, {}", latitude, longitude); + let geocoder = reverse_geocoder::ReverseGeocoder::new(); - let country_name = country_emoji::name(&result.record.cc).unwrap(); // this should always exist ~ ananas + let latitude = dms_to_decimal!(latitude); + let longitude = dms_to_decimal!(longitude); + log::debug!("converted coords to decimal: {}, {}", latitude, longitude); - let formatted_location = format!("{}, {}", result.record.name, country_name); - // TODO: add possiblity to change the default map to google maps or custom one by formatting. - let url = format!("https://www.openstreetmap.org?mlat={}&mlon={}#map=18/{}/{}", latitude, longitude, latitude, longitude); + let result = geocoder.search((latitude, longitude)); - location = Some((formatted_location, url)); - } + if let Some(country_name) = country_emoji::name(&result.record.cc) { + let formatted_location = format!("{}, {}", result.record.name, country_name); + let url = format!("https://www.openstreetmap.org?mlat={}&mlon={}#map=18/{}/{}", + latitude, longitude, latitude, longitude); + locked_data.location = Some((formatted_location, url)); + } - if let Some(metadata) = file_metadata { - file_modified_time = match metadata.modified() { - Ok(time) => { - let datetime: DateTime = time.into(); - Some(datetime.format(date_format).to_string()) - }, - Err(error) => { - log::warn!("Failed to retrieve image file modified date! Error: {}", error); + locked_data.memory_allocated_for_image = match resource { + ImageResource::Texture(texture_handle) => texture_handle.byte_size() as f64, + ImageResource::AnimatedTexture(frames) => { + let mut size = 0; - None - }, - }; + for (texture_handler, _) in frames { + size += texture_handler.byte_size(); + } - file_size = Some(metadata.len() as f64); - } + size as f64 + }, + }; + } + }); - Self { - file_name, - file_size, - file_relative_path, - image_created_time, - file_modified_time, - memory_allocated_for_image: match image_resource { - ImageResource::Texture(texture_handle) => texture_handle.byte_size() as f64, - ImageResource::AnimatedTexture(frames) => { - let mut size = 0; + std::thread::sleep(std::time::Duration::from_millis(10)); // Thread is spawning too slow for the locking to work. ~ ananas - for (texture_handler, _) in frames { - size += texture_handler.byte_size(); - } + mutex_data + } - size as f64 - }, - }, - location + pub fn get(&self, index: &str) -> Option { + match index { + "file_name" => Some(self.file_name.clone()), + "file_relative_path" => Some(self.file_relative_path.clone()), + "image_created_time" => self.image_created_time.clone(), + "file_modified_time" => self.file_modified_time.clone(), + _ => None, } } } pub struct ImageInfoWindow { data: Option, + + processing_expensive_data: Option>> } impl ImageInfoWindow { pub fn new() -> Self { Self { - data: None + data: None, + + processing_expensive_data: None } } @@ -204,7 +279,7 @@ impl ImageInfoWindow { fn show_image_info_grid( ui: &mut Ui, - expensive_data: &ExpensiveData, + expensive_data: &Option, image: &Image, image_info: &ImageInfo, max_grid_width: f32, @@ -216,8 +291,8 @@ impl ImageInfoWindow { .max_col_width(max_grid_width) .show(ui, |ui| { ui_non_select_label(ui, "Name:"); - ui.label(&expensive_data.file_name) - .on_hover_text(&expensive_data.file_relative_path); + ui.label(rich_text_or_init!(&expensive_data, "file_name")) + .on_hover_text(rich_text_or_init!(&expensive_data, "file_relative_path")); ui.end_row(); ui_non_select_label(ui, "Dimensions:"); @@ -243,30 +318,25 @@ impl ImageInfoWindow { date is NOT accurate!)"; ui_non_select_label(ui, "Created:").on_hover_text(created_hint); - ui.label( - match &expensive_data.image_created_time { - Some(time_string) => RichText::new(time_string), - None => RichText::new("Unknown").weak(), - } - ).on_hover_text(created_hint); + ui.label(rich_text_or_init!(&expensive_data, "image_created_time")).on_hover_text(created_hint); ui.end_row(); if show_extra { ui_non_select_label(ui, "File Modified:"); - ui.label( - match &expensive_data.file_modified_time { - Some(time_string) => RichText::new(time_string), - None => RichText::new("Unknown").weak(), - } - ); + ui.label(rich_text_or_init!(&expensive_data, "file_modified_time")); ui.end_row(); } ui_non_select_label(ui, "File size:"); ui.label( - match expensive_data.file_size { - Some(size) => RichText::new(re_format::format_bytes(size)), - None => RichText::new("Unknown").weak(), + match &expensive_data { + Some(data) => { + match data.file_size { + Some(size) => RichText::new(re_format::format_bytes(size)), + None => RichText::new("Unknown").weak() + } + }, + None => RichText::new("Initializing...").weak(), } ); ui.end_row(); @@ -292,12 +362,24 @@ impl ImageInfoWindow { ui.label(rich_text_or_unknown!("{}s", &image_info.metadata.exposure_time)); ui.end_row(); - if let Some(location) = &expensive_data.location { - ui_non_select_label(ui, "Location:"); - if ui.button(&location.0).clicked() { - ui.ctx().open_url( - OpenUrl::new_tab(&location.1) - ); + ui_non_select_label(ui, "Location:"); + match &expensive_data { + Some(data) => { + match &data.location { + Some(location) => { + if ui.button(&location.0).clicked() { + ui.ctx().open_url( + OpenUrl::new_tab(&location.1) + ); + } + }, + None => { + ui.label(RichText::new("Unknown").weak()); + } + } + }, + None => { + ui.label(RichText::new("Initializing...").weak()); } } } @@ -306,7 +388,7 @@ impl ImageInfoWindow { fn show_misc_info_grid( ui: &mut Ui, - expensive_data: &ExpensiveData, + expensive_data: &Option, image: &Image, image_info: &ImageInfo, max_grid_width: f32, @@ -331,9 +413,16 @@ impl ImageInfoWindow { ui_non_select_label(ui, "Image Mem Alloc:") .on_hover_text(mem_allocation_by_image_hint); ui.label( - RichText::new(re_format::format_bytes( - expensive_data.memory_allocated_for_image) - ) + match expensive_data { + Some(data) => { + RichText::new(re_format::format_bytes( + data.memory_allocated_for_image) + ) + }, + None => { + RichText::new("Initializing...") + } + } ).on_hover_text(mem_allocation_by_image_hint); ui.end_row(); }); @@ -348,9 +437,21 @@ impl ImageInfoWindow { image_info: &ImageInfo, show_extra: bool ) -> Response { - let image_info_data = self.data.get_or_insert_with( - || ExpensiveData::new(image_resource, &image_info.metadata, image) - ); + if self.data.is_none() { + self.processing_expensive_data.get_or_insert_with( + || ExpensiveData::new(image_resource, &image_info.metadata, image) + ); + } + + match self.processing_expensive_data.clone() { + Some(mutex) => { + if let Ok(data) = mutex.try_lock() { + self.data = Some(data.clone()); + self.processing_expensive_data = None; + } + }, + None => {} + }; let main_frame = egui::Frame::group(&ui.style()) .inner_margin(8.0); @@ -432,7 +533,7 @@ impl ImageInfoWindow { ui.vertical(|ui| { Self::show_image_info_grid( ui, - image_info_data, + &self.data, image, image_info, 180.0, @@ -444,7 +545,7 @@ impl ImageInfoWindow { Self::show_misc_info_grid( ui, - image_info_data, + &self.data, image, image_info, 180.0, @@ -456,7 +557,7 @@ impl ImageInfoWindow { false => { ui.vertical(|ui| { Self::show_image_info_grid( - ui, image_info_data, image, image_info, 160.0, soon_text, show_extra + ui, &self.data, image, image_info, 160.0, soon_text, show_extra ); }); }, From 7230d27580eb02acdd3fcb9e59f2cc27d0d6d993 Mon Sep 17 00:00:00 2001 From: Ananas Date: Wed, 31 Dec 2025 03:27:14 +0100 Subject: [PATCH 05/10] feat: disable location in config --- app/assets/config.template.toml | 91 +++++++++++++++++---------------- app/src/app.rs | 4 +- app/src/config/models/ui.rs | 20 +++++++- app/src/windows/info.rs | 78 +++++++++++++++------------- app/src/windows/mod.rs | 4 +- 5 files changed, 113 insertions(+), 84 deletions(-) diff --git a/app/assets/config.template.toml b/app/assets/config.template.toml index 7b9b5aa..12d3201 100644 --- a/app/assets/config.template.toml +++ b/app/assets/config.template.toml @@ -3,22 +3,22 @@ version = 1 [image] [image.loading] -# Setting this to "true" will make the image load on a separate thread to the GUI initially. This -# means the GUI may load up before the image is ready to display itself. This will have an impact +# Setting this to "true" will make the image load on a separate thread to the GUI initially. This +# means the GUI may load up before the image is ready to display itself. This will have an impact # on peak memory hence it's "false" by default. initial.lazy_loading = false # Same as the above but this is now for when you select and load an image from the GUI. # For example: Picking an image from the file picker or dropping an image into the window. -# Not setting this to "true" will avoid spawning a separate thread for image loading but will +# Not setting this to "true" will avoid spawning a separate thread for image loading but will # cause the GUI to freeze up (or appear as if it's not responding) until the image has been loaded. gui.lazy_loading = true [image.backend] -# Setting this to "zune-image" will tell roseate-core to decode images with the zune-image backend, -# the implementation of this backend in Roseate is experimental and WIP, but it can be faster than image-rs +# Setting this to "zune-image" will tell roseate-core to decode images with the zune-image backend, +# the implementation of this backend in Roseate is experimental and WIP, but it can be faster than image-rs # while also having lower memory usage. -# +# # However we default to "image-rs" due to it's wider support and stability in Roseate. decoder = "image-rs" @@ -28,56 +28,56 @@ decoder = "image-rs" mode = "default" # Downsamples the image roughly to the resolution of your monitor. -# -# Images don't always have to be displayed at their full native resolution, especially when -# the image is significantly bigger than your monitor can even display, so to save GPU memory -# we downsample the image. Downsampling decreases the amount of GPU memory eaten up by the image +# +# Images don't always have to be displayed at their full native resolution, especially when +# the image is significantly bigger than your monitor can even display, so to save GPU memory +# we downsample the image. Downsampling decreases the amount of GPU memory eaten up by the image # at the cost of CPU time wasted actually resizing the image (depending on the decoder). -# The bigger the image, the more time it will take to downsample but we think the memory savings -# from doing so are more valuable for most users. Also some decoders have the capability -# of decoding a specific size of an image (thumbnail) from the get go, which means CPU time is -# actually never wasted but rather significantly reduced (making image loading even faster with +# The bigger the image, the more time it will take to downsample but we think the memory savings +# from doing so are more valuable for most users. Also some decoders have the capability +# of decoding a specific size of an image (thumbnail) from the get go, which means CPU time is +# actually never wasted but rather significantly reduced (making image loading even faster with # monitor downsampling enabled). -# -# If you do not wish for such memory savings (like those with a beefy GPU) and +# +# If you do not wish for such memory savings (like those with a beefy GPU) and # you prefer overall faster image load times disable this optimization. -# -# If you want your image quality back when zooming into your image, +# +# If you want your image quality back when zooming into your image, # you might want to also enable the "dynamic_sampling" image optimization too. monitor_downsampling = {enabled = true, strength = 1.4} -# Enabling this allows Roseate to upload RGBA decoded images directly -# to the GPU without duplicating it's memory (zero-copy). If the image is -# RGBA, peak heap memory (on the CPU side) will be almost HALVED, it's very +# Enabling this allows Roseate to upload RGBA decoded images directly +# to the GPU without duplicating it's memory (zero-copy). If the image is +# RGBA, peak heap memory (on the CPU side) will be almost HALVED, it's very # memory efficient. -# -# If your image is not in the RGBA colour format, zero-copy will NOT be possible -# (memory will peak for a very short moment), however you will still benefit as +# +# If your image is not in the RGBA colour format, zero-copy will NOT be possible +# (memory will peak for a very short moment), however you will still benefit as # memory for the decoded image will still be immediately freed AFTER GPU upload. -# +# # This optimization is disabled if you enable "dynamic_sampling". experimental_consume_pixels_during_gpu_upload = true -# Enabling this will enable the extremely experimental -# dynamic sampling feature that upsamples your image when you zoom in +# Enabling this will enable the extremely experimental +# dynamic sampling feature that upsamples your image when you zoom in # to bring back the detail lost from monitor downsampling when necessary. -# +# # Disabled if "monitor_downsampling" is not enabled. -# -# Again, this is VERY experimental, incomplete and untested! +# +# Again, this is VERY experimental, incomplete and untested! # Expect a very broken implementation and bugs. experimental_dynamic_sampling = {enabled = false, also_downsample = true} -# If you enable this Roseate will use it's fast +# If you enable this Roseate will use it's fast # multi-threaded function when downsampling images. -# -# If you want to limit or increase the amount of threads used for parallelisation, -# you may with the "threads" key. Otherwise please leave the key undefined, Roseate -# will consult with your operating system on how many threads it may use and it will +# +# If you want to limit or increase the amount of threads used for parallelisation, +# you may with the "threads" key. Otherwise please leave the key undefined, Roseate +# will consult with your operating system on how many threads it may use and it will # always use two threads less than it's allowed. -# +# # experimental_multi_threaded_sampling = {enabled = false, threads = 8} -# +# # This is VERY experimental and can break. experimental_multi_threaded_sampling = false @@ -95,22 +95,27 @@ padding = 2 zoom_into_cursor = true # If set to "true", the viewport will automatically fit the image to the window. fit_to_window = true -# Set to "false" if you want the image to instantly snap to the window size otherwise +# Set to "false" if you want the image to instantly snap to the window size otherwise # the viewport will perform a fancy animation while fitting the image to the window. animate_fit_to_window = true # If set to "true", the viewport will animate pan and zoom when they are reset. animate_reset = true [ui.selection_menu] -# If set to "true", the image selection menu will display an +# If set to "true", the image selection menu will display an # "Open Image" button otherwise to open an image you can click on the rose. show_open_image_button = true +[ui.info_panel] +# If set to "true", it will show you the location the image was taken. +# otherwise it'll show disabled where the location would be. +show_location = true + [key_binds] show_image_info = "I" show_extra_image_info = "CTRL+I" show_app_about = "CTRL+A" -# Key bind to reset the image pan +# Key bind to reset the image pan # position and zoom scale back to default. reset_viewport = "R" # Key bind to toggle all your UI controls like the magnification panel. @@ -123,7 +128,7 @@ show_ui_controls = "C" # override_monitor_size = {width = 1920, height = 1080} [misc.experimental] -# Settings to toggle experimental features that aren't yet ready yet to be +# Settings to toggle experimental features that aren't yet ready yet to be # used by the wider user audience. This exists for the sole purpose of testing. -# -# Remember these are EXPERIMENTAL, bugs WILL be present. \ No newline at end of file +# +# Remember these are EXPERIMENTAL, bugs WILL be present. diff --git a/app/src/app.rs b/app/src/app.rs index a9b0911..70563bb 100644 --- a/app/src/app.rs +++ b/app/src/app.rs @@ -32,7 +32,7 @@ impl Roseate { config_manager: ConfigManager ) -> Self { let viewport = Viewport::new(); - let windows_manager = WindowsManager::new(); + let windows_manager = WindowsManager::new(config_manager.config.ui.info_panel.show_location); let settings_menu = SettingsMenu::new(); let about_window = AboutWindow::new(); let selection_menu = ImageSelectionMenu::new(); @@ -121,7 +121,7 @@ impl eframe::App for Roseate { (Some(image), Some(image_resource))=> { egui::Frame::NONE .show(ui, |ui| { - // handle inputs here that you do not + // handle inputs here that you do not // want toggling outside the viewport self.context_menu.handle_input(&ctx, &self.windows_manager); diff --git a/app/src/config/models/ui.rs b/app/src/config/models/ui.rs index eed3523..ee292f8 100644 --- a/app/src/config/models/ui.rs +++ b/app/src/config/models/ui.rs @@ -9,7 +9,9 @@ pub struct UI { #[serde(default)] pub viewport: Viewport, #[serde(default)] - pub selection_menu: SelectionMenu + pub selection_menu: SelectionMenu, + #[serde(default)] + pub info_panel: InfoPanel } @@ -84,4 +86,18 @@ impl Default for SelectionMenu { show_open_image_button: true } } -} \ No newline at end of file +} + +#[derive(Serialize, Deserialize, Hash)] +pub struct InfoPanel { + #[serde(default = "super::true_default")] + pub show_location: bool +} + +impl Default for InfoPanel { + fn default() -> Self { + Self { + show_location: true + } + } +} diff --git a/app/src/windows/info.rs b/app/src/windows/info.rs index cfd0d52..8a021d5 100644 --- a/app/src/windows/info.rs +++ b/app/src/windows/info.rs @@ -83,7 +83,7 @@ struct ExpensiveData { } impl ExpensiveData { - pub fn new(image_resource: &ImageResource, image_metadata: &ImageMetadata, image: &Image) -> Arc> { + pub fn new(image_resource: &ImageResource, image_metadata: &ImageMetadata, image: &Image, show_location: bool) -> Arc> { let date_format = "%d/%m/%Y %H:%M %p"; let path = image.path.clone(); @@ -166,24 +166,25 @@ impl ExpensiveData { std::thread::spawn(move || { let mut locked_data = mutex_data_clone.lock().unwrap(); - if let Some(latitude) = &image_metadata_clone.location.latitude + if show_location { + if let Some(latitude) = &image_metadata_clone.location.latitude && let Some(longitude) = &image_metadata_clone.location.longitude { - log::debug!("original coords: {}, {}", latitude, longitude); - let geocoder = reverse_geocoder::ReverseGeocoder::new(); + log::debug!("original coords: {}, {}", latitude, longitude); + let geocoder = reverse_geocoder::ReverseGeocoder::new(); - let latitude = dms_to_decimal!(latitude); - let longitude = dms_to_decimal!(longitude); - log::debug!("converted coords to decimal: {}, {}", latitude, longitude); + let latitude = dms_to_decimal!(latitude); + let longitude = dms_to_decimal!(longitude); + log::debug!("converted coords to decimal: {}, {}", latitude, longitude); - let result = geocoder.search((latitude, longitude)); - - if let Some(country_name) = country_emoji::name(&result.record.cc) { - let formatted_location = format!("{}, {}", result.record.name, country_name); - let url = format!("https://www.openstreetmap.org?mlat={}&mlon={}#map=18/{}/{}", - latitude, longitude, latitude, longitude); - locked_data.location = Some((formatted_location, url)); - } + let result = geocoder.search((latitude, longitude)); + if let Some(country_name) = country_emoji::name(&result.record.cc) { + let formatted_location = format!("{}, {}", result.record.name, country_name); + let url = format!("https://www.openstreetmap.org?mlat={}&mlon={}#map=18/{}/{}", + latitude, longitude, latitude, longitude); + locked_data.location = Some((formatted_location, url)); + } + } locked_data.memory_allocated_for_image = match resource { ImageResource::Texture(texture_handle) => texture_handle.byte_size() as f64, ImageResource::AnimatedTexture(frames) => { @@ -218,15 +219,17 @@ impl ExpensiveData { pub struct ImageInfoWindow { data: Option, - processing_expensive_data: Option>> + processing_expensive_data: Option>>, + show_location: bool } impl ImageInfoWindow { - pub fn new() -> Self { + pub fn new(show_location: bool) -> Self { Self { data: None, - processing_expensive_data: None + processing_expensive_data: None, + show_location } } @@ -278,6 +281,7 @@ impl ImageInfoWindow { } fn show_image_info_grid( + &self, ui: &mut Ui, expensive_data: &Option, image: &Image, @@ -363,24 +367,28 @@ impl ImageInfoWindow { ui.end_row(); ui_non_select_label(ui, "Location:"); - match &expensive_data { - Some(data) => { - match &data.location { - Some(location) => { - if ui.button(&location.0).clicked() { - ui.ctx().open_url( - OpenUrl::new_tab(&location.1) - ); + if self.show_location { + match &expensive_data { + Some(data) => { + match &data.location { + Some(location) => { + if ui.button(&location.0).clicked() { + ui.ctx().open_url( + OpenUrl::new_tab(&location.1) + ); + } + }, + None => { + ui.label(RichText::new("Unknown").weak()); } - }, - None => { - ui.label(RichText::new("Unknown").weak()); } + }, + None => { + ui.label(RichText::new("Initializing...").weak()); } - }, - None => { - ui.label(RichText::new("Initializing...").weak()); } + } else { + ui.label(RichText::new("Disabled").weak()); } } }); @@ -439,7 +447,7 @@ impl ImageInfoWindow { ) -> Response { if self.data.is_none() { self.processing_expensive_data.get_or_insert_with( - || ExpensiveData::new(image_resource, &image_info.metadata, image) + || ExpensiveData::new(image_resource, &image_info.metadata, image, self.show_location) ); } @@ -531,7 +539,7 @@ impl ImageInfoWindow { ui.add(egui::Separator::default().grow(4.0)); ui.vertical(|ui| { - Self::show_image_info_grid( + self.show_image_info_grid( ui, &self.data, image, @@ -556,7 +564,7 @@ impl ImageInfoWindow { }, false => { ui.vertical(|ui| { - Self::show_image_info_grid( + self.show_image_info_grid( ui, &self.data, image, image_info, 160.0, soon_text, show_extra ); }); diff --git a/app/src/windows/mod.rs b/app/src/windows/mod.rs index 63a5164..a7007e7 100644 --- a/app/src/windows/mod.rs +++ b/app/src/windows/mod.rs @@ -16,8 +16,8 @@ pub struct WindowsManager { } impl WindowsManager { - pub fn new() -> Self { - let info_window = ImageInfoWindow::new(); + pub fn new(show_location: bool) -> Self { + let info_window = ImageInfoWindow::new(show_location); Self { info_window, From 86deff73f1e192a053c8a5fc6c3bc3be2793ab44 Mon Sep 17 00:00:00 2001 From: Goldy <66202304+THEGOLDENPRO@users.noreply.github.com> Date: Fri, 2 Jan 2026 20:27:34 +0000 Subject: [PATCH 06/10] refactor: pass `show_location` bool through update loop instead and rename config key --- app/assets/config.template.toml | 7 ++++--- app/src/app.rs | 5 +++-- app/src/config/config.rs | 8 +++++--- app/src/config/models/ui/mod.rs | 11 ++++++----- app/src/windows/info.rs | 26 +++++++++++++++++--------- app/src/windows/mod.rs | 6 ++++-- 6 files changed, 39 insertions(+), 24 deletions(-) diff --git a/app/assets/config.template.toml b/app/assets/config.template.toml index efa2805..64c14c2 100644 --- a/app/assets/config.template.toml +++ b/app/assets/config.template.toml @@ -112,9 +112,10 @@ animate_reset = true # "Open Image" button otherwise to open an image you can click on the rose. show_open_image_button = true -[ui.info_panel] -# If set to "true", it will show you the location the image was taken. -# otherwise it'll show disabled where the location would be. +[ui.image_info] +# When enabled the image info window will display the location +# of where the image was taken. Otherwise if disabled no offline +# lookup will be performed and no location will be shown. show_location = true [key_binds] diff --git a/app/src/app.rs b/app/src/app.rs index 7cd5657..b213bb0 100644 --- a/app/src/app.rs +++ b/app/src/app.rs @@ -33,7 +33,7 @@ impl Roseate { config_manager: ConfigManager ) -> Self { let viewport = Viewport::new(); - let windows_manager = WindowsManager::new(config_manager.config.ui.info_panel.show_location); + let windows_manager = WindowsManager::new(); let settings_menu = SettingsMenu::new(); let about_window = AboutWindow::new(); let selection_menu = ImageSelectionMenu::new(); @@ -148,7 +148,8 @@ impl eframe::App for Roseate { &self.image_handler.image_optimizations, image, // leaving this unwrap here for now, I'll defiantly improve this soon - self.image_handler.decoded_image_info.as_ref().unwrap() + self.image_handler.decoded_image_info.as_ref().unwrap(), + config.ui.image_info.show_location, ); self.context_menu.show(ui, &mut self.windows_manager); diff --git a/app/src/config/config.rs b/app/src/config/config.rs index b8a5fe8..050ddba 100644 --- a/app/src/config/config.rs +++ b/app/src/config/config.rs @@ -1,7 +1,7 @@ use cirrus_config::v1::config::CConfig; use serde::{Serialize, Deserialize}; -use crate::config::models::ui::{SelectionMenu, Viewport, controls::Controls}; +use crate::config::models::ui::{ImageInfo, SelectionMenu, Viewport, controls::Controls}; use super::models::{image::Image, key_binds::KeyBinds, misc::Misc, ui::UI}; @@ -38,7 +38,8 @@ impl Config { viewport: Viewport::default(), selection_menu: SelectionMenu { show_open_image_button: true, - } + }, + image_info: ImageInfo::default(), } }, UIConfigMode::Minimalist => { @@ -50,7 +51,8 @@ impl Config { viewport: Viewport::default(), selection_menu: SelectionMenu { show_open_image_button: false, - } + }, + image_info: ImageInfo::default(), } }, } diff --git a/app/src/config/models/ui/mod.rs b/app/src/config/models/ui/mod.rs index 75ef8dd..37f5c5e 100644 --- a/app/src/config/models/ui/mod.rs +++ b/app/src/config/models/ui/mod.rs @@ -16,7 +16,7 @@ pub struct UI { #[serde(default)] pub selection_menu: SelectionMenu, #[serde(default)] - pub info_panel: InfoPanel + pub image_info: ImageInfo } @@ -78,16 +78,17 @@ impl Default for SelectionMenu { } } -#[derive(Serialize, Deserialize, Hash)] -pub struct InfoPanel { + +#[derive(Serialize, Deserialize, Hash, Clone)] +pub struct ImageInfo { #[serde(default = "super::true_default")] pub show_location: bool } -impl Default for InfoPanel { +impl Default for ImageInfo { fn default() -> Self { Self { show_location: true } } -} +} \ No newline at end of file diff --git a/app/src/windows/info.rs b/app/src/windows/info.rs index 803258b..21fcba8 100644 --- a/app/src/windows/info.rs +++ b/app/src/windows/info.rs @@ -220,16 +220,14 @@ pub struct ImageInfoWindow { data: Option, processing_expensive_data: Option>>, - show_location: bool } impl ImageInfoWindow { - pub fn new(show_location: bool) -> Self { + pub fn new() -> Self { Self { data: None, processing_expensive_data: None, - show_location } } @@ -288,7 +286,8 @@ impl ImageInfoWindow { image_info: &ImageInfo, max_grid_width: f32, soon_text: Arc, - show_extra: bool + show_extra: bool, + show_location_in_image_info: bool, ) { egui::Grid::new("base_image_info_grid") .striped(true) @@ -367,7 +366,7 @@ impl ImageInfoWindow { ui.end_row(); ui_non_select_label(ui, "Location:"); - if self.show_location { + if show_location_in_image_info { match &expensive_data { Some(data) => { match &data.location { @@ -443,11 +442,12 @@ impl ImageInfoWindow { image_optimizations: &ImageOptimizations, image: &Image, image_info: &ImageInfo, - show_extra: bool + show_extra: bool, + show_location_in_image_info: bool ) -> Response { if self.data.is_none() { self.processing_expensive_data.get_or_insert_with( - || ExpensiveData::new(image_resource, &image_info.metadata, image, self.show_location) + || ExpensiveData::new(image_resource, &image_info.metadata, image, show_location_in_image_info) ); } @@ -546,7 +546,8 @@ impl ImageInfoWindow { image_info, 180.0, soon_text.clone(), - show_extra + show_extra, + show_location_in_image_info, ); ui.separator(); @@ -565,7 +566,14 @@ impl ImageInfoWindow { false => { ui.vertical(|ui| { self.show_image_info_grid( - ui, &self.data, image, image_info, 160.0, soon_text, show_extra + ui, + &self.data, + image, + image_info, + 160.0, + soon_text, + show_extra, + show_location_in_image_info, ); }); }, diff --git a/app/src/windows/mod.rs b/app/src/windows/mod.rs index 1cb1937..037d5d9 100644 --- a/app/src/windows/mod.rs +++ b/app/src/windows/mod.rs @@ -23,8 +23,8 @@ pub struct WindowsManager { } impl WindowsManager { - pub fn new(show_location: bool) -> Self { - let info_window = ImageInfoWindow::new(show_location); + pub fn new() -> Self { + let info_window = ImageInfoWindow::new(); Self { info_window, @@ -95,6 +95,7 @@ impl WindowsManager { image_optimizations: &ImageOptimizations, image: &Image, image_info: &ImageInfo, + show_location_in_image_info: bool ) { let mut new_rect: Rect = Rect::NOTHING; @@ -106,6 +107,7 @@ impl WindowsManager { image, image_info, self.show_extra_info, + show_location_in_image_info, ); new_rect = new_rect.union(response.rect); From 45bee9396aa84f6c57f54a90980f1cee1e82d9be Mon Sep 17 00:00:00 2001 From: Goldy <66202304+THEGOLDENPRO@users.noreply.github.com> Date: Fri, 2 Jan 2026 23:18:56 +0000 Subject: [PATCH 07/10] refactor: only thread location look up, improve logic and place expensive data into a separate source file --- app/src/windows/info/expensive_data.rs | 185 ++++++++ app/src/windows/{info.rs => info/mod.rs} | 563 ++++++++--------------- 2 files changed, 370 insertions(+), 378 deletions(-) create mode 100644 app/src/windows/info/expensive_data.rs rename app/src/windows/{info.rs => info/mod.rs} (55%) diff --git a/app/src/windows/info/expensive_data.rs b/app/src/windows/info/expensive_data.rs new file mode 100644 index 0000000..1815e39 --- /dev/null +++ b/app/src/windows/info/expensive_data.rs @@ -0,0 +1,185 @@ +use std::{path::PathBuf, sync::{Arc, Mutex}, thread}; + +use chrono::{DateTime, Local, NaiveDateTime}; +use log::debug; +use roseate_core::image_info::metadata::ImageMetadata; + +use crate::{image_handler::resource::ImageResource}; + +pub struct ExpensiveData { + pub file_name: String, + pub file_size: Option, + pub file_relative_path: String, + pub image_created_time: Option, + pub file_modified_time: Option, + pub memory_allocated_for_image: f64, + + pub location: Arc>> +} + +impl ExpensiveData { + pub fn new(image_path: &Arc, image_resource: &ImageResource, image_metadata: &ImageMetadata) -> Self { + let file_name = image_path.file_name().unwrap().to_string_lossy().to_string(); + let file_relative_path = image_path.to_string_lossy().to_string(); + + let file_metadata = match image_path.metadata() { + Ok(metadata) => Some(metadata), + Err(error) => { + log::error!( + "Failed to retrive image file metadata from file system! Error: {}", + error + ); + + None + }, + }; + + let mut file_size = None; + let mut image_created_time = None; + let mut file_modified_time = None; + + let date_format = "%d/%m/%Y %H:%M %p"; + + if let Some(metadata) = file_metadata { + image_created_time = match metadata.created() { + Ok(time) => { + let datetime: DateTime = time.into(); + Some(datetime.format(date_format).to_string()) + }, + Err(error) => { + log::warn!("Failed to retrieve image file created date! Error: {}", error); + + None + }, + }; + + file_modified_time = match metadata.modified() { + Ok(time) => { + let datetime: DateTime = time.into(); + Some(datetime.format(date_format).to_string()) + }, + Err(error) => { + log::warn!("Failed to retrieve image file modified date! Error: {}", error); + + None + }, + }; + + file_size = Some(metadata.len() as f64); + } + + if let Some(image_original_creation_time) = &image_metadata.originally_created { + debug!("Parsing image original creation date..."); + match NaiveDateTime::parse_from_str(image_original_creation_time, "%Y-%m-%d %H:%M:%S") { + Ok(datetime) => { + image_created_time = Some(datetime.format(date_format).to_string()); + }, + Err(error) => { + log::warn!("Failed to parse image original creation date! Error: {}", error); + } + } + } + + Self { + file_name, + file_size, + file_relative_path, + image_created_time, + file_modified_time, + memory_allocated_for_image: match image_resource { + ImageResource::Texture(texture_handle) => texture_handle.byte_size() as f64, + ImageResource::AnimatedTexture(frames) => { + let mut size = 0; + + for (texture_handler, _) in frames { + size += texture_handler.byte_size(); + } + + size as f64 + }, + }, + + location: Arc::new(Mutex::new(None)) + } + } + + pub fn start_location_lookup_thread(&mut self, image_metadata: &ImageMetadata) -> &mut Self { + let location = self.location.clone(); + + let image_location_latitude = image_metadata.location.latitude.clone(); + let image_location_longitude = image_metadata.location.longitude.clone(); + + debug!("Spawning location lookup thread..."); + + thread::spawn(move || { + if let Some(latitude) = image_location_latitude + && let Some(longitude) = image_location_longitude { + // Locking at the beginning will tell the image info to display + // "Loading..." while the reverse geocoder initializes and finds the location. + let mut location_mutex = location.lock().unwrap(); + + debug!("Initializing reverse geocoder..."); + let geocoder = reverse_geocoder::ReverseGeocoder::new(); + + debug!( + "Converting coordinates (latitude: {}, longitude: {}) to decimal...", + latitude, longitude, + ); + + let latitude = dms_to_decimal(&latitude); + let longitude = dms_to_decimal(&longitude); + + let result = geocoder.search((latitude, longitude)); + + debug!("Fetching image location country name..."); + + if let Some(country_name) = country_emoji::name(&result.record.cc) { + let formatted_location = format!("{}, {}", result.record.name, country_name); + + let url = format!( + "https://www.openstreetmap.org?mlat={}&mlon={}#map=18/{}/{}", + latitude, longitude, latitude, longitude + ); + + *location_mutex = Some((formatted_location, url)); + } + } + + }); + + self + } +} + +// NOTE: there was no benefit having this be a macro +fn dms_to_decimal(dms_string: &String) -> f64 { + let parts: Vec<&str> = dms_string + .split(|c: char| !c.is_ascii_digit() && c != '.' && c != '/') + .filter(|s| !s.is_empty()) + .collect(); + + let mut numbers = Vec::new(); + + for part in parts { + if part.contains("/") { + let (first, second) = part.split_once("/").unwrap(); + if let (Ok(first), Ok(second)) = ( + first.parse::(), + second.parse::() + ) { + numbers.push(first / second); + } else { + log::warn!("GPS Data format is unknown: {}", dms_string); + } + } else if let Ok(num) = part.parse::() { + numbers.push(num); + } + } + + match numbers.len() { + 3 => numbers[0] + numbers[1] / 60.0 + numbers[2] / 3600.0, + 2 => numbers[0] + numbers[1] / 60.0, + 1 => numbers[0], + _ => 0.0, + } +} \ No newline at end of file diff --git a/app/src/windows/info.rs b/app/src/windows/info/mod.rs similarity index 55% rename from app/src/windows/info.rs rename to app/src/windows/info/mod.rs index 21fcba8..9af058a 100644 --- a/app/src/windows/info.rs +++ b/app/src/windows/info/mod.rs @@ -1,12 +1,13 @@ -use std::{alloc, sync::{Arc, Mutex}}; +mod expensive_data; + +use std::{alloc, sync::{Arc, TryLockError}}; use cap::Cap; -use chrono::{DateTime, Local, NaiveDateTime}; -use egui::{Color32, Label, OpenUrl, Pos2, RichText, TextureHandle, Ui, WidgetText}; use eframe::egui::{self, Response}; -use roseate_core::image_info::{info::ImageInfo, metadata::ImageMetadata}; +use egui::{Color32, CursorIcon, Label, OpenUrl, Pos2, RichText, TextureHandle, Ui, WidgetText}; +use roseate_core::image_info::{info::ImageInfo}; -use crate::{image::Image, image_handler::{optimization::ImageOptimizations, resource::ImageResource}}; +use crate::{image::Image, image_handler::{optimization::ImageOptimizations, resource::ImageResource}, windows::info::expensive_data::ExpensiveData}; #[global_allocator] static ALLOCATOR: Cap = Cap::new(alloc::System, usize::max_value()); @@ -18,217 +19,167 @@ macro_rules! rich_text_or_unknown { None => RichText::new("Unknown").weak(), } }; +} - ($opt:expr) => { - match &$opt { - Some(string) => RichText::new(string), - None => RichText::new("Unknown").weak(), - } - }; +pub struct ImageInfoWindow { + data: Option, } -macro_rules! rich_text_or_init { - ($data:expr, $arg:literal) => { - match &$data { - Some(data) => rich_text_or_unknown!(data.get($arg)), - None => RichText::new("Initializing...").weak(), +impl ImageInfoWindow { + pub fn new() -> Self { + Self { + data: None } - }; -} + } -macro_rules! dms_to_decimal { - ($dms_str:expr) => {{ - let parts: Vec<&str> = $dms_str - .split(|c: char| !c.is_ascii_digit() && c != '.' && c != '/') - .filter(|s| !s.is_empty()) - .collect(); - - let mut numbers = Vec::new(); - - for part in parts { - if part.contains("/") { - let (first, second) = part.split_once("/").unwrap(); - if let (Ok(first), Ok(second)) = ( - first.parse::(), - second.parse::() - ) { - numbers.push(first / second); - } else { - log::warn!("GPS Data format is unknown: {}", $dms_str); + pub fn show( + &mut self, + ui: &Ui, + image_resource: &ImageResource, + image_optimizations: &ImageOptimizations, + image: &Image, + image_info: &ImageInfo, + show_extra: bool, + show_location_in_image_info: bool, + ) -> Response { + let image_info_data = self.data.get_or_insert_with( + || { + let mut data = ExpensiveData::new( + &image.path, + image_resource, + &image_info.metadata + ); + + if show_location_in_image_info { + data.start_location_lookup_thread( + &image_info.metadata + ); } - } else if let Ok(num) = part.parse::() { - numbers.push(num); - } - } - match numbers.len() { - 3 => numbers[0] + numbers[1] / 60.0 + numbers[2] / 3600.0, - 2 => numbers[0] + numbers[1] / 60.0, - 1 => numbers[0], - _ => 0.0, - } - }}; -} + data + } + ); -#[derive(Debug, Clone)] -struct ExpensiveData { - pub file_name: String, - pub file_size: Option, - pub file_relative_path: String, - pub image_created_time: Option, - pub file_modified_time: Option, - pub memory_allocated_for_image: f64, + let main_frame = egui::Frame::group(&ui.style()) + .inner_margin(8.0); - pub location: Option<(String, String)> -} + let window = egui::Window::new( + WidgetText::RichText( + match show_extra { + false => RichText::new("ℹ Image Info"), + true => RichText::new("ℹ Image Info (Extra)"), + }.size(15.0).into() + ) + ); -impl ExpensiveData { - pub fn new(image_resource: &ImageResource, image_metadata: &ImageMetadata, image: &Image, show_location: bool) -> Arc> { - let date_format = "%d/%m/%Y %H:%M %p"; + window.default_pos(Pos2::new(200.0, 200.0)) + .min_width(150.0) + .max_width(300.0) + .resizable(false) + .fade_in(false) + .fade_out(false) + .show(ui.ctx(), |ui| { + // let available_width = ui.available_width(); - let path = image.path.clone(); - let image_metadata_clone = image_metadata.clone(); + // let should_stack = match self.grid_width_used { + // Some(grid_width_used) => available_width < grid_width_used + 20.0, + // None => true + // }; - let mut image_created_time = if let Some(time) = &image_metadata_clone.originally_created { - match NaiveDateTime::parse_from_str(time, "%Y-%m-%d %H:%M:%S") { - Ok(datetime) => { - Some(datetime.format(date_format).to_string()) - }, - Err(err) => { - log::warn!("Failed to parse image created date! Error: {}", err); + // let main_layout = match should_stack { + // true => Layout::top_down(egui::Align::Min), + // false => Layout::left_to_right(egui::Align::Center) + // }; - None - } - } - } else { - None - }; - - let (file_size, file_modified_time) = match path.metadata() { - Ok(metadata) => { - if image_created_time.is_none() { - image_created_time = match metadata.created() { - Ok(time) => { - let datetime: DateTime = time.into(); - Some(datetime.format(date_format).to_string()) - }, - Err(error) => { - log::warn!("Failed to retrieve image file creation date! Error: {}", error); - - None - }, - }; - } + // let grid_width = (self.window_width.unwrap_or(available_width) / match should_stack {true => 2.0, false => 4.0}).min(200.0); - let file_modified_time = match metadata.modified() { - Ok(time) => { - let datetime: DateTime = time.into(); - Some(datetime.format(date_format).to_string()) - }, - Err(error) => { - log::warn!("Failed to retrieve image file modified date! Error: {}", error); - - None - }, - }; - - (Some(metadata.len() as f64), file_modified_time) - }, - Err(error) => { - log::error!( - "Failed to retrive image file metadata from file system! Error: {}", - error - ); + main_frame.show(ui, |ui| { + ui.shrink_height_to_current(); - (None, None) - }, - }; - - - let initial_data = Self { - file_name: path.file_name() - .unwrap_or_default() - .to_string_lossy() - .to_string(), - file_size, - file_relative_path: path.to_string_lossy().to_string(), - image_created_time, - file_modified_time, - memory_allocated_for_image: 0.0, - location: None, - }; - - let mutex_data = Arc::new(Mutex::new(initial_data)); - let mutex_data_clone = mutex_data.clone(); - - let resource = image_resource.clone(); - - std::thread::spawn(move || { - let mut locked_data = mutex_data_clone.lock().unwrap(); - - if show_location { - if let Some(latitude) = &image_metadata_clone.location.latitude - && let Some(longitude) = &image_metadata_clone.location.longitude { - log::debug!("original coords: {}, {}", latitude, longitude); - let geocoder = reverse_geocoder::ReverseGeocoder::new(); - - let latitude = dms_to_decimal!(latitude); - let longitude = dms_to_decimal!(longitude); - log::debug!("converted coords to decimal: {}, {}", latitude, longitude); - - let result = geocoder.search((latitude, longitude)); - - if let Some(country_name) = country_emoji::name(&result.record.cc) { - let formatted_location = format!("{}, {}", result.record.name, country_name); - let url = format!("https://www.openstreetmap.org?mlat={}&mlon={}#map=18/{}/{}", - latitude, longitude, latitude, longitude); - locked_data.location = Some((formatted_location, url)); - } - } - locked_data.memory_allocated_for_image = match resource { - ImageResource::Texture(texture_handle) => texture_handle.byte_size() as f64, - ImageResource::AnimatedTexture(frames) => { - let mut size = 0; + ui.horizontal(|ui| { + let app_memory_allocated = ALLOCATOR.allocated(); - for (texture_handler, _) in frames { - size += texture_handler.byte_size(); - } + let soon_text = Arc::new( + RichText::new("Coming Soon...").weak() + ); - size as f64 - }, - }; - } - }); + match show_extra { + true => { + let texture_handle: Option<&TextureHandle> = match image_resource { + ImageResource::Texture(texture_handle) => Some(texture_handle), + ImageResource::AnimatedTexture(frames) => { + frames.get(0) + .and_then( + |(texture_handle,_)| Some(texture_handle) + ) + } + }; - std::thread::sleep(std::time::Duration::from_millis(10)); // Thread is spawning too slow for the locking to work. ~ ananas + ui.vertical(|ui| { + if let Some(texture) = texture_handle { + ui.add( + egui::Image::from_texture(texture) + // 16 is the padding from + // the image optimizations grid + .max_size([200.0 + 16.0, 140.0].into()) + .corner_radius(8) + ); + } - mutex_data - } + ui.add_space(5.0); - pub fn get(&self, index: &str) -> Option { - match index { - "file_name" => Some(self.file_name.clone()), - "file_relative_path" => Some(self.file_relative_path.clone()), - "image_created_time" => self.image_created_time.clone(), - "file_modified_time" => self.file_modified_time.clone(), - _ => None, - } - } -} + egui::ScrollArea::vertical() + .min_scrolled_height(150.0) + .show(ui, |ui| { + Self::show_image_optimizations_grid(ui, image_optimizations); + }); + }); -pub struct ImageInfoWindow { - data: Option, + ui.add(egui::Separator::default().grow(4.0)); - processing_expensive_data: Option>>, -} + ui.vertical(|ui| { + Self::show_image_info_grid( + ui, + image_info_data, + image, + image_info, + 180.0, + soon_text.clone(), + show_extra, + show_location_in_image_info, + ); -impl ImageInfoWindow { - pub fn new() -> Self { - Self { - data: None, + ui.separator(); - processing_expensive_data: None, - } + Self::show_misc_info_grid( + ui, + image_info_data, + image, + image_info, + 180.0, + soon_text.clone(), + app_memory_allocated as f64, + ); + }); + }, + false => { + ui.vertical(|ui| { + Self::show_image_info_grid( + ui, + image_info_data, + image, + image_info, + 160.0, + soon_text, + show_extra, + show_location_in_image_info, + ); + }); + }, + } + }); + }); + }).unwrap().response } fn show_image_optimizations_grid(ui: &mut Ui, image_optimizations: &ImageOptimizations) { @@ -279,9 +230,8 @@ impl ImageInfoWindow { } fn show_image_info_grid( - &self, ui: &mut Ui, - expensive_data: &Option, + expensive_data: &ExpensiveData, image: &Image, image_info: &ImageInfo, max_grid_width: f32, @@ -294,8 +244,8 @@ impl ImageInfoWindow { .max_col_width(max_grid_width) .show(ui, |ui| { ui_non_select_label(ui, "Name:"); - ui.label(rich_text_or_init!(&expensive_data, "file_name")) - .on_hover_text(rich_text_or_init!(&expensive_data, "file_relative_path")); + ui.label(&expensive_data.file_name) + .on_hover_text(&expensive_data.file_relative_path); ui.end_row(); ui_non_select_label(ui, "Dimensions:"); @@ -321,25 +271,30 @@ impl ImageInfoWindow { date is NOT accurate!)"; ui_non_select_label(ui, "Created:").on_hover_text(created_hint); - ui.label(rich_text_or_init!(&expensive_data, "image_created_time")).on_hover_text(created_hint); + ui.label( + match &expensive_data.image_created_time { + Some(time_string) => RichText::new(time_string), + None => RichText::new("Unknown").weak(), + } + ).on_hover_text(created_hint); ui.end_row(); if show_extra { ui_non_select_label(ui, "File Modified:"); - ui.label(rich_text_or_init!(&expensive_data, "file_modified_time")); + ui.label( + match &expensive_data.file_modified_time { + Some(time_string) => RichText::new(time_string), + None => RichText::new("Unknown").weak(), + } + ); ui.end_row(); } ui_non_select_label(ui, "File size:"); ui.label( - match &expensive_data { - Some(data) => { - match data.file_size { - Some(size) => RichText::new(re_format::format_bytes(size)), - None => RichText::new("Unknown").weak() - } - }, - None => RichText::new("Initializing...").weak(), + match expensive_data.file_size { + Some(size) => RichText::new(re_format::format_bytes(size)), + None => RichText::new("Unknown").weak(), } ); ui.end_row(); @@ -365,13 +320,16 @@ impl ImageInfoWindow { ui.label(rich_text_or_unknown!("{}s", &image_info.metadata.exposure_time)); ui.end_row(); - ui_non_select_label(ui, "Location:"); if show_location_in_image_info { - match &expensive_data { - Some(data) => { - match &data.location { + ui_non_select_label(ui, "Location:"); + match expensive_data.location.try_lock() { + Ok(location_lock) => { + match location_lock.as_ref() { Some(location) => { - if ui.button(&location.0).clicked() { + let button = ui.button(&location.0) + .on_hover_cursor(CursorIcon::PointingHand); + + if button.clicked() { ui.ctx().open_url( OpenUrl::new_tab(&location.1) ); @@ -379,15 +337,19 @@ impl ImageInfoWindow { }, None => { ui.label(RichText::new("Unknown").weak()); - } + }, } }, - None => { - ui.label(RichText::new("Initializing...").weak()); - } - } - } else { - ui.label(RichText::new("Disabled").weak()); + Err(error) => { + ui.label(RichText::new("Loading...").italics()); + + if let TryLockError::Poisoned(error) = error { + log::error!( + "Thread spawned to perform location lookup on image got poisoned! Error: {error}" + ); + } + }, + }; } } }); @@ -395,7 +357,7 @@ impl ImageInfoWindow { fn show_misc_info_grid( ui: &mut Ui, - expensive_data: &Option, + expensive_data: &ExpensiveData, image: &Image, image_info: &ImageInfo, max_grid_width: f32, @@ -420,170 +382,15 @@ impl ImageInfoWindow { ui_non_select_label(ui, "Image Mem Alloc:") .on_hover_text(mem_allocation_by_image_hint); ui.label( - match expensive_data { - Some(data) => { - RichText::new(re_format::format_bytes( - data.memory_allocated_for_image) - ) - }, - None => { - RichText::new("Initializing...") - } - } + RichText::new(re_format::format_bytes( + expensive_data.memory_allocated_for_image) + ) ).on_hover_text(mem_allocation_by_image_hint); ui.end_row(); }); } - - pub fn show( - &mut self, - ui: &Ui, - image_resource: &ImageResource, - image_optimizations: &ImageOptimizations, - image: &Image, - image_info: &ImageInfo, - show_extra: bool, - show_location_in_image_info: bool - ) -> Response { - if self.data.is_none() { - self.processing_expensive_data.get_or_insert_with( - || ExpensiveData::new(image_resource, &image_info.metadata, image, show_location_in_image_info) - ); - } - - match self.processing_expensive_data.clone() { - Some(mutex) => { - if let Ok(data) = mutex.try_lock() { - self.data = Some(data.clone()); - self.processing_expensive_data = None; - } - }, - None => {} - }; - - let main_frame = egui::Frame::group(&ui.style()) - .inner_margin(8.0); - - let window = egui::Window::new( - WidgetText::RichText( - match show_extra { - false => RichText::new("ℹ Image Info"), - true => RichText::new("ℹ Image Info (Extra)"), - }.size(15.0).into() - ) - ); - - window.default_pos(Pos2::new(200.0, 200.0)) - .min_width(150.0) - .max_width(300.0) - .resizable(false) - .fade_in(false) - .fade_out(false) - .show(ui.ctx(), |ui| { - // let available_width = ui.available_width(); - - // let should_stack = match self.grid_width_used { - // Some(grid_width_used) => available_width < grid_width_used + 20.0, - // None => true - // }; - - // let main_layout = match should_stack { - // true => Layout::top_down(egui::Align::Min), - // false => Layout::left_to_right(egui::Align::Center) - // }; - - // let grid_width = (self.window_width.unwrap_or(available_width) / match should_stack {true => 2.0, false => 4.0}).min(200.0); - - main_frame.show(ui, |ui| { - ui.shrink_height_to_current(); - - ui.horizontal(|ui| { - let app_memory_allocated = ALLOCATOR.allocated(); - - let soon_text = Arc::new( - RichText::new("Coming Soon...").weak() - ); - - match show_extra { - true => { - let texture_handle: Option<&TextureHandle> = match image_resource { - ImageResource::Texture(texture_handle) => Some(texture_handle), - ImageResource::AnimatedTexture(frames) => { - frames.get(0) - .and_then( - |(texture_handle,_)| Some(texture_handle) - ) - } - }; - - ui.vertical(|ui| { - if let Some(texture) = texture_handle { - ui.add( - egui::Image::from_texture(texture) - // 16 is the padding from - // the image optimizations grid - .max_size([200.0 + 16.0, 140.0].into()) - .corner_radius(8) - ); - } - - ui.add_space(5.0); - - egui::ScrollArea::vertical() - .min_scrolled_height(150.0) - .show(ui, |ui| { - Self::show_image_optimizations_grid(ui, image_optimizations); - }); - }); - - ui.add(egui::Separator::default().grow(4.0)); - - ui.vertical(|ui| { - self.show_image_info_grid( - ui, - &self.data, - image, - image_info, - 180.0, - soon_text.clone(), - show_extra, - show_location_in_image_info, - ); - - ui.separator(); - - Self::show_misc_info_grid( - ui, - &self.data, - image, - image_info, - 180.0, - soon_text.clone(), - app_memory_allocated as f64, - ); - }); - }, - false => { - ui.vertical(|ui| { - self.show_image_info_grid( - ui, - &self.data, - image, - image_info, - 160.0, - soon_text, - show_extra, - show_location_in_image_info, - ); - }); - }, - } - }); - }); - }).unwrap().response - } } fn ui_non_select_label(ui: &mut Ui, text: impl Into) -> Response { ui.add(Label::new(text).selectable(false)) -} +} \ No newline at end of file From 738c79aee796c207685527a646ecf09893dbdb7c Mon Sep 17 00:00:00 2001 From: Goldy <66202304+THEGOLDENPRO@users.noreply.github.com> Date: Fri, 2 Jan 2026 23:55:17 +0000 Subject: [PATCH 08/10] docs: remove exif tags not in use warning --- app/src/windows/info/mod.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/src/windows/info/mod.rs b/app/src/windows/info/mod.rs index 9af058a..06d9565 100644 --- a/app/src/windows/info/mod.rs +++ b/app/src/windows/info/mod.rs @@ -266,9 +266,8 @@ impl ImageInfoWindow { ui.end_row(); } - let created_hint = "Shows the date and time the image was taken or created \ - according to your filesystem. (WARNING: EXIF tags are not used YET, so creation \ - date is NOT accurate!)"; + let created_hint = "The best estimate of when the image was taken or created \ + according to EXIF tags or your filesystem."; ui_non_select_label(ui, "Created:").on_hover_text(created_hint); ui.label( From a0e16e6005000db4bb60f14055903a790ef8d9a9 Mon Sep 17 00:00:00 2001 From: Goldy <66202304+THEGOLDENPRO@users.noreply.github.com> Date: Sat, 3 Jan 2026 20:43:27 +0000 Subject: [PATCH 09/10] feat: put geo look up logic and dependency behind a feature flag and remove some unnecessary deps --- Cargo.lock | 252 +------------------------ app/Cargo.toml | 25 ++- app/src/windows/info/expensive_data.rs | 83 +------- app/src/windows/info/location.rs | 87 +++++++++ app/src/windows/info/mod.rs | 8 +- 5 files changed, 118 insertions(+), 337 deletions(-) create mode 100644 app/src/windows/info/location.rs diff --git a/Cargo.lock b/Cargo.lock index ea9def3..0bbeb1a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -605,12 +605,6 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b7e4c2464d97fe331d41de9d5db0def0a96f4d823b8b32a2efd503578988973" -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - [[package]] name = "bit-set" version = "0.8.0" @@ -1172,12 +1166,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" -[[package]] -name = "data-url" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376" - [[package]] name = "derive_more" version = "2.1.0" @@ -1426,13 +1414,11 @@ checksum = "550e844e608e356f4ad6843c510aa9bb5838b427e4700ed0056e9746ceeed866" dependencies = [ "ahash", "egui", - "ehttp", "enum-map", "image", "log", "mime_guess2", "profiling", - "resvg", ] [[package]] @@ -1452,20 +1438,6 @@ dependencies = [ "winit", ] -[[package]] -name = "ehttp" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59a81c221a1e4dad06cb9c9deb19aea1193a5eea084e8cd42d869068132bf876" -dependencies = [ - "document-features", - "js-sys", - "ureq", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - [[package]] name = "either" version = "1.15.0" @@ -1623,15 +1595,6 @@ version = "3.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" -[[package]] -name = "euclid" -version = "0.22.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad9cdb4b747e485a12abb0e6566612956c7a1bafa3bdb8d682c5b6d403589e48" -dependencies = [ - "num-traits", -] - [[package]] name = "event-listener" version = "5.4.1" @@ -1732,12 +1695,6 @@ dependencies = [ "miniz_oxide", ] -[[package]] -name = "float-cmp" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" - [[package]] name = "foldhash" version = "0.1.5" @@ -2266,7 +2223,7 @@ dependencies = [ "image-webp", "moxcms", "num-traits", - "png 0.18.0", + "png", "ravif", "rayon", "tiff", @@ -2284,12 +2241,6 @@ dependencies = [ "quick-error", ] -[[package]] -name = "imagesize" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edcd27d72f2f071c64249075f42e205ff93c9a4c5f6c6da53e79ed9f9832c285" - [[package]] name = "imagesize" version = "0.14.0" @@ -2608,17 +2559,6 @@ dependencies = [ "ubyte", ] -[[package]] -name = "kurbo" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c62026ae44756f8a599ba21140f350303d4f08dcdcc71b5ad9c9bb8128c13c62" -dependencies = [ - "arrayvec", - "euclid", - "smallvec", -] - [[package]] name = "lazy_static" version = "1.5.0" @@ -3438,12 +3378,6 @@ dependencies = [ "unicase", ] -[[package]] -name = "pico-args" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" - [[package]] name = "pin-project" version = "1.1.10" @@ -3493,19 +3427,6 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" -[[package]] -name = "png" -version = "0.17.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" -dependencies = [ - "bitflags 1.3.2", - "crc32fast", - "fdeflate", - "flate2", - "miniz_oxide", -] - [[package]] name = "png" version = "0.18.0" @@ -3874,20 +3795,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" -[[package]] -name = "resvg" -version = "0.45.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8928798c0a55e03c9ca6c4c6846f76377427d2c1e1f7e6de3c06ae57942df43" -dependencies = [ - "log", - "pico-args", - "rgb", - "svgtypes", - "tiny-skia", - "usvg", -] - [[package]] name = "reverse_geocoder" version = "4.1.1" @@ -3929,23 +3836,6 @@ name = "rgb" version = "0.8.52" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce" -dependencies = [ - "bytemuck", -] - -[[package]] -name = "ring" -version = "0.17.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" -dependencies = [ - "cc", - "cfg-if", - "getrandom 0.2.16", - "libc", - "untrusted", - "windows-sys 0.52.0", -] [[package]] name = "roseate" @@ -3990,7 +3880,7 @@ version = "0.1.0" dependencies = [ "env_logger", "image", - "imagesize 0.14.0", + "imagesize", "kamadak-exif 0.6.1", "log", "rayon", @@ -4051,41 +3941,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "rustls" -version = "0.23.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f" -dependencies = [ - "log", - "once_cell", - "ring", - "rustls-pki-types", - "rustls-webpki", - "subtle", - "zeroize", -] - -[[package]] -name = "rustls-pki-types" -version = "1.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21e6f2ab2928ca4291b86736a8bd920a277a399bba1589409d72154ff87c1282" -dependencies = [ - "zeroize", -] - -[[package]] -name = "rustls-webpki" -version = "0.103.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" -dependencies = [ - "ring", - "rustls-pki-types", - "untrusted", -] - [[package]] name = "rustversion" version = "1.0.22" @@ -4246,15 +4101,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "832ddd7df0d98d6fd93b973c330b7c8e0742d5cb8f1afc7dea89dba4d2531aa1" -[[package]] -name = "simplecss" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a9c6883ca9c3c7c90e888de77b7a5c849c779d25d74a1269b0218b14e8b136c" -dependencies = [ - "log", -] - [[package]] name = "siphasher" version = "1.0.1" @@ -4392,9 +4238,6 @@ name = "strict-num" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" -dependencies = [ - "float-cmp", -] [[package]] name = "strsim" @@ -4402,12 +4245,6 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - [[package]] name = "svg_metadata" version = "0.5.1" @@ -4420,16 +4257,6 @@ dependencies = [ "roxmltree", ] -[[package]] -name = "svgtypes" -version = "0.15.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68c7541fff44b35860c1a7a47a7cadf3e4a304c457b58f9870d9706ece028afc" -dependencies = [ - "kurbo", - "siphasher", -] - [[package]] name = "syn" version = "2.0.111" @@ -4559,7 +4386,6 @@ dependencies = [ "bytemuck", "cfg-if", "log", - "png 0.17.16", "tiny-skia-path", ] @@ -4774,28 +4600,6 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "402bb19d8e03f1d1a7450e2bd613980869438e0666331be3e073089124aa1adc" -[[package]] -name = "untrusted" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" - -[[package]] -name = "ureq" -version = "2.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d" -dependencies = [ - "base64", - "flate2", - "log", - "once_cell", - "rustls", - "rustls-pki-types", - "url", - "webpki-roots 0.26.11", -] - [[package]] name = "url" version = "2.5.7" @@ -4814,28 +4618,6 @@ version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" -[[package]] -name = "usvg" -version = "0.45.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80be9b06fbae3b8b303400ab20778c80bbaf338f563afe567cf3c9eea17b47ef" -dependencies = [ - "base64", - "data-url", - "flate2", - "imagesize 0.13.0", - "kurbo", - "log", - "pico-args", - "roxmltree", - "simplecss", - "siphasher", - "strict-num", - "svgtypes", - "tiny-skia-path", - "xmlwriter", -] - [[package]] name = "utf8_iter" version = "1.0.4" @@ -5136,24 +4918,6 @@ dependencies = [ "web-sys", ] -[[package]] -name = "webpki-roots" -version = "0.26.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" -dependencies = [ - "webpki-roots 1.0.4", -] - -[[package]] -name = "webpki-roots" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2878ef029c47c6e8cf779119f20fcf52bde7ad42a731b2a304bc221df17571e" -dependencies = [ - "rustls-pki-types", -] - [[package]] name = "weezl" version = "0.1.12" @@ -5921,12 +5685,6 @@ version = "0.8.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" -[[package]] -name = "xmlwriter" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" - [[package]] name = "y4m" version = "0.8.0" @@ -6095,12 +5853,6 @@ dependencies = [ "synstructure", ] -[[package]] -name = "zeroize" -version = "1.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" - [[package]] name = "zerotrie" version = "0.2.3" diff --git a/app/Cargo.toml b/app/Cargo.toml index cc35bb7..ef97fe4 100644 --- a/app/Cargo.toml +++ b/app/Cargo.toml @@ -8,6 +8,17 @@ license = "GPL-3.0" repository = "https://github.com/cloudy-org/roseate/" rust-version = "1.89" +[features] +default = ["full"] + +basic = [] +full = ["basic", "geo"] + +geo = [ + "dep:reverse_geocoder", + "dep:country-emoji", +] + [dependencies] cirrus_egui = { path = "../cirrus/egui" } cirrus_edit = { path = "../cirrus/edit" } @@ -20,8 +31,14 @@ cirrus_softbinds = { path = "../cirrus/soft_binds", features = ["egui"] } roseate-core = { path = "../core" } egui = "=0.33.2" -eframe = { version = "=0.33.2", features = ["default"] } -egui_extras = { version = "=0.33.2", features = ["all_loaders"]} +eframe = { version = "=0.33.2", features = [ + "accesskit", + "default_fonts", + "glow", + "x11", + "wayland", +] } +egui_extras = { version = "=0.33.2", features = ["image"]} egui_animation = "0.10.0" egui-notify = "0.21.0" @@ -42,8 +59,8 @@ serde = {version = "1.0", features = ["derive"]} chrono = "0.4.42" derive_more = { version = "2.1.0", features = ["from", "display", "debug"] } -reverse_geocoder = "4.1.1" -country-emoji = "0.3.2" +reverse_geocoder = {version = "4.1.1", optional = true} +country-emoji = {version = "0.3.2", optional = true} # I've now disabled compiling release builds of dependencies to speed up dev compile time. # diff --git a/app/src/windows/info/expensive_data.rs b/app/src/windows/info/expensive_data.rs index 1815e39..bbe380a 100644 --- a/app/src/windows/info/expensive_data.rs +++ b/app/src/windows/info/expensive_data.rs @@ -1,7 +1,7 @@ -use std::{path::PathBuf, sync::{Arc, Mutex}, thread}; +use std::{path::PathBuf, sync::{Arc, Mutex}}; -use chrono::{DateTime, Local, NaiveDateTime}; use log::debug; +use chrono::{DateTime, Local, NaiveDateTime}; use roseate_core::image_info::metadata::ImageMetadata; use crate::{image_handler::resource::ImageResource}; @@ -102,84 +102,5 @@ impl ExpensiveData { location: Arc::new(Mutex::new(None)) } } - - pub fn start_location_lookup_thread(&mut self, image_metadata: &ImageMetadata) -> &mut Self { - let location = self.location.clone(); - - let image_location_latitude = image_metadata.location.latitude.clone(); - let image_location_longitude = image_metadata.location.longitude.clone(); - - debug!("Spawning location lookup thread..."); - - thread::spawn(move || { - if let Some(latitude) = image_location_latitude - && let Some(longitude) = image_location_longitude { - // Locking at the beginning will tell the image info to display - // "Loading..." while the reverse geocoder initializes and finds the location. - let mut location_mutex = location.lock().unwrap(); - - debug!("Initializing reverse geocoder..."); - let geocoder = reverse_geocoder::ReverseGeocoder::new(); - - debug!( - "Converting coordinates (latitude: {}, longitude: {}) to decimal...", - latitude, longitude, - ); - - let latitude = dms_to_decimal(&latitude); - let longitude = dms_to_decimal(&longitude); - - let result = geocoder.search((latitude, longitude)); - - debug!("Fetching image location country name..."); - - if let Some(country_name) = country_emoji::name(&result.record.cc) { - let formatted_location = format!("{}, {}", result.record.name, country_name); - - let url = format!( - "https://www.openstreetmap.org?mlat={}&mlon={}#map=18/{}/{}", - latitude, longitude, latitude, longitude - ); - - *location_mutex = Some((formatted_location, url)); - } - } - - }); - - self - } } -// NOTE: there was no benefit having this be a macro -fn dms_to_decimal(dms_string: &String) -> f64 { - let parts: Vec<&str> = dms_string - .split(|c: char| !c.is_ascii_digit() && c != '.' && c != '/') - .filter(|s| !s.is_empty()) - .collect(); - - let mut numbers = Vec::new(); - - for part in parts { - if part.contains("/") { - let (first, second) = part.split_once("/").unwrap(); - if let (Ok(first), Ok(second)) = ( - first.parse::(), - second.parse::() - ) { - numbers.push(first / second); - } else { - log::warn!("GPS Data format is unknown: {}", dms_string); - } - } else if let Ok(num) = part.parse::() { - numbers.push(num); - } - } - - match numbers.len() { - 3 => numbers[0] + numbers[1] / 60.0 + numbers[2] / 3600.0, - 2 => numbers[0] + numbers[1] / 60.0, - 1 => numbers[0], - _ => 0.0, - } -} \ No newline at end of file diff --git a/app/src/windows/info/location.rs b/app/src/windows/info/location.rs new file mode 100644 index 0000000..bd17007 --- /dev/null +++ b/app/src/windows/info/location.rs @@ -0,0 +1,87 @@ +use log::debug; +use std::thread; +use roseate_core::image_info::metadata::ImageMetadata; + +use crate::windows::info::expensive_data::ExpensiveData; + +impl ExpensiveData { + pub fn start_location_lookup_thread(&mut self, image_metadata: &ImageMetadata) -> &mut Self { + let location = self.location.clone(); + + let image_location_latitude = image_metadata.location.latitude.clone(); + let image_location_longitude = image_metadata.location.longitude.clone(); + + debug!("Spawning location lookup thread..."); + + thread::spawn(move || { + if let Some(latitude) = image_location_latitude + && let Some(longitude) = image_location_longitude { + // Locking at the beginning will tell the image info to display + // "Loading..." while the reverse geocoder initializes and finds the location. + let mut location_mutex = location.lock().unwrap(); + + debug!("Initializing reverse geocoder..."); + let geocoder = reverse_geocoder::ReverseGeocoder::new(); + + debug!( + "Converting coordinates (latitude: {}, longitude: {}) to decimal...", + latitude, longitude, + ); + + let latitude = dms_to_decimal(&latitude); + let longitude = dms_to_decimal(&longitude); + + let result = geocoder.search((latitude, longitude)); + + debug!("Fetching image location country name..."); + + if let Some(country_name) = country_emoji::name(&result.record.cc) { + let formatted_location = format!("{}, {}", result.record.name, country_name); + + let url = format!( + "https://www.openstreetmap.org?mlat={}&mlon={}#map=18/{}/{}", + latitude, longitude, latitude, longitude + ); + + *location_mutex = Some((formatted_location, url)); + } + } + + }); + + self + } +} + +// NOTE: there was no benefit having this be a macro +fn dms_to_decimal(dms_string: &String) -> f64 { + let parts: Vec<&str> = dms_string + .split(|c: char| !c.is_ascii_digit() && c != '.' && c != '/') + .filter(|s| !s.is_empty()) + .collect(); + + let mut numbers = Vec::new(); + + for part in parts { + if part.contains("/") { + let (first, second) = part.split_once("/").unwrap(); + if let (Ok(first), Ok(second)) = ( + first.parse::(), + second.parse::() + ) { + numbers.push(first / second); + } else { + log::warn!("GPS Data format is unknown: {}", dms_string); + } + } else if let Ok(num) = part.parse::() { + numbers.push(num); + } + } + + match numbers.len() { + 3 => numbers[0] + numbers[1] / 60.0 + numbers[2] / 3600.0, + 2 => numbers[0] + numbers[1] / 60.0, + 1 => numbers[0], + _ => 0.0, + } +} \ No newline at end of file diff --git a/app/src/windows/info/mod.rs b/app/src/windows/info/mod.rs index 06d9565..dd7044d 100644 --- a/app/src/windows/info/mod.rs +++ b/app/src/windows/info/mod.rs @@ -1,5 +1,3 @@ -mod expensive_data; - use std::{alloc, sync::{Arc, TryLockError}}; use cap::Cap; @@ -9,6 +7,11 @@ use roseate_core::image_info::{info::ImageInfo}; use crate::{image::Image, image_handler::{optimization::ImageOptimizations, resource::ImageResource}, windows::info::expensive_data::ExpensiveData}; +#[cfg(feature = "geo")] +mod location; + +mod expensive_data; + #[global_allocator] static ALLOCATOR: Cap = Cap::new(alloc::System, usize::max_value()); @@ -50,6 +53,7 @@ impl ImageInfoWindow { &image_info.metadata ); + #[cfg(feature = "geo")] if show_location_in_image_info { data.start_location_lookup_thread( &image_info.metadata From f97343bfd06d99f5353fd3d131d13358df377208 Mon Sep 17 00:00:00 2001 From: Goldy <66202304+THEGOLDENPRO@users.noreply.github.com> Date: Sat, 3 Jan 2026 22:20:46 +0000 Subject: [PATCH 10/10] feat: improve image info layout and spacing --- app/src/windows/info/mod.rs | 182 +++++++++++++++++++++--------------- 1 file changed, 106 insertions(+), 76 deletions(-) diff --git a/app/src/windows/info/mod.rs b/app/src/windows/info/mod.rs index dd7044d..9f096a6 100644 --- a/app/src/windows/info/mod.rs +++ b/app/src/windows/info/mod.rs @@ -2,7 +2,7 @@ use std::{alloc, sync::{Arc, TryLockError}}; use cap::Cap; use eframe::egui::{self, Response}; -use egui::{Color32, CursorIcon, Label, OpenUrl, Pos2, RichText, TextureHandle, Ui, WidgetText}; +use egui::{Color32, CursorIcon, Label, Margin, OpenUrl, Pos2, RichText, TextureHandle, Ui, Vec2, WidgetText}; use roseate_core::image_info::{info::ImageInfo}; use crate::{image::Image, image_handler::{optimization::ImageOptimizations, resource::ImageResource}, windows::info::expensive_data::ExpensiveData}; @@ -125,45 +125,60 @@ impl ImageInfoWindow { egui::Image::from_texture(texture) // 16 is the padding from // the image optimizations grid - .max_size([200.0 + 16.0, 140.0].into()) + .max_size([180.0 + 16.0, 140.0].into()) .corner_radius(8) ); } ui.add_space(5.0); - egui::ScrollArea::vertical() - .min_scrolled_height(150.0) - .show(ui, |ui| { - Self::show_image_optimizations_grid(ui, image_optimizations); - }); + ui.scope(|ui| { + let grid_response = egui::ScrollArea::vertical() + .id_salt("image_optimizations_and_misc_info") + .min_scrolled_height(150.0) + .show(ui, |ui| { + Self::show_image_optimizations_grid( + ui, image_optimizations + ) + }).inner; + + ui.add_space(5.0); + + ui.set_max_width(grid_response.rect.width()); + ui.add(egui::Separator::default().shrink(40.0)); + + egui::Frame::default() + .outer_margin(Margin {left: 3, top: 5, ..Default::default()}) + .show(ui, |ui| { + Self::show_misc_info_grid( + ui, + image_info_data, + image, + image_info, + soon_text.clone(), + app_memory_allocated as f64, + ); + }); + }); }); - ui.add(egui::Separator::default().grow(4.0)); + ui.add(egui::Separator::default().spacing(0.0)); ui.vertical(|ui| { - Self::show_image_info_grid( - ui, - image_info_data, - image, - image_info, - 180.0, - soon_text.clone(), - show_extra, - show_location_in_image_info, - ); - - ui.separator(); - - Self::show_misc_info_grid( - ui, - image_info_data, - image, - image_info, - 180.0, - soon_text.clone(), - app_memory_allocated as f64, - ); + egui::ScrollArea::vertical() + .id_salt("base_image_info_scroll_area") + .show_viewport(ui, |ui, _| { + Self::show_image_info_grid( + ui, + image_info_data, + image, + image_info, + 160.0, + soon_text.clone(), + show_extra, + show_location_in_image_info, + ); + }); }); }, false => { @@ -186,7 +201,7 @@ impl ImageInfoWindow { }).unwrap().response } - fn show_image_optimizations_grid(ui: &mut Ui, image_optimizations: &ImageOptimizations) { + fn show_image_optimizations_grid(ui: &mut Ui, image_optimizations: &ImageOptimizations) -> Response { egui::Frame::default() .inner_margin(8) .corner_radius(8) @@ -195,6 +210,7 @@ impl ImageInfoWindow { egui::Grid::new("image_optimizations_grid") .max_col_width(120.0) + .spacing(Vec2::new(0.0, 5.0)) .striped(false) .show(ui, |ui| { // I'm using let Some() because in the future @@ -230,7 +246,7 @@ impl ImageInfoWindow { } }); - }); + }).response } fn show_image_info_grid( @@ -303,6 +319,10 @@ impl ImageInfoWindow { ui.end_row(); if show_extra { + if show_location_in_image_info { + Self::show_location_field(ui, expensive_data); + } + ui_non_select_label(ui, "Camera:"); ui.label(rich_text_or_unknown!("{}", &image_info.metadata.model)); ui.end_row(); @@ -322,38 +342,6 @@ impl ImageInfoWindow { ui_non_select_label(ui, "Exposure Time:"); ui.label(rich_text_or_unknown!("{}s", &image_info.metadata.exposure_time)); ui.end_row(); - - if show_location_in_image_info { - ui_non_select_label(ui, "Location:"); - match expensive_data.location.try_lock() { - Ok(location_lock) => { - match location_lock.as_ref() { - Some(location) => { - let button = ui.button(&location.0) - .on_hover_cursor(CursorIcon::PointingHand); - - if button.clicked() { - ui.ctx().open_url( - OpenUrl::new_tab(&location.1) - ); - } - }, - None => { - ui.label(RichText::new("Unknown").weak()); - }, - } - }, - Err(error) => { - ui.label(RichText::new("Loading...").italics()); - - if let TryLockError::Poisoned(error) = error { - log::error!( - "Thread spawned to perform location lookup on image got poisoned! Error: {error}" - ); - } - }, - }; - } } }); } @@ -363,35 +351,77 @@ impl ImageInfoWindow { expensive_data: &ExpensiveData, image: &Image, image_info: &ImageInfo, - max_grid_width: f32, soon_text: Arc, app_memory_allocated: f64, ) { egui::Grid::new("misc_image_info_grid") - .max_col_width(max_grid_width) .striped(false) .show(ui, |ui| { - let mem_allocation_hint = "How much memory has been allocated to the entire application \ - (this includes the decoded image, if it's still in memory)."; + let font_size: f32 = 12.0; + + let mem_allocation_hint = "How much memory has been allocated to the entire \ + application (this includes the decoded image, if it's still in memory)."; - ui_non_select_label(ui, "App Mem Alloc:") - .on_hover_text(mem_allocation_hint); - ui.label(RichText::new(re_format::format_bytes(app_memory_allocated))) - .on_hover_text(mem_allocation_hint); + ui_non_select_label( + ui, + RichText::new("App Mem Alloc:").size(font_size) + ).on_hover_text(mem_allocation_hint); + ui.label( + RichText::new( + re_format::format_bytes(app_memory_allocated) + ).size(font_size) + ).on_hover_text(mem_allocation_hint); ui.end_row(); - let mem_allocation_by_image_hint = "How much memory has been allocated to display the image on the GPU."; + let mem_allocation_by_image_hint = "How much memory has been allocated to \ + display the image on the GPU."; - ui_non_select_label(ui, "Image Mem Alloc:") - .on_hover_text(mem_allocation_by_image_hint); + ui_non_select_label( + ui, + RichText::new("Image Mem Alloc:").size(font_size) + ).on_hover_text(mem_allocation_by_image_hint); ui.label( RichText::new(re_format::format_bytes( expensive_data.memory_allocated_for_image) - ) + ).size(font_size) ).on_hover_text(mem_allocation_by_image_hint); ui.end_row(); }); } + + fn show_location_field(ui: &mut Ui, expensive_data: &ExpensiveData) { + ui_non_select_label(ui, "Location:"); + match expensive_data.location.try_lock() { + Ok(location_lock) => { + match location_lock.as_ref() { + Some(location) => { + let button = ui.button(&location.0) + .on_hover_cursor(CursorIcon::PointingHand); + + if button.clicked() { + ui.ctx().open_url( + OpenUrl::new_tab(&location.1) + ); + } + }, + None => { + ui.label(RichText::new("Unknown").weak()); + }, + } + }, + Err(error) => { + ui.label(RichText::new("Loading...").italics()); + + if let TryLockError::Poisoned(error) = error { + log::error!( + "Thread spawned to perform location lookup on image got poisoned! Error: {error}" + ); + } + }, + }; + + ui.end_row(); + } } fn ui_non_select_label(ui: &mut Ui, text: impl Into) -> Response {