From 16f46bbd7568ba73ab4b4eb07a04808480315a1b Mon Sep 17 00:00:00 2001 From: rishab Date: Thu, 9 Jan 2025 21:44:35 +0530 Subject: [PATCH] memories feature added --- frontend/package.json | 3 +- frontend/src-tauri/Cargo.lock | 3 + frontend/src-tauri/Cargo.toml | 5 +- frontend/src-tauri/src/main.rs | 1 + frontend/src-tauri/src/services/mod.rs | 73 +++++ frontend/src/components/Memories/Memories.tsx | 265 ++++++++++++++++++ .../components/Navigation/Sidebar/Sidebar.tsx | 12 +- frontend/src/constants/routes.ts | 1 + frontend/src/pages/Memories/Memories.tsx | 11 + .../src/routes/LayoutRoutes/LayoutRoutes.tsx | 2 + 10 files changed, 372 insertions(+), 4 deletions(-) create mode 100644 frontend/src/components/Memories/Memories.tsx create mode 100644 frontend/src/pages/Memories/Memories.tsx diff --git a/frontend/package.json b/frontend/package.json index a0c049ed..19fea37b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -27,6 +27,7 @@ "axios": "^1.7.2", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", + "framer-motion": "^11.16.3", "ldrs": "^1.0.2", "lucide-react": "^0.400.0", "react": "^18.2.0", @@ -34,8 +35,8 @@ "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-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 a833a60c..6d4ef921 100644 --- a/frontend/src-tauri/Cargo.lock +++ b/frontend/src-tauri/Cargo.lock @@ -7,8 +7,11 @@ name = "Pictopy" version = "0.0.0" dependencies = [ "anyhow", + "base64 0.21.7", "chrono", "image", + "rand 0.8.5", + "serde", "serde_json", "tauri", "tauri-build", diff --git a/frontend/src-tauri/Cargo.toml b/frontend/src-tauri/Cargo.toml index 363cf6b5..9e7837a8 100644 --- a/frontend/src-tauri/Cargo.toml +++ b/frontend/src-tauri/Cargo.toml @@ -14,13 +14,16 @@ tauri-build = { version = "2.0.0-beta", features = [] } tauri = { version = "2.0.0-beta", features = [ "protocol-asset"] } walkdir = "2.3" serde_json = "1" +serde = { version = "1.0", features = ["derive"] } anyhow = "1.0" tauri-plugin-fs = "2.0.0-beta.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" +chrono = { version = "0.4.26", features = ["serde"] } image = "0.24.6" +base64 = "0.21.0" +rand = "0.8.5" [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/main.rs b/frontend/src-tauri/src/main.rs index 695505d6..4b8e8022 100644 --- a/frontend/src-tauri/src/main.rs +++ b/frontend/src-tauri/src/main.rs @@ -29,6 +29,7 @@ fn main() { services::delete_cache, services::share_file, services::save_edited_image, + services::get_random_memories, ]) .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 8128e25f..b8a9d02f 100644 --- a/frontend/src-tauri/src/services/mod.rs +++ b/frontend/src-tauri/src/services/mod.rs @@ -8,6 +8,17 @@ pub use cache_service::CacheService; use chrono::{DateTime, Datelike, Utc}; pub use file_service::FileService; use image::{DynamicImage, GenericImageView, ImageBuffer, Rgba}; +use rand::seq::SliceRandom; +use std::collections::HashSet; +use serde::{Serialize, Deserialize}; +use std::path::Path; + +#[derive(Debug, Serialize, Deserialize)] +pub struct MemoryImage { + path: String, + #[serde(with = "chrono::serde::ts_seconds")] + created_at: DateTime, +} #[tauri::command] pub fn get_folders_with_images( @@ -285,6 +296,68 @@ fn adjust_brightness_contrast(img: &DynamicImage, brightness: i32, contrast: i32 DynamicImage::ImageRgba8(adjusted_img) } +#[tauri::command] +pub fn get_random_memories(directories: Vec, count: usize) -> Result, String> { + let mut all_images = Vec::new(); + let mut used_paths = HashSet::new(); + + for dir in directories { + let images = get_images_from_directory(&dir)?; + all_images.extend(images); + } + + let mut rng = rand::thread_rng(); + all_images.shuffle(&mut rng); + + let selected_images = all_images + .into_iter() + .filter(|img| used_paths.insert(img.path.clone())) + .take(count) + .collect(); + + Ok(selected_images) +} + +fn get_images_from_directory(dir: &str) -> Result, String> { + let path = Path::new(dir); + if !path.is_dir() { + return Err(format!("{} is not a directory", dir)); + } + + let mut images = Vec::new(); + + for entry in std::fs::read_dir(path).map_err(|e| e.to_string())? { + let entry = entry.map_err(|e| e.to_string())?; + let path = entry.path(); + + if path.is_dir() { + // Recursively call get_images_from_directory for subdirectories + let sub_images = get_images_from_directory(path.to_str().unwrap())?; + images.extend(sub_images); + } else if path.is_file() && is_image_file(&path) { + if let Ok(metadata) = std::fs::metadata(&path) { + if let Ok(created) = metadata.created() { + let created_at: DateTime = created.into(); + images.push(MemoryImage { + path: path.to_string_lossy().into_owned(), + created_at, + }); + } + } + } + } + + Ok(images) +} + +fn is_image_file(path: &Path) -> bool { + let extensions = ["jpg", "jpeg", "png", "gif"]; + path.extension() + .and_then(|ext| ext.to_str()) + .map(|ext| extensions.contains(&ext.to_lowercase().as_str())) + .unwrap_or(false) +} + #[tauri::command] pub fn delete_cache(cache_service: State<'_, CacheService>) -> bool { cache_service.delete_all_caches() diff --git a/frontend/src/components/Memories/Memories.tsx b/frontend/src/components/Memories/Memories.tsx new file mode 100644 index 00000000..02b68a22 --- /dev/null +++ b/frontend/src/components/Memories/Memories.tsx @@ -0,0 +1,265 @@ +import React, { useState, useEffect, useMemo, useCallback } from 'react'; +import { invoke } from '@tauri-apps/api/core'; +import { motion, AnimatePresence } from 'framer-motion'; +import MediaView from '../Media/MediaView'; +import PaginationControls from '../ui/PaginationControls'; +import { convertFileSrc } from '@tauri-apps/api/core'; +import { Loader2, X } from 'lucide-react'; +import { useLocalStorage } from '@/hooks/LocalStorage'; + +interface MemoryImage { + path: string; + created_at: string; +} + +const Memories: React.FC = () => { + const [memories, setMemories] = useState([]); + const [currentPage, setCurrentPage] = useState(1); + const [showMediaView, setShowMediaView] = useState(false); + const [selectedMemoryIndex, setSelectedMemoryIndex] = useState(0); + const [isLoading, setIsLoading] = useState(true); + const [showStoryView, setShowStoryView] = useState(true); + const [storyIndex, setStoryIndex] = useState(0); + const itemsPerPage = 12; + const [currentPath] = useLocalStorage('folderPath', ''); + const [currentPaths] = useLocalStorage('folderPaths', []); + const storyDuration = 3000; // 3 seconds per story + + useEffect(() => { + fetchMemories(); + }, []); + + const fetchMemories = async () => { + try { + setIsLoading(true); + const result = await invoke('get_random_memories', { + directories: + currentPaths && currentPaths.length > 0 + ? currentPaths + : [currentPath], + count: 10, // Request 10 memories + }); + setMemories(result); + } catch (error) { + console.error('Failed to fetch memories:', error); + } finally { + setIsLoading(false); + } + }; + + // Story view auto-advance + useEffect(() => { + if (showStoryView && memories.length > 0) { + const timer = setTimeout(() => { + if (storyIndex < memories.slice(0, 10).length - 1) { + setStoryIndex(storyIndex + 1); + } else { + setShowStoryView(false); + } + }, storyDuration); + + return () => clearTimeout(timer); + } + }, [storyIndex, showStoryView, memories]); + + const currentMemories = useMemo(() => { + const indexOfLastItem = currentPage * itemsPerPage; + const indexOfFirstItem = indexOfLastItem - itemsPerPage; + return memories.slice(indexOfFirstItem, indexOfLastItem); + }, [memories, currentPage]); + + const totalPages = Math.ceil(memories.length / itemsPerPage); + + const openMediaView = useCallback((index: number) => { + setSelectedMemoryIndex(index); + setShowMediaView(true); + }, []); + + const closeMediaView = useCallback(() => { + setShowMediaView(false); + }, []); + + const getTimeAgo = (dateStr: string) => { + try { + const timestamp = parseInt(dateStr) * 1000; + const date = new Date(timestamp); + const now = new Date(); + const diffTime = Math.abs(now.getTime() - date.getTime()); + const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); + const diffMonths = Math.floor(diffDays / 30); + const diffYears = Math.floor(diffDays / 365); + + if (diffYears > 0) { + return `${diffYears} year${diffYears > 1 ? 's' : ''} ago`; + } else if (diffMonths > 0) { + return `${diffMonths} month${diffMonths > 1 ? 's' : ''} ago`; + } else if (diffDays > 0) { + return `${diffDays} day${diffDays > 1 ? 's' : ''} ago`; + } else { + return 'Today'; + } + } catch (error) { + console.error('Error parsing date:', error); + return 'Recently'; + } + }; + + if (isLoading) { + return ( +
+
+ +

+ Loading your memories... +

+
+
+ ); + } + + if (memories.length === 0) { + return ( +
+

Your Memories

+
+

+ No memories found in the selected folder. +

+
+
+ ); + } + + if (showStoryView && memories.length > 0) { + const currentMemory = memories[storyIndex]; + return ( +
+ + + { + if (storyIndex < 9) { + setStoryIndex(storyIndex + 1); + } + }} + onDoubleClick={() => { + if (storyIndex > 0) { + setStoryIndex(storyIndex - 1); + } + }} + className="relative flex h-full w-full items-center justify-center" + > + {`Memory +
+ +
+
+ + {getTimeAgo(currentMemory.created_at)} + +
+
+
+
+ {memories.slice(0, 10).map((_, idx) => ( +
+ ))} +
+
+ ); + } + + return ( +
+

Your Memories

+ + + {currentMemories.map((memory, index) => ( + openMediaView(index)} + > +
+ {`Memory +
+
+
+

+ {getTimeAgo(memory.created_at)} +

+
+ + ))} + + + + {totalPages > 1 && ( +
+ +
+ )} + + {showMediaView && ( + ({ + url: convertFileSrc(memory.path), + path: memory.path, + }))} + currentPage={currentPage} + itemsPerPage={itemsPerPage} + type="image" + /> + )} +
+ ); +}; + +export default Memories; diff --git a/frontend/src/components/Navigation/Sidebar/Sidebar.tsx b/frontend/src/components/Navigation/Sidebar/Sidebar.tsx index d01ae0d7..0081557b 100644 --- a/frontend/src/components/Navigation/Sidebar/Sidebar.tsx +++ b/frontend/src/components/Navigation/Sidebar/Sidebar.tsx @@ -1,5 +1,12 @@ import { Link, useLocation } from 'react-router-dom'; -import { Home, Sparkles, Video, Images, Settings } from 'lucide-react'; +import { + Home, + Sparkles, + Video, + Images, + Settings, + BookImage, +} from 'lucide-react'; import { useState } from 'react'; const Sidebar = () => { @@ -25,7 +32,7 @@ const Sidebar = () => { return (
{ { path: '/videos', label: 'Videos', Icon: Video }, { path: '/albums', label: 'Albums', Icon: Images }, { path: '/settings', label: 'Settings', Icon: Settings }, + { path: '/memories', label: 'Memories', Icon: BookImage }, ].map(({ path, label, Icon }) => ( { + return ( + <> + + + ); +}; + +export default Memories; diff --git a/frontend/src/routes/LayoutRoutes/LayoutRoutes.tsx b/frontend/src/routes/LayoutRoutes/LayoutRoutes.tsx index 6134a36f..fee51337 100644 --- a/frontend/src/routes/LayoutRoutes/LayoutRoutes.tsx +++ b/frontend/src/routes/LayoutRoutes/LayoutRoutes.tsx @@ -2,6 +2,7 @@ import { ROUTES } from '@/constants/routes'; import AITagging from '@/pages/AITagging/AITaging'; import Album from '@/pages/Album/Album'; import Dashboard from '@/pages/Dashboard/Dashboard'; +import Memories from '@/pages/Memories/Memories'; import Settings from '@/pages/SettingsPage/Settings'; import Videos from '@/pages/VideosPage/Videos'; @@ -15,5 +16,6 @@ export const LayoutRoutes: React.FC = () => ( } /> } /> } /> + } /> );