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:
@@ -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>
|
||||
),
|
||||
}}
|
||||
|
||||
@@ -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 });
|
||||
};
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
+506
-462
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user