Merge branch 'prerelease' into feat/upgrade-nextjs
This commit is contained in:
commit
2dd783f949
11 files changed
+1663
-8
No files matched your search
@@ -2,7 +2,7 @@ import "yet-another-react-lightbox/styles.css";
|
||||
import "react-photo-album/rows.css";
|
||||
|
||||
import { IAsset } from '@/types/asset';
|
||||
import React, { forwardRef, useEffect, useImperativeHandle, useMemo, useState } from 'react'
|
||||
import React, { forwardRef, useEffect, useImperativeHandle, useMemo, useState, useCallback } from 'react'
|
||||
import Lightbox from 'yet-another-react-lightbox';
|
||||
import { RowsPhotoAlbum } from "react-photo-album";
|
||||
import type { Photo, RenderImageContext, RenderImageProps } from "react-photo-album";
|
||||
@@ -11,6 +11,12 @@ import Download from "yet-another-react-lightbox/plugins/download";
|
||||
import Video from "yet-another-react-lightbox/plugins/video";
|
||||
import { usePhotoSelectionContext } from '@/contexts/PhotoSelectionContext';
|
||||
import { useConfig } from '@/contexts/ConfigContext';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { Heart, Info, Trash2, ExternalLink } from 'lucide-react';
|
||||
import { updateAssets } from '@/handlers/api/asset.handler';
|
||||
import { toast } from '@/components/ui/use-toast';
|
||||
|
||||
const AssetInfoPanel = dynamic(() => import('@/components/asset-info/AssetInfoPanel'), { ssr: false });
|
||||
|
||||
export interface AssetPhoto extends Photo {
|
||||
id: string;
|
||||
@@ -24,6 +30,8 @@ interface AssetGridProps {
|
||||
isInternal?: boolean;
|
||||
selectable?: boolean;
|
||||
onSelectionChange?: (ids: string[]) => void;
|
||||
onDeleteAsset?: (id: string) => void;
|
||||
onFavoriteAsset?: (id: string, isFavorite: boolean) => void;
|
||||
}
|
||||
|
||||
interface AssetGridRef {
|
||||
@@ -32,13 +40,59 @@ interface AssetGridRef {
|
||||
unselectAll: () => void;
|
||||
}
|
||||
|
||||
const AssetGrid = forwardRef<AssetGridRef, AssetGridProps>(({ assets, isInternal = true, selectable = false, onSelectionChange }, ref) => {
|
||||
const AssetGrid = forwardRef<AssetGridRef, AssetGridProps>(({ assets, isInternal = true, selectable = false, onSelectionChange, onDeleteAsset, onFavoriteAsset }, ref) => {
|
||||
const [index, setIndex] = useState(-1);
|
||||
const [lastSelectedIndex, setLastSelectedIndex] = useState(-1);
|
||||
const [showInfoPanel, setShowInfoPanel] = useState(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
return localStorage.getItem('assetInfoPanelOpen') === 'true'
|
||||
}
|
||||
return false
|
||||
});
|
||||
const [favoriteIds, setFavoriteIds] = useState<Set<string>>(() => new Set(assets.filter(a => a.isFavorite).map(a => a.id)));
|
||||
const { exImmichUrl } = useConfig();
|
||||
// Use context for selection state
|
||||
const { selectedIds, updateContext } = usePhotoSelectionContext();
|
||||
|
||||
// Sync favoriteIds when assets change
|
||||
useEffect(() => {
|
||||
setFavoriteIds(new Set(assets.filter(a => a.isFavorite).map(a => a.id)));
|
||||
}, [assets]);
|
||||
|
||||
const handleToggleFavorite = useCallback(async (assetId: string) => {
|
||||
const isFav = favoriteIds.has(assetId);
|
||||
const newFav = !isFav;
|
||||
// Optimistic update
|
||||
setFavoriteIds(prev => {
|
||||
const next = new Set(prev);
|
||||
newFav ? next.add(assetId) : next.delete(assetId);
|
||||
return next;
|
||||
});
|
||||
try {
|
||||
await updateAssets({ ids: [assetId], isFavorite: newFav });
|
||||
toast({ title: newFav ? "Added to favorites" : "Removed from favorites" });
|
||||
onFavoriteAsset?.(assetId, newFav);
|
||||
} catch {
|
||||
// Revert on failure
|
||||
setFavoriteIds(prev => {
|
||||
const next = new Set(prev);
|
||||
isFav ? next.add(assetId) : next.delete(assetId);
|
||||
return next;
|
||||
});
|
||||
toast({ title: "Error", description: "Failed to update favorite", variant: "destructive" });
|
||||
}
|
||||
}, [favoriteIds, onFavoriteAsset]);
|
||||
|
||||
const toggleInfoPanel = useCallback(() => {
|
||||
setShowInfoPanel((v) => {
|
||||
const next = !v
|
||||
localStorage.setItem('assetInfoPanelOpen', String(next))
|
||||
return next
|
||||
})
|
||||
}, []);
|
||||
|
||||
const currentAsset = index >= 0 && index < assets.length ? assets[index] : null;
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
getSelectedIds: () => selectedIds,
|
||||
selectAll: () => {
|
||||
@@ -159,6 +213,62 @@ const AssetGrid = forwardRef<AssetGridRef, AssetGridProps>(({ assets, isInternal
|
||||
);
|
||||
};
|
||||
|
||||
const handleOpenInImmich = useCallback(() => {
|
||||
if (currentAsset) {
|
||||
window.open(exImmichUrl + "/photos/" + currentAsset.id, "_blank");
|
||||
}
|
||||
}, [currentAsset, exImmichUrl]);
|
||||
|
||||
const toolbarButtons = useMemo(() => {
|
||||
if (!currentAsset) return [];
|
||||
const isFav = favoriteIds.has(currentAsset.id);
|
||||
return [
|
||||
<button
|
||||
key="favorite"
|
||||
type="button"
|
||||
className="yarl__button"
|
||||
title={isFav ? "Unfavorite" : "Favorite"}
|
||||
onClick={() => handleToggleFavorite(currentAsset.id)}
|
||||
>
|
||||
<Heart
|
||||
className={`h-6 w-6 ${isFav ? 'fill-red-500 text-red-500' : 'text-white'}`}
|
||||
/>
|
||||
</button>,
|
||||
<button
|
||||
key="info"
|
||||
type="button"
|
||||
className="yarl__button"
|
||||
title="Info"
|
||||
onClick={toggleInfoPanel}
|
||||
>
|
||||
<Info className={`h-6 w-6 ${showInfoPanel ? 'text-blue-400' : 'text-white'}`} />
|
||||
</button>,
|
||||
<button
|
||||
key="open-immich"
|
||||
type="button"
|
||||
className="yarl__button"
|
||||
title="Open in Immich"
|
||||
onClick={handleOpenInImmich}
|
||||
>
|
||||
<ExternalLink className="h-6 w-6 text-white" />
|
||||
</button>,
|
||||
...(onDeleteAsset ? [
|
||||
<button
|
||||
key="delete"
|
||||
type="button"
|
||||
className="yarl__button"
|
||||
title="Delete"
|
||||
onClick={() => {
|
||||
onDeleteAsset(currentAsset.id);
|
||||
setIndex(-1);
|
||||
}}
|
||||
>
|
||||
<Trash2 className="h-6 w-6 text-white" />
|
||||
</button>,
|
||||
] : []),
|
||||
];
|
||||
}, [currentAsset, showInfoPanel, onDeleteAsset, onFavoriteAsset, handleOpenInImmich, favoriteIds, handleToggleFavorite]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Lightbox
|
||||
@@ -166,7 +276,36 @@ const AssetGrid = forwardRef<AssetGridRef, AssetGridProps>(({ assets, isInternal
|
||||
plugins={[Download, Video]}
|
||||
open={index >= 0}
|
||||
index={index}
|
||||
close={() => setIndex(-1)}
|
||||
close={() => { setIndex(-1); }}
|
||||
on={{
|
||||
view: ({ index }) => setIndex(index),
|
||||
}}
|
||||
toolbar={{
|
||||
buttons: [
|
||||
...toolbarButtons,
|
||||
"download",
|
||||
"close",
|
||||
],
|
||||
}}
|
||||
render={{
|
||||
slideContainer: ({ children }) => (
|
||||
<div className="flex h-full w-full">
|
||||
<div className="flex-1 flex items-center justify-center overflow-hidden">
|
||||
{children}
|
||||
</div>
|
||||
{showInfoPanel && currentAsset && (
|
||||
<AssetInfoPanel
|
||||
assetId={currentAsset.id}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
}}
|
||||
styles={{
|
||||
container: {
|
||||
backgroundColor: "rgba(0, 0, 0, 0.95)",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<RowsPhotoAlbum
|
||||
photos={images}
|
||||
|
||||
@@ -63,13 +63,27 @@ export default function ShareAssetsTrigger({ filters, buttonProps }: ShareAssets
|
||||
});
|
||||
}
|
||||
|
||||
const handleCopy = () => {
|
||||
if (generatedLink) {
|
||||
navigator.clipboard.writeText(generatedLink);
|
||||
const handleCopy = async () => {
|
||||
if (!generatedLink) return;
|
||||
try {
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
await navigator.clipboard.writeText(generatedLink);
|
||||
} else {
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.value = generatedLink;
|
||||
textArea.style.position = 'fixed';
|
||||
textArea.style.left = '-9999px';
|
||||
document.body.appendChild(textArea);
|
||||
textArea.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(textArea);
|
||||
}
|
||||
setCopied(true);
|
||||
setTimeout(() => {
|
||||
setCopied(false);
|
||||
}, 2000);
|
||||
} catch {
|
||||
setErrorMessage('Failed to copy link');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user