feat: redesign import-shared page and support password-protected links

Redesign import-shared with sticky import bar, dense gallery grid,
progress bar, and cleaner landing state. Add password-protected
shared link support - detects 401 "Password required" from Immich
and shows a password input. Also reduce asset grid spacing and
replace "Open in Immich" text with a link icon.
This commit is contained in:
Varun
2026-04-07 23:12:34 +05:30
parent 2ff863b296
commit 15f90121a1
4 changed files with 493 additions and 424 deletions
+4 -2
View File
@@ -301,6 +301,8 @@ const AssetGrid = forwardRef<AssetGridRef, AssetGridProps>(({ assets, isInternal
photos={images}
targetRowHeight={150}
rowConstraints={{ singleRowMaxHeight: 300 }}
spacing={2}
padding={0}
onClick={({ index, event, photo }) => handleClick(index, photo, event)}
render={{
image: renderImage,
@@ -309,10 +311,10 @@ const AssetGrid = forwardRef<AssetGridRef, AssetGridProps>(({ assets, isInternal
href={exImmichUrl + "/photos/" + photo.id}
target="_blank"
rel="noopener noreferrer"
className="absolute bottom-1 left-1 bg-black/60 text-yellow-300 text-xs px-2 py-0.5 rounded"
className="absolute bottom-1 left-1 bg-black/60 p-1 rounded"
onClick={(e) => e.stopPropagation()}
>
Open in Immich
<ExternalLink className="h-3.5 w-3.5 text-white" />
</a>
),
}}
+2 -2
View File
@@ -1,6 +1,6 @@
import { IMPORT_SHARED_PATH } from "@/config/routes";
import API from "@/lib/api";
export const importShared = async (link: string) => {
return API.post(IMPORT_SHARED_PATH, { link });
export const importShared = async (link: string, password?: string) => {
return API.post(IMPORT_SHARED_PATH, { link, password: password || undefined });
};
+25 -2
View File
@@ -134,6 +134,18 @@ const fetchJson = async <T>(url: string) => {
headers: { accept: "application/json" },
});
if (!response.ok) {
let errorBody: { message?: string } = {};
try { errorBody = await response.json(); } catch { /* ignore */ }
if (response.status === 401 && errorBody.message === "Password required") {
const err = new Error("PASSWORD_REQUIRED");
(err as any).code = "PASSWORD_REQUIRED";
throw err;
}
if (response.status === 401 && errorBody.message === "Invalid password") {
const err = new Error("Invalid password for this shared link.");
(err as any).code = "INVALID_PASSWORD";
throw err;
}
throw new Error(`Request failed for ${url} (status ${response.status})`);
}
return (await response.json()) as T;
@@ -147,6 +159,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const payload = typeof req.body === "string" ? JSON.parse(req.body) : req.body;
const link = payload?.link;
const password = payload?.password;
if (!link || typeof link !== "string") {
return respondWithError(res, 400, "A shared album link is required");
@@ -157,9 +170,13 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
return respondWithError(res, 400, "Invalid Immich share link");
}
const authQuery = password
? `key=${parsed.key}&password=${encodeURIComponent(password)}`
: `key=${parsed.key}`;
try {
const sharedLink = await fetchJson<ImmichSharedLinkResponse>(
`${parsed.origin}/api/shared-links/me?key=${parsed.key}`
`${parsed.origin}/api/shared-links/me?${authQuery}`
);
const albumId = sharedLink.album?.id;
@@ -167,7 +184,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
if (albumId) {
const album = await fetchJson<ImmichAlbumResponse>(
`${parsed.origin}/api/albums/${albumId}?key=${parsed.key}&withoutAssets=false`
`${parsed.origin}/api/albums/${albumId}?${authQuery}&withoutAssets=false`
);
const assets = (album.assets || []).map((asset) => ({
@@ -216,6 +233,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
return res.status(200).json(responseBody);
} catch (error: any) {
if (error?.code === "PASSWORD_REQUIRED") {
return res.status(401).json({ error: "PASSWORD_REQUIRED" });
}
if (error?.code === "INVALID_PASSWORD") {
return res.status(401).json({ error: error.message });
}
console.error("Import shared error", error);
return respondWithError(res, 500, error?.message ?? "Failed to import shared album");
}
File diff suppressed because it is too large Load Diff