Skip to content

Commit

Permalink
Merge pull request #112 from varun-raj/feat-albums-enhancement
Browse files Browse the repository at this point in the history
Feat: Albums enhancement and ux enhancements
  • Loading branch information
varun-raj authored Jan 11, 2025
2 parents 1234f49 + 1132a04 commit eb3aefd
Show file tree
Hide file tree
Showing 41 changed files with 1,074 additions and 272 deletions.
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

0 comments on commit eb3aefd

Please sign in to comment.