From 93d666235bc2f5e1c8e13cdc128213ee1effc925 Mon Sep 17 00:00:00 2001 From: rishab Date: Fri, 3 Jan 2025 21:22:03 +0530 Subject: [PATCH 1/2] added-edit-share --- frontend/api/api-functions/images.ts | 6 +- frontend/package.json | 1 + frontend/src-tauri/Cargo.lock | 2 +- frontend/src-tauri/capabilities/migrated.json | 55 ++- frontend/src-tauri/src/main.rs | 5 +- frontend/src-tauri/src/services/mod.rs | 60 +++ frontend/src-tauri/tauri.conf.json | 13 +- .../src/components/AITagging/AIgallery.tsx | 92 ++--- frontend/src/components/Album/Album.tsx | 9 +- frontend/src/components/Album/Albumview.tsx | 4 +- .../src/components/Media/MediaGallery.tsx | 4 +- frontend/src/components/Media/MediaView.tsx | 342 +++++++++++++++--- frontend/src/hooks/useImages.ts | 2 + frontend/src/types/Media.ts | 4 +- 14 files changed, 485 insertions(+), 114 deletions(-) diff --git a/frontend/api/api-functions/images.ts b/frontend/api/api-functions/images.ts index 783dc465..10e91b25 100644 --- a/frontend/api/api-functions/images.ts +++ b/frontend/api/api-functions/images.ts @@ -2,6 +2,7 @@ import { imagesEndpoints } from '../apiEndpoints'; import { convertFileSrc } from '@tauri-apps/api/core'; import { APIResponse, Image } from '../../src/types/image'; import { extractThumbnailPath } from '@/hooks/useImages'; +import { message } from '@tauri-apps/plugin-dialog'; export const fetchAllImages = async () => { const response = await fetch(imagesEndpoints.allImages, { @@ -35,7 +36,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 +54,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 3b73a6ba..3bb670f2 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -31,6 +31,7 @@ "lucide-react": "^0.400.0", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-image-crop": "^11.0.7", "react-router-dom": "^6.24.1", "tailwind-merge": "^2.3.0", "tailwindcss-animate": "^1.0.7" diff --git a/frontend/src-tauri/Cargo.lock b/frontend/src-tauri/Cargo.lock index 768ad00c..134eb6bd 100644 --- a/frontend/src-tauri/Cargo.lock +++ b/frontend/src-tauri/Cargo.lock @@ -4940,4 +4940,4 @@ dependencies = [ "proc-macro2", "quote", "syn 1.0.109", -] +] \ No newline at end of file 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..b2301b8c 100644 --- a/frontend/src-tauri/src/services/mod.rs +++ b/frontend/src-tauri/src/services/mod.rs @@ -171,6 +171,66 @@ 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> { + use std::path::PathBuf; + use std::fs; + + 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() + )); + + println!("Saving edited image to {:?}", edited_path.to_str()); + + fs::write(&edited_path, image_data).map_err(|e| e.to_string())?; + + Ok(()) +} + #[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 6eae9883..d3f55b8c 100644 --- a/frontend/src-tauri/tauri.conf.json +++ b/frontend/src-tauri/tauri.conf.json @@ -21,11 +21,12 @@ "version": "0.0.0", "identifier": "com.yourcompany.pictopy", "plugins": { + "fs": { + "active": true + }, "updater": { "active": true, - "endpoints": [ - "https://server.com//api/v1/tauri/update" - ], + "endpoints": ["https://server.com//api/v1/tauri/update"], "dialog": true, "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEI3ODc5QjNDMERGMzlCMUEKUldRYW0vTU5QSnVIdHplYktOV3hKKzdmSGd6ZVN3eTg4Q2ExbmVTbk4yN0pMWjZLYXl1M1ZzN3AK" } @@ -43,12 +44,10 @@ ], "security": { "assetProtocol": { - "scope": [ - "**" - ], + "scope": ["**"], "enable": true }, "csp": null } } -} \ No newline at end of file +} diff --git a/frontend/src/components/AITagging/AIgallery.tsx b/frontend/src/components/AITagging/AIgallery.tsx index e2a89f8e..143398d4 100644 --- a/frontend/src/components/AITagging/AIgallery.tsx +++ b/frontend/src/components/AITagging/AIgallery.tsx @@ -52,13 +52,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, loading]); + const [pageNo, setpageNo] = useState(20); const currentItems = useMemo(() => { const indexOfLastItem = currentPage * pageNo; @@ -119,44 +118,47 @@ const [pageNo,setpageNo] = useState(20); openMediaViewer={openMediaViewer} type={type} /> -
- {/* Pagination Controls - Centered */} - +
+ {/* Pagination Controls - Centered */} + - {/* Dropdown Menu - Right-Aligned */} -
- - - - - + + + + + + setpageNo(Number(value))} > - setpageNo(Number(value))} - > - {noOfPages.map((itemsPerPage) => ( - - {itemsPerPage} - - ))} - - - -
+ {noOfPages.map((itemsPerPage) => ( + + {itemsPerPage} + + ))} + + + +
)} @@ -164,7 +166,9 @@ const [pageNo,setpageNo] = useState(20); 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 ff768912..a4917576 100644 --- a/frontend/src/components/Album/Album.tsx +++ b/frontend/src/components/Album/Album.tsx @@ -13,7 +13,7 @@ import { fetchAllAlbums, } from '../../../api/api-functions/albums'; const AlbumsView: React.FC = () => { - const { successData: albums, isLoading} = usePictoQuery({ + const { successData: albums, isLoading } = usePictoQuery({ queryFn: fetchAllAlbums, queryKey: ['all-albums'], }); @@ -35,10 +35,11 @@ const AlbumsView: React.FC = () => { const showErrorDialog = (title: string, err: unknown) => { setErrorDialogContent({ title, - description: err instanceof Error ? err.message : 'An unknown error occurred', + description: + err instanceof Error ? err.message : 'An unknown error occurred', }); }; - if ( !albums || albums.length === 0) { + if (!albums || albums.length === 0) { return (
@@ -58,7 +59,7 @@ const AlbumsView: React.FC = () => { onSuccess={() => { setIsCreateFormOpen(false); }} - onError={(err)=>showErrorDialog("Error",err)} + 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 08d014e0..4e5280ba 100644 --- a/frontend/src/components/Media/MediaView.tsx +++ b/frontend/src/components/Media/MediaView.tsx @@ -1,6 +1,19 @@ import { MediaViewProps } from '@/types/Media'; -import React, { useEffect, useState } from 'react'; -import { ChevronLeft, ChevronRight } from 'lucide-react'; +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'; const MediaView: React.FC = ({ initialIndex, @@ -17,6 +30,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); useEffect(() => { setGlobalIndex((currentPage - 1) * itemsPerPage + initialIndex); @@ -83,17 +106,145 @@ const MediaView: React.FC = ({ 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(canvas, 0, 0); + + 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 (
@@ -116,72 +267,165 @@ const MediaView: React.FC = ({ {type === 'image' ? (
{ + if (e.target === e.currentTarget) { + onClose(); + } + }} onMouseDown={handleMouseDown} onMouseMove={handleMouseMove} onMouseUp={handleMouseUp} onMouseLeave={handleMouseUp} className="relative flex h-full w-full items-center justify-center overflow-hidden align-middle" > - {`image-${globalIndex}`} -
- - - + {`image-${globalIndex}`} + + ) : ( + {`image-${globalIndex}`} + )} +
+ {isEditing ? ( + <> + + + +
+ + setBrightness(Number(e.target.value))} + className="w-24" + /> +
+
+ + setContrast(Number(e.target.value))} + className="w-24" + /> +
+ + ) : ( + <> + + + + {allMedia[globalIndex]?.path && ( + <> + + + + )} + + )}
) : (
+ {notification && ( +
+ {notification.message} +
+ )}
); }; -export default MediaView; \ No newline at end of file +export default MediaView; 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'; From 66a1cd9d27df6594c632ec9083fee168b1465ddd Mon Sep 17 00:00:00 2001 From: rishab Date: Fri, 3 Jan 2025 23:19:18 +0530 Subject: [PATCH 2/2] minor fixes --- frontend/api/api-functions/images.ts | 1 - frontend/src-tauri/Cargo.lock | 192 +++++++++++++++++++- frontend/src-tauri/Cargo.toml | 1 + frontend/src-tauri/src/services/mod.rs | 64 ++++++- frontend/src/components/Media/MediaView.tsx | 3 +- 5 files changed, 248 insertions(+), 13 deletions(-) diff --git a/frontend/api/api-functions/images.ts b/frontend/api/api-functions/images.ts index 10e91b25..e827e603 100644 --- a/frontend/api/api-functions/images.ts +++ b/frontend/api/api-functions/images.ts @@ -2,7 +2,6 @@ import { imagesEndpoints } from '../apiEndpoints'; import { convertFileSrc } from '@tauri-apps/api/core'; import { APIResponse, Image } from '../../src/types/image'; import { extractThumbnailPath } from '@/hooks/useImages'; -import { message } from '@tauri-apps/plugin-dialog'; export const fetchAllImages = async () => { const response = await fetch(imagesEndpoints.allImages, { diff --git a/frontend/src-tauri/Cargo.lock b/frontend/src-tauri/Cargo.lock index 134eb6bd..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" @@ -4940,4 +5122,4 @@ dependencies = [ "proc-macro2", "quote", "syn 1.0.109", -] \ No newline at end of file +] 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/src/services/mod.rs b/frontend/src-tauri/src/services/mod.rs index b2301b8c..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( @@ -210,9 +211,20 @@ pub async fn save_edited_image( brightness: i32, contrast: i32, ) -> Result<(), String> { - use std::path::PathBuf; - use std::fs; + 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(); @@ -224,13 +236,55 @@ pub async fn save_edited_image( extension.to_string_lossy() )); - println!("Saving edited image to {:?}", edited_path.to_str()); - - fs::write(&edited_path, image_data).map_err(|e| e.to_string())?; + 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/components/Media/MediaView.tsx b/frontend/src/components/Media/MediaView.tsx index 4e5280ba..0ff192dd 100644 --- a/frontend/src/components/Media/MediaView.tsx +++ b/frontend/src/components/Media/MediaView.tsx @@ -165,7 +165,6 @@ const MediaView: React.FC = ({ // Create an image element to load the file const img = new Image(); img.src = imageUrl; - await new Promise((resolve) => { img.onload = resolve; }); @@ -198,7 +197,7 @@ const MediaView: React.FC = ({ } ctx.filter = `${filter} brightness(${brightness}%) contrast(${contrast}%)`; - ctx.drawImage(canvas, 0, 0); + ctx.drawImage(img, 0, 0, canvas.width, canvas.height); console.log('Canvas prepared, attempting to create blob');