+
{formatDate(album.firstPhotoDate.toString(), 'MMM d, yyyy')} - {formatDate(album.lastPhotoDate.toString(), 'MMM d, yyyy')}
-
-
- {humanizeNumber(album.assetCount)}
+
+
+
+
+ {humanizeNumber(album.assetCount)}
-
- {album.faceCount}
+
+ {numberOfDays === 0 ? '1 day' : `${numberOfDays.toLocaleString() } days`}
{album.albumName}
-
{
- numberOfDays === 0 ? '1 day' : `${numberOfDays.toLocaleString() } days`
- }
+
+
{humanizeNumber(album.faceCount)} {
+ pluralize(album.faceCount, 'person', 'people')
+ }
+
{humanizeBytes(parseInt(album.size))}
+
)
diff --git a/src/config/constants/sidebarNavs.tsx b/src/config/constants/sidebarNavs.tsx
index 1da0988..7e0a839 100644
--- a/src/config/constants/sidebarNavs.tsx
+++ b/src/config/constants/sidebarNavs.tsx
@@ -22,7 +22,7 @@ export const sidebarNavs = [
icon:
,
},
{
- title: "Albums",
+ title: "Manage Albums",
link: "/albums",
icon:
,
},
diff --git a/src/handlers/api/album.handler.ts b/src/handlers/api/album.handler.ts
index 3342c35..2e39f7e 100644
--- a/src/handlers/api/album.handler.ts
+++ b/src/handlers/api/album.handler.ts
@@ -23,8 +23,8 @@ export const listPotentialAlbumsAssets = async (filters: IPotentialAlbumsDatesFi
return API.get(LIST_POTENTIAL_ALBUMS_ASSETS_PATH, filters).then((assets) => assets.map(cleanUpAsset));
}
-export const listAlbums = async () => {
-return API.get(LIST_ALBUMS_PATH);
+export const listAlbums = async (filters?: { sortBy?: string, sortOrder?: string }) => {
+ return API.get(LIST_ALBUMS_PATH, filters);
}
export const getAlbumInfo = async (id: string) => {
return API.get(ALBUM_INFO_PATH(id));
diff --git a/src/helpers/string.helper.ts b/src/helpers/string.helper.ts
index 7f5612e..0963cb3 100644
--- a/src/helpers/string.helper.ts
+++ b/src/helpers/string.helper.ts
@@ -1,3 +1,20 @@
export const humanizeNumber = (number: number) => {
return number.toLocaleString();
-}
\ No newline at end of file
+}
+
+export const pluralize = (number: number, word: string, pluralWord: string) => {
+ return number === 1 ? word : pluralWord
+}
+
+export const humanizeBytes = (bytes: number) => {
+ if (bytes < 1024) {
+ return `${bytes} bytes`
+ }
+ if (bytes < 1024 * 1024) {
+ return `${(bytes / 1024).toFixed(1)} KB`
+ }
+ if (bytes < 1024 * 1024 * 1024) {
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
+ }
+ return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`
+}
diff --git a/src/pages/albums/index.tsx b/src/pages/albums/index.tsx
index 83041ec..16954da 100644
--- a/src/pages/albums/index.tsx
+++ b/src/pages/albums/index.tsx
@@ -7,20 +7,42 @@ import { listAlbums } from '@/handlers/api/album.handler'
import { IAlbum } from '@/types/album'
import Image from 'next/image'
import Link from 'next/link'
-import React, { useEffect, useState } from 'react'
+import React, { useEffect, useMemo, useState } from 'react'
import AlbumThumbnail from '@/components/albums/list/AlbumThumbnail'
import { Button } from '@/components/ui/button'
+import { Select, SelectItem, SelectContent, SelectTrigger, SelectValue } from '@/components/ui/select'
+import { useRouter } from 'next/router'
+import { ChevronDownIcon, ChevronUpIcon } from '@radix-ui/react-icons'
+import { SortAsc, SortDesc } from 'lucide-react'
+const SORT_BY_OPTIONS = [
+ { value: 'lastPhotoDate', label: 'Last Photo Date' },
+ { value: 'firstPhotoDate', label: 'First Photo Date' },
+ { value: 'assetCount', label: 'Asset Count' },
+ { value: 'createdAt', label: 'Created At' },
+ { value: 'updatedAt', label: 'Updated At' },
+ { value: 'albumName', label: 'Album Name' },
+ { value: 'albumSize', label: 'Album Size' },
+ { value: 'faceCount', label: 'Number of People' },
+]
export default function AlbumListPage() {
const { exImmichUrl } = useConfig()
+ const router = useRouter()
+ const { query, pathname } = router
+ const { sortBy, sortOrder } = query as { sortBy: string, sortOrder: string }
const [albums, setAlbums] = useState
([])
const [loading, setLoading] = useState(false)
const [errorMessage, setErrorMessage] = useState('')
const [selectedAlbums, setSelectedAlbums] = useState([])
+ const selectedSortBy = useMemo(() => SORT_BY_OPTIONS.find((option) => option.value === sortBy), [sortBy])
+
const fetchAlbums = async () => {
setLoading(true)
- listAlbums()
+ listAlbums({
+ sortBy,
+ sortOrder
+ })
.then(setAlbums)
.catch((error) => {
setErrorMessage(error.message)
@@ -32,7 +54,7 @@ export default function AlbumListPage() {
useEffect(() => {
fetchAlbums()
- }, [])
+ }, [sortBy, sortOrder])
const handleSelect = (checked: boolean, albumId: string) => {
if (checked) {
@@ -42,6 +64,7 @@ export default function AlbumListPage() {
}
}
+
const renderContent = () => {
if (loading) {
@@ -62,6 +85,38 @@ export default function AlbumListPage() {
+
+
+
+ }
/>
{renderContent()}
diff --git a/src/pages/api/albums/list.ts b/src/pages/api/albums/list.ts
index 45fee66..53592c0 100644
--- a/src/pages/api/albums/list.ts
+++ b/src/pages/api/albums/list.ts
@@ -4,7 +4,7 @@ import { db } from "@/config/db";
import { getCurrentUser } from "@/handlers/serverUtils/user.utils";
import { NextApiResponse } from "next";
import { albums } from "@/schema/albums.schema";
-import { count, desc, eq, min, max, sql, and } from "drizzle-orm";
+import { count, desc, eq, min, max, sql, and, sum } from "drizzle-orm";
import { assets } from "@/schema/assets.schema";
import { albumsAssetsAssets } from "@/schema/albumAssetsAssets.schema";
import { users } from "@/schema/users.schema";
@@ -28,6 +28,15 @@ const sortAlbums = (albums: IAlbum[], sortBy: string, sortOrder: string) => {
if (sortBy === 'assetCount') {
return albums.sort((a, b) => sortOrder === 'asc' ? a.assetCount - b.assetCount : b.assetCount - a.assetCount);
}
+ if (sortBy === 'albumName') {
+ return albums.sort((a, b) => sortOrder === 'asc' ? a.albumName.localeCompare(b.albumName) : b.albumName.localeCompare(a.albumName));
+ }
+ if (sortBy === 'albumSize') {
+ return albums.sort((a, b) => sortOrder === 'asc' ? parseInt(a.size) - parseInt(b.size) : parseInt(b.size) - parseInt(a.size));
+ }
+ if (sortBy === 'faceCount') {
+ return albums.sort((a, b) => sortOrder === 'asc' ? a.faceCount - b.faceCount : b.faceCount - a.faceCount);
+ }
return albums;
}
export default async function handler(
@@ -49,6 +58,12 @@ export default async function handler(
assetCount: count(assets.id),
firstPhotoDate: min(exif.dateTimeOriginal),
lastPhotoDate: max(exif.dateTimeOriginal),
+ size: sum(exif.fileSizeInByte),
+ order: albums.order,
+ isActivityEnabled: albums.isActivityEnabled,
+ ownerId: albums.ownerId,
+ description: albums.description,
+ lastModifiedAssetTimestamp: max(exif.dateTimeOriginal),
faceCount: count(sql
`DISTINCT ${assetFaces.personId}`), // Ensure unique personId
})
.from(albums)
diff --git a/src/types/album.d.ts b/src/types/album.d.ts
index 066ebc8..0e3f281 100644
--- a/src/types/album.d.ts
+++ b/src/types/album.d.ts
@@ -6,13 +6,13 @@ export interface IAlbum {
updatedAt: Date;
id: string;
ownerId: string;
- owner: IAlbumOwner;
- albumUsers: any[];
- shared: boolean;
- hasSharedLink: boolean;
- startDate: Date;
- endDate: Date;
- assets: any[];
+ owner?: IAlbumOwner;
+ albumUsers?: any[];
+ shared?: boolean;
+ hasSharedLink?: boolean;
+ startDate?: Date;
+ endDate?: Date;
+ assets?: any[];
assetCount: number;
isActivityEnabled: boolean;
order: string;
@@ -20,6 +20,7 @@ export interface IAlbum {
firstPhotoDate: Date;
lastPhotoDate: Date;
faceCount: number;
+ size: string;
}
export interface IAlbumOwner {