Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified bun.lockb
Binary file not shown.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"dependencies": {
"@google/generative-ai": "^0.21.0",
"@radix-ui/react-accordion": "^1.2.1",
"@radix-ui/react-alert-dialog": "^1.1.4",
"@radix-ui/react-avatar": "^1.1.0",
"@radix-ui/react-checkbox": "^1.1.2",
"@radix-ui/react-dialog": "^1.1.1",
Expand Down Expand Up @@ -60,6 +61,7 @@
"react-day-picker": "9.0.8",
"react-dom": "^18",
"react-grid-gallery": "^1.0.1",
"react-hot-toast": "^2.5.1",
"react-leaflet": "^4.2.1",
"react-mentions": "^4.4.10",
"react-query": "^3.39.3",
Expand Down
2 changes: 1 addition & 1 deletion src/components/albums/AlbumCreateDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export default function AlbumCreateDialog({ onSubmit, assetIds }: IProps) {
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button>Create Album</Button>
<Button size={"sm"}>Create Album</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
Expand Down
2 changes: 1 addition & 1 deletion src/components/albums/AlbumSelectorDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ export default function AlbumSelectorDialog({ onSelected }: IProps) {
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button>Select Album</Button>
<Button size={"sm"}>Select Album</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
Expand Down
9 changes: 4 additions & 5 deletions src/components/albums/info/AlbumImages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -125,22 +125,21 @@ export default function AlbumImages({ album }: AlbumImagesProps) {
index={index}
close={() => setIndex(-1)}
/>
<div className="w-full grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-2 p-2">
<div className="w-full grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-2 p-2">
{images.map((image) => (
<div
key={image.id}
className="w-full h-[200px] overflow-hidden relative"
className="w-full h-[180px] overflow-hidden relative"
>
<LazyImage
loading="lazy"
key={image.id}
src={image.original}
alt={image.originalFileName}
className='overflow-hidden'
className='overflow-hidden max-h-[180px] max-w-[180px] min-h-[180px] min-w-[180px]'
style={{
objectPosition: 'center',
objectFit: 'cover',
height: '100%',
objectFit: 'cover'
}}
onClick={() => handleClick(images.indexOf(image))}
/>
Expand Down
4 changes: 2 additions & 2 deletions src/components/albums/info/AlbumPeople.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ export default function AlbumPeople({ album, onSelect }: AlbumPeopleProps) {
}

return (
<div className="overflow-y-auto min-w-[200px] sticky top-0 py-4 max-h-[calc(100vh-60px)] min-h-[calc(100vh-60px)] dark:bg-zinc-900 bg-gray-200 flex flex-col gap-2 px-2">
<div className="overflow-y-auto min-w-[200px] sticky top-0 py-4 max-h-[calc(100vh-60px)] min-h-[calc(100vh-60px)] border-r border-gray-200 dark:border-zinc-800 flex flex-col gap-2 px-2">

{selectedPerson && (
<div className='flex flex-col gap-2 bg-white dark:bg-zinc-900 p-2 rounded-md'>
Expand Down Expand Up @@ -157,7 +157,7 @@ export default function AlbumPeople({ album, onSelect }: AlbumPeopleProps) {
</Button>
)}
{selectedPeople.length > 0 && (
<div className='absolute mx-auto bottom-0 w-full py-2 dark:bg-white bg-black -mx-2 px-2'>
<div className='absolute mx-auto bottom-0 w-full py-2 bg-white darl:bg-black -mx-2 px-2'>
<Button variant="outline" className="!py-0.5 !px-2 text-xs h-7" onClick={handleHideSelectedPeople}>
Hide {selectedPeople.length} people
</Button>
Expand Down
13 changes: 7 additions & 6 deletions src/components/albums/list/AlbumThumbnail.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
import React, { useMemo } from 'react'
import React, { useMemo, useState } from 'react'

import Link from 'next/link';
import { useConfig } from '@/contexts/ConfigContext';
import { humanizeBytes, humanizeNumber, pluralize } from '@/helpers/string.helper';
import LazyImage from '@/components/ui/lazy-image';
import { ASSET_THUMBNAIL_PATH } from '@/config/routes';
import { IAlbum } from '@/types/album';
import { Badge } from '@/components/ui/badge';
import { formatDate } from '@/helpers/date.helper';
import { Checkbox } from '@/components/ui/checkbox';
import { differenceInDays } from 'date-fns';
import { FaceIcon } from '@radix-ui/react-icons';
import { Calendar, Camera, Image, User } from 'lucide-react';
import { Calendar, Camera } from 'lucide-react';

interface IAlbumThumbnailProps {
album: IAlbum;
onSelect: (checked: boolean) => void;
selected: boolean;
}
export default function AlbumThumbnail({ album, onSelect }: IAlbumThumbnailProps) {
export default function AlbumThumbnail({ album, onSelect, selected }: IAlbumThumbnailProps) {
const [isSelected, setIsSelected] = useState(selected);

const numberOfDays = useMemo(() => {
return differenceInDays(album.lastPhotoDate, album.firstPhotoDate);
}, [album.firstPhotoDate, album.lastPhotoDate]);
Expand All @@ -39,6 +39,7 @@ export default function AlbumThumbnail({ album, onSelect }: IAlbumThumbnailProps
{formatDate(album.firstPhotoDate.toString(), 'MMM d, yyyy')} - {formatDate(album.lastPhotoDate.toString(), 'MMM d, yyyy')}
</div>
<Checkbox
defaultChecked={isSelected}
onCheckedChange={onSelect}
className="absolute top-2 left-2 w-6 h-6 rounded-full border-gray-300 focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
/>
Expand Down
32 changes: 24 additions & 8 deletions src/components/albums/potential-albums/PotentialAlbumsAssets.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import "yet-another-react-lightbox/styles.css";
import { usePotentialAlbumContext } from "@/contexts/PotentialAlbumContext";
import { listPotentialAlbumsAssets } from "@/handlers/api/album.handler";
import { IAsset } from "@/types/asset";
import React, { useEffect, useMemo, useState } from "react";
import type { IAsset } from "@/types/asset";
import React, { type MouseEvent, useEffect, useMemo, useState } from "react";
import { Gallery } from "react-grid-gallery";
import Lightbox, { SlideImage, SlideTypes } from "yet-another-react-lightbox";
import Captions from "yet-another-react-lightbox/plugins/captions";
Expand All @@ -20,6 +20,7 @@ export default function PotentialAlbumsAssets() {
const [errorMessage, setErrorMessage] = useState<string | null>(null);

const [index, setIndex] = useState(-1);
const [lastSelectedIndex, setLastSelectedIndex] = useState(-1);

const fetchAssets = async () => {
setLoading(true);
Expand All @@ -44,14 +45,15 @@ export default function PotentialAlbumsAssets() {
{
title: "Immich Link",
value: (
<a href={exImmichUrl + "/photos/" + p.id} target="_blank">
<a href={`${exImmichUrl}/photos/${p.id}`} target="_blank" rel="noreferrer">
Open in Immich
</a>
),
},
],
}));
}, [assets, selectedIds]);
}, [assets, selectedIds, exImmichUrl]);


const slides = useMemo(
() =>
Expand All @@ -73,17 +75,31 @@ export default function PotentialAlbumsAssets() {
[images]
);

const handleClick = (idx: number) => setIndex(idx);

const handleSelect = (_idx: number, asset: IAsset) => {
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 {
updateContext({ selectedIds: [...selectedIds, asset.id] });
const clickedIndex = images.findIndex((image) => {
return image.id === asset.id
});
if (event.shiftKey) {
const startIndex = Math.min(clickedIndex, lastSelectedIndex);
const endIndex = Math.max(clickedIndex, lastSelectedIndex);
const newSelectedIds = images.slice(startIndex, endIndex + 1).map((image) => image.id);
const allSelectedIds = [...selectedIds, ...newSelectedIds];
const uniqueSelectedIds = [...new Set(allSelectedIds)];
updateContext({ selectedIds: uniqueSelectedIds });
} else {
updateContext({ selectedIds: [...selectedIds, asset.id] });
}
setLastSelectedIndex(clickedIndex);
}

};

useEffect(() => {
Expand Down Expand Up @@ -121,7 +137,7 @@ export default function PotentialAlbumsAssets() {
<div className="w-full overflow-y-auto max-h-[calc(100vh-60px)]">
<Gallery
images={images}
onClick={handleClick}
onClick={setIndex}
enableImageSelection={true}
onSelect={handleSelect}
thumbnailImageComponent={LazyGridImage}
Expand Down
19 changes: 10 additions & 9 deletions src/components/albums/potential-albums/PotentialAlbumsDates.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export default function PotentialAlbumsDates() {
}, [filters]);

return (
<div className="overflow-y-auto min-w-[200px] py-4 max-h-[calc(100vh-60px)] min-h-[calc(100vh-60px)] dark:bg-zinc-900 bg-gray-200 flex flex-col gap-2 px-2">
<div className="min-w-[200px] py-4 max-h-[calc(100vh-60px)] min-h-[calc(100vh-60px)] border-r border-gray-200 dark:border-zinc-800 flex flex-col gap-2 px-1">
<div className="flex justify-between items-center gap-2">
<Select
defaultValue={filters.sortBy}
Expand All @@ -69,14 +69,15 @@ export default function PotentialAlbumsDates() {
</Button>
</div>
</div>

{dateRecords.map((record) => (
<PotentialDateItem
key={record.date}
record={record}
onSelect={handleSelect}
/>
))}
<div className="overflow-y-auto">
{dateRecords.map((record) => (
<PotentialDateItem
key={record.date}
record={record}
onSelect={handleSelect}
/>
))}
</div>
</div>
);
}
4 changes: 2 additions & 2 deletions src/components/albums/potential-albums/PotentialDateItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@ export default function PotentialDateItem({ record, onSelect }: IProps) {
onClick={() => onSelect(record.date)}
key={record.date}
className={
cn("flex gap-1 flex-col p-2 py-1 rounded-lg hover:dark:bg-zinc-800 border border-transparent hover:bg-zinc-100 px-4",
cn("flex gap-1 flex-col p-2 py-1 rounded-lg hover:dark:bg-zinc-800 border border-transparent hover:bg-zinc-100",
startDate === record.date ? "bg-zinc-100 dark:bg-zinc-800 border-gray-300 dark:border-zinc-700" : "")
}
>
<p className="font-mono text-sm">{dateLabel}</p>
<p className="text-sm">{dateLabel}</p>
<p className="text-xs text-foreground/50">{record.asset_count} Orphan Assets</p>
</div>
);
Expand Down
137 changes: 137 additions & 0 deletions src/components/albums/share/AlbumShareDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox';
import { Dialog, DialogTitle, DialogHeader, DialogContent } from '@/components/ui/dialog';
import { Label } from '@/components/ui/label';
import { shareAlbums } from '@/handlers/api/album.handler';
import { IAlbum } from '@/types/album';
import React, { ForwardedRef, forwardRef, useImperativeHandle, useState } from 'react'

export interface IAlbumShareDialogProps {

}

export interface IAlbumShareDialogRef {
open: (selectedAlbums: IAlbum[]) => void;
close: () => void;
}

interface IAlbumWithLink extends IAlbum {
shareLink?: string;
allowDownload?: boolean;
allowUpload?: boolean;
showMetadata?: boolean;
}

const AlbumShareDialog = forwardRef(({ }: IAlbumShareDialogProps, ref: ForwardedRef<IAlbumShareDialogRef>) => {
const [open, setOpen] = useState(false);
const [selectedAlbums, setSelectedAlbums] = useState<IAlbumWithLink[]>([]);
const [generating, setGenerating] = useState(false);
const [error, setError] = useState<string | null>(null);
const [generated, setGenerated] = useState<boolean>(false);

const handleGenerateShareLink = async () => {
setGenerating(true);
const data = selectedAlbums.map((album) => ({
albumId: album.id,
albumName: album.albumName,
assetCount: album.assetCount,
allowDownload: !!album.allowDownload,
allowUpload: !!album.allowUpload,
showMetadata: !!album.showMetadata,
}));

return shareAlbums(data).then((updatedAlbums) => {
setSelectedAlbums(updatedAlbums);
setGenerated(true);
})
.catch((error) => {
setError(error.message);
})
.finally(() => {
setGenerating(false);
});
}

const handleAllowPropertyChange = (albumId: string, property: string, checked: boolean) => {
setSelectedAlbums((prevAlbums) => prevAlbums.map((album) => album.id === albumId ? { ...album, [property]: checked } : album));
}


useImperativeHandle(ref, () => ({
open: (selectedAlbums: IAlbum[]) => {
setSelectedAlbums(selectedAlbums.map((album) => ({ ...album, allowDownload: true, allowUpload: true, showMetadata: true })));
setOpen(true);
},
close: () => {
setOpen(false);
}
}));

return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="max-w-4xl">
<DialogHeader>
<DialogTitle>Share {selectedAlbums.length} albums</DialogTitle>
</DialogHeader>
<div className="flex flex-col gap-2">
{error && <div className="text-red-500">{error}</div>}
<ol className="flex flex-col gap-4 list-decimal px-4 py-4">
{selectedAlbums.map((album) => (
<li key={album.id}>
<div className="flex justify-between gap-1">
<h3 className="text-sm font-medium">{album.albumName}</h3>
<p className="text-xs border truncate rounded-md px-1 py-0.5 text-muted-foreground">{album.assetCount} Items</p>
</div>
<div className="flex flex-col gap-2">
{album.shareLink ?
<p className="text-xs text-muted-foreground truncate font-mono overflow-x-auto">{album.shareLink}</p> : (
<>
<p className="text-xs text-muted-foreground">No share link generated</p>
<div className="flex gap-2">
<div className="flex items-center gap-1">
<Checkbox checked={album.allowDownload} onCheckedChange={(checked) => handleAllowPropertyChange(album.id, 'allowDownload', !!checked)} />
<Label className="text-xs">Allow Download</Label>
</div>
<div className="flex items-center gap-1">
<Checkbox checked={album.allowUpload} onCheckedChange={(checked) => handleAllowPropertyChange(album.id, 'allowUpload', !!checked)} />
<Label className="text-xs">Allow Upload</Label>
</div>
<div className="flex items-center gap-1">
<Checkbox checked={album.showMetadata} onCheckedChange={(checked) => handleAllowPropertyChange(album.id, 'showMetadata', !!checked)} />
<Label className="text-xs">Show Metadata</Label>
</div>

</div>
</>
)}
</div>
</li>

))}
</ol>
{generated ? (
<>
<p className="text-sm py-2 text-muted-foreground text-center">Share links all generated</p>
</>
) : (
<>
{generating ? <div className="flex justify-center gap-2">
<p className="text-sm py-2 text-muted-foreground">Generating share links...</p>
</div> : <div className="flex justify-center gap-2">
<Button onClick={handleGenerateShareLink} disabled={generating}>
Generate For {selectedAlbums.length} albums
</Button>

</div>}
</>
)}

</div>
</DialogContent>
</Dialog>
)
})

AlbumShareDialog.displayName = "AlbumShareDialog";

export default AlbumShareDialog;
2 changes: 1 addition & 1 deletion src/components/analytics/exif/AssetHeatMap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export default function AssetHeatMap() {
const threshold2 = minCount + range * 0.4;
const threshold3 = minCount + range * 0.6;
const threshold4 = minCount + range * 0.8;
if (count === 0) return "bg-zinc-800";
if (count === 0) return "bg-zinc-200 dark:bg-zinc-800";
if (count <= threshold1) return "bg-green-200";
if (count <= threshold2) return "bg-green-400";
if (count <= threshold3) return "bg-green-500";
Expand Down
Loading
Loading