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.
|
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**
|
**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
|
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
|
#### 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.
|
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
|
## 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 { listAlbumAssets } from "@/handlers/api/album.handler";
|
||||||
import { useConfig } from "@/contexts/ConfigContext";
|
import { useConfig } from "@/contexts/ConfigContext";
|
||||||
import { IAlbum, IAlbumPerson } from "@/types/album";
|
import { IAlbum, IAlbumPerson } from "@/types/album";
|
||||||
import { IAsset } from "@/types/asset";
|
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 { Hourglass } from "lucide-react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { Button } from "@/components/ui/button";
|
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 { usePhotoSelectionContext } from "@/contexts/PhotoSelectionContext";
|
||||||
import { PERSON_THUBNAIL_PATH } from "@/config/routes";
|
import { PERSON_THUBNAIL_PATH } from "@/config/routes";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
import AssetGrid from "@/components/shared/AssetGrid";
|
||||||
|
|
||||||
interface AlbumImagesProps {
|
interface AlbumImagesProps {
|
||||||
album: IAlbum;
|
album: IAlbum;
|
||||||
@@ -40,8 +36,6 @@ export default function AlbumImages({ album, selectedPerson }: AlbumImagesProps)
|
|||||||
page: 1,
|
page: 1,
|
||||||
});
|
});
|
||||||
const [hasMore, setHasMore] = useState(true);
|
const [hasMore, setHasMore] = useState(true);
|
||||||
const [index, setIndex] = useState(-1);
|
|
||||||
const [lastSelectedIndex, setLastSelectedIndex] = useState(-1);
|
|
||||||
|
|
||||||
const fetchAssets = async () => {
|
const fetchAssets = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -58,74 +52,24 @@ export default function AlbumImages({ album, selectedPerson }: AlbumImagesProps)
|
|||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
};
|
};
|
||||||
|
|
||||||
const images = useMemo(() => {
|
const processedAssets = useMemo(() => {
|
||||||
return assets.map((p) => ({
|
return assets.map((asset) => ({
|
||||||
...p,
|
...asset,
|
||||||
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: [
|
tags: [
|
||||||
{
|
{
|
||||||
title: "Immich Link",
|
title: "Immich Link",
|
||||||
value: (
|
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
|
Open in Immich
|
||||||
</a>
|
</a>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}));
|
}));
|
||||||
}, [assets, selectedIds, exImmichUrl]);
|
}, [assets, exImmichUrl]);
|
||||||
|
|
||||||
const slides = useMemo(
|
const handleSelectionChange = (ids: string[]) => {
|
||||||
() =>
|
updateContext({ selectedIds: ids });
|
||||||
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);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -160,46 +104,28 @@ export default function AlbumImages({ album, selectedPerson }: AlbumImagesProps)
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="w-full p-2 overflow-y-auto max-h-[calc(100vh-60px)]">
|
||||||
|
<AssetGrid
|
||||||
<Lightbox
|
assets={processedAssets}
|
||||||
slides={slides}
|
isInternal={true}
|
||||||
plugins={[Captions]}
|
selectable={true}
|
||||||
open={index >= 0}
|
onSelectionChange={handleSelectionChange}
|
||||||
index={index}
|
|
||||||
close={() => setIndex(-1)}
|
|
||||||
/>
|
/>
|
||||||
<div className="w-full p-2 overflow-y-auto max-h-[calc(100vh-60px)]">
|
{hasMore && (
|
||||||
<Gallery
|
<Button
|
||||||
images={images}
|
variant="outline"
|
||||||
onClick={handleClick}
|
className="w-full mt-4"
|
||||||
enableImageSelection={true}
|
onClick={() => {
|
||||||
onSelect={handleSelect}
|
setFilters({
|
||||||
thumbnailImageComponent={LazyGridImage}
|
...filters,
|
||||||
tagStyle={{
|
page: filters.page + 1,
|
||||||
color: "white",
|
});
|
||||||
fontSize: "12px",
|
|
||||||
backgroundColor: "rgba(0, 0, 0)",
|
|
||||||
padding: "2px",
|
|
||||||
borderRadius: "5px",
|
|
||||||
}}
|
}}
|
||||||
/>
|
disabled={loading}
|
||||||
{hasMore && (
|
>
|
||||||
<Button
|
{loading ? "Loading..." : "Load More"}
|
||||||
variant="outline"
|
</Button>
|
||||||
className="w-full mt-4"
|
)}
|
||||||
onClick={() => {
|
</div>
|
||||||
setFilters({
|
|
||||||
...filters,
|
|
||||||
page: filters.page + 1,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
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 { usePhotoSelectionContext } from "@/contexts/PhotoSelectionContext";
|
||||||
import { listPotentialAlbumsAssets } from "@/handlers/api/album.handler";
|
import { listPotentialAlbumsAssets } from "@/handlers/api/album.handler";
|
||||||
import type { IAsset } from "@/types/asset";
|
import type { IAsset } from "@/types/asset";
|
||||||
import React, { type MouseEvent, useEffect, useMemo, useState } from "react";
|
import React, { 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 { CalendarArrowUp, Hourglass } from "lucide-react";
|
import { CalendarArrowUp, Hourglass } from "lucide-react";
|
||||||
import Video from "yet-another-react-lightbox/plugins/video";
|
|
||||||
import { useConfig } from "@/contexts/ConfigContext";
|
import { useConfig } from "@/contexts/ConfigContext";
|
||||||
import LazyGridImage from "@/components/ui/lazy-grid-image";
|
import AssetGrid from "@/components/shared/AssetGrid";
|
||||||
|
|
||||||
export default function PotentialAlbumsAssets() {
|
export default function PotentialAlbumsAssets() {
|
||||||
const { exImmichUrl } = useConfig();
|
const { exImmichUrl } = useConfig();
|
||||||
@@ -24,9 +19,6 @@ export default function PotentialAlbumsAssets() {
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||||
|
|
||||||
const [index, setIndex] = useState(-1);
|
|
||||||
const [lastSelectedIndex, setLastSelectedIndex] = useState(-1);
|
|
||||||
|
|
||||||
const fetchAssets = async () => {
|
const fetchAssets = async () => {
|
||||||
if (!startDate) return;
|
if (!startDate) return;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -40,7 +32,7 @@ export default function PotentialAlbumsAssets() {
|
|||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
};
|
};
|
||||||
|
|
||||||
const images = useMemo(() => {
|
const processedAssets = useMemo(() => {
|
||||||
const sortedAssets = [...assets].sort((a, b) => {
|
const sortedAssets = [...assets].sort((a, b) => {
|
||||||
try {
|
try {
|
||||||
return new Date(a.dateTimeOriginal ?? 0).getTime() - new Date(b.dateTimeOriginal ?? 0).getTime();
|
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) => ({
|
return sortedAssets.map((asset) => ({
|
||||||
...p,
|
...asset,
|
||||||
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: [
|
tags: [
|
||||||
{
|
{
|
||||||
title: "Immich Link",
|
title: "Immich Link",
|
||||||
value: (
|
value: (
|
||||||
<a href={`${exImmichUrl}/photos/${p.id}`} target="_blank" rel="noreferrer">
|
<a href={`${exImmichUrl}/photos/${asset.id}`} target="_blank" rel="noreferrer">
|
||||||
Open in Immich
|
Open in Immich
|
||||||
</a>
|
</a>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}));
|
}));
|
||||||
}, [assets, selectedIds, exImmichUrl]);
|
}, [assets, 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const handleSelectionChange = (ids: string[]) => {
|
||||||
|
updateContext({ selectedIds: ids });
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -158,33 +83,15 @@ export default function PotentialAlbumsAssets() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="w-full overflow-y-auto max-h-[calc(100vh-60px)]">
|
||||||
<Lightbox
|
<AssetGrid
|
||||||
slides={slides}
|
assets={processedAssets}
|
||||||
plugins={[Captions, Video]}
|
isInternal={true}
|
||||||
open={index >= 0}
|
selectable={true}
|
||||||
index={index}
|
onSelectionChange={handleSelectionChange}
|
||||||
close={() => setIndex(-1)}
|
|
||||||
/>
|
/>
|
||||||
<div className="w-full overflow-y-auto max-h-[calc(100vh-60px)]">
|
</div>
|
||||||
<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>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
import "yet-another-react-lightbox/styles.css";
|
|
||||||
import { IAsset } from "@/types/asset";
|
import { IAsset } from "@/types/asset";
|
||||||
import React, { useEffect, useMemo, useState, MouseEvent } from "react";
|
import React, { 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 {
|
import {
|
||||||
CalendarArrowUp,
|
CalendarArrowUp,
|
||||||
Hourglass,
|
Hourglass,
|
||||||
@@ -11,7 +7,7 @@ import {
|
|||||||
import { usePhotoSelectionContext } from "@/contexts/PhotoSelectionContext";
|
import { usePhotoSelectionContext } from "@/contexts/PhotoSelectionContext";
|
||||||
import { listMissingLocationAssets } from "@/handlers/api/asset.handler";
|
import { listMissingLocationAssets } from "@/handlers/api/asset.handler";
|
||||||
import { useConfig } from "@/contexts/ConfigContext";
|
import { useConfig } from "@/contexts/ConfigContext";
|
||||||
import LazyGridImage from "@/components/ui/lazy-grid-image";
|
import AssetGrid from "@/components/shared/AssetGrid";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
groupBy: "date" | "album";
|
groupBy: "date" | "album";
|
||||||
@@ -30,9 +26,6 @@ export default function MissingLocationAssets({ groupBy }: IProps) {
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||||
|
|
||||||
const [index, setIndex] = useState(-1);
|
|
||||||
const [lastSelectedIndex, setLastSelectedIndex] = useState(-1);
|
|
||||||
|
|
||||||
const fetchAssets = async () => {
|
const fetchAssets = async () => {
|
||||||
if (!startDate && !albumId) return;
|
if (!startDate && !albumId) return;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -46,7 +39,7 @@ export default function MissingLocationAssets({ groupBy }: IProps) {
|
|||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
};
|
};
|
||||||
|
|
||||||
const images = useMemo(() => {
|
const processedAssets = useMemo(() => {
|
||||||
let sortedAssets: IAsset[];
|
let sortedAssets: IAsset[];
|
||||||
try {
|
try {
|
||||||
if (sortOrder === "desc")
|
if (sortOrder === "desc")
|
||||||
@@ -58,76 +51,23 @@ export default function MissingLocationAssets({ groupBy }: IProps) {
|
|||||||
sortedAssets = [...assets];
|
sortedAssets = [...assets];
|
||||||
}
|
}
|
||||||
|
|
||||||
return sortedAssets.
|
return sortedAssets.map((asset) => ({
|
||||||
map((p) => ({
|
...asset,
|
||||||
...p,
|
tags: [
|
||||||
src: p.url as string,
|
{
|
||||||
original: p.previewUrl as string,
|
title: "Immich Link",
|
||||||
width: p.exifImageWidth as number,
|
value: (
|
||||||
height: p.exifImageHeight as number,
|
<a href={exImmichUrl + "/photos/" + asset.id} target="_blank" rel="noopener noreferrer">
|
||||||
isSelected: selectedIds.includes(p.id),
|
Open in Immich
|
||||||
orientation: 1,
|
</a>
|
||||||
tags: [
|
),
|
||||||
{
|
},
|
||||||
title: "Immich Link",
|
],
|
||||||
value: (
|
}));
|
||||||
<a href={exImmichUrl + "/photos/" + p.id} target="_blank" rel="noopener noreferrer">
|
}, [assets, sortOrder, exImmichUrl]);
|
||||||
Open in Immich
|
|
||||||
</a>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}));
|
|
||||||
}, [assets, selectedIds, sortOrder, exImmichUrl]);
|
|
||||||
|
|
||||||
const slides = useMemo(
|
const handleSelectionChange = (ids: string[]) => {
|
||||||
() =>
|
updateContext({ selectedIds: ids });
|
||||||
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);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -155,30 +95,13 @@ export default function MissingLocationAssets({ groupBy }: IProps) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="w-full overflow-y-auto max-h-[calc(100vh-60px)]">
|
||||||
<Lightbox
|
<AssetGrid
|
||||||
slides={slides}
|
assets={processedAssets}
|
||||||
plugins={[Captions]}
|
isInternal={true}
|
||||||
open={index >= 0}
|
selectable={true}
|
||||||
index={index}
|
onSelectionChange={handleSelectionChange}
|
||||||
close={() => setIndex(-1)}
|
|
||||||
/>
|
/>
|
||||||
<div className="w-full overflow-y-auto max-h-[calc(100vh-60px)]">
|
</div>
|
||||||
<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>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user