diff --git a/frontend/api/api-functions/images.ts b/frontend/api/api-functions/images.ts index 783dc465..e827e603 100644 --- a/frontend/api/api-functions/images.ts +++ b/frontend/api/api-functions/images.ts @@ -35,7 +35,7 @@ const parseAndSortImageData = (data: APIResponse['data']): Image[] => { extractThumbnailPath(data.folder_path, src), ); return { - imagePath:src, + imagePath: src, title: src.substring(src.lastIndexOf('\\') + 1), thumbnailUrl, url, @@ -53,7 +53,8 @@ export const getAllImageObjects = async () => { const newObj = { data: parsedAndSortedImages, success: data.success, - error: data.error, + error: data?.error, + message: data?.message, }; return newObj; diff --git a/frontend/package.json b/frontend/package.json index a6af0937..a0c049ed 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -34,6 +34,7 @@ "react-dnd": "^16.0.1", "react-dnd-html5-backend": "^16.0.1", "react-dom": "^18.2.0", + "react-image-crop": "^11.0.7", "react-icons": "^5.4.0", "react-router-dom": "^6.24.1", "tailwind-merge": "^2.3.0", diff --git a/frontend/src-tauri/Cargo.lock b/frontend/src-tauri/Cargo.lock index 768ad00c..a833a60c 100644 --- a/frontend/src-tauri/Cargo.lock +++ b/frontend/src-tauri/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "Pictopy" @@ -8,6 +8,7 @@ version = "0.0.0" dependencies = [ "anyhow", "chrono", + "image", "serde_json", "tauri", "tauri-build", @@ -33,6 +34,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + [[package]] name = "aho-corasick" version = "1.1.3" @@ -260,7 +267,7 @@ dependencies = [ "cc", "cfg-if", "libc", - "miniz_oxide", + "miniz_oxide 0.7.3", "object", "rustc-demangle", ] @@ -277,6 +284,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bit_field" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61" + [[package]] name = "bitflags" version = "1.3.2" @@ -534,6 +547,12 @@ dependencies = [ "objc", ] +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + [[package]] name = "combine" version = "4.6.7" @@ -626,12 +645,37 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + [[package]] name = "crypto-common" version = "0.1.6" @@ -844,6 +888,12 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + [[package]] name = "embed-resource" version = "2.4.2" @@ -947,6 +997,21 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "exr" +version = "1.73.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83197f59927b46c04a183a619b7c29df34e63e63c7869320862268c0ef687e0" +dependencies = [ + "bit_field", + "half", + "lebe", + "miniz_oxide 0.8.2", + "rayon-core", + "smallvec", + "zune-inflate", +] + [[package]] name = "fastrand" version = "2.1.0" @@ -979,7 +1044,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" dependencies = [ "crc32fast", - "miniz_oxide", + "miniz_oxide 0.7.3", ] [[package]] @@ -1272,6 +1337,16 @@ dependencies = [ "wasi 0.11.0+wasi-snapshot-preview1", ] +[[package]] +name = "gif" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb2d69b19215e18bb912fa30f7ce15846e301408695e44e0ef719f1da9e19f2" +dependencies = [ + "color_quant", + "weezl", +] + [[package]] name = "gimli" version = "0.29.0" @@ -1426,6 +1501,16 @@ dependencies = [ "syn 2.0.66", ] +[[package]] +name = "half" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" +dependencies = [ + "cfg-if", + "crunchy", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -1616,6 +1701,24 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "image" +version = "0.24.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "exr", + "gif", + "jpeg-decoder", + "num-traits", + "png", + "qoi", + "tiff", +] + [[package]] name = "indexmap" version = "1.9.3" @@ -1738,6 +1841,15 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +[[package]] +name = "jpeg-decoder" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" +dependencies = [ + "rayon", +] + [[package]] name = "js-sys" version = "0.3.69" @@ -1788,6 +1900,12 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +[[package]] +name = "lebe" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" + [[package]] name = "libappindicator" version = "0.9.0" @@ -1956,6 +2074,15 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "miniz_oxide" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394" +dependencies = [ + "adler2", +] + [[package]] name = "mio" version = "0.8.11" @@ -2565,7 +2692,7 @@ dependencies = [ "crc32fast", "fdeflate", "flate2", - "miniz_oxide", + "miniz_oxide 0.7.3", ] [[package]] @@ -2660,6 +2787,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + [[package]] name = "quick-xml" version = "0.31.0" @@ -2771,6 +2907,26 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "redox_syscall" version = "0.5.1" @@ -3805,6 +3961,17 @@ dependencies = [ "once_cell", ] +[[package]] +name = "tiff" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e" +dependencies = [ + "flate2", + "jpeg-decoder", + "weezl", +] + [[package]] name = "time" version = "0.3.36" @@ -4424,6 +4591,12 @@ dependencies = [ "windows-core 0.57.0", ] +[[package]] +name = "weezl" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" + [[package]] name = "winapi" version = "0.3.9" @@ -4904,6 +5077,15 @@ dependencies = [ "zvariant", ] +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] + [[package]] name = "zvariant" version = "4.0.0" diff --git a/frontend/src-tauri/Cargo.toml b/frontend/src-tauri/Cargo.toml index 09b38712..363cf6b5 100644 --- a/frontend/src-tauri/Cargo.toml +++ b/frontend/src-tauri/Cargo.toml @@ -20,6 +20,7 @@ tauri-plugin-shell = "2.0.0-beta.5" tauri-plugin-dialog = "2.0.0-beta.9" tauri-plugin-store = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" } chrono = "0.4" +image = "0.24.6" [features] # This feature is used for production builds or when a dev server is not specified, DO NOT REMOVE!! diff --git a/frontend/src-tauri/capabilities/migrated.json b/frontend/src-tauri/capabilities/migrated.json index 8555932d..ad742f2c 100644 --- a/frontend/src-tauri/capabilities/migrated.json +++ b/frontend/src-tauri/capabilities/migrated.json @@ -12,7 +12,6 @@ "menu:default", "tray:default", "shell:allow-open", - "fs:default", "path:default", "event:default", "window:default", @@ -22,6 +21,60 @@ "tray:default", "window:allow-set-title", "dialog:allow-open", + "fs:default", + "fs:allow-app-write", + "fs:allow-app-write-recursive", + "fs:allow-appcache-write", + "fs:allow-appcache-write-recursive", + "fs:allow-appconfig-write", + "fs:allow-appconfig-write-recursive", + "fs:allow-appdata-write", + "fs:allow-appdata-write-recursive", + "fs:allow-applocaldata-write", + "fs:allow-applocaldata-write-recursive", + "fs:allow-applog-write", + "fs:allow-applog-write-recursive", + "fs:allow-audio-write", + "fs:allow-audio-write-recursive", + "fs:allow-cache-write", + "fs:allow-cache-write-recursive", + "fs:allow-config-write", + "fs:allow-config-write-recursive", + "fs:allow-create", + "fs:allow-data-write", + "fs:allow-data-write-recursive", + "fs:allow-desktop-write", + "fs:allow-desktop-write-recursive", + "fs:allow-document-write", + "fs:allow-document-write-recursive", + "fs:allow-download-write", + "fs:allow-download-write-recursive", + "fs:allow-exe-write", + "fs:allow-exe-write-recursive", + "fs:allow-font-write", + "fs:allow-font-write-recursive", + "fs:allow-home-write", + "fs:allow-home-write-recursive", + "fs:allow-localdata-write", + "fs:allow-localdata-write-recursive", + "fs:allow-log-write", + "fs:allow-log-write-recursive", + "fs:allow-picture-write", + "fs:allow-picture-write-recursive", + "fs:allow-public-write", + "fs:allow-public-write-recursive", + "fs:allow-resource-write", + "fs:allow-resource-write-recursive", + "fs:allow-runtime-write", + "fs:allow-runtime-write-recursive", + "fs:allow-temp-write", + "fs:allow-temp-write-recursive", + "fs:allow-template-write", + "fs:allow-template-write-recursive", + "fs:allow-video-write", + "fs:allow-video-write-recursive", + "fs:write-all", + "fs:write-files", { "identifier": "fs:scope", "allow": ["**"] diff --git a/frontend/src-tauri/src/main.rs b/frontend/src-tauri/src/main.rs index c07f3fa6..695505d6 100644 --- a/frontend/src-tauri/src/main.rs +++ b/frontend/src-tauri/src/main.rs @@ -1,14 +1,13 @@ // Prevents additional console window on Windows in release, DO NOT REMOVE!! #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] -// Learn more about Tauri commands at https://tauri.app/v1/guides/features/command -use tauri::Manager; mod repositories; mod services; mod models; mod utils; use crate::services::{FileService, CacheService}; +use tauri::Manager; fn main() { tauri::Builder::default() @@ -28,6 +27,8 @@ fn main() { services::get_all_images_with_cache, services::get_all_videos_with_cache, services::delete_cache, + services::share_file, + services::save_edited_image, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/frontend/src-tauri/src/services/mod.rs b/frontend/src-tauri/src/services/mod.rs index 068dac2e..8128e25f 100644 --- a/frontend/src-tauri/src/services/mod.rs +++ b/frontend/src-tauri/src/services/mod.rs @@ -7,6 +7,7 @@ mod file_service; pub use cache_service::CacheService; use chrono::{DateTime, Datelike, Utc}; pub use file_service::FileService; +use image::{DynamicImage, GenericImageView, ImageBuffer, Rgba}; #[tauri::command] pub fn get_folders_with_images( @@ -171,6 +172,119 @@ pub fn get_all_videos_with_cache( Ok(videos_by_year_month) } +#[tauri::command] +pub async fn share_file(path: String) -> Result<(), String> { + use std::process::Command; + + #[cfg(target_os = "windows")] + { + Command::new("explorer") + .args(["/select,", &path]) + .spawn() + .map_err(|e| e.to_string())?; + } + + #[cfg(target_os = "macos")] + { + Command::new("open") + .args(["-R", &path]) + .spawn() + .map_err(|e| e.to_string())?; + } + + #[cfg(target_os = "linux")] + { + Command::new("xdg-open") + .arg(&path) + .spawn() + .map_err(|e| e.to_string())?; + } + + Ok(()) +} + +#[tauri::command] +pub async fn save_edited_image( + image_data: Vec, + original_path: String, + filter: String, + brightness: i32, + contrast: i32, +) -> Result<(), String> { + let mut img = image::load_from_memory(&image_data).map_err(|e| e.to_string())?; + + // Apply filter + match filter.as_str() { + "grayscale(100%)" => img = img.grayscale(), + "sepia(100%)" => img = apply_sepia(&img), + "invert(100%)" => img.invert(), + _ => {} + } + + // Apply brightness and contrast + img = adjust_brightness_contrast(&img, brightness, contrast); + + // Save the edited image + let path = PathBuf::from(original_path); + let file_stem = path.file_stem().unwrap_or_default(); + let extension = path.extension().unwrap_or_default(); + + let mut edited_path = path.clone(); + edited_path.set_file_name(format!( + "{}_edited.{}", + file_stem.to_string_lossy(), + extension.to_string_lossy() + )); + + img.save(&edited_path).map_err(|e| e.to_string())?; + + Ok(()) +} + +fn apply_sepia(img: &DynamicImage) -> DynamicImage { + let (width, height) = img.dimensions(); + let mut sepia_img = ImageBuffer::new(width, height); + + for (x, y, pixel) in img.pixels() { + let r = pixel[0] as f32; + let g = pixel[1] as f32; + let b = pixel[2] as f32; + + let sepia_r = (0.393 * r + 0.769 * g + 0.189 * b).min(255.0) as u8; + let sepia_g = (0.349 * r + 0.686 * g + 0.168 * b).min(255.0) as u8; + let sepia_b = (0.272 * r + 0.534 * g + 0.131 * b).min(255.0) as u8; + + sepia_img.put_pixel(x, y, Rgba([sepia_r, sepia_g, sepia_b, pixel[3]])); + } + + DynamicImage::ImageRgba8(sepia_img) +} + +fn adjust_brightness_contrast(img: &DynamicImage, brightness: i32, contrast: i32) -> DynamicImage { + let (width, height) = img.dimensions(); + let mut adjusted_img = ImageBuffer::new(width, height); + + let brightness_factor = brightness as f32 / 100.0; + let contrast_factor = contrast as f32 / 100.0; + + for (x, y, pixel) in img.pixels() { + let mut new_pixel = [0; 4]; + for c in 0..3 { + let mut color = pixel[c] as f32; + // Apply brightness + color += 255.0 * (brightness_factor - 1.0); + // Apply contrast + color = (color - 128.0) * contrast_factor + 128.0; + new_pixel[c] = color.max(0.0).min(255.0) as u8; + } + new_pixel[3] = pixel[3]; // Keep original alpha + + adjusted_img.put_pixel(x, y, Rgba(new_pixel)); + } + + DynamicImage::ImageRgba8(adjusted_img) +} + #[tauri::command] pub fn delete_cache(cache_service: State<'_, CacheService>) -> bool { cache_service.delete_all_caches() diff --git a/frontend/src-tauri/tauri.conf.json b/frontend/src-tauri/tauri.conf.json index 6db42328..d3f55b8c 100644 --- a/frontend/src-tauri/tauri.conf.json +++ b/frontend/src-tauri/tauri.conf.json @@ -21,6 +21,9 @@ "version": "0.0.0", "identifier": "com.yourcompany.pictopy", "plugins": { + "fs": { + "active": true + }, "updater": { "active": true, "endpoints": ["https://server.com//api/v1/tauri/update"], diff --git a/frontend/src/components/AITagging/AIgallery.tsx b/frontend/src/components/AITagging/AIgallery.tsx index 1dc8c084..2589cf34 100644 --- a/frontend/src/components/AITagging/AIgallery.tsx +++ b/frontend/src/components/AITagging/AIgallery.tsx @@ -49,6 +49,12 @@ export default function AIGallery({ const filteredMediaItems = useMemo(() => { return filterTag + ? mediaItems?.filter((mediaItem: any) => + mediaItem?.tags?.includes(filterTag), + ) + : mediaItems; + }, [filterTag, mediaItems, loading]); + const [pageNo, setpageNo] = useState(20); ? mediaItems.filter((mediaItem: any) => mediaItem.tags?.includes(filterTag)) : mediaItems; }, [filterTag, mediaItems]); @@ -111,11 +117,15 @@ export default function AIGallery({ type={type} />
+ + + {/* Dropdown Menu - Right-Aligned */} +
@@ -123,6 +133,24 @@ export default function AIGallery({ variant="outline" className="flex items-center gap-2 border-gray-500 hover:bg-accent dark:hover:bg-white/10" > +

+ Num of images per page : {pageNo} +

+ +
+ + setpageNo(Number(value))} + > + {noOfPages.map((itemsPerPage) => ( +

Num of images per page: {pageNo}

@@ -148,7 +176,9 @@ export default function AIGallery({ item.url)} + allMedia={filteredMediaItems.map((item: any) => { + return { url: item.url, path: item?.imagePath }; + })} currentPage={currentPage} itemsPerPage={pageNo} type={type} diff --git a/frontend/src/components/Album/Album.tsx b/frontend/src/components/Album/Album.tsx index 4727023a..b3875fb8 100644 --- a/frontend/src/components/Album/Album.tsx +++ b/frontend/src/components/Album/Album.tsx @@ -15,13 +15,11 @@ import { } from '../../../api/api-functions/albums'; const AlbumsView: React.FC = () => { - const { successData: albums, isLoading, error } = usePictoQuery({ - queryFn: fetchAllAlbums, queryKey: ['all-albums'], }); - + const { mutate: deleteAlbum } = usePictoMutation({ mutationFn: deleteAlbums, autoInvalidateTags: ['all-albums'], @@ -39,9 +37,15 @@ const AlbumsView: React.FC = () => { if (isLoading) { return ; } + const showErrorDialog = (title: string, err: unknown) => { + setErrorDialogContent({ + title, + description: + err instanceof Error ? err.message : 'An unknown error occurred', + }); + }; if (!albums || albums.length === 0) { - return (
@@ -61,10 +65,7 @@ const AlbumsView: React.FC = () => { onSuccess={() => { setIsCreateFormOpen(false); }} - onError={(err) => showErrorDialog("Error", err)} - - /> = ({ image.url)} + allMedia={convertedImagePaths.map((image) => { + return { url: image.url }; + })} currentPage={1} itemsPerPage={albumData.photos.length} type="image" diff --git a/frontend/src/components/Media/MediaGallery.tsx b/frontend/src/components/Media/MediaGallery.tsx index f1cc477c..3a249587 100644 --- a/frontend/src/components/Media/MediaGallery.tsx +++ b/frontend/src/components/Media/MediaGallery.tsx @@ -71,7 +71,9 @@ export default function MediaGallery({ item.url)} + allMedia={sortedMedia.map((item) => { + return { url: item.url, path: item?.imagePath }; + })} currentPage={currentPage} itemsPerPage={itemsPerPage} type={type} diff --git a/frontend/src/components/Media/MediaView.tsx b/frontend/src/components/Media/MediaView.tsx index 57f57a79..3c8ad7b6 100644 --- a/frontend/src/components/Media/MediaView.tsx +++ b/frontend/src/components/Media/MediaView.tsx @@ -1,4 +1,20 @@ import { MediaViewProps } from '@/types/Media'; +import React, { useEffect, useState, useCallback } from 'react'; +import { + ChevronLeft, + ChevronRight, + Edit, + Share2, + Check, + X, + SunMoon, + Contrast, +} from 'lucide-react'; +import ReactCrop, { Crop } from 'react-image-crop'; +import 'react-image-crop/dist/ReactCrop.css'; +import { invoke } from '@tauri-apps/api/core'; +import { readFile } from '@tauri-apps/plugin-fs'; + import React, { useEffect, useState } from 'react'; import { ChevronLeft, ChevronRight, X, Play, Pause, RotateCw, Heart, Share2, ZoomIn, ZoomOut } from 'lucide-react'; @@ -27,6 +43,16 @@ const MediaView: React.FC = ({ const [position, setPosition] = useState({ x: 0, y: 0 }); const [isDragging, setIsDragging] = useState(false); const [dragStart, setDragStart] = useState({ x: 0, y: 0 }); + const [isEditing, setIsEditing] = useState(false); + const [crop, setCrop] = useState(); + const [completedCrop, setCompletedCrop] = useState(); + const [filter, setFilter] = useState(''); + const [brightness, setBrightness] = useState(100); + const [contrast, setContrast] = useState(100); + const [notification, setNotification] = useState<{ + message: string; + type: 'success' | 'error'; + } | null>(null); const [isSlideshowActive, setIsSlideshowActive] = useState(false); const [rotation, setRotation] = useState(0); const [favorites, setFavorites] = useState(() => { @@ -125,6 +151,156 @@ const MediaView: React.FC = ({ setRotation(0); }; + useEffect(() => { + const element = document.getElementById('zoomable-image'); + element?.addEventListener('wheel', handleWheel, { passive: false }); + return () => element?.removeEventListener('wheel', handleWheel); + }, [scale]); + + function handlePrevItem() { + if (globalIndex > 0) { + setGlobalIndex(globalIndex - 1); + } else { + setGlobalIndex(allMedia.length - 1); + } + resetZoom(); + resetEditing(); + } + + function handleNextItem() { + if (globalIndex < allMedia.length - 1) { + setGlobalIndex(globalIndex + 1); + } else { + setGlobalIndex(0); + } + resetZoom(); + resetEditing(); + } + + const resetEditing = () => { + setIsEditing(false); + setCrop(undefined); + setCompletedCrop(undefined); + setFilter(''); + setBrightness(100); + setContrast(100); + setPosition({ x: 0, y: 0 }); + setScale(1); + }; + + const showNotification = (message: string, type: 'success' | 'error') => { + setNotification({ message, type }); + setTimeout(() => setNotification(null), 5000); + console.log(`Notification: ${type} - ${message}`); // Add console logging for debugging + }; + + const handleShare = async () => { + try { + const filePath = allMedia[globalIndex].path; + await invoke('share_file', { path: filePath }); + showNotification('File shared successfully', 'success'); + } catch (err: any) { + showNotification(`Failed to share: ${err}`, 'error'); + } + }; + + const handleEditComplete = useCallback(async () => { + console.log('Starting handleEditComplete'); + + try { + // Read the image file using Tauri's filesystem API + const imageData = await readFile(allMedia[globalIndex].path || ''); + console.log('Image file read successfully'); + + // Create a Blob from the file data + const blob = new Blob([imageData], { type: 'image/png' }); + const imageUrl = URL.createObjectURL(blob); + + // Create an image element to load the file + const img = new Image(); + img.src = imageUrl; + await new Promise((resolve) => { + img.onload = resolve; + }); + + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + + if (!ctx) { + throw new Error('Unable to create image context'); + } + + if (completedCrop) { + canvas.width = completedCrop.width; + canvas.height = completedCrop.height; + ctx.drawImage( + img, + completedCrop.x, + completedCrop.y, + completedCrop.width, + completedCrop.height, + 0, + 0, + completedCrop.width, + completedCrop.height, + ); + } else { + canvas.width = img.width; + canvas.height = img.height; + ctx.drawImage(img, 0, 0); + } + + ctx.filter = `${filter} brightness(${brightness}%) contrast(${contrast}%)`; + ctx.drawImage(img, 0, 0, canvas.width, canvas.height); + + console.log('Canvas prepared, attempting to create blob'); + + const editedBlob = await new Promise((resolve) => { + canvas.toBlob(resolve, 'image/png'); + }); + + if (!editedBlob) { + throw new Error('Failed to create edited image blob'); + } + + console.log('Edited blob created successfully'); + + const arrayBuffer = await editedBlob.arrayBuffer(); + const uint8Array = new Uint8Array(arrayBuffer); + + console.log('Invoking save_edited_image'); + await invoke('save_edited_image', { + imageData: Array.from(uint8Array), + originalPath: allMedia[globalIndex].path, + filter, + brightness, + contrast, + }); + + console.log('Image saved successfully'); + showNotification('Image saved successfully', 'success'); + + // Clean up the object URL + URL.revokeObjectURL(imageUrl); + } catch (error) { + console.error('Error in handleEditComplete:', error); + showNotification(`Failed to save edited image: ${error}`, 'error'); + } + + setIsEditing(false); + }, [ + completedCrop, + filter, + brightness, + contrast, + allMedia, + globalIndex, + showNotification, + ]); + + return ( +
+
const handlePrevItem = () => { setGlobalIndex(globalIndex > 0 ? globalIndex - 1 : allMedia.length - 1); resetZoom(); @@ -198,14 +374,133 @@ const MediaView: React.FC = ({ > {type === 'image' ? (
{ + if (e.target === e.currentTarget) { + onClose(); + } + }} onMouseDown={handleMouseDown} onMouseMove={handleMouseMove} onMouseUp={handleMouseUp} onMouseLeave={handleMouseUp} className="relative h-full w-full flex items-center justify-center overflow-hidden" > + {isEditing ? ( + setCrop(c)} + onComplete={(c) => setCompletedCrop(c)} + > + {`image-${globalIndex}`} + + ) : ( + {`image-${globalIndex}`} + )} +
+ {isEditing ? ( + <> + + + +
+ + setBrightness(Number(e.target.value))} + className="w-24" + /> +
+
+ + setContrast(Number(e.target.value))} + className="w-24" + /> +
+ + ) : ( + <> + + + + {allMedia[globalIndex]?.path && ( + <> + + + + )} + + )} {`media-${globalIndex}`} = ({
) : (
))}
+ {notification && ( +
+ {notification.message} +
+ )}
); }; diff --git a/frontend/src/hooks/useImages.ts b/frontend/src/hooks/useImages.ts index 1410df6e..c594cd28 100644 --- a/frontend/src/hooks/useImages.ts +++ b/frontend/src/hooks/useImages.ts @@ -11,6 +11,7 @@ interface ImageData { title: string; date: string; tags: string[]; + imagePath: string; } interface ResponseData { @@ -105,6 +106,7 @@ export const useImages = (folderPath: string) => { return { original, url, + imagePath, thumbnailUrl, title: `Image ${imagePath}`, date, diff --git a/frontend/src/types/Media.ts b/frontend/src/types/Media.ts index 3219c37d..b037a01a 100644 --- a/frontend/src/types/Media.ts +++ b/frontend/src/types/Media.ts @@ -5,7 +5,7 @@ export interface MediaItem { date?: string; title?: string; tags?: string[]; - imagePath:string; + imagePath: string; } export interface MediaCardProps { item: MediaItem; @@ -27,7 +27,7 @@ export interface MediaGridProps { export interface MediaViewProps { initialIndex: number; onClose: () => void; - allMedia: string[]; + allMedia: { url: string; path?: string }[]; currentPage: number; itemsPerPage: number; type: 'image' | 'video';