From 367d1fce8ceded85c1464086fcf060a2b9cf4fd3 Mon Sep 17 00:00:00 2001 From: fidoriel <49869342+fidoriel@users.noreply.github.com> Date: Sun, 27 Oct 2024 18:54:40 +0100 Subject: [PATCH] overhaul image ui --- README.md | 2 +- frontend/Model.tsx | 139 +++++++++++++++++++----- frontend/components/ui/aspect-ratio.tsx | 5 + package-lock.json | 24 ++++ package.json | 1 + 5 files changed, 143 insertions(+), 28 deletions(-) create mode 100644 frontend/components/ui/aspect-ratio.tsx diff --git a/README.md b/README.md index 816becf..ec5b283 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # MeshVault -> Disclaimer: Do not use in any production like environment until the first version has been released. There __WILL__ be breaking changes and no migration guides. You have been warned. Have fun. +> Disclaimer: Do not use in any production like environment until the first version has been released. There **WILL** be breaking changes and no migration guides. You have been warned. Have fun. A blazingly fast and simple self-hosted 3D files platform written in rust and typescript centered around a 3D model packaging format. diff --git a/frontend/Model.tsx b/frontend/Model.tsx index 6010818..3e91056 100644 --- a/frontend/Model.tsx +++ b/frontend/Model.tsx @@ -2,11 +2,12 @@ import { Button } from "@/components/ui/button"; import { Card, CardContent } from "@/components/ui/card"; import { Download, Heart, MoreVertical, RefreshCcw, Bookmark } from "lucide-react"; -import { useState, useEffect } from "react"; +import { useState, useEffect, useRef } from "react"; import { useParams } from "react-router-dom"; import { DetailedModelResponse } from "./bindings"; import { BACKEND_BASE_URL } from "./lib/api"; import { saveAs } from "file-saver"; +import { ChevronLeft, ChevronRight } from "lucide-react"; import { DropdownMenu, @@ -16,6 +17,7 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; +import { AspectRatio } from "./components/ui/aspect-ratio"; function OptionsDropdownMenu() { return ( @@ -36,39 +38,122 @@ function OptionsDropdownMenu() { } function ImageGallery({ model }: { model: DetailedModelResponse }) { - const [selectedImage, setSelectedImage] = useState(model.images[0] || undefined); + const [selectedImage, setSelectedImage] = useState(0); + const thumbnailsRef = useRef(null); + + const nextImage = () => { + setSelectedImage((prev) => (prev + 1) % model.images.length); + }; + + const previousImage = () => { + setSelectedImage((prev) => (prev - 1 + model.images.length) % model.images.length); + }; + + // Scroll selected thumbnail into view + useEffect(() => { + const thumbnailsContainer = thumbnailsRef.current; + if (!thumbnailsContainer) return; + + const selectedThumbnail = thumbnailsContainer.children[selectedImage] as HTMLElement; + if (!selectedThumbnail) return; + + const scrollLeft = + selectedThumbnail.offsetLeft - thumbnailsContainer.offsetWidth / 2 + selectedThumbnail.offsetWidth / 2; + thumbnailsContainer.scrollTo({ + left: scrollLeft, + behavior: "smooth", + }); + }, [selectedImage]); + + // Keyboard navigation + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === "ArrowLeft") { + previousImage(); + } else if (e.key === "ArrowRight") { + nextImage(); + } + }; + + window.addEventListener("keydown", handleKeyDown); + return () => window.removeEventListener("keydown", handleKeyDown); + }, []); + + const scrollThumbnails = (direction: "left" | "right") => { + const thumbnailsContainer = thumbnailsRef.current; + if (!thumbnailsContainer) return; + + const scrollAmount = 200; // Adjust this value to control scroll distance + const newScrollLeft = thumbnailsContainer.scrollLeft + (direction === "left" ? -scrollAmount : scrollAmount); + thumbnailsContainer.scrollTo({ + left: newScrollLeft, + behavior: "smooth", + }); + }; return (
- + - Model Preview + + Model Preview + + + -
- {model.images.map((img, index) => ( -
- -
- ))} +
+ + +
+ {model.images.map((img, index) => ( +
+ +
+ ))} +
+ +
); diff --git a/frontend/components/ui/aspect-ratio.tsx b/frontend/components/ui/aspect-ratio.tsx new file mode 100644 index 0000000..c9e6f4b --- /dev/null +++ b/frontend/components/ui/aspect-ratio.tsx @@ -0,0 +1,5 @@ +import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"; + +const AspectRatio = AspectRatioPrimitive.Root; + +export { AspectRatio }; diff --git a/package-lock.json b/package-lock.json index b847820..963ac79 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "meshvault", "version": "0.0.0", "dependencies": { + "@radix-ui/react-aspect-ratio": "^1.1.0", "@radix-ui/react-collapsible": "^1.1.1", "@radix-ui/react-dropdown-menu": "^2.1.2", "@radix-ui/react-icons": "^1.3.0", @@ -819,6 +820,29 @@ } } }, + "node_modules/@radix-ui/react-aspect-ratio": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-aspect-ratio/-/react-aspect-ratio-1.1.0.tgz", + "integrity": "sha512-dP87DM/Y7jFlPgUZTlhx6FF5CEzOiaxp2rBCKlaXlpH5Ip/9Fg5zZ9lDOQ5o/MOfUlf36eak14zoWYpgcgGoOg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.0.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-collapsible": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.1.tgz", diff --git a/package.json b/package.json index 4d01901..9a4fb41 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "precommit": "npm run format && npm run lint:fix && npm run build" }, "dependencies": { + "@radix-ui/react-aspect-ratio": "^1.1.0", "@radix-ui/react-collapsible": "^1.1.1", "@radix-ui/react-dropdown-menu": "^2.1.2", "@radix-ui/react-icons": "^1.3.0",