Merge branch 'prerelease' into feat/upgrade-nextjs

This commit is contained in:
sai80082 committed 2026-04-07 00:20:50 +05:30
commit 2dd783f949
11 files changed
+1663 -8

No files matched your search

+142 -3
View File
@@ -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}
+17 -3
View File
@@ -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');
}
}