diff --git a/src/components/assets/missing-location/MissingLocationAssets.tsx b/src/components/assets/missing-location/MissingLocationAssets.tsx new file mode 100644 index 0000000..3aa6026 --- /dev/null +++ b/src/components/assets/missing-location/MissingLocationAssets.tsx @@ -0,0 +1,110 @@ +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 { Gallery } from "react-grid-gallery"; +import Lightbox from "yet-another-react-lightbox"; +import Captions from "yet-another-react-lightbox/plugins/captions"; +import { CalendarArrowDown, CalendarArrowUp, Hourglass } from "lucide-react"; +import { useMissingLocationContext } from "@/contexts/MissingLocationContext"; +import { listMissingLocationAssets } from "@/handlers/api/asset.handler"; +import { formatDate, parseDate } from "@/helpers/date.helper"; +import { addDays } from "date-fns"; + +export default function MissingLocationAssets() { + const { startDate, selectedIds, assets, updateContext } = useMissingLocationContext(); + + const [loading, setLoading] = useState(false); + const [errorMessage, setErrorMessage] = useState(null); + + const [index, setIndex] = useState(-1); + + const fetchAssets = async () => { + if (!startDate) return; + setLoading(true); + updateContext({ + assets: [], + }) + return listMissingLocationAssets({ startDate }) + .then((assets) => updateContext({ assets })) + .catch(setErrorMessage) + .finally(() => setLoading(false)); + }; + + const images = useMemo(() => { + return assets.map((p) => ({ + ...p, + src: p.url as string, + original: p.previewUrl as string, + width: p.exifImageWidth as number, + height: p.exifImageHeight as number, + isSelected: selectedIds.includes(p.id), + })); + }, [assets, selectedIds]); + console.log(images) + + const slides = useMemo( + () => + images.map(({ original, width, height }) => ({ + src: original, + width, + height, + })), + [images] + ); + + const handleClick = (idx: number) => setIndex(idx); + + const handleSelect = (_idx: number, asset: IAsset) => { + const isPresent =selectedIds.includes(asset.id) + if(isPresent){ + updateContext({selectedIds:selectedIds.filter((id)=>id!==asset.id)}) + } + else{ + updateContext({selectedIds:[...selectedIds,asset.id]}) + } + + }; + + useEffect(() => { + if (startDate) fetchAssets(); + }, [startDate]); + + if (loading) return ( +
+ +

Loading...

+
+ ) + + if (!startDate) return ( +
+ +

Please select a date

+

+ When you select a date from the left, you will see all the orphan assets captured on that date +

+
+ ) + return ( + <> + = 0} + index={index} + close={() => setIndex(-1)} + /> +
+ +
+ + ); +} diff --git a/src/components/assets/missing-location/MissingLocationDateItem.tsx b/src/components/assets/missing-location/MissingLocationDateItem.tsx new file mode 100644 index 0000000..650625a --- /dev/null +++ b/src/components/assets/missing-location/MissingLocationDateItem.tsx @@ -0,0 +1,27 @@ +import { useMissingLocationContext } from "@/contexts/MissingLocationContext"; + +import { IMissingLocationDatesResponse } from "@/handlers/api/asset.handler"; +import { cn } from "@/lib/utils"; +import React from "react"; + +interface IProps { + record: IMissingLocationDatesResponse; + onSelect: (date: string) => void; +} +export default function MissingLocationDateItem({ record, onSelect }: IProps) { + const { startDate } = useMissingLocationContext() + return ( +
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", + startDate === record.date ? "bg-zinc-100 dark:bg-zinc-800 border-gray-300 dark:border-zinc-700" : "") + } + > +

{record.date}

+

{record.asset_count} W/O Location

+
+ ); +} diff --git a/src/components/assets/missing-location/MissingLocationDates.tsx b/src/components/assets/missing-location/MissingLocationDates.tsx new file mode 100644 index 0000000..8af86ec --- /dev/null +++ b/src/components/assets/missing-location/MissingLocationDates.tsx @@ -0,0 +1,43 @@ +import React, { use, useEffect, useState } from "react"; +import { usePotentialAlbumContext } from "@/contexts/PotentialAlbumContext"; +import { IMissingLocationDatesResponse, listMissingLocationDates } from "@/handlers/api/asset.handler"; +import MissingLocationDateItem from "./MissingLocationDateItem"; +import { useMissingLocationContext } from "@/contexts/MissingLocationContext"; + +export default function MissingLocationDates() { + const { updateContext } = useMissingLocationContext(); + const [dateRecords, setDateRecords] = React.useState< + IMissingLocationDatesResponse[] + >([]); + + const [loading, setLoading] = useState(false); + + const [errorMessage, setErrorMessage] = useState(null); + + const fetchData = async () => { + return listMissingLocationDates({}) + .then(setDateRecords) + .catch(setErrorMessage) + .finally(() => setLoading(false)); + }; + + const handleSelect = (date: string) => { + updateContext({ startDate: date }); + }; + + useEffect(() => { + fetchData(); + }, []); + + return ( +
+ {dateRecords.map((record) => ( + + ))} +
+ ); +} diff --git a/src/components/shared/ProfileInfo.tsx b/src/components/shared/ProfileInfo.tsx new file mode 100644 index 0000000..837fb67 --- /dev/null +++ b/src/components/shared/ProfileInfo.tsx @@ -0,0 +1,53 @@ +import { getMe } from "@/handlers/api/user.handler"; +import { IUser } from "@/types/user"; +import React, { useEffect, useState } from "react"; + +import Link from "next/link"; +import { useConfig } from "@/contexts/ConfigContext"; + +export default function ProfileInfo() { + const { immichURL } = useConfig(); + const [user, setUser] = React.useState(null); + const [loading, setLoading] = React.useState(true); + const [errorMessage, setErrorMessage] = useState(null); + + const fetchData = () => { + return getMe() + .then(setUser) + .catch((error) => { + setErrorMessage(error.message); + }) + .finally(() => setLoading(false)); + }; + + useEffect(() => { + fetchData(); + }, []); + + if (loading) return
Loading...
; + if (errorMessage) return
{errorMessage}
; + + return ( + <> +
+

Connected to

+ + {immichURL} + +

{user?.name}

+
+
+

+ Made with ♥ by{" "} + + @zathvarun + +

+
+ + ); +} diff --git a/src/components/shared/Sidebar.tsx b/src/components/shared/Sidebar.tsx index 9e4c61c..69ceb45 100644 --- a/src/components/shared/Sidebar.tsx +++ b/src/components/shared/Sidebar.tsx @@ -1,24 +1,18 @@ import Link from "next/link"; -import { Bell, Camera, Package2 } from "lucide-react"; -import { Button } from "@/components/ui/button"; import { sidebarNavs } from "@/config/constants/sidebarNavs"; import { cn } from "@/lib/utils"; import { useRouter } from "next/router"; -import { Badge } from "../ui/badge"; -import { ENV } from "@/config/environment"; -import { useTheme } from "next-themes"; import dynamic from "next/dynamic"; import Image from "next/image"; -import { useConfig } from "@/contexts/ConfigContext"; +import ProfileInfo from "./ProfileInfo"; const ThemeSwitcher = dynamic(() => import("@/components/shared/ThemeSwitcher"), { ssr: false, }); export default function Sidebar() { - const { immichURL } = useConfig(); const router = useRouter(); const { pathname } = router; @@ -58,24 +52,8 @@ export default function Sidebar() {
-
-

Connected to

- - {immichURL} - -
-
-

- Made with ♥ by{" "} - - @zathvarun - -

-
+ +
diff --git a/src/config/routes.ts b/src/config/routes.ts index 589c621..0067432 100644 --- a/src/config/routes.ts +++ b/src/config/routes.ts @@ -1,6 +1,8 @@ export const BASE_API_ENDPOINT = '/api'; export const BASE_PROXY_ENDPOINT = BASE_API_ENDPOINT + '/immich-proxy'; +export const GET_ME_PATH = BASE_PROXY_ENDPOINT + "/users/me"; + export const LIST_PEOPLE_PATH = BASE_API_ENDPOINT + "/people/list"; export const SEARCH_PEOPLE_PATH = BASE_PROXY_ENDPOINT + "/search/person"; export const PERSON_THUBNAIL_PATH = (id: string) => BASE_PROXY_ENDPOINT + "/thumbnail/" + id; @@ -13,7 +15,12 @@ export const EXIF_DISTRIBUTION_PATH = (column: string) => BASE_API_ENDPOINT + "/ // Albums export const LIST_POTENTIAL_ALBUMS_DATES_PATH = BASE_API_ENDPOINT + "/albums/potential-albums-dates"; export const LIST_POTENTIAL_ALBUMS_ASSETS_PATH = BASE_API_ENDPOINT + "/albums/potential-albums-assets"; -export const ASSET_THUMBNAIL_PATH = (id: string) => BASE_PROXY_ENDPOINT + "/asset/thumbnail/" + id; -export const ASSET_PREVIEW_PATH = (id: string) => BASE_PROXY_ENDPOINT + "/asset/thumbnail/" + id + "?size=preview"; export const LIST_ALBUMS_PATH = BASE_PROXY_ENDPOINT + "/albums"; -export const ADD_ASSETS_ALBUMS_PATH = (id: string) => BASE_PROXY_ENDPOINT + "/albums/" + id + "/assets"; \ No newline at end of file +export const ADD_ASSETS_ALBUMS_PATH = (id: string) => BASE_PROXY_ENDPOINT + "/albums/" + id + "/assets"; + +// Assets +export const LIST_MISSING_LOCATION_DATES_PATH = BASE_API_ENDPOINT + "/assets/missing-location-dates"; +export const LIST_MISSING_LOCATION_ASSETS_PATH = BASE_API_ENDPOINT + "/assets/missing-location-assets"; + +export const ASSET_THUMBNAIL_PATH = (id: string) => BASE_PROXY_ENDPOINT + "/asset/thumbnail/" + id; +export const ASSET_PREVIEW_PATH = (id: string) => BASE_PROXY_ENDPOINT + "/asset/thumbnail/" + id + "?size=preview"; \ No newline at end of file diff --git a/src/contexts/MissingLocationContext.tsx b/src/contexts/MissingLocationContext.tsx new file mode 100644 index 0000000..5044a12 --- /dev/null +++ b/src/contexts/MissingLocationContext.tsx @@ -0,0 +1,26 @@ +import { IAsset } from "@/types/asset"; +import { createContext, useContext } from "react"; + +export interface IMissingLocationConfig { + startDate?: string; + selectedIds: string[]; + assets: IAsset[]; +} + +export interface MissingLocationContext extends IMissingLocationConfig { + updateContext: (newConfig: Partial) => void; +} +const MissingLocationContext = createContext({ + startDate: undefined, + selectedIds: [], + assets: [], + updateContext: () => { }, +}); + +export default MissingLocationContext; + +const useMissingLocationContext = () => { + return useContext(MissingLocationContext) as MissingLocationContext; +} +export { useMissingLocationContext }; + diff --git a/src/handlers/api/asset.handler.ts b/src/handlers/api/asset.handler.ts new file mode 100644 index 0000000..693d945 --- /dev/null +++ b/src/handlers/api/asset.handler.ts @@ -0,0 +1,32 @@ +import { + ADD_ASSETS_ALBUMS_PATH, + LIST_ALBUMS_PATH, + LIST_MISSING_LOCATION_ASSETS_PATH, + LIST_MISSING_LOCATION_DATES_PATH, +} from "@/config/routes"; +import { cleanUpAsset } from "@/helpers/asset.helper"; +import API from "@/lib/api"; +import { IAsset } from "@/types/asset"; + +interface IMissingAssetAlbumsFilters { + startDate?: string; + endDate?: string; +} +export interface IMissingLocationDatesResponse { + date: string; + asset_count: number; +} + +export const listMissingLocationDates = async ( + filters: IMissingAssetAlbumsFilters +): Promise => { + return API.get(LIST_MISSING_LOCATION_DATES_PATH, filters); +}; + +export const listMissingLocationAssets = async ( + filters: IMissingAssetAlbumsFilters +): Promise => { + return API.get(LIST_MISSING_LOCATION_ASSETS_PATH, filters).then((assets) => + assets.map(cleanUpAsset) + ); +}; diff --git a/src/handlers/api/user.handler.ts b/src/handlers/api/user.handler.ts new file mode 100644 index 0000000..534de92 --- /dev/null +++ b/src/handlers/api/user.handler.ts @@ -0,0 +1,6 @@ +import { GET_ME_PATH } from "@/config/routes"; +import API from "@/lib/api"; + +export const getMe = async () => { + return API.get(GET_ME_PATH); +} \ No newline at end of file diff --git a/src/handlers/serverUtils/user.utils.ts b/src/handlers/serverUtils/user.utils.ts new file mode 100644 index 0000000..bebe532 --- /dev/null +++ b/src/handlers/serverUtils/user.utils.ts @@ -0,0 +1,16 @@ +import { ENV } from "@/config/environment" + +export const getCurrentUser = () => { + console.log(ENV.IMMICH_URL + "/api/users/me") + return fetch(ENV.IMMICH_URL + "/api/users/me", { + headers: { + 'x-api-key': ENV.IMMICH_API_KEY, + Accept: "application/json", + }, + }).then((res) => { + if (res.ok) { + return res.json() + } + throw new Error("Failed to fetch user") + }) +} \ No newline at end of file diff --git a/src/helpers/date.helper.ts b/src/helpers/date.helper.ts index 0b2a138..2f72fc2 100644 --- a/src/helpers/date.helper.ts +++ b/src/helpers/date.helper.ts @@ -6,4 +6,10 @@ export const formatDate = (date: string, outputFormat?: string): string => { export const parseDate = (date: string, inputFormat: string): Date => { return parse(date, inputFormat, new Date()); -} \ No newline at end of file +} + +export const addDays = (date: Date, days: number): Date => { + const result = new Date(date); + result.setDate(result.getDate() + days); + return result; +} diff --git a/src/lib/api.ts b/src/lib/api.ts index 4b64e33..c649762 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -3,58 +3,6 @@ import axios from 'axios' import qs from 'qs' -// const handleErrors = (error: AxiosError | any) => { -// console.error('error_message', error.message) -// if (error.message === 'canceled') { -// const cancelledError = { -// code: 'ERR_CANCELED', -// message: 'Request canceled', -// } -// return Promise.reject(cancelledError) -// } -// let errorObject = {} -// if (error.response) { -// const { status } = error.response -// if (status === 401) { -// errorObject = { -// ...error.response.data, -// message: error.response.data.message, -// } -// if (typeof window !== 'undefined' && window.location) { -// // return resetSession().then(() => { -// // window.location.href = '/auth/signin' -// // return Promise.reject(errorObject) -// // }) -// } -// } else if (status === 403) { -// errorObject = { -// ...error.response.data, -// message: -// 'You do not have access to this page. Please contact your administrator!', -// } -// if (typeof window !== 'undefined' && window.location) { -// // deleteLocalHashData() -// window.location.href = '/404' -// return Promise.reject(errorObject) -// } -// } else if (status === 422) { -// errorObject = { -// ...error.response.data, -// message: 'Unable to process this request, please try again later', -// } -// } else { -// errorObject = { -// ...error.response.data, -// } -// } -// return Promise.reject(errorObject) -// } -// errorObject = { -// message: 'Network Error. Please check your connection and try again!', -// } -// return Promise.reject(errorObject) -// } - export default class API { diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index b824c6d..7ecce1c 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -4,7 +4,8 @@ import type { AppProps } from "next/app"; import { ThemeProvider } from "next-themes"; import { ENV } from "@/config/environment"; import ConfigContext from "@/contexts/ConfigContext"; -import { useRef } from "react"; +import { useEffect, useRef, useState } from "react"; +import { getMe } from "@/handlers/api/user.handler"; interface AppPropsWithProps extends AppProps { props: { diff --git a/src/pages/api/albums/potential-albums-assets.ts b/src/pages/api/albums/potential-albums-assets.ts index 3151c23..e5c753e 100644 --- a/src/pages/api/albums/potential-albums-assets.ts +++ b/src/pages/api/albums/potential-albums-assets.ts @@ -1,11 +1,12 @@ // Next.js API route support: https://nextjs.org/docs/api-routes/introduction import { CHART_COLORS } from "@/config/constants/chart.constant"; import { db } from "@/config/db"; +import { getCurrentUser } from "@/handlers/serverUtils/user.utils"; import { exif } from "@/schema"; import { count, desc, isNotNull, ne, sql } from "drizzle-orm"; import type { NextApiRequest, NextApiResponse } from "next"; -const SELECT_ORPHAN_PHOTOS = (date: string) => +const SELECT_ORPHAN_PHOTOS = (date: string, ownerId: string) => sql.raw(` SELECT a."id", @@ -35,7 +36,8 @@ const SELECT_ORPHAN_PHOTOS = (date: string) => ON a.id = e."assetId" WHERE aaa."albumsId" IS NULL - AND a."localDateTime"::date = '${date}'; + AND a."ownerId" = '${ownerId}' + AND a."localDateTime"::date = '${date}' `); export default async function handler( @@ -43,8 +45,9 @@ export default async function handler( res: NextApiResponse ) { try { + const currentUser = await getCurrentUser() const { startDate } = req.query as { startDate: string }; - const { rows } = await db.execute(SELECT_ORPHAN_PHOTOS(startDate)); + const { rows } = await db.execute(SELECT_ORPHAN_PHOTOS(startDate, currentUser.id)); return res.status(200).json(rows); } catch (error: any) { res.status(500).json({ diff --git a/src/pages/api/albums/potential-albums-dates.ts b/src/pages/api/albums/potential-albums-dates.ts index 41434cf..0108b4c 100644 --- a/src/pages/api/albums/potential-albums-dates.ts +++ b/src/pages/api/albums/potential-albums-dates.ts @@ -1,11 +1,12 @@ // Next.js API route support: https://nextjs.org/docs/api-routes/introduction import { CHART_COLORS } from "@/config/constants/chart.constant"; import { db } from "@/config/db"; +import { getCurrentUser } from "@/handlers/serverUtils/user.utils"; import { exif } from "@/schema"; import { count, desc, isNotNull, ne, sql } from "drizzle-orm"; import type { NextApiRequest, NextApiResponse } from "next"; -const SELECT_ORPHAN_PHOTOS = sql` +const SELECT_ORPHAN_PHOTOS = (ownerId: string) => sql` SELECT DATE(a."localDateTime") AS "date", COUNT(a."id") AS "asset_count" @@ -15,7 +16,8 @@ LEFT JOIN "albums_assets_assets" aaa ON a."id" = aaa."assetsId" WHERE aaa."albumsId" IS NULL -GROUP BY + AND a."ownerId" = ${ownerId} +GROUP BY DATE(a."localDateTime") ORDER BY "asset_count" DESC; @@ -26,7 +28,8 @@ export default async function handler( res: NextApiResponse ) { try { - const { rows } = await db.execute(SELECT_ORPHAN_PHOTOS); + const currentUser = await getCurrentUser() + const { rows } = await db.execute(SELECT_ORPHAN_PHOTOS(currentUser.id)); return res.status(200).json(rows); } catch (error: any) { res.status(500).json({ diff --git a/src/pages/api/analytics/exif/[property].ts b/src/pages/api/analytics/exif/[property].ts index 8390668..cb30ba3 100644 --- a/src/pages/api/analytics/exif/[property].ts +++ b/src/pages/api/analytics/exif/[property].ts @@ -1,8 +1,9 @@ // Next.js API route support: https://nextjs.org/docs/api-routes/introduction import { CHART_COLORS } from "@/config/constants/chart.constant"; import { db } from "@/config/db"; -import { exif } from "@/schema"; -import { count, desc, isNotNull, ne } from "drizzle-orm"; +import { getCurrentUser } from "@/handlers/serverUtils/user.utils"; +import { assets, exif } from "@/schema"; +import { and, count, desc, eq, isNotNull, ne } from "drizzle-orm"; import type { NextApiRequest, NextApiResponse } from "next"; const columnMap = { @@ -33,12 +34,17 @@ export default async function handler( } try { + const currentUser = await getCurrentUser(); const dataFromDB = await db.select({ value: count(), label: column, }) .from(exif) - .where(isNotNull(column)) + .leftJoin(assets, eq(assets.id, exif.assetId)) + .where(and( + isNotNull(column), + eq(assets.ownerId, currentUser.id), + )) .limit(20) .groupBy(column).orderBy(desc(count())); diff --git a/src/pages/api/assets/missing-location-assets.ts b/src/pages/api/assets/missing-location-assets.ts new file mode 100644 index 0000000..ba4cd46 --- /dev/null +++ b/src/pages/api/assets/missing-location-assets.ts @@ -0,0 +1,65 @@ +// Next.js API route support: https://nextjs.org/docs/api-routes/introduction +import { db } from "@/config/db"; +import { getCurrentUser } from "@/handlers/serverUtils/user.utils"; +import { parseDate } from "@/helpers/date.helper"; +import { assets, exif } from "@/schema"; +import { addDays } from "date-fns"; +import { and, count, desc, eq, gte, isNotNull, isNull, lte, ne, sql } from "drizzle-orm"; +import type { NextApiRequest, NextApiResponse } from "next"; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + const { startDate } = req.query as { + startDate: string; + }; + + const currentUser = await getCurrentUser(); + + if (!startDate) { + return res.status(400).json({ + error: "startDate and endDate are required", + }); + } + + const startDateDate = parseDate(startDate, "yyyy-MM-dd"); + const endDateDate = addDays(parseDate(startDate, "yyyy-MM-dd"), 1); + + try { + const rows = await db + .select({ + id: assets.id, + deviceId: assets.deviceId, + type: assets.type, + originalPath: assets.originalPath, + previewPath: assets.previewPath, + isFavorite: assets.isFavorite, + duration: assets.duration, + thumbnailPath: assets.thumbnailPath, + encodedVideoPath: assets.encodedVideoPath, + originalFileName: assets.originalFileName, + sidecarPath: assets.sidecarPath, + deletedAt: assets.deletedAt, + localDateTime: assets.localDateTime, + exifImageWidth: exif.exifImageWidth, + exifImageHeight: exif.exifImageHeight, + ownerId: assets.ownerId, + }) + .from(assets) + .leftJoin(exif, eq(exif.assetId, assets.id)) + .where(and( + isNull(exif.latitude), + isNotNull(assets.createdAt), + gte(exif.dateTimeOriginal, startDateDate), + lte(exif.dateTimeOriginal, endDateDate), + eq(assets.ownerId, currentUser.id), + )); + + return res.status(200).json(rows); + } catch (error: any) { + res.status(500).json({ + error: error?.message, + }); + } +} diff --git a/src/pages/api/assets/missing-location-dates.ts b/src/pages/api/assets/missing-location-dates.ts new file mode 100644 index 0000000..c9e7013 --- /dev/null +++ b/src/pages/api/assets/missing-location-dates.ts @@ -0,0 +1,35 @@ +// Next.js API route support: https://nextjs.org/docs/api-routes/introduction +import { CHART_COLORS } from "@/config/constants/chart.constant"; +import { db } from "@/config/db"; +import { getCurrentUser } from "@/handlers/serverUtils/user.utils"; +import { assets, exif } from "@/schema"; +import { and, count, desc, eq, isNotNull, isNull, ne, sql } from "drizzle-orm"; +import type { NextApiRequest, NextApiResponse } from "next"; + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + try { + const currentUser = await getCurrentUser(); + console.log({ currentUser }); + const rows = await db + .select({ + asset_count: desc(count(assets.id)), + date: sql`DATE(${exif.dateTimeOriginal})`, + }) + .from(assets) + .leftJoin(exif, eq(exif.assetId, assets.id)) + .where(and( + isNull(exif.latitude), + eq(assets.ownerId, currentUser.id), + )) + .groupBy(sql`DATE(${exif.dateTimeOriginal})`) + .orderBy(desc(count(assets.id))); + return res.status(200).json(rows); + } catch (error: any) { + res.status(500).json({ + error: error?.message, + }); + } +} diff --git a/src/pages/api/immich-proxy/asset/thumbnail/[id].ts b/src/pages/api/immich-proxy/asset/thumbnail/[id].ts index f57e1ba..5a73569 100644 --- a/src/pages/api/immich-proxy/asset/thumbnail/[id].ts +++ b/src/pages/api/immich-proxy/asset/thumbnail/[id].ts @@ -28,6 +28,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) }) if (!response.ok) { + console.error('HTTP error:', response) throw new Error(`HTTP error! status: ${response.status}`) } diff --git a/src/pages/api/people/list.ts b/src/pages/api/people/list.ts index 924abeb..d21b504 100644 --- a/src/pages/api/people/list.ts +++ b/src/pages/api/people/list.ts @@ -1,6 +1,7 @@ // Next.js API route support: https://nextjs.org/docs/api-routes/introduction import { CHART_COLORS } from "@/config/constants/chart.constant"; import { db } from "@/config/db"; +import { getCurrentUser } from "@/handlers/serverUtils/user.utils"; import { stringToBoolean } from "@/helpers/data.helper"; import { assetFaces, assets, exif, person } from "@/schema"; import { @@ -43,12 +44,15 @@ export default async function handler( sortOrder = "desc", } = req.query as any as IQuery; + const currentUser = await getCurrentUser(); + const maximumAssetCount = !maxValue || maxValue <= 0 ? 1000000 : maxValue; const whereClause = and( eq(person.isHidden, false), isNull(assets.duplicateId), eq(assets.isVisible, true), eq(assets.isArchived, false), + eq(assets.ownerId, currentUser.id), stringToBoolean(nameLessOnly) ? eq(person.name, "") : undefined ); diff --git a/src/pages/assets/missing-locations.tsx b/src/pages/assets/missing-locations.tsx new file mode 100644 index 0000000..86129b2 --- /dev/null +++ b/src/pages/assets/missing-locations.tsx @@ -0,0 +1,106 @@ +import AlbumSelectorDialog from "@/components/albums/AlbumSelectorDialog"; +import PotentialAlbumsAssets from "@/components/albums/potential-albums/PotentialAlbumsAssets"; +import PotentialAlbumsDates from "@/components/albums/potential-albums/PotentialAlbumsDates"; +import MissingLocationAssets from "@/components/assets/missing-location/MissingLocationAssets"; +import MissingLocationDates from "@/components/assets/missing-location/MissingLocationDates"; +import PageLayout from "@/components/layouts/PageLayout"; +import Header from "@/components/shared/Header"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { useToast } from "@/components/ui/use-toast"; +import MissingLocationContext, { + IMissingLocationConfig, +} from "@/contexts/MissingLocationContext"; + +import { addAssetToAlbum } from "@/handlers/api/album.handler"; +import { IAlbum } from "@/types/album"; +import React from "react"; + +export default function MissingLocations() { + const { toast } = useToast(); + + const [config, setConfig] = React.useState({ + startDate: undefined, + selectedIds: [], + assets: [], + }); + + const handleSelect = (album: IAlbum) => { + return addAssetToAlbum(album.id, config.selectedIds) + .then(() => { + setConfig({ ...config, selectedIds: [] }); + setConfig({ + ...config, + assets: config.assets.filter( + (asset) => !config.selectedIds.includes(asset.id) + ), + selectedIds: [], + }); + }) + .then(() => { + toast({ + title: `Assets added to ${album.albumName}`, + description: `${config.selectedIds.length} assets added to album`, + }); + }) + .catch(() => { + toast({ + title: "Error", + description: "Failed to add assets album", + variant: "destructive", + }); + }); + }; + + return ( + +
+ + {config.selectedIds.length} Selected + + {config.selectedIds.length === config.assets.length ? ( + + ) : ( + + )} + + } + /> + ) => + setConfig({ ...config, ...newConfig }), + }} + > +
+ + +
+
+ + ); +} diff --git a/src/schema/assets.schema.ts b/src/schema/assets.schema.ts index d1e8268..908e35a 100644 --- a/src/schema/assets.schema.ts +++ b/src/schema/assets.schema.ts @@ -3,7 +3,7 @@ import { pgTable, uuid, varchar, timestamp, boolean } from 'drizzle-orm/pg-core' export const assets = pgTable('assets', { id: uuid('id').defaultRandom().primaryKey().notNull(), deviceAssetId: varchar('deviceAssetId').notNull(), - // ownerId: uuid('ownerId').notNull().references(() => users.id, { onDelete: 'cascade', onUpdate: 'cascade' }), + ownerId: uuid('ownerId').notNull(), deviceId: varchar('deviceId').notNull(), type: varchar('type').notNull(), originalPath: varchar('originalPath').notNull(), diff --git a/src/types/user.d.ts b/src/types/user.d.ts new file mode 100644 index 0000000..45daa33 --- /dev/null +++ b/src/types/user.d.ts @@ -0,0 +1,18 @@ +export interface IUser { + id: string; + email: string; + name: string; + profileImagePath: string; + avatarColor: string; + storageLabel: string; + shouldChangePassword: boolean; + isAdmin: boolean; + createdAt: Date; + deletedAt: null; + updatedAt: Date; + oauthId: string; + quotaSizeInBytes: null; + quotaUsageInBytes: number; + status: string; + license: null; +}