fix: unified asset grid across the tool
Signed-off-by: Varun Raj <varun@skcript.com>
This commit is contained in:
+9
-1
@@ -28,4 +28,12 @@ You can find the API route here `pages/api/immich-proxy/[...path].ts`
|
||||
Since the `thumbnailPath` of Immich API doesnt give us a direct link to the image, I've created a proxy api that fetches the image and sends it back as a response.
|
||||
|
||||
**For Person's Thumbnail**
|
||||
You can find the API route here `pages/api/immich-proxy/thumbnail/[id].ts`
|
||||
You can find the API route here `pages/api/immich-proxy/thumbnail/[id].ts`
|
||||
|
||||
### Branching Strategy
|
||||
|
||||
- Always create a new branch for your changes
|
||||
- Always rebase your branch before creating a PR
|
||||
- Always squash your commits before merging
|
||||
- Always merge your branch into the `prerelease` branch
|
||||
- Always delete your branch after merging
|
||||
|
||||
@@ -61,6 +61,8 @@ GEMINI_API_KEY= # Gemini API Key
|
||||
```
|
||||
Refer here for obtaining Immich API Key: https://immich.app/docs/features/command-line-interface#obtain-the-api-key
|
||||
|
||||
> [!NOTE] When creating the API key, make sure you select all the permissions for the API key.
|
||||
|
||||
#### Method 2 - Portainer
|
||||
|
||||
If you're using portainer, run the docker using `docker run` and add the power tools to the same network as immich.
|
||||
@@ -149,4 +151,4 @@ Google Gemini 1.5 Flash model is used for parsing your search query in "Find" pa
|
||||
|
||||
## Contributing
|
||||
|
||||
Feel free to contribute to this project, I'm open to any suggestions and improvements. Please read the [CONTRIBUTING.md](./CONTRIBUTING.md) for more details.
|
||||
Feel free to contribute to this project, I'm open to any suggestions and improvements. Please read the [CONTRIBUTING.md](./CONTRIBUTING.md) for more details.
|
||||
|
||||
@@ -1,19 +1,15 @@
|
||||
import "yet-another-react-lightbox/styles.css";
|
||||
import { listAlbumAssets } from "@/handlers/api/album.handler";
|
||||
import { useConfig } from "@/contexts/ConfigContext";
|
||||
import { IAlbum, IAlbumPerson } from "@/types/album";
|
||||
import { IAsset } from "@/types/asset";
|
||||
import React, { useEffect, useMemo, useRef, useState, MouseEvent } from "react";
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { Hourglass } from "lucide-react";
|
||||
import { useRouter } from "next/router";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Gallery } from "react-grid-gallery";
|
||||
import Lightbox from "yet-another-react-lightbox";
|
||||
import Captions from "yet-another-react-lightbox/plugins/captions";
|
||||
import LazyGridImage from "@/components/ui/lazy-grid-image";
|
||||
import { usePhotoSelectionContext } from "@/contexts/PhotoSelectionContext";
|
||||
import { PERSON_THUBNAIL_PATH } from "@/config/routes";
|
||||
import Image from "next/image";
|
||||
import AssetGrid from "@/components/shared/AssetGrid";
|
||||
|
||||
interface AlbumImagesProps {
|
||||
album: IAlbum;
|
||||
@@ -40,8 +36,6 @@ export default function AlbumImages({ album, selectedPerson }: AlbumImagesProps)
|
||||
page: 1,
|
||||
});
|
||||
const [hasMore, setHasMore] = useState(true);
|
||||
const [index, setIndex] = useState(-1);
|
||||
const [lastSelectedIndex, setLastSelectedIndex] = useState(-1);
|
||||
|
||||
const fetchAssets = async () => {
|
||||
setLoading(true);
|
||||
@@ -58,74 +52,24 @@ export default function AlbumImages({ album, selectedPerson }: AlbumImagesProps)
|
||||
.finally(() => setLoading(false));
|
||||
};
|
||||
|
||||
const images = useMemo(() => {
|
||||
return assets.map((p) => ({
|
||||
...p,
|
||||
src: p.url as string,
|
||||
original: p.previewUrl as string,
|
||||
width: p.exifImageWidth as number,
|
||||
height: p.exifImageHeight as number,
|
||||
isSelected: selectedIds.includes(p.id),
|
||||
orientation: 1,
|
||||
const processedAssets = useMemo(() => {
|
||||
return assets.map((asset) => ({
|
||||
...asset,
|
||||
tags: [
|
||||
{
|
||||
title: "Immich Link",
|
||||
value: (
|
||||
<a href={exImmichUrl + "/photos/" + p.id} target="_blank" rel="noopener noreferrer">
|
||||
<a href={exImmichUrl + "/photos/" + asset.id} target="_blank" rel="noopener noreferrer">
|
||||
Open in Immich
|
||||
</a>
|
||||
),
|
||||
},
|
||||
],
|
||||
}));
|
||||
}, [assets, selectedIds, exImmichUrl]);
|
||||
}, [assets, exImmichUrl]);
|
||||
|
||||
const slides = useMemo(
|
||||
() =>
|
||||
images.map(({ original, width, height }) => ({
|
||||
src: original,
|
||||
width,
|
||||
height,
|
||||
})),
|
||||
[images]
|
||||
);
|
||||
|
||||
const handleClick = (idx: number, asset: IAsset, event: MouseEvent<HTMLElement>) => {
|
||||
if (selectedIds.length > 0) {
|
||||
handleSelect(idx, asset, event);
|
||||
} else {
|
||||
setIndex(idx);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelect = (_idx: number, asset: IAsset, event: MouseEvent<HTMLElement>) => {
|
||||
event.stopPropagation();
|
||||
const isPresent = selectedIds.includes(asset.id);
|
||||
if (isPresent) {
|
||||
updateContext({
|
||||
selectedIds: selectedIds.filter((id) => id !== asset.id),
|
||||
});
|
||||
} else {
|
||||
const clickedIndex = images.findIndex((image) => {
|
||||
return image.id === asset.id;
|
||||
});
|
||||
if (event.shiftKey && lastSelectedIndex !== -1) {
|
||||
const startIndex = Math.min(clickedIndex, lastSelectedIndex);
|
||||
const endIndex = Math.max(clickedIndex, lastSelectedIndex);
|
||||
if (startIndex >= 0 && endIndex < images.length) {
|
||||
const newSelectedIds = images.slice(startIndex, endIndex + 1).map((image) => image.id);
|
||||
const allSelectedIds = [...selectedIds, ...newSelectedIds];
|
||||
const uniqueSelectedIds = [...new Set(allSelectedIds)];
|
||||
updateContext({ selectedIds: uniqueSelectedIds });
|
||||
} else {
|
||||
console.warn("Shift-select index out of bounds");
|
||||
updateContext({ selectedIds: [...selectedIds, asset.id] });
|
||||
}
|
||||
} else {
|
||||
updateContext({ selectedIds: [...selectedIds, asset.id] });
|
||||
}
|
||||
setLastSelectedIndex(clickedIndex);
|
||||
}
|
||||
const handleSelectionChange = (ids: string[]) => {
|
||||
updateContext({ selectedIds: ids });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -160,46 +104,28 @@ export default function AlbumImages({ album, selectedPerson }: AlbumImagesProps)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
<Lightbox
|
||||
slides={slides}
|
||||
plugins={[Captions]}
|
||||
open={index >= 0}
|
||||
index={index}
|
||||
close={() => setIndex(-1)}
|
||||
<div className="w-full p-2 overflow-y-auto max-h-[calc(100vh-60px)]">
|
||||
<AssetGrid
|
||||
assets={processedAssets}
|
||||
isInternal={true}
|
||||
selectable={true}
|
||||
onSelectionChange={handleSelectionChange}
|
||||
/>
|
||||
<div className="w-full p-2 overflow-y-auto max-h-[calc(100vh-60px)]">
|
||||
<Gallery
|
||||
images={images}
|
||||
onClick={handleClick}
|
||||
enableImageSelection={true}
|
||||
onSelect={handleSelect}
|
||||
thumbnailImageComponent={LazyGridImage}
|
||||
tagStyle={{
|
||||
color: "white",
|
||||
fontSize: "12px",
|
||||
backgroundColor: "rgba(0, 0, 0)",
|
||||
padding: "2px",
|
||||
borderRadius: "5px",
|
||||
{hasMore && (
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full mt-4"
|
||||
onClick={() => {
|
||||
setFilters({
|
||||
...filters,
|
||||
page: filters.page + 1,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
{hasMore && (
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full mt-4"
|
||||
onClick={() => {
|
||||
setFilters({
|
||||
...filters,
|
||||
page: filters.page + 1,
|
||||
});
|
||||
}}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? "Loading..." : "Load More"}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? "Loading..." : "Load More"}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
import "yet-another-react-lightbox/styles.css";
|
||||
import { usePhotoSelectionContext } from "@/contexts/PhotoSelectionContext";
|
||||
import { listPotentialAlbumsAssets } from "@/handlers/api/album.handler";
|
||||
import type { IAsset } from "@/types/asset";
|
||||
import React, { type MouseEvent, useEffect, useMemo, useState } from "react";
|
||||
import { Gallery } from "react-grid-gallery";
|
||||
import Lightbox from "yet-another-react-lightbox";
|
||||
import Captions from "yet-another-react-lightbox/plugins/captions";
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { CalendarArrowUp, Hourglass } from "lucide-react";
|
||||
import Video from "yet-another-react-lightbox/plugins/video";
|
||||
import { useConfig } from "@/contexts/ConfigContext";
|
||||
import LazyGridImage from "@/components/ui/lazy-grid-image";
|
||||
import AssetGrid from "@/components/shared/AssetGrid";
|
||||
|
||||
export default function PotentialAlbumsAssets() {
|
||||
const { exImmichUrl } = useConfig();
|
||||
@@ -24,9 +19,6 @@ export default function PotentialAlbumsAssets() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
|
||||
const [index, setIndex] = useState(-1);
|
||||
const [lastSelectedIndex, setLastSelectedIndex] = useState(-1);
|
||||
|
||||
const fetchAssets = async () => {
|
||||
if (!startDate) return;
|
||||
setLoading(true);
|
||||
@@ -40,7 +32,7 @@ export default function PotentialAlbumsAssets() {
|
||||
.finally(() => setLoading(false));
|
||||
};
|
||||
|
||||
const images = useMemo(() => {
|
||||
const processedAssets = useMemo(() => {
|
||||
const sortedAssets = [...assets].sort((a, b) => {
|
||||
try {
|
||||
return new Date(a.dateTimeOriginal ?? 0).getTime() - new Date(b.dateTimeOriginal ?? 0).getTime();
|
||||
@@ -49,90 +41,23 @@ export default function PotentialAlbumsAssets() {
|
||||
}
|
||||
});
|
||||
|
||||
return sortedAssets.map((p) => ({
|
||||
...p,
|
||||
src: p.url as string,
|
||||
original: p.previewUrl as string,
|
||||
width: p.exifImageWidth as number,
|
||||
height: p.exifImageHeight as number,
|
||||
isSelected: selectedIds.includes(p.id),
|
||||
orientation: 1,
|
||||
return sortedAssets.map((asset) => ({
|
||||
...asset,
|
||||
tags: [
|
||||
{
|
||||
title: "Immich Link",
|
||||
value: (
|
||||
<a href={`${exImmichUrl}/photos/${p.id}`} target="_blank" rel="noreferrer">
|
||||
<a href={`${exImmichUrl}/photos/${asset.id}`} target="_blank" rel="noreferrer">
|
||||
Open in Immich
|
||||
</a>
|
||||
),
|
||||
},
|
||||
],
|
||||
}));
|
||||
}, [assets, selectedIds, exImmichUrl]);
|
||||
|
||||
|
||||
const slides = useMemo(
|
||||
() =>
|
||||
images.map(({ original, width, height, type, videoURL}) => ({
|
||||
src: original,
|
||||
width,
|
||||
height,
|
||||
type: (type === "VIDEO" ? "video" : "image") as any,
|
||||
sources:
|
||||
type === "VIDEO"
|
||||
? [
|
||||
{
|
||||
src: videoURL,
|
||||
type: "video/mp4",
|
||||
},
|
||||
]
|
||||
: [],
|
||||
})),
|
||||
[images]
|
||||
);
|
||||
|
||||
const handleClick = (idx: number, asset: IAsset, event: MouseEvent<HTMLElement>) => {
|
||||
if (selectedIds.length > 0 && (event.ctrlKey || event.metaKey || event.shiftKey)) {
|
||||
handleSelect(idx, asset, event);
|
||||
} else {
|
||||
setIndex(idx);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelect = (_idx: number, asset: IAsset, event: MouseEvent<HTMLElement>) => {
|
||||
event.stopPropagation();
|
||||
const isPresent = selectedIds.includes(asset.id);
|
||||
|
||||
if (isPresent) {
|
||||
if (event.ctrlKey || event.metaKey || selectedIds.length === 1) {
|
||||
updateContext({
|
||||
selectedIds: selectedIds.filter((id) => id !== asset.id),
|
||||
});
|
||||
} else if (!event.shiftKey) {
|
||||
updateContext({ selectedIds: [asset.id] });
|
||||
}
|
||||
} else {
|
||||
const clickedIndex = images.findIndex((image) => image.id === asset.id);
|
||||
|
||||
if (event.shiftKey && lastSelectedIndex !== -1) {
|
||||
const startIndex = Math.min(clickedIndex, lastSelectedIndex);
|
||||
const endIndex = Math.max(clickedIndex, lastSelectedIndex);
|
||||
if (startIndex >= 0 && endIndex < images.length) {
|
||||
const newSelectedIds = images.slice(startIndex, endIndex + 1).map((image) => image.id);
|
||||
const combined = event.ctrlKey || event.metaKey ? [...selectedIds, ...newSelectedIds] : newSelectedIds;
|
||||
const uniqueSelectedIds = [...new Set(combined)];
|
||||
updateContext({ selectedIds: uniqueSelectedIds });
|
||||
} else {
|
||||
updateContext({ selectedIds: [...selectedIds, asset.id] });
|
||||
}
|
||||
} else if (event.ctrlKey || event.metaKey) {
|
||||
updateContext({ selectedIds: [...selectedIds, asset.id] });
|
||||
} else {
|
||||
updateContext({ selectedIds: [asset.id] });
|
||||
}
|
||||
setLastSelectedIndex(clickedIndex);
|
||||
}
|
||||
}, [assets, exImmichUrl]);
|
||||
|
||||
const handleSelectionChange = (ids: string[]) => {
|
||||
updateContext({ selectedIds: ids });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -158,33 +83,15 @@ export default function PotentialAlbumsAssets() {
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Lightbox
|
||||
slides={slides}
|
||||
plugins={[Captions, Video]}
|
||||
open={index >= 0}
|
||||
index={index}
|
||||
close={() => setIndex(-1)}
|
||||
<div className="w-full overflow-y-auto max-h-[calc(100vh-60px)]">
|
||||
<AssetGrid
|
||||
assets={processedAssets}
|
||||
isInternal={true}
|
||||
selectable={true}
|
||||
onSelectionChange={handleSelectionChange}
|
||||
/>
|
||||
<div className="w-full overflow-y-auto max-h-[calc(100vh-60px)]">
|
||||
<Gallery
|
||||
images={images}
|
||||
onClick={handleClick}
|
||||
enableImageSelection={true}
|
||||
onSelect={handleSelect}
|
||||
thumbnailImageComponent={LazyGridImage}
|
||||
rowHeight={220}
|
||||
margin={3}
|
||||
tagStyle={{
|
||||
color: "white",
|
||||
fontSize: "12px",
|
||||
backgroundColor: "rgba(0, 0, 0, 0.7)",
|
||||
padding: "2px 4px",
|
||||
borderRadius: "3px",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import "yet-another-react-lightbox/styles.css";
|
||||
import { IAsset } from "@/types/asset";
|
||||
import React, { useEffect, useMemo, useState, MouseEvent } from "react";
|
||||
import { Gallery } from "react-grid-gallery";
|
||||
import Lightbox from "yet-another-react-lightbox";
|
||||
import Captions from "yet-another-react-lightbox/plugins/captions";
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
CalendarArrowUp,
|
||||
Hourglass,
|
||||
@@ -11,7 +7,7 @@ import {
|
||||
import { usePhotoSelectionContext } from "@/contexts/PhotoSelectionContext";
|
||||
import { listMissingLocationAssets } from "@/handlers/api/asset.handler";
|
||||
import { useConfig } from "@/contexts/ConfigContext";
|
||||
import LazyGridImage from "@/components/ui/lazy-grid-image";
|
||||
import AssetGrid from "@/components/shared/AssetGrid";
|
||||
|
||||
interface IProps {
|
||||
groupBy: "date" | "album";
|
||||
@@ -30,9 +26,6 @@ export default function MissingLocationAssets({ groupBy }: IProps) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
|
||||
const [index, setIndex] = useState(-1);
|
||||
const [lastSelectedIndex, setLastSelectedIndex] = useState(-1);
|
||||
|
||||
const fetchAssets = async () => {
|
||||
if (!startDate && !albumId) return;
|
||||
setLoading(true);
|
||||
@@ -46,7 +39,7 @@ export default function MissingLocationAssets({ groupBy }: IProps) {
|
||||
.finally(() => setLoading(false));
|
||||
};
|
||||
|
||||
const images = useMemo(() => {
|
||||
const processedAssets = useMemo(() => {
|
||||
let sortedAssets: IAsset[];
|
||||
try {
|
||||
if (sortOrder === "desc")
|
||||
@@ -58,76 +51,23 @@ export default function MissingLocationAssets({ groupBy }: IProps) {
|
||||
sortedAssets = [...assets];
|
||||
}
|
||||
|
||||
return sortedAssets.
|
||||
map((p) => ({
|
||||
...p,
|
||||
src: p.url as string,
|
||||
original: p.previewUrl as string,
|
||||
width: p.exifImageWidth as number,
|
||||
height: p.exifImageHeight as number,
|
||||
isSelected: selectedIds.includes(p.id),
|
||||
orientation: 1,
|
||||
tags: [
|
||||
{
|
||||
title: "Immich Link",
|
||||
value: (
|
||||
<a href={exImmichUrl + "/photos/" + p.id} target="_blank" rel="noopener noreferrer">
|
||||
Open in Immich
|
||||
</a>
|
||||
),
|
||||
},
|
||||
],
|
||||
}));
|
||||
}, [assets, selectedIds, sortOrder, exImmichUrl]);
|
||||
return sortedAssets.map((asset) => ({
|
||||
...asset,
|
||||
tags: [
|
||||
{
|
||||
title: "Immich Link",
|
||||
value: (
|
||||
<a href={exImmichUrl + "/photos/" + asset.id} target="_blank" rel="noopener noreferrer">
|
||||
Open in Immich
|
||||
</a>
|
||||
),
|
||||
},
|
||||
],
|
||||
}));
|
||||
}, [assets, sortOrder, exImmichUrl]);
|
||||
|
||||
const slides = useMemo(
|
||||
() =>
|
||||
images.map(({ original, width, height }) => ({
|
||||
src: original,
|
||||
width,
|
||||
height,
|
||||
})),
|
||||
[images]
|
||||
);
|
||||
|
||||
const handleClick = (idx: number, asset: IAsset, event: MouseEvent<HTMLElement>) => {
|
||||
if (selectedIds.length > 0) {
|
||||
handleSelect(idx, asset, event);
|
||||
}
|
||||
else
|
||||
{
|
||||
setIndex(idx);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelect = (_idx: number, asset: IAsset, event: MouseEvent<HTMLElement>) => {
|
||||
event.stopPropagation();
|
||||
const isPresent = selectedIds.includes(asset.id);
|
||||
if (isPresent) {
|
||||
updateContext({
|
||||
selectedIds: selectedIds.filter((id) => id !== asset.id),
|
||||
});
|
||||
} else {
|
||||
const clickedIndex = images.findIndex((image) => {
|
||||
return image.id === asset.id;
|
||||
});
|
||||
if (event.shiftKey && lastSelectedIndex !== -1) {
|
||||
const startIndex = Math.min(clickedIndex, lastSelectedIndex);
|
||||
const endIndex = Math.max(clickedIndex, lastSelectedIndex);
|
||||
if (startIndex >= 0 && endIndex < images.length) {
|
||||
const newSelectedIds = images.slice(startIndex, endIndex + 1).map((image) => image.id);
|
||||
const allSelectedIds = [...selectedIds, ...newSelectedIds];
|
||||
const uniqueSelectedIds = [...new Set(allSelectedIds)];
|
||||
updateContext({ selectedIds: uniqueSelectedIds });
|
||||
} else {
|
||||
console.warn("Shift-select index out of bounds");
|
||||
updateContext({ selectedIds: [...selectedIds, asset.id] });
|
||||
}
|
||||
} else {
|
||||
updateContext({ selectedIds: [...selectedIds, asset.id] });
|
||||
}
|
||||
setLastSelectedIndex(clickedIndex);
|
||||
}
|
||||
const handleSelectionChange = (ids: string[]) => {
|
||||
updateContext({ selectedIds: ids });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -155,30 +95,13 @@ export default function MissingLocationAssets({ groupBy }: IProps) {
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Lightbox
|
||||
slides={slides}
|
||||
plugins={[Captions]}
|
||||
open={index >= 0}
|
||||
index={index}
|
||||
close={() => setIndex(-1)}
|
||||
<div className="w-full overflow-y-auto max-h-[calc(100vh-60px)]">
|
||||
<AssetGrid
|
||||
assets={processedAssets}
|
||||
isInternal={true}
|
||||
selectable={true}
|
||||
onSelectionChange={handleSelectionChange}
|
||||
/>
|
||||
<div className="w-full overflow-y-auto max-h-[calc(100vh-60px)]">
|
||||
<Gallery
|
||||
images={images}
|
||||
onClick={handleClick}
|
||||
enableImageSelection={true}
|
||||
onSelect={handleSelect}
|
||||
thumbnailImageComponent={LazyGridImage}
|
||||
tagStyle={{
|
||||
color: "white",
|
||||
fontSize: "12px",
|
||||
backgroundColor: "rgba(0, 0, 0)",
|
||||
padding: "2px",
|
||||
borderRadius: "5px",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user