diff --git a/.github/workflows/pr-check-tests.yml b/.github/workflows/pr-check-tests.yml index f6d62580..09be0dde 100644 --- a/.github/workflows/pr-check-tests.yml +++ b/.github/workflows/pr-check-tests.yml @@ -88,47 +88,3 @@ jobs: cd backend pytest - # Tauri Test Job - tauri: - runs-on: ubuntu-latest - name: Tauri Tests - steps: - - name: Checkout Code - uses: actions/checkout@v3 - - - name: Install Dependencies for Tauri - run: | - sudo apt-get update -y - - echo "deb http://archive.ubuntu.com/ubuntu jammy main universe multiverse" | sudo tee /etc/apt/sources.list.d/ubuntu-jammy.list - - echo "deb http://security.ubuntu.com/ubuntu jammy-security main universe multiverse" | sudo tee -a /etc/apt/sources.list.d/ubuntu-jammy-security.list - - sudo apt-get update -y - - sudo apt-get install -y \ - curl build-essential libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev \ - wget xz-utils libssl-dev libglib2.0-dev libgirepository1.0-dev pkg-config \ - software-properties-common libjavascriptcoregtk-4.0-dev libjavascriptcoregtk-4.1-dev \ - libsoup-3.0-dev libwebkit2gtk-4.1-dev librsvg2-dev file libglib2.0-dev libgl1-mesa-glx - - - name: Set up Rust - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - override: true - - - name: Set up Node.js - uses: actions/setup-node@v3 - with: - node-version: "18" - - - name: Install Frontend Dependencies - run: | - cd frontend - npm install - - - name: Run Tauri Tests - run: | - cd frontend/src-tauri - cargo test diff --git a/docs/assets/screenshots/ai-tagging.png b/docs/assets/screenshots/ai-tagging.png new file mode 100644 index 00000000..ab2fe0e0 Binary files /dev/null and b/docs/assets/screenshots/ai-tagging.png differ diff --git a/docs/assets/screenshots/ai-tagging2.png b/docs/assets/screenshots/ai-tagging2.png new file mode 100644 index 00000000..1f540cda Binary files /dev/null and b/docs/assets/screenshots/ai-tagging2.png differ diff --git a/docs/assets/screenshots/home.png b/docs/assets/screenshots/home.png new file mode 100644 index 00000000..537caa94 Binary files /dev/null and b/docs/assets/screenshots/home.png differ diff --git a/docs/assets/screenshots/settings.png b/docs/assets/screenshots/settings.png new file mode 100644 index 00000000..2efcde34 Binary files /dev/null and b/docs/assets/screenshots/settings.png differ diff --git a/docs/backend/backend_rust/api.md b/docs/backend/backend_rust/api.md index 011620b2..02b4d5af 100644 --- a/docs/backend/backend_rust/api.md +++ b/docs/backend/backend_rust/api.md @@ -1,162 +1,12 @@ ## API Documentation -The Rust backend provides the following commands that can be invoked from the frontend: +The Rust backend provides the following command that can be invoked from the frontend: -### 1. get_folders_with_images - -- **Description**: Retrieves folders containing images from a specified directory. -- **Parameters**: - - `directory`: String -- **Returns**: `Vec` - -### 2. get_images_in_folder - -- **Description**: Gets all images in a specific folder. -- **Parameters**: - - `folder_path`: String -- **Returns**: `Vec` - -### 3. get_all_images_with_cache - -- **Description**: Retrieves all images from multiple directories, organized by year and month, with caching. -- **Parameters**: - - `directories`: Vec -- **Returns**: `Result>>, String>` - -### 4. get_all_videos_with_cache - -- **Description**: Retrieves all videos from multiple directories, organized by year and month, with caching. -- **Parameters**: - - `directories`: Vec -- **Returns**: `Result>>, String>` - -### 5. delete_cache - -- **Description**: Deletes all cached data. -- **Parameters**: None -- **Returns**: `bool` - -### 6. share_file - -- **Description**: Opens the file in the system's default file manager and selects it. -- **Parameters**: - - `path`: String -- **Returns**: `Result<(), String>` - -### 7. save_edited_image - -- **Description**: Saves an edited image with applied filters and adjustments. -- **Parameters**: - - `image_data`: Vec - - `save_path`: String - - `filter`: String - - `brightness`: i32 - - `contrast`: i32 - - `vibrance`: i32 - - `exposure`: i32 - - `temperature`: i32 - - `sharpness`: i32 - - `vignette`: i32 - - `highlights`: i32 -- **Returns**: `Result<(), String>` - -### 8. get_server_path +### get_server_path - **Description**: Retrieves the path to the server resources directory. - **Parameters**: *None* - **Returns**: `Result` -### 9. move_to_secure_folder - -- **Description**: Moves a file to the secure folder with encryption. -- **Parameters**: - - `path`: String - - `password`: String -- **Returns**: `Result<(), String>` - -### 10. create_secure_folder - -- **Description**: Creates a new secure folder with password protection. -- **Parameters**: - - `password`: String -- **Returns**: `Result<(), String>` - -### 11. unlock_secure_folder - -- **Description**: Unlocks the secure folder with the provided password. -- **Parameters**: - - `password`: String -- **Returns**: `Result` - -### 12. get_secure_media - -- **Description**: Retrieves all media files from the secure folder. -- **Parameters**: - - `password`: String -- **Returns**: `Result, String>` - -### 13. remove_from_secure_folder - -- **Description**: Removes a file from the secure folder. -- **Parameters**: - - `file_name`: String - - `password`: String -- **Returns**: `Result<(), String>` - -### 14. check_secure_folder_status - -- **Description**: Checks if the secure folder is set up. -- **Parameters**: None -- **Returns**: `Result` - -### 15. get_random_memories - -- **Description**: Retrieves random memory images from specified directories. -- **Parameters**: - - `directories`: Vec - - `count`: usize -- **Returns**: `Result, String>` - -### 16. open_folder - -- **Description**: Opens the parent folder of the specified file path. -- **Parameters**: - - `path`: String -- **Returns**: `Result<(), String>` - -### 17. open_with - -- **Description**: Opens a file with the system's "Open With" dialog. -- **Parameters**: - - `path`: String -- **Returns**: `Result<(), String>` - -### 18. set_wallpaper - -- **Description**: Sets an image as the desktop wallpaper. -- **Parameters**: - - `path`: String -- **Returns**: `Result<(), String>` - -## Data Structures - -### SecureMedia - -```rust -pub struct SecureMedia { - pub id: String, - pub url: String, - pub path: String, -} -``` - -### MemoryImage - -```rust -pub struct MemoryImage { - path: String, - created_at: DateTime, -} -``` ## Usage Examples @@ -164,75 +14,13 @@ pub struct MemoryImage { // In your frontend JavaScript/TypeScript code: import { invoke } from "@tauri-apps/api/tauri"; -// Example: Get all images with cache from multiple directories -const imagesData = await invoke("get_all_images_with_cache", { - directories: ["/path/to/images1", "/path/to/images2"], -}); - -// Example: Share a file -await invoke("share_file", { path: "/path/to/file.jpg" }); - -// Example: Save edited image -await invoke("save_edited_image", { - image_data: imageBytes, - save_path: "/path/to/save/edited.jpg", - filter: "grayscale(100%)", - brightness: 10, - contrast: 20, - vibrance: 15, - exposure: 5, - temperature: 0, - sharpness: 10, - vignette: 0, - highlights: 0 -}); - -// Example: Create secure folder -await invoke("create_secure_folder", { password: "mySecurePassword" }); - -// Example: Move file to secure folder -await invoke("move_to_secure_folder", { - path: "/path/to/file.jpg", - password: "mySecurePassword" -}); - -// Example: Get random memories -const memories = await invoke("get_random_memories", { - directories: ["/path/to/photos"], - count: 5 -}); - -// Example: Set wallpaper -await invoke("set_wallpaper", { path: "/path/to/wallpaper.jpg" }); - -// Example: Delete cache -const cacheDeleted = await invoke("delete_cache"); +// Example: Get server path +const serverPath = await invoke("get_server_path"); +console.log("Server path:", serverPath); ``` -## Key Components - -- **FileService**: Handles file system operations for images and videos. -- **CacheService**: Manages caching of folders, images, and videos. -- **FileRepository**: Interacts directly with the file system to retrieve file information. -- **CacheRepository**: Handles reading from and writing to cache files. -- **Secure Storage**: Provides encrypted storage functionality with password protection. -- **Image Processing**: Handles image editing operations including filters, brightness, contrast, and other adjustments. -- **System Integration**: Provides integration with the operating system for file operations, wallpaper setting, and file management. - -## Security Features - -The API includes comprehensive security features for protecting sensitive media: - -- **Encryption**: Files moved to secure folders are encrypted using AES-256-GCM -- **Password Protection**: Secure folders require password authentication -- **Salt-based Hashing**: Uses PBKDF2 with SHA-256 for password hashing -- **Secure Random**: Uses cryptographically secure random number generation - ## Cross-Platform Support -The API provides cross-platform support for: -- **Windows**: File operations, wallpaper setting, and system integration -- **macOS**: Native file operations and AppleScript integration -- **Linux**: Support for GNOME and KDE desktop environments +The API provides cross-platform support using Tauri's unified `AppHandle.path().resolve(..., BaseDirectory::Resource)` for path resolution across Windows, macOS, and Linux. -This backend architecture provides efficient file management, caching capabilities, secure storage, image processing, and system integration, enhancing the overall functionality of the Tauri application. +This backend provides essential path resolution functionality for the Tauri application. \ No newline at end of file diff --git a/docs/frontend/gallery-view.md b/docs/frontend/gallery-view.md deleted file mode 100644 index c3bfd848..00000000 --- a/docs/frontend/gallery-view.md +++ /dev/null @@ -1,282 +0,0 @@ -# Gallery View - -The Gallery View is a core feature of our Tauri application, providing users with an intuitive interface to browse, sort, and interact with their media items (images and videos). - -## Components Overview - -1. MediaGallery -2. MediaGrid -3. MediaCard -4. MediaView -5. SortingControls -6. PaginationControls - -## MediaGallery - -The main container component for the gallery view. - -### Key Features - -- Manages sorting and pagination state -- Renders the grid of media items -- Handles opening and closing of full-screen media view - -### Usage - -```jsx -import MediaGallery from "./MediaGallery"; - -; -``` - -### Implementation Details - -```jsx -export default function MediaGallery({ - mediaItems, - title, - type, -}: MediaGalleryProps) { - const [sortBy, setSortBy] = useState("date"); - const [currentPage, setCurrentPage] = useState(1); - const [showMediaViewer, setShowMediaViewer] = useState(false); - const [selectedMediaIndex, setSelectedMediaIndex] = useState(0); - - // Memoized sorted and paginated media items - const sortedMedia = useMemo(() => sortMedia(mediaItems, sortBy), [mediaItems, sortBy]); - const currentItems = useMemo(() => { - const indexOfLastItem = currentPage * itemsPerPage; - const indexOfFirstItem = indexOfLastItem - itemsPerPage; - return sortedMedia.slice(indexOfFirstItem, indexOfLastItem); - }, [sortedMedia, currentPage]); - - // ... other memoized values and callback functions - - return ( -
-
- {/* Title and SortingControls */} - - - {showMediaViewer && ( - item.src)} - currentPage={currentPage} - itemsPerPage={itemsPerPage} - type={type} - /> - )} -
-
- ); -} -``` - -## MediaGrid - -Renders a grid of MediaCard components. - -### Usage - -```jsx - -``` - -### Implementation - -```jsx -export default function MediaGrid({ - mediaItems, - itemsPerRow, - openMediaViewer, - type, -}: MediaGridProps) { - if (mediaItems.length === 0) { - return
-

No media found

-
; - } - - return ( -
- {mediaItems.map((item, index) => ( -
openMediaViewer(index)} className="cursor-pointer"> - -
- ))} -
- ); -} -``` - -## MediaCard - -Represents an individual media item in the grid. - -### Usage - -```jsx - -``` - -### Implementation - -```jsx -export default function MediaCard({ item, type }: MediaCardProps) { - return ( -
- - View - - {type === "image" ? ( - {item.title} - ) : ( -
- ); -} -``` - -## MediaView - -Provides a full-screen view of media items with navigation. - -### Usage - -```jsx - item.src)} - currentPage={currentPage} - itemsPerPage={itemsPerPage} - type="image" -/> -``` - -### Implementation - -```jsx -const MediaView: React.FC = ({ - initialIndex, - onClose, - allMedia, - currentPage, - itemsPerPage, - type, -}) => { - const [globalIndex, setGlobalIndex] = useState( - (currentPage - 1) * itemsPerPage + initialIndex - ); - - // ... navigation handlers - - return ( -
- - {type === "image" ? ( - {`image-${globalIndex}`} - ) : ( -
- ); -}; -``` - -## SortingControls - -Provides sorting options for media items. - -### Usage - -```jsx - -``` - -### Implementation - -```jsx -const SortingControls: React.FC = ({ - sortBy, - setSortBy, - mediaItems, -}) => { - // ... year options generation logic - - return ( - - - - - - - Date - {yearOptions.map((option) => ( - - {option.label} - - ))} - - - - ); -}; -``` - -## Best Practices - -1. Use TypeScript for type safety and better developer experience. -2. Implement proper error handling and loading states. -3. Optimize performance using React hooks like `useMemo` and `useCallback`. -4. Ensure accessibility in all components, especially in the MediaView for keyboard navigation. -5. Use Tailwind CSS for consistent and responsive styling. - -This documentation provides an overview of the Gallery View components in your Tauri application. For more detailed information on specific components or functionalities, refer to the individual component files diff --git a/docs/frontend/screenshots.md b/docs/frontend/screenshots.md new file mode 100644 index 00000000..d4cf97d1 --- /dev/null +++ b/docs/frontend/screenshots.md @@ -0,0 +1,27 @@ +# Screenshots + +This section showcases the PictoPy application interface with sample photos featuring people. The screenshots demonstrate the various features and user interface components of the application. + +## Main Gallery View + +The main gallery displays photos in a grid layout, organized by date with filtering options. + +![Main Gallery](../assets/screenshots/home.png) +*Main gallery view showing a collection of photos with people, organized in a responsive grid layout* + +## AI Tagging Features + +The application includes AI-powered features for intelligent photo organization, including automatic object detection, face recognition, and smart clustering. + +![AI Tagging](../assets/screenshots/ai-tagging.png) +*AI Tagging interface showing face collections, object detection, and intelligent photo organization* + +## Settings Panel + +The settings panel allows users to configure directories, preferences, and application behavior. + +![Settings](../assets/screenshots/settings.png) +*Settings panel showing directory configuration and user preferences* + + + diff --git a/docs/frontend/state-management.md b/docs/frontend/state-management.md index 458a0a5a..4995a95d 100644 --- a/docs/frontend/state-management.md +++ b/docs/frontend/state-management.md @@ -1,151 +1,237 @@ -# State Management +# State Management with Redux -This guide outlines the state management strategies used in our Tauri application, focusing on React hooks and component-level state management. +This guide outlines the Redux-based state management system used in our PictoPy application, focusing on Redux slices and store configuration. ## Overview -Our application primarily uses React's built-in hooks for state management, including: +Our application uses Redux Toolkit for state management, which provides: -- `useState` for local component state -- `useMemo` for memoized values -- `useCallback` for memoized functions -- `Custom hooks` for shared logic and state +- **Redux slices** for feature-based state organization +- **Immutable state updates** with Immer +- **TypeScript integration** for type safety -We also utilize props for passing data and functions between components. +The Redux store serves as the single source of truth for application state that needs to be shared across multiple components. -## Key Concepts +## Store Structure -### 1. Local Component State +Our Redux store is organized into the following slices: -We use `useState` for managing local component state. This is suitable for state that doesn't need to be shared across multiple components. +### 1. Images Slice -Example from `AlbumsView`: +Manages the state for images and media viewing operations. -```javascript -const [isCreateFormOpen, setIsCreateFormOpen] = useState(false); -const [editingAlbum, setEditingAlbum] = (useState < Album) | (null > null); -const [currentAlbum, setCurrentAlbum] = (useState < string) | (null > null); +**State Structure:** + +```typescript +interface ImageState { + images: Image[]; + currentViewIndex: number; + totalImages: number; + error: string | null; +} ``` -### 2. Memoization +**Key Actions:** + +- `setImages` - Updates the images array +- `addImages` - Adds new images to the array +- `setCurrentViewIndex` - Sets the currently viewed image index +- `nextImage` - Navigates to the next image +- `previousImage` - Navigates to the previous image +- `closeImageView` - Closes the image viewer +- `updateImage` - Updates specific image data +- `removeImage` - Removes an image from the array +- `setError` - Sets error state +- `clearImages` - Clears all image data -We use `useMemo` for expensive computations or to prevent unnecessary re-renders. +### 2. Folders Slice -Example from `MediaGallery`: +Manages folder-related state and operations. -```javascript -const sortedMedia = useMemo(() => { - return sortMedia(mediaItems, sortBy); -}, [mediaItems, sortBy]); +**State Structure:** -const currentItems = useMemo(() => { - const indexOfLastItem = currentPage * itemsPerPage; - const indexOfFirstItem = indexOfLastItem - itemsPerPage; - return sortedMedia.slice(indexOfFirstItem, indexOfLastItem); -}, [sortedMedia, currentPage, itemsPerPage]); +```typescript +interface FolderState { + folders: FolderDetails[]; +} ``` -### 3. Memoized Callbacks +**Key Actions:** + +- `setFolders` - Updates the folders array +- `addFolder` - Adds a new folder or updates existing one +- `updateFolder` - Modifies an existing folder +- `removeFolders` - Removes folders by IDs +- `clearFolders` - Clears all folder data -`useCallback` is used to memoize functions, particularly event handlers. This helps to maintain referential equality between renders. +### 3. Face Clusters Slice -Example from `MediaGallery`: +Handles face recognition clusters and naming. -```javascript -const handleSetSortBy = useCallback((value: string) => { - setSortBy(value); -}, []); +**State Structure:** -const openMediaViewer = useCallback((index: number) => { - setSelectedMediaIndex(index); - setShowMediaViewer(true); -}, []); +```typescript +interface FaceClustersState { + clusters: Cluster[]; +} ``` -### 4. Custom Hooks +**Key Actions:** + +- `setClusters` - Updates the clusters array +- `updateClusterName` - Updates a cluster's name + +### 4. Onboarding Slice + +Manages the user onboarding process and user profile. -We create custom hooks to encapsulate and share logic and state across components. +**State Structure:** -Examples: +```typescript +interface OnboardingState { + currentStepIndex: number; + currentStepName: string; + stepStatus: boolean[]; + avatar: string | null; + name: string; +} +``` -- `useAllAlbums` -- `useDeleteAlbum` -- `useAIImage` -- `etc` +**Key Actions:** -These hooks often manage their own state and provide functions to interact with that state. +- `setAvatar` - Sets user avatar +- `setName` - Sets user name +- `markCompleted` - Marks an onboarding step as completed +- `previousStep` - Goes back to the previous onboarding step -### 5. Prop Drilling +### 5. Loader Slice -We pass state and functions as props to child components. While this works for our current application structure, for deeper component trees, we might consider using Context API or a state management library. +Manages loading states across the application. -Example from `AlbumsView`: +**State Structure:** -```javascript - { - const album = albums.find((a) => a.album_name === albumId); - if (album) { - setEditingAlbum(album); - } - }} - onDeleteAlbum={handleDeleteAlbum} -/> +```typescript +interface LoaderState { + loading: boolean; + message: string; +} ``` -## State Management Patterns +**Key Actions:** -### 1. Lifting State Up +- `showLoader` - Shows loading state with message +- `hideLoader` - Hides loading state -When state needs to be shared between sibling components, we lift it up to their closest common ancestor. This is seen in the `AlbumsView` component, which manages state for its child components. +### 6. Info Dialog Slice -### 2. Derived State +Manages information dialog display and content. -We use `useMemo` to create derived state based on props or other state values. This ensures that expensive calculations are only performed when necessary. +**State Structure:** -Example from `AIGallery`: +```typescript +interface InfoDialogProps { + isOpen: boolean; + title: string; + message: string; + variant: InfoDialogVariant; + showCloseButton: boolean; +} +``` -```javascript -const filteredMediaItems = useMemo(() => { - return filterTag - ? mediaItems.filter((mediaItem: any) => mediaItem.tags.includes(filterTag)) - : mediaItems; -}, [filterTag, mediaItems]); +**Key Actions:** + +- `showInfoDialog` - Shows information dialog with content +- `hideInfoDialog` - Hides information dialog + +## Redux Toolkit Configuration + +### Store Setup + +```typescript +import { configureStore } from "@reduxjs/toolkit"; +import loaderReducer from "@/features/loaderSlice"; +import onboardingReducer from "@/features/onboardingSlice"; +import imageReducer from "@/features/imageSlice"; +import faceClustersReducer from "@/features/faceClustersSlice"; +import infoDialogReducer from "@/features/infoDialogSlice"; +import folderReducer from "@/features/folderSlice"; + +export const store = configureStore({ + reducer: { + loader: loaderReducer, + onboarding: onboardingReducer, + images: imageReducer, + faceClusters: faceClustersReducer, + infoDialog: infoDialogReducer, + folders: folderReducer, + }, +}); + +export type RootState = ReturnType; +export type AppDispatch = typeof store.dispatch; ``` -### 3. State Initialization from Props +## Usage in Components -When initializing state based on props, we do it in the component body rather than inside useEffect to avoid unnecessary re-renders. +### Connecting Components -### 4. Error State Management +Use the `useSelector` and `useDispatch` hooks to connect components to the Redux store: -We manage error states at the component level and use a centralized error dialog to display errors. +```typescript +import { useSelector, useDispatch } from "react-redux"; +import { RootState, AppDispatch } from "../app/store"; +import { setImages, nextImage } from "../features/imageSlice"; +import { showLoader, hideLoader } from "../features/loaderSlice"; -Example from `AlbumsView`: +const ImageViewer = () => { + const dispatch = useDispatch(); + const { images, currentViewIndex } = useSelector( + (state: RootState) => state.images + ); + const { loading, message } = useSelector((state: RootState) => state.loader); -```javascript -const [errorDialogContent, setErrorDialogContent] = useState<{ - title: string; - description: string; -} | null>(null); - -const showErrorDialog = (title: string, err: unknown) => { - setErrorDialogContent({ - title, - description: err instanceof Error ? err.message : "An unknown error occurred", - }); + const handleNextImage = () => { + dispatch(nextImage()); + }; + + // Component logic... }; ``` +### Typed Hooks + +For better TypeScript support, we use typed versions of the hooks: + +```typescript +import { useDispatch, useSelector, TypedUseSelectorHook } from "react-redux"; +import type { RootState, AppDispatch } from "../app/store"; + +export const useAppDispatch = () => useDispatch(); +export const useAppSelector: TypedUseSelectorHook = useSelector; +``` + ## Best Practices -1. Keep state as close to where it's used as possible. -2. Use `useMemo` and `useCallback` judiciously to optimize performance. -3. Create custom hooks to encapsulate complex state logic and side effects. -4. Use TypeScript to ensure type safety in state management. -5. Consider using Context API or a state management library if prop drilling becomes cumbersome. +1. **Keep slices focused** - Each slice should manage a specific domain of your application +2. **Normalize state shape** - Use normalized data structures for complex relational data +3. **Use selectors** - Create reusable selectors for complex state derivations (see `folderSelectors.ts`, `imageSelectors.ts`, `onboardingSelectors.ts`) + +## Selectors + +The application uses dedicated selector files for complex state derivations: + +- `folderSelectors.ts` - Folder-related selectors +- `imageSelectors.ts` - Image-related selectors +- `onboardingSelectors.ts` - Onboarding state selectors + +Example selector usage: + +```typescript +import { getFolderById } from "@/features/folderSelectors"; + +const folder = useSelector((state: RootState) => + getFolderById(state, folderId) +); +``` -By following these patterns and best practices, we maintain a clean and scalable state management system throughout our application. +This Redux-based architecture provides a scalable and maintainable state management solution that grows with our application's complexity. diff --git a/docs/frontend/ui-components.md b/docs/frontend/ui-components.md deleted file mode 100644 index 138018fe..00000000 --- a/docs/frontend/ui-components.md +++ /dev/null @@ -1,235 +0,0 @@ -# UI Components - -## Core Components - -### 1. Dialog - -A modal dialog component based on Radix UI. - -Key features: - -- Customizable content, header, and footer -- Accessible design -- Animated transitions - -Usage: - -```jsx - - - - Title - - {/* Content */} - - -``` - -### 2. Input - -A styled input component. - -Usage: - -```jsx - -``` - -### 3. Button - -A versatile button component with various styles. - -Usage: - -```jsx - -``` - -### 4. Dropdown Menu - -A customizable dropdown menu component. - -Usage: - -```jsx - - Open - - Item 1 - Item 2 - - -``` - -## Media Components - -### 1. MediaCard - -Displays an individual media item (image or video). - -Usage: - -```jsx - -``` - -### 2. MediaGrid - -Renders a grid of MediaCard components. - -Usage: - -```jsx - -``` - -### 3. MediaView - -Provides a full-screen view of media items with navigation. - -Usage: - -```jsx - -``` - -## Album Components - -### 1. AlbumCard - -Displays an individual album with cover image and actions. - -Usage: - -```jsx - -``` - -### 2. AlbumList - -Renders a grid of AlbumCard components. - -Usage: - -```jsx - -``` - -### 3. AlbumView - -Displays the contents of a single album. - -Usage: - -```jsx - -``` - -## Utility Components - -### 1. LoadingScreen - -Displays a full-screen loading indicator. - -Usage: - -```jsx -{ - isLoading && ; -} -``` - -### 2. ErrorDialog - -Displays error messages in a dialog. - -Usage: - -```jsx - -``` - -### 3. PaginationControls - -Provides pagination controls for lists or grids. - -Usage: - -```jsx - -``` - -## Form Components - -### 1. CreateAlbumForm - -A form dialog for creating new albums. - -Usage: - -```jsx - -``` - -### 2. EditAlbumDialog - -A dialog for editing album details. - -Usage: - -```jsx - -``` - -## Best Practices - -1. Use TypeScript for improved type safety. -2. Leverage Tailwind CSS for consistent styling. -3. Implement proper error handling and loading states. -4. Ensure accessibility in all components. -5. Optimize performance with React hooks like useMemo and useCallback. - -## Customization - -Most components accept a `className` prop for additional styling. Modify the base styles in component files or use a global CSS file for overrides. - -For more detailed information on specific components, refer to the individual component files or consult the development team. diff --git a/frontend/package-lock.json b/frontend/package-lock.json index a9e02c43..47073989 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "PictoPy", - "version": "0.0.1", + "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "PictoPy", - "version": "0.0.1", + "version": "1.0.0", "dependencies": { "@radix-ui/react-aspect-ratio": "^1.1.7", "@radix-ui/react-avatar": "^1.1.10", @@ -14673,20 +14673,6 @@ "dev": true, "license": "ISC" }, - "node_modules/yaml": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", - "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", - "license": "ISC", - "optional": true, - "peer": true, - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - } - }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", diff --git a/frontend/src-tauri/src/lib.rs b/frontend/src-tauri/src/lib.rs index 79d46c45..4e379ae7 100644 --- a/frontend/src-tauri/src/lib.rs +++ b/frontend/src-tauri/src/lib.rs @@ -1,4 +1 @@ -pub mod models; -pub mod repositories; pub mod services; -pub mod utils; diff --git a/frontend/src-tauri/src/main.rs b/frontend/src-tauri/src/main.rs index b4ade21a..b46b886e 100644 --- a/frontend/src-tauri/src/main.rs +++ b/frontend/src-tauri/src/main.rs @@ -1,13 +1,8 @@ // Prevents additional console window on Windows in release, DO NOT REMOVE!! #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] -mod models; -mod repositories; mod services; -mod utils; -use crate::services::{CacheService, FileService}; -use std::env; use tauri::path::BaseDirectory; use tauri::Manager; @@ -20,36 +15,13 @@ fn main() { .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_process::init()) .setup(|app| { - let file_service = FileService::new(); - let cache_service = CacheService::new(); let resource_path = app .path() .resolve("resources/backend", BaseDirectory::Resource)?; println!("Resource path: {:?}", resource_path); - app.manage(file_service); - app.manage(cache_service); Ok(()) }) - .invoke_handler(tauri::generate_handler![ - services::get_folders_with_images, - services::get_images_in_folder, - services::get_all_images_with_cache, - services::get_all_videos_with_cache, - services::delete_cache, - services::share_file, - services::save_edited_image, - services::get_server_path, - services::move_to_secure_folder, - services::create_secure_folder, - services::unlock_secure_folder, - services::get_secure_media, - services::remove_from_secure_folder, - services::check_secure_folder_status, - services::get_random_memories, - services::open_folder, - services::open_with, - services::set_wallpaper, - ]) + .invoke_handler(tauri::generate_handler![services::get_server_path,]) .run(tauri::generate_context!()) .expect("error while running tauri application"); } diff --git a/frontend/src-tauri/src/models.rs b/frontend/src-tauri/src/models.rs deleted file mode 100644 index d19849b0..00000000 --- a/frontend/src-tauri/src/models.rs +++ /dev/null @@ -1,12 +0,0 @@ -use std::path::PathBuf; - -pub struct FileInfo { - pub path: PathBuf, - pub file_type: FileType, -} - -pub enum FileType { - Image, - Video, - Other, -} diff --git a/frontend/src-tauri/src/repositories/cache_repository.rs b/frontend/src-tauri/src/repositories/cache_repository.rs deleted file mode 100644 index 595a5bf1..00000000 --- a/frontend/src-tauri/src/repositories/cache_repository.rs +++ /dev/null @@ -1,36 +0,0 @@ -use std::fs::{File, OpenOptions}; -use std::io::{BufRead, BufReader, Write}; -use std::path::PathBuf; - -pub struct CacheRepository; - -impl CacheRepository { - pub fn read_cache(cache_file_path: &str) -> Option> { - File::open(cache_file_path).ok().map(|file| { - let reader = BufReader::new(file); - reader - .lines() - .filter_map(|line| line.ok().map(PathBuf::from)) - .collect() - }) - } - - pub fn write_cache(cache_file_path: &str, paths: &[PathBuf]) -> std::io::Result<()> { - let mut file = OpenOptions::new() - .write(true) - .create(true) - .truncate(true) - .open(cache_file_path)?; - - for path in paths { - if let Some(path_str) = path.to_str() { - writeln!(file, "{}", path_str)?; - } - } - Ok(()) - } - - pub fn delete_cache(cache_file_path: &str) -> std::io::Result<()> { - std::fs::remove_file(cache_file_path) - } -} diff --git a/frontend/src-tauri/src/repositories/file_repository.rs b/frontend/src-tauri/src/repositories/file_repository.rs deleted file mode 100644 index 5b50d873..00000000 --- a/frontend/src-tauri/src/repositories/file_repository.rs +++ /dev/null @@ -1,39 +0,0 @@ -use crate::models::{FileInfo, FileType}; -use crate::utils::file_utils::{is_image_extension, is_video_extension}; -use walkdir::WalkDir; - -pub struct FileRepository; - -impl FileRepository { - pub fn get_files_in_directory(directory: &str) -> Vec { - WalkDir::new(directory) - .into_iter() - .filter_map(|e| e.ok()) - .filter(|e| { - // Exclude the PictoPy.thumbnails directory - !e.path() - .to_str() - .map_or(false, |path| path.contains("PictoPy.thumbnails")) - }) - .filter(|e| e.file_type().is_file()) - .filter_map(|e| { - e.path() - .extension() - .and_then(|ext| ext.to_str()) - .map(|ext| { - let file_type = if is_image_extension(ext) { - FileType::Image - } else if is_video_extension(ext) { - FileType::Video - } else { - FileType::Other - }; - FileInfo { - path: e.path().to_owned(), - file_type, - } - }) - }) - .collect() - } -} diff --git a/frontend/src-tauri/src/repositories/mod.rs b/frontend/src-tauri/src/repositories/mod.rs deleted file mode 100644 index 2aa2e63d..00000000 --- a/frontend/src-tauri/src/repositories/mod.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod cache_repository; -pub mod file_repository; - -pub use cache_repository::CacheRepository; -pub use file_repository::FileRepository; diff --git a/frontend/src-tauri/src/services/cache_service.rs b/frontend/src-tauri/src/services/cache_service.rs deleted file mode 100644 index c3ec0513..00000000 --- a/frontend/src-tauri/src/services/cache_service.rs +++ /dev/null @@ -1,56 +0,0 @@ -use crate::repositories::CacheRepository; -use std::path::PathBuf; - -const FOLDERS_CACHE_FILE_PATH: &str = "folders_cache.txt"; -const IMAGES_CACHE_FILE_PATH: &str = "images_cache.txt"; -const VIDEOS_CACHE_FILE_PATH: &str = "videos_cache.txt"; - -pub struct CacheService; - -impl CacheService { - pub fn new() -> Self { - CacheService - } - - pub fn get_cached_folders(&self) -> Option> { - CacheRepository::read_cache(FOLDERS_CACHE_FILE_PATH) - } - - pub fn cache_folders(&self, folders: &[PathBuf]) -> std::io::Result<()> { - CacheRepository::write_cache(FOLDERS_CACHE_FILE_PATH, folders) - } - - pub fn get_cached_images(&self) -> Option> { - CacheRepository::read_cache(IMAGES_CACHE_FILE_PATH) - } - - pub fn cache_images(&self, images: &[PathBuf]) -> std::io::Result<()> { - CacheRepository::write_cache(IMAGES_CACHE_FILE_PATH, images) - } - - pub fn get_cached_videos(&self) -> Option> { - CacheRepository::read_cache(VIDEOS_CACHE_FILE_PATH) - } - - pub fn cache_videos(&self, videos: &[PathBuf]) -> std::io::Result<()> { - CacheRepository::write_cache(VIDEOS_CACHE_FILE_PATH, videos) - } - - pub fn delete_all_caches(&self) -> bool { - let mut success = false; - - if CacheRepository::delete_cache(FOLDERS_CACHE_FILE_PATH).is_ok() { - success = true; - } - - if CacheRepository::delete_cache(IMAGES_CACHE_FILE_PATH).is_ok() { - success = true; - } - - if CacheRepository::delete_cache(VIDEOS_CACHE_FILE_PATH).is_ok() { - success = true; - } - - success - } -} diff --git a/frontend/src-tauri/src/services/file_service.rs b/frontend/src-tauri/src/services/file_service.rs deleted file mode 100644 index b710a749..00000000 --- a/frontend/src-tauri/src/services/file_service.rs +++ /dev/null @@ -1,45 +0,0 @@ -use crate::models::FileType; -use crate::repositories::FileRepository; -use std::path::PathBuf; - -pub struct FileService; - -impl FileService { - pub fn new() -> Self { - FileService - } - - pub fn get_folders_with_images(&self, directory: &str) -> Vec { - FileRepository::get_files_in_directory(directory) - .into_iter() - .filter(|file| matches!(file.file_type, FileType::Image)) - .map(|file| file.path.parent().unwrap().to_owned()) - .collect::>() - .into_iter() - .collect() - } - - pub fn get_images_in_folder(&self, folder_path: &str) -> Vec { - FileRepository::get_files_in_directory(folder_path) - .into_iter() - .filter(|file| matches!(file.file_type, FileType::Image)) - .map(|file| file.path) - .collect() - } - - pub fn get_all_images(&self, directory: &str) -> Vec { - FileRepository::get_files_in_directory(directory) - .into_iter() - .filter(|file| matches!(file.file_type, FileType::Image)) - .map(|file| file.path) - .collect() - } - - pub fn get_all_videos(&self, directory: &str) -> Vec { - FileRepository::get_files_in_directory(directory) - .into_iter() - .filter(|file| matches!(file.file_type, FileType::Video)) - .map(|file| file.path) - .collect() - } -} diff --git a/frontend/src-tauri/src/services/mod.rs b/frontend/src-tauri/src/services/mod.rs index 684f38bc..a0945e81 100644 --- a/frontend/src-tauri/src/services/mod.rs +++ b/frontend/src-tauri/src/services/mod.rs @@ -1,964 +1,6 @@ -use std::collections::HashMap; -use std::path::{Path, PathBuf}; -use std::time::SystemTime; -use tauri::State; -mod cache_service; -mod file_service; -pub use cache_service::CacheService; -use chrono::{DateTime, Datelike, Utc}; -use data_encoding::BASE64; -use directories::ProjectDirs; -pub use file_service::FileService; -use image::{DynamicImage, Rgba, RgbaImage}; -use rand::seq::SliceRandom; -use ring::aead::{Aad, LessSafeKey, Nonce, UnboundKey, AES_256_GCM}; -use ring::digest; -use ring::pbkdf2; -use ring::rand::{SecureRandom, SystemRandom}; -use serde::{Deserialize, Serialize}; -use std::collections::HashSet; -use std::fs; -use std::num::NonZeroU32; -use std::process::Command; use tauri::path::BaseDirectory; use tauri::Manager; -pub const SECURE_FOLDER_NAME: &str = "secure_folder"; -const SALT_LENGTH: usize = 16; -const NONCE_LENGTH: usize = 12; - -#[derive(Serialize, Deserialize)] -pub struct SecureMedia { - pub id: String, - pub url: String, - pub path: String, -} - -#[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( - directory: &str, - file_service: State<'_, FileService>, - cache_service: State<'_, CacheService>, -) -> Vec { - if let Some(cached_folders) = cache_service.get_cached_folders() { - return cached_folders; - } - - let folders = file_service.get_folders_with_images(directory); - let _ = cache_service.cache_folders(&folders); - folders -} - -#[tauri::command] -pub fn get_images_in_folder( - folder_path: &str, - file_service: State<'_, FileService>, -) -> Vec { - file_service.get_images_in_folder(folder_path) -} - -#[tauri::command] -pub fn get_all_images_with_cache( - state: tauri::State, - cache_state: tauri::State, - directories: Vec, -) -> Result>>, String> { - let cached_images = cache_state.get_cached_images(); - - let mut images_by_year_month = if let Some(cached) = cached_images { - let mut map: HashMap>> = HashMap::new(); - for path in cached { - if let Ok(metadata) = std::fs::metadata(&path) { - let date = metadata - .created() - .or_else(|_| metadata.modified()) - .unwrap_or_else(|_| SystemTime::now()); - - let datetime: DateTime = date.into(); - let year = datetime.year() as u32; - let month = datetime.month(); - map.entry(year) - .or_insert_with(HashMap::new) - .entry(month) - .or_insert_with(Vec::new) - .push(path.to_str().unwrap_or_default().to_string()); - } - } - map - } else { - let mut map: HashMap>> = HashMap::new(); - let mut all_image_paths: Vec = Vec::new(); - - for directory in directories { - let all_images = state.get_all_images(&directory); - - for path in all_images { - if let Ok(metadata) = std::fs::metadata(&path) { - let date = metadata - .created() - .or_else(|_| metadata.modified()) - .unwrap_or_else(|_| SystemTime::now()); - - let datetime: DateTime = date.into(); - let year = datetime.year() as u32; - let month = datetime.month(); - map.entry(year) - .or_insert_with(HashMap::new) - .entry(month) - .or_insert_with(Vec::new) - .push(path.to_str().unwrap_or_default().to_string()); - - all_image_paths.push(path); // Collect all paths for caching - } - } - } - - // Cache the flattened list of image paths - if let Err(e) = cache_state.cache_images(&all_image_paths) { - eprintln!("Failed to cache images: {}", e); - } - - map - }; - - // Sort the images within each month - for year_map in images_by_year_month.values_mut() { - for month_vec in year_map.values_mut() { - month_vec.sort(); - } - } - - Ok(images_by_year_month) -} - -#[tauri::command] -pub fn get_all_videos_with_cache( - state: tauri::State, - cache_state: tauri::State, - directories: Vec, // Updated to take an array of directories -) -> Result>>, String> { - let cached_videos = cache_state.get_cached_videos(); - - let mut videos_by_year_month: HashMap>> = - if let Some(cached) = cached_videos { - let mut map: HashMap>> = HashMap::new(); - for path in cached { - if let Ok(metadata) = std::fs::metadata(&path) { - if let Ok(created) = metadata.created() { - let datetime: DateTime = created.into(); - let year = datetime.year() as u32; - let month = datetime.month(); - map.entry(year) - .or_insert_with(HashMap::new) - .entry(month) - .or_insert_with(Vec::new) - .push(path.to_str().unwrap_or_default().to_string()); - } - } - } - map - } else { - let mut map: HashMap>> = HashMap::new(); - for directory in directories { - let all_videos = state.get_all_videos(&directory); - for path in all_videos { - if let Ok(metadata) = std::fs::metadata(&path) { - if let Ok(created) = metadata.created() { - let datetime: DateTime = created.into(); - let year = datetime.year() as u32; - let month = datetime.month(); - map.entry(year) - .or_insert_with(HashMap::new) - .entry(month) - .or_insert_with(Vec::new) - .push(path.to_str().unwrap_or_default().to_string()); - } - } - } - } - - // Cache the aggregated video paths - let flattened: Vec = map - .values() - .flat_map(|year_map| year_map.values()) - .flatten() - .map(|s| PathBuf::from(s)) - .collect(); - if let Err(e) = cache_state.cache_videos(&flattened) { - eprintln!("Failed to cache videos: {}", e); - } - - map - }; - - // Sort the videos within each month - for year_map in videos_by_year_month.values_mut() { - for month_vec in year_map.values_mut() { - month_vec.sort(); - } - } - - 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, - save_path: String, - filter: String, - brightness: i32, - contrast: i32, - vibrance: i32, - exposure: i32, - temperature: i32, - sharpness: i32, - vignette: i32, - highlights: i32, -) -> Result<(), String> { - use std::path::PathBuf; - 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(); - } - "saturate(200%)" => img = apply_saturation(&img, 2.0), - _ => {} - } - - // Convert the selected save path to PathBuf - let save_path = PathBuf::from(save_path); - // Apply adjustments - img = adjust_brightness_contrast(&img, brightness, contrast); - - // Save the edited image to the selected path - img = apply_vibrance(&img, vibrance); - img = apply_exposure(&img, exposure); - img = apply_temperature(&img, temperature); - img = apply_sharpness(&img, sharpness); - img = apply_vignette(&img, vignette); - img = apply_highlights(&img, highlights); - - img.save(&save_path).map_err(|e| e.to_string())?; - - Ok(()) -} - -pub fn apply_sepia(img: &DynamicImage) -> DynamicImage { - let mut sepia = img.to_rgb8(); - for pixel in sepia.pixels_mut() { - let r = pixel[0] as f32; - let g = pixel[1] as f32; - let b = pixel[2] as f32; - pixel[0] = ((r * 0.393) + (g * 0.769) + (b * 0.189)).min(255.0) as u8; - pixel[1] = ((r * 0.349) + (g * 0.686) + (b * 0.168)).min(255.0) as u8; - pixel[2] = ((r * 0.272) + (g * 0.534) + (b * 0.131)).min(255.0) as u8; - } - DynamicImage::ImageRgb8(sepia) -} - -pub fn apply_saturation(img: &DynamicImage, factor: f32) -> DynamicImage { - let mut saturated = img.to_rgb8(); - for pixel in saturated.pixels_mut() { - let r = pixel[0] as f32 / 255.0; - let g = pixel[1] as f32 / 255.0; - let b = pixel[2] as f32 / 255.0; - let max = r.max(g).max(b); - let min = r.min(g).min(b); - let delta = max - min; - if delta != 0.0 { - let sat = (delta / max) * factor; - pixel[0] = (((r - 0.5) * sat + 0.5).max(0.0).min(1.0) * 255.0) as u8; - pixel[1] = (((g - 0.5) * sat + 0.5).max(0.0).min(1.0) * 255.0) as u8; - pixel[2] = (((b - 0.5) * sat + 0.5).max(0.0).min(1.0) * 255.0) as u8; - } - } - DynamicImage::ImageRgb8(saturated) -} - -pub fn adjust_brightness_contrast( - img: &DynamicImage, - brightness: i32, - contrast: i32, -) -> DynamicImage { - let mut adjusted = img.to_rgb8(); - for pixel in adjusted.pixels_mut() { - for c in 0..3 { - let mut color = pixel[c] as f32; - // Apply brightness - color += brightness as f32 * 2.55; - // Apply contrast - color = ((color - 128.0) * (contrast as f32 / 100.0 + 1.0)) + 128.0; - pixel[c] = color.max(0.0).min(255.0) as u8; - } - } - DynamicImage::ImageRgb8(adjusted) -} - -pub fn apply_vibrance(img: &DynamicImage, vibrance: i32) -> DynamicImage { - let mut vibrant = img.to_rgb8(); - let vibrance_factor = vibrance as f32 / 100.0; - - for pixel in vibrant.pixels_mut() { - let r = pixel[0] as f32 / 255.0; - let g = pixel[1] as f32 / 255.0; - let b = pixel[2] as f32 / 255.0; - - let max = r.max(g).max(b); - let avg = (r + g + b) / 3.0; - - let amt = (max - avg) * 2.0 * vibrance_factor; - - pixel[0] = ((r + (max - r) * amt) * 255.0).clamp(0.0, 255.0) as u8; - pixel[1] = ((g + (max - g) * amt) * 255.0).clamp(0.0, 255.0) as u8; - pixel[2] = ((b + (max - b) * amt) * 255.0).clamp(0.0, 255.0) as u8; - } - - DynamicImage::ImageRgb8(vibrant) -} - -pub fn apply_exposure(img: &DynamicImage, exposure: i32) -> DynamicImage { - let mut exposed = img.to_rgb8(); - let factor = 2.0f32.powf(exposure as f32 / 100.0); - - for pixel in exposed.pixels_mut() { - for c in 0..3 { - pixel[c] = ((pixel[c] as f32 * factor).clamp(0.0, 255.0)) as u8; - } - } - - DynamicImage::ImageRgb8(exposed) -} - -pub fn apply_temperature(img: &DynamicImage, temperature: i32) -> DynamicImage { - let mut temp_adjusted = img.to_rgb8(); - let factor = temperature as f32 / 100.0; - for pixel in temp_adjusted.pixels_mut() { - let r = (pixel[0] as f32 * (1.0 + factor)).min(255.0) as u8; - let b = (pixel[2] as f32 * (1.0 - factor)).max(0.0) as u8; - pixel[0] = r; - pixel[2] = b; - } - DynamicImage::ImageRgb8(temp_adjusted) -} - -pub fn apply_sharpness(img: &DynamicImage, sharpness: i32) -> DynamicImage { - let rgba_img = img.to_rgba8(); - let (width, height) = rgba_img.dimensions(); - let mut sharpened = RgbaImage::new(width, height); - - let kernel: [f32; 9] = [-1.0, -1.0, -1.0, -1.0, 9.0, -1.0, -1.0, -1.0, -1.0]; - - let sharpness_factor = sharpness as f32 / 100.0; - - for y in 1..height - 1 { - for x in 1..width - 1 { - let mut new_pixel = [0.0; 4]; - for ky in 0..3 { - for kx in 0..3 { - let pixel = rgba_img.get_pixel(x + kx - 1, y + ky - 1); - for c in 0..3 { - new_pixel[c] += pixel[c] as f32 * kernel[(ky * 3 + kx) as usize]; - } - } - } - let original = rgba_img.get_pixel(x, y); - for c in 0..3 { - new_pixel[c] = - original[c] as f32 * (1.0 - sharpness_factor) + new_pixel[c] * sharpness_factor; - new_pixel[c] = new_pixel[c].max(0.0).min(255.0); - } - new_pixel[3] = original[3] as f32; - sharpened.put_pixel( - x, - y, - Rgba([ - new_pixel[0] as u8, - new_pixel[1] as u8, - new_pixel[2] as u8, - new_pixel[3] as u8, - ]), - ); - } - } - - DynamicImage::ImageRgba8(sharpened) -} - -pub fn apply_vignette(img: &DynamicImage, vignette: i32) -> DynamicImage { - let mut vignetted = img.to_rgba8(); - let (width, height) = vignetted.dimensions(); - let center_x = width as f32 / 2.0; - let center_y = height as f32 / 2.0; - let max_dist = (center_x.powi(2) + center_y.powi(2)).sqrt(); - let factor = vignette as f32 / 100.0; - - for (x, y, pixel) in vignetted.enumerate_pixels_mut() { - let dist = ((x as f32 - center_x).powi(2) + (y as f32 - center_y).powi(2)).sqrt(); - let vignette_factor = 1.0 - (dist / max_dist * factor).clamp(0.0, 1.0); // Smooth falloff - - for c in 0..3 { - pixel[c] = ((pixel[c] as f32 * vignette_factor).clamp(0.0, 255.0)) as u8; - } - } - - DynamicImage::ImageRgba8(vignetted) -} - -pub fn apply_highlights(img: &DynamicImage, highlights: i32) -> DynamicImage { - let mut highlighted = img.to_rgb8(); - let factor = highlights as f32 / 100.0; - - for pixel in highlighted.pixels_mut() { - for c in 0..3 { - let value = pixel[c] as f32; - if value > 128.0 { - let alpha = ((value - 128.0) / 127.0) * factor; - // Blend the original value with white (255) based on alpha - let new_value = value * (1.0 - alpha) + 255.0 * alpha; - pixel[c] = new_value.clamp(0.0, 255.0) as u8; - } - } - } - - DynamicImage::ImageRgb8(highlighted) -} - -pub fn get_secure_folder_path() -> Result { - let project_dirs = ProjectDirs::from("com", "AOSSIE", "Pictopy") - .ok_or_else(|| "Failed to get project directories".to_string())?; - let mut path = project_dirs.data_dir().to_path_buf(); - path.push(SECURE_FOLDER_NAME); - Ok(path) -} - -pub fn generate_salt() -> [u8; SALT_LENGTH] { - let mut salt = [0u8; SALT_LENGTH]; - SystemRandom::new().fill(&mut salt).unwrap(); - salt -} - -#[tauri::command] -pub async fn move_to_secure_folder(path: String, password: String) -> Result<(), String> { - let secure_folder = get_secure_folder_path()?; - let file_name = Path::new(&path).file_name().ok_or("Invalid file name")?; - let dest_path = secure_folder.join(file_name); - - let content = fs::read(&path).map_err(|e| e.to_string())?; - let ciphertext_length = content.len() + AES_256_GCM.tag_len(); - let _expected_length = SALT_LENGTH + NONCE_LENGTH + ciphertext_length + 16; - let encrypted = encrypt_data(&content, &password).map_err(|e| e.to_string())?; - println!("Encrypted file size: {}", encrypted.len()); - fs::write(&dest_path, encrypted).map_err(|e| e.to_string())?; - println!("Encrypted file saved to: {:?}", secure_folder); - fs::remove_file(&path).map_err(|e| e.to_string())?; - - let thumbnails_folder = Path::new(&path) - .parent() // Get parent directory of the original file - .and_then(|parent| parent.join("PictoPy.thumbnails").canonicalize().ok()) // Navigate to PictoPy.thumbnails - .ok_or("Unable to locate thumbnails directory")?; - let thumbnail_path = thumbnails_folder.join(file_name); - - if thumbnail_path.exists() { - fs::remove_file(&thumbnail_path).map_err(|e| e.to_string())?; - println!("Thumbnail deleted: {:?}", thumbnail_path); - } else { - println!("Thumbnail not found: {:?}", thumbnail_path); - } - - // Store the original path - let metadata_path = secure_folder.join("metadata.json"); - let mut metadata: HashMap = if metadata_path.exists() { - serde_json::from_str(&fs::read_to_string(&metadata_path).map_err(|e| e.to_string())?) - .map_err(|e| e.to_string())? - } else { - HashMap::new() - }; - metadata.insert(file_name.to_string_lossy().to_string(), path); - fs::write(&metadata_path, serde_json::to_string(&metadata).unwrap()) - .map_err(|e| e.to_string())?; - - Ok(()) -} - -#[tauri::command] -pub async fn remove_from_secure_folder(file_name: String, password: String) -> Result<(), String> { - let secure_folder = get_secure_folder_path()?; - let file_path = secure_folder.join(&file_name); - let metadata_path = secure_folder.join("metadata.json"); - - // Read and decrypt the file - let encrypted_content = fs::read(&file_path).map_err(|e| e.to_string())?; - let decrypted_content = - decrypt_data(&encrypted_content, &password).map_err(|e| e.to_string())?; - - // Get the original path - let metadata: HashMap = - serde_json::from_str(&fs::read_to_string(&metadata_path).map_err(|e| e.to_string())?) - .map_err(|e| e.to_string())?; - let original_path = metadata.get(&file_name).ok_or("Original path not found")?; - - // Write the decrypted content back to the original path - fs::write(original_path, decrypted_content).map_err(|e| e.to_string())?; - - // Remove the file from the secure folder and update metadata - fs::remove_file(&file_path).map_err(|e| e.to_string())?; - let mut updated_metadata = metadata; - updated_metadata.remove(&file_name); - fs::write( - &metadata_path, - serde_json::to_string(&updated_metadata).unwrap(), - ) - .map_err(|e| e.to_string())?; - - Ok(()) -} - -#[tauri::command] -pub async fn create_secure_folder(password: String) -> Result<(), String> { - let secure_folder = get_secure_folder_path()?; - fs::create_dir_all(&secure_folder).map_err(|e| e.to_string())?; - println!("Secure folder path: {:?}", secure_folder); - - let salt = generate_salt(); - let hashed_password = hash_password(&password, &salt); - - let config_path = secure_folder.join("config.json"); - let config = serde_json::json!({ - "salt": BASE64.encode(&salt), - "hashed_password": BASE64.encode(&hashed_password), - }); - fs::write(config_path, serde_json::to_string(&config).unwrap()).map_err(|e| e.to_string())?; - - let nomedia_path = secure_folder.join(".nomedia"); - fs::write(nomedia_path, "").map_err(|e| e.to_string())?; - - Ok(()) -} - -#[tauri::command] -pub async fn get_secure_media(password: String) -> Result, String> { - let secure_folder = get_secure_folder_path()?; - let mut secure_media = Vec::new(); - - for entry in fs::read_dir(secure_folder).map_err(|e| e.to_string())? { - let entry = entry.map_err(|e| e.to_string())?; - let path = entry.path(); - - if path.is_file() - && path - .extension() - .map_or(false, |ext| ext == "jpg" || ext == "png") - { - let content = fs::read(&path).map_err(|e| e.to_string())?; - let decrypted = decrypt_data(&content, &password).map_err(|e| e.to_string())?; - - let temp_dir = std::env::temp_dir(); - let temp_file = temp_dir.join(path.file_name().unwrap()); - fs::write(&temp_file, decrypted).map_err(|e| e.to_string())?; - println!("SecureMedia: {:?}", path.to_string_lossy().to_string()); - println!("SecureMedia: {:?}", temp_file.to_string_lossy().to_string()); - - secure_media.push(SecureMedia { - id: path.file_name().unwrap().to_string_lossy().to_string(), - url: format!("file://{}", temp_file.to_string_lossy().to_string()), - path: path.to_string_lossy().to_string(), - }); - } - } - - println!("SECURE MEDIA: {:?}", secure_media.len()); - - Ok(secure_media) -} - -pub fn hash_password(password: &str, salt: &[u8]) -> Vec { - let mut hash = [0u8; digest::SHA256_OUTPUT_LEN]; - pbkdf2::derive( - pbkdf2::PBKDF2_HMAC_SHA256, - NonZeroU32::new(100_000).unwrap(), - salt, - password.as_bytes(), - &mut hash, - ); - hash.to_vec() -} - -pub fn encrypt_data(data: &[u8], password: &str) -> Result, ring::error::Unspecified> { - let salt = generate_salt(); - let key = derive_key(password, &salt); - let nonce = generate_nonce(); - - let mut in_out = data.to_vec(); - let tag = key.seal_in_place_separate_tag( - Nonce::assume_unique_for_key(nonce), - Aad::empty(), - &mut in_out, - )?; - - let mut result = Vec::new(); - result.extend_from_slice(&salt); - result.extend_from_slice(&nonce); - result.extend_from_slice(&in_out); - result.extend_from_slice(tag.as_ref()); - - Ok(result) -} - -pub fn decrypt_data(encrypted: &[u8], password: &str) -> Result, String> { - println!("Decrypting data..."); - - if encrypted.len() < SALT_LENGTH + NONCE_LENGTH + 16 { - return Err(format!( - "Encrypted data too short: {} bytes", - encrypted.len() - )); - } - - let salt = &encrypted[..SALT_LENGTH]; - let nonce = &encrypted[SALT_LENGTH..SALT_LENGTH + NONCE_LENGTH]; - let tag_len = 16; - let (ciphertext, tag) = encrypted[SALT_LENGTH + NONCE_LENGTH..] - .split_at(encrypted.len() - SALT_LENGTH - NONCE_LENGTH - tag_len); - - let key = derive_key(password, salt); - let nonce = match Nonce::try_assume_unique_for_key(nonce) { - Ok(n) => n, - Err(e) => return Err(format!("Nonce error: {:?}", e)), - }; - - let mut plaintext = ciphertext.to_vec(); - plaintext.extend_from_slice(tag); - - match key.open_in_place(nonce, Aad::empty(), &mut plaintext) { - Ok(decrypted) => { - println!( - "Decryption successful! Decrypted length: {}", - decrypted.len() - ); - Ok(decrypted.to_vec()) - } - Err(e) => Err(format!("Decryption error: {:?}", e)), - } -} - -#[tauri::command] -pub async fn unlock_secure_folder(password: String) -> Result { - let secure_folder = get_secure_folder_path()?; - let config_path = secure_folder.join("config.json"); - - if !config_path.exists() { - return Err("Secure folder not set up".to_string()); - } - - let config: serde_json::Value = - serde_json::from_str(&fs::read_to_string(config_path).map_err(|e| e.to_string())?) - .map_err(|e| e.to_string())?; - - let salt = BASE64 - .decode(config["salt"].as_str().ok_or("Invalid salt")?.as_bytes()) - .map_err(|e| e.to_string())?; - let stored_hash = BASE64 - .decode( - config["hashed_password"] - .as_str() - .ok_or("Invalid hash")? - .as_bytes(), - ) - .map_err(|e| e.to_string())?; - - let input_hash = hash_password(&password, &salt); - - Ok(input_hash == stored_hash) -} - -pub fn derive_key(password: &str, salt: &[u8]) -> LessSafeKey { - let mut key_bytes = [0u8; 32]; - pbkdf2::derive( - pbkdf2::PBKDF2_HMAC_SHA256, - NonZeroU32::new(100_000).unwrap(), - salt, - password.as_bytes(), - &mut key_bytes, - ); - - let unbound_key = UnboundKey::new(&AES_256_GCM, &key_bytes).unwrap(); - LessSafeKey::new(unbound_key) -} - -#[tauri::command] -pub async fn check_secure_folder_status() -> Result { - let secure_folder = get_secure_folder_path()?; - let config_path = secure_folder.join("config.json"); - Ok(config_path.exists()) -} - -pub fn generate_nonce() -> [u8; NONCE_LENGTH] { - let mut nonce = [0u8; NONCE_LENGTH]; - SystemRandom::new().fill(&mut nonce).unwrap(); - nonce -} - -#[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) -} - -pub 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) -} - -pub 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() -} - -#[tauri::command] -pub async fn set_wallpaper(path: String) -> Result<(), String> { - let uri = format!("file://{}", path); - println!("Setting wallpaper to: {}", uri); - #[cfg(target_os = "windows")] - { - use std::ffi::OsStr; - use std::os::windows::ffi::OsStrExt; - use winapi::um::winuser::{ - SystemParametersInfoW, SPIF_SENDCHANGE, SPIF_UPDATEINIFILE, SPI_SETDESKWALLPAPER, - }; - - let wide: Vec = OsStr::new(&path).encode_wide().chain(Some(0)).collect(); - let result = unsafe { - SystemParametersInfoW( - SPI_SETDESKWALLPAPER, - 0, - wide.as_ptr() as *mut _, - SPIF_UPDATEINIFILE | SPIF_SENDCHANGE, - ) - }; - - if result == 0 { - return Err("Failed to set wallpaper".to_string()); - } - } - - #[cfg(target_os = "macos")] - { - let script = format!( - r#"tell application "Finder" to set desktop picture to POSIX file "{}""#, - path - ); - let output = Command::new("osascript") - .arg("-e") - .arg(&script) - .output() - .map_err(|e| e.to_string())?; - - if !output.status.success() { - return Err(String::from_utf8_lossy(&output.stderr).to_string()); - } - } - - #[cfg(target_os = "linux")] - { - // This assumes a GNOME-based desktop environment - let desktop_env = std::env::var("XDG_CURRENT_DESKTOP") - .unwrap_or_default() - .to_lowercase(); - - if desktop_env.contains("gnome") { - let output = Command::new("gsettings") - .args(&[ - "set", - "org.gnome.desktop.background", - "picture-uri", - &format!("file://{}", path), - ]) - .output() - .map_err(|e| e.to_string())?; - - if !output.status.success() { - return Err(String::from_utf8_lossy(&output.stderr).to_string()); - } - } else if desktop_env.contains("kde") { - let script = format!( - r#"qdbus org.kde.plasmashell /PlasmaShell org.kde.PlasmaShell.evaluateScript 'var allDesktops = desktops(); for (i=0; i Result<(), String> { - let parent = std::path::Path::new(&path) - .parent() - .ok_or_else(|| "Unable to get parent directory".to_string())?; - - #[cfg(target_os = "windows")] - { - Command::new("explorer") - .arg(parent) - .spawn() - .map_err(|e| e.to_string())?; - } - - #[cfg(target_os = "macos")] - { - Command::new("open") - .arg(parent) - .spawn() - .map_err(|e| e.to_string())?; - } - - #[cfg(target_os = "linux")] - { - Command::new("xdg-open") - .arg(parent) - .spawn() - .map_err(|e| e.to_string())?; - } - - Ok(()) -} - -#[tauri::command] -pub async fn open_with(path: String) -> Result<(), String> { - #[cfg(target_os = "windows")] - { - Command::new("rundll32.exe") - .args(&["shell32.dll,OpenAs_RunDLL", &path]) - .spawn() - .map_err(|e| e.to_string())?; - } - - #[cfg(target_os = "macos")] - { - Command::new("open") - .args(&["-a", &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 fn get_server_path(handle: tauri::AppHandle) -> Result { let resource_path = handle diff --git a/frontend/src-tauri/src/utils/file_utils.rs b/frontend/src-tauri/src/utils/file_utils.rs deleted file mode 100644 index 6cb31e48..00000000 --- a/frontend/src-tauri/src/utils/file_utils.rs +++ /dev/null @@ -1,13 +0,0 @@ -pub fn is_image_extension(extension: &str) -> bool { - matches!( - extension.to_lowercase().as_str(), - "jpg" | "jpeg" | "png" | "gif" | "bmp" | "tiff" | "webp" - ) -} - -pub fn is_video_extension(extension: &str) -> bool { - matches!( - extension.to_lowercase().as_str(), - "mp4" | "avi" | "mkv" | "mov" | "flv" | "m4v" - ) -} diff --git a/frontend/src-tauri/src/utils/mod.rs b/frontend/src-tauri/src/utils/mod.rs deleted file mode 100644 index 131d5ab2..00000000 --- a/frontend/src-tauri/src/utils/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod file_utils; diff --git a/frontend/src-tauri/tests/mod_test.rs b/frontend/src-tauri/tests/mod_test.rs deleted file mode 100644 index fe745eef..00000000 --- a/frontend/src-tauri/tests/mod_test.rs +++ /dev/null @@ -1,259 +0,0 @@ -use std::fs; -use std::path::Path; - -use image::{DynamicImage, GenericImageView, ImageOutputFormat, RgbImage}; -use tauri::State; -use tempfile::tempdir; -use tokio; - -use picto_py::services::{ - adjust_brightness_contrast, apply_sepia, check_secure_folder_status, create_secure_folder, - decrypt_data, derive_key, encrypt_data, generate_salt, get_folders_with_images, - get_images_in_folder, get_random_memories, get_secure_folder_path, hash_password, - is_image_file, move_to_secure_folder, remove_from_secure_folder, save_edited_image, share_file, - unlock_secure_folder, CacheService, FileService, SECURE_FOLDER_NAME, -}; - -/// This unsafe helper is for testing only. -fn state_from(t: &'static T) -> State<'static, T> { - unsafe { std::mem::transmute(t) } -} - -fn real_file_service_state() -> State<'static, FileService> { - state_from(Box::leak(Box::new(FileService::new()))) -} - -fn real_cache_service_state() -> State<'static, CacheService> { - state_from(Box::leak(Box::new(CacheService::new()))) -} - -// -// Integration Tests -// - -#[test] -fn test_get_folders_with_images() { - let directory = "test_dir"; - let fs_state = real_file_service_state(); - let cs_state = real_cache_service_state(); - let folders = get_folders_with_images(directory, fs_state, cs_state); - // Adjust this assertion according to expected behavior. - // Here, we simply check that the function returns a vector. - assert!(folders.len() >= 0); -} - -#[test] -fn test_get_images_in_folder() { - let folder = "folder_path"; - let fs_state = real_file_service_state(); - let images = get_images_in_folder(folder, fs_state); - assert!(images.len() >= 0); -} - -// #[test] -// fn test_get_all_images_with_cache_fallback() { -// let temp_dir = tempdir().unwrap(); -// let dummy_img = temp_dir.path().join("image1.jpg"); -// fs::write(&dummy_img, b"dummy").unwrap(); - -// let fs_state = real_file_service_state(); -// let cs_state = real_cache_service_state(); -// let result = get_all_images_with_cache(fs_state, cs_state, temp_dir.path().to_str().unwrap()); -// assert!(result.is_ok()); -// let map = result.unwrap(); -// assert!(!map.is_empty(), "Expected at least one year key in the map"); -// } - -// #[test] -// fn test_get_all_videos_with_cache_fallback() { -// let temp_dir = tempdir().unwrap(); -// let dummy_video = temp_dir.path().join("video1.mp4"); -// fs::write(&dummy_video, b"dummy video").unwrap(); - -// let fs_state = real_file_service_state(); -// let cs_state = real_cache_service_state(); -// let result = get_all_videos_with_cache(fs_state, cs_state, temp_dir.path().to_str().unwrap()); -// assert!(result.is_ok()); -// let map = result.unwrap(); -// assert!(!map.is_empty(), "Expected at least one year key in the map"); -// } - -// #[test] -// fn test_delete_cache() { -// let cs_state = real_cache_service_state(); -// let success = delete_cache(cs_state); -// assert!(success, "delete_all_caches should return true"); -// } - -#[tokio::test] -async fn test_share_file() { - let result = share_file("dummy_path".to_string()).await; - assert!(result.is_ok() || result.is_err()); -} - -async fn test_save_edited_image() { - // Create a simple test image - let img = DynamicImage::ImageRgb8(RgbImage::new(10, 10)); - let mut buffer = Vec::new(); - img.write_to( - &mut std::io::Cursor::new(&mut buffer), - ImageOutputFormat::Png, - ) - .unwrap(); - - // Create a temporary directory - let temp_dir = tempdir().unwrap(); - let original_path = temp_dir.path().join("test_image.png"); - - // Save the original image - fs::write(&original_path, &buffer).unwrap(); - - // Call the function to save the edited image - let result = save_edited_image( - buffer.clone(), - original_path.to_string_lossy().to_string(), // Correct save path - "grayscale(100%)".to_string(), - 100, - 100, - 0, - 0, - 0, - 0, - 0, - 0, - ) - .await; - - assert!(result.is_ok(), "save_edited_image should succeed"); - - // Check if the edited file exists at the correct path - assert!( - original_path.exists(), - "Edited image file should exist at the original path" - ); -} - -#[test] -fn test_apply_sepia() { - let img = DynamicImage::new_rgb8(10, 10); - let sepia_img = apply_sepia(&img); - assert_eq!(sepia_img.dimensions(), (10, 10)); -} - -#[test] -fn test_adjust_brightness_contrast() { - let img = DynamicImage::new_rgb8(10, 10); - let adjusted = adjust_brightness_contrast(&img, 10, 20); - assert_eq!(adjusted.dimensions(), (10, 10)); -} - -#[test] -fn test_get_secure_folder_path() { - let path = get_secure_folder_path().unwrap(); - assert!(path.to_string_lossy().contains(SECURE_FOLDER_NAME)); -} - -#[test] -fn test_hash_password() { - let salt = generate_salt(); - let hash = hash_password("password", &salt); - assert_eq!(hash.len(), ring::digest::SHA256_OUTPUT_LEN); -} - -#[test] -fn test_encrypt_decrypt_data() { - let data = b"test data"; - let password = "secret"; - let encrypted = encrypt_data(data, password).unwrap(); - let decrypted = decrypt_data(&encrypted, password).unwrap(); - assert_eq!(decrypted, data); -} - -#[test] -fn test_derive_key() { - let salt = generate_salt(); - let key = derive_key("password", &salt); - // We cannot access the inner key bytes, so we simply assume key derivation succeeded. - assert!(true, "Key derived successfully"); -} - -#[test] -fn test_is_image_file() { - let jpg_path = Path::new("image.jpg"); - let txt_path = Path::new("document.txt"); - assert!(is_image_file(jpg_path)); - assert!(!is_image_file(txt_path)); -} - -#[tokio::test] -async fn test_move_and_remove_from_secure_folder() { - let temp_dir = tempdir().unwrap(); - let secure_folder = temp_dir.path().join("secure_folder"); - fs::create_dir_all(&secure_folder).unwrap(); - - let file_content = b"secure content"; - let temp_file = temp_dir.path().join("test.txt"); - fs::write(&temp_file, file_content).unwrap(); - let password = "test_password"; - - let move_result = move_to_secure_folder( - temp_file.to_string_lossy().to_string(), - password.to_string(), - ) - .await; - assert!(move_result.is_ok() || move_result.is_err()); - - let remove_result = - remove_from_secure_folder("test.txt".to_string(), password.to_string()).await; - assert!(remove_result.is_ok() || remove_result.is_err()); -} - -#[tokio::test] -async fn test_create_and_unlock_secure_folder() { - let password = "secret"; - let create_result = create_secure_folder(password.to_string()).await; - assert!(create_result.is_ok() || create_result.is_err()); - - let unlock_result = unlock_secure_folder(password.to_string()).await; - assert!(unlock_result.is_ok() || unlock_result.is_err()); -} - -// #[tokio::test] -// async fn test_get_secure_media() { -// let temp_dir = tempdir().unwrap(); -// let secure_folder = temp_dir.path().join("secure_folder"); -// fs::create_dir_all(&secure_folder).unwrap(); - -// // Instead of writing plain data, we encrypt dummy image data with the same password. -// let password = "dummy_password"; -// let dummy_data = b"dummy image data"; -// let encrypted = encrypt_data(dummy_data, password).unwrap(); -// let img_path = secure_folder.join("dummy.jpg"); -// fs::write(&img_path, &encrypted).unwrap(); - -// let result = get_secure_media(password.to_string()).await; -// assert!(result.is_ok(), "get_secure_media should return Ok"); -// } - -#[tokio::test] -async fn test_check_secure_folder_status() { - let result = check_secure_folder_status().await; - assert!(result.is_ok()); -} - -#[test] -fn test_get_random_memories() { - let tmp = tempdir().unwrap(); - let sub = tmp.path().join("subdir"); - fs::create_dir_all(&sub).unwrap(); - - let fake_img = sub.join("image.jpg"); - fs::write(&fake_img, b"fake").unwrap(); - - let dirs = vec![tmp.path().to_string_lossy().to_string()]; - let result = get_random_memories(dirs, 5); - assert!(result.is_ok()); - let images = result.unwrap(); - // With one image available, expect exactly one image. - assert_eq!(images.len(), 1); -} diff --git a/mkdocs.yml b/mkdocs.yml index a5b47407..9647153f 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -70,9 +70,8 @@ nav: - Python Backend Image Processing: backend/backend_python/image-processing.md - Rust Backend API: backend/backend_rust/api.md - Frontend: - - UI Components: frontend/ui-components.md - State Management: frontend/state-management.md - - Gallery View: frontend/gallery-view.md + - Screenshots: frontend/screenshots.md extra: social: