diff --git a/backend/app/api/images.py b/backend/app/api/images.py index c4014f1..2c94049 100644 --- a/backend/app/api/images.py +++ b/backend/app/api/images.py @@ -3,10 +3,9 @@ from uuid import UUID from fastapi import APIRouter, Depends, File, HTTPException, UploadFile, status -from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.orm import Session -from app.auth.jwt import get_current_user -from app.core.deps import get_db +from app.core.deps import get_current_user, get_db from app.database.models.board import Board from app.database.models.user import User from app.images.processing import generate_thumbnails diff --git a/backend/app/database/models/image.py b/backend/app/database/models/image.py index 0ad8010..5bfa442 100644 --- a/backend/app/database/models/image.py +++ b/backend/app/database/models/image.py @@ -36,7 +36,7 @@ class Image(Base): mime_type: Mapped[str] = mapped_column(String(100), nullable=False) width: Mapped[int] = mapped_column(Integer, nullable=False) height: Mapped[int] = mapped_column(Integer, nullable=False) - metadata: Mapped[dict] = mapped_column(JSONB, nullable=False) + image_metadata: Mapped[dict] = mapped_column(JSONB, nullable=False) created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=datetime.utcnow) reference_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0) diff --git a/backend/app/images/repository.py b/backend/app/images/repository.py index 2944caa..14b321e 100644 --- a/backend/app/images/repository.py +++ b/backend/app/images/repository.py @@ -3,8 +3,7 @@ from collections.abc import Sequence from uuid import UUID -from sqlalchemy import select -from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.orm import Session from app.database.models.board_image import BoardImage from app.database.models.image import Image @@ -13,11 +12,11 @@ from app.database.models.image import Image class ImageRepository: """Repository for image database operations.""" - def __init__(self, db: AsyncSession): + def __init__(self, db: Session): """Initialize repository with database session.""" self.db = db - async def create_image( + def create_image( self, user_id: UUID, filename: str, @@ -28,22 +27,7 @@ class ImageRepository: height: int, metadata: dict, ) -> Image: - """ - Create new image record. - - Args: - user_id: Owner user ID - filename: Original filename - storage_path: Path in MinIO - file_size: File size in bytes - mime_type: MIME type - width: Image width in pixels - height: Image height in pixels - metadata: Additional metadata (format, checksum, thumbnails, etc) - - Returns: - Created Image instance - """ + """Create new image record.""" image = Image( user_id=user_id, filename=filename, @@ -52,98 +36,59 @@ class ImageRepository: mime_type=mime_type, width=width, height=height, - metadata=metadata, + image_metadata=metadata, ) self.db.add(image) - await self.db.commit() - await self.db.refresh(image) + self.db.commit() + self.db.refresh(image) return image - async def get_image_by_id(self, image_id: UUID) -> Image | None: - """ - Get image by ID. + def get_image_by_id(self, image_id: UUID) -> Image | None: + """Get image by ID.""" + return self.db.query(Image).filter(Image.id == image_id).first() - Args: - image_id: Image ID - - Returns: - Image instance or None - """ - result = await self.db.execute(select(Image).where(Image.id == image_id)) - return result.scalar_one_or_none() - - async def get_user_images(self, user_id: UUID, limit: int = 50, offset: int = 0) -> tuple[Sequence[Image], int]: - """ - Get all images for a user with pagination. - - Args: - user_id: User ID - limit: Maximum number of images to return - offset: Number of images to skip - - Returns: - Tuple of (images, total_count) - """ - # Get total count - count_result = await self.db.execute(select(Image).where(Image.user_id == user_id)) - total = len(count_result.scalars().all()) - - # Get paginated results - result = await self.db.execute( - select(Image).where(Image.user_id == user_id).order_by(Image.created_at.desc()).limit(limit).offset(offset) + def get_user_images( + self, user_id: UUID, limit: int = 50, offset: int = 0 + ) -> tuple[Sequence[Image], int]: + """Get all images for a user with pagination.""" + total = self.db.query(Image).filter(Image.user_id == user_id).count() + images = ( + self.db.query(Image) + .filter(Image.user_id == user_id) + .order_by(Image.created_at.desc()) + .limit(limit) + .offset(offset) + .all() ) - images = result.scalars().all() - return images, total - async def delete_image(self, image_id: UUID) -> bool: - """ - Delete image record. - - Args: - image_id: Image ID - - Returns: - True if deleted, False if not found - """ - image = await self.get_image_by_id(image_id) + def delete_image(self, image_id: UUID) -> bool: + """Delete image record.""" + image = self.get_image_by_id(image_id) if not image: return False - await self.db.delete(image) - await self.db.commit() + self.db.delete(image) + self.db.commit() return True - async def increment_reference_count(self, image_id: UUID) -> None: - """ - Increment reference count for image. - - Args: - image_id: Image ID - """ - image = await self.get_image_by_id(image_id) + def increment_reference_count(self, image_id: UUID) -> None: + """Increment reference count for image.""" + image = self.get_image_by_id(image_id) if image: image.reference_count += 1 - await self.db.commit() + self.db.commit() - async def decrement_reference_count(self, image_id: UUID) -> int: - """ - Decrement reference count for image. - - Args: - image_id: Image ID - - Returns: - New reference count - """ - image = await self.get_image_by_id(image_id) + def decrement_reference_count(self, image_id: UUID) -> int: + """Decrement reference count for image.""" + image = self.get_image_by_id(image_id) if image and image.reference_count > 0: image.reference_count -= 1 - await self.db.commit() + self.db.commit() return image.reference_count return 0 - async def add_image_to_board( + def add_image_to_board( self, board_id: UUID, image_id: UUID, @@ -151,19 +96,7 @@ class ImageRepository: transformations: dict, z_order: int = 0, ) -> BoardImage: - """ - Add image to board. - - Args: - board_id: Board ID - image_id: Image ID - position: Canvas position {x, y} - transformations: Image transformations - z_order: Layer order - - Returns: - Created BoardImage instance - """ + """Add image to board.""" board_image = BoardImage( board_id=board_id, image_id=image_id, @@ -174,50 +107,36 @@ class ImageRepository: self.db.add(board_image) # Increment reference count - await self.increment_reference_count(image_id) + self.increment_reference_count(image_id) - await self.db.commit() - await self.db.refresh(board_image) + self.db.commit() + self.db.refresh(board_image) return board_image - async def get_board_images(self, board_id: UUID) -> Sequence[BoardImage]: - """ - Get all images for a board, ordered by z-order. - - Args: - board_id: Board ID - - Returns: - List of BoardImage instances - """ - result = await self.db.execute( - select(BoardImage).where(BoardImage.board_id == board_id).order_by(BoardImage.z_order.asc()) + def get_board_images(self, board_id: UUID) -> Sequence[BoardImage]: + """Get all images for a board, ordered by z-order.""" + return ( + self.db.query(BoardImage) + .filter(BoardImage.board_id == board_id) + .order_by(BoardImage.z_order.asc()) + .all() ) - return result.scalars().all() - async def remove_image_from_board(self, board_id: UUID, image_id: UUID) -> bool: - """ - Remove image from board. - - Args: - board_id: Board ID - image_id: Image ID - - Returns: - True if removed, False if not found - """ - result = await self.db.execute( - select(BoardImage).where(BoardImage.board_id == board_id, BoardImage.image_id == image_id) + def remove_image_from_board(self, board_id: UUID, image_id: UUID) -> bool: + """Remove image from board.""" + board_image = ( + self.db.query(BoardImage) + .filter(BoardImage.board_id == board_id, BoardImage.image_id == image_id) + .first() ) - board_image = result.scalar_one_or_none() if not board_image: return False - await self.db.delete(board_image) + self.db.delete(board_image) # Decrement reference count - await self.decrement_reference_count(image_id) + self.decrement_reference_count(image_id) - await self.db.commit() + self.db.commit() return True diff --git a/frontend/src/lib/api/images.ts b/frontend/src/lib/api/images.ts index d61feb6..3526f65 100644 --- a/frontend/src/lib/api/images.ts +++ b/frontend/src/lib/api/images.ts @@ -9,32 +9,14 @@ import type { Image, BoardImage, ImageListResponse } from '$lib/types/images'; * Upload a single image */ export async function uploadImage(file: File): Promise { - const formData = new FormData(); - formData.append('file', file); - - const response = await apiClient.post('/images/upload', formData, { - headers: { - 'Content-Type': 'multipart/form-data', - }, - }); - - return response; + return await apiClient.uploadFile('/images/upload', file); } /** * Upload multiple images from a ZIP file */ export async function uploadZip(file: File): Promise { - const formData = new FormData(); - formData.append('file', file); - - const response = await apiClient.post('/images/upload-zip', formData, { - headers: { - 'Content-Type': 'multipart/form-data', - }, - }); - - return response; + return await apiClient.uploadFile('/images/upload-zip', file); } /** diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte new file mode 100644 index 0000000..403d77f --- /dev/null +++ b/frontend/src/routes/+page.svelte @@ -0,0 +1,57 @@ + + + + Reference Board Viewer + + +
+
+

Loading...

+
+ + diff --git a/frontend/src/routes/+page.ts b/frontend/src/routes/+page.ts new file mode 100644 index 0000000..502318d --- /dev/null +++ b/frontend/src/routes/+page.ts @@ -0,0 +1,2 @@ +// Disable server-side rendering for the root page +export const ssr = false; diff --git a/frontend/src/routes/boards/[id]/+page.svelte b/frontend/src/routes/boards/[id]/+page.svelte new file mode 100644 index 0000000..98f0ccc --- /dev/null +++ b/frontend/src/routes/boards/[id]/+page.svelte @@ -0,0 +1,506 @@ + + + + {$currentBoard?.title || 'Board'} - Reference Board Viewer + + + + + diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 0000000..3893b92 --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,11 @@ +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [sveltekit()], + server: { + port: 5173, + strictPort: false, + }, +}); +