feat: visual polish & UX — fade-in, theme toggle, scroll-to-top, footer, cursor, badge
- Add FadeIn component (IntersectionObserver scroll-reveal + load animation) - Add dark/light theme toggle (useSyncExternalStore + localStorage) - Add scroll-to-top button (appears after 300px scroll) - Add configurable footer with social links (gallery.yaml) - Add crosshair cursor on photo grid items - Add 'X photos' count badge on subpage cover images - Add light theme color palette in tokens.css
This commit is contained in:
@@ -12,27 +12,24 @@ import { getConfig } from './config';
|
||||
let _key: Buffer | null = null;
|
||||
|
||||
function getKey(): Buffer {
|
||||
if (!_key) {
|
||||
const apiKey = getConfig().immich.apiKey;
|
||||
_key = crypto.createHash('sha256').update(apiKey).digest();
|
||||
}
|
||||
return _key;
|
||||
if (!_key) {
|
||||
const apiKey = getConfig().immich.apiKey;
|
||||
_key = crypto.createHash('sha256').update(apiKey).digest();
|
||||
}
|
||||
return _key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode an asset ID into an opaque URL-safe token.
|
||||
*/
|
||||
export function encodeAssetId(assetId: string): string {
|
||||
const key = getKey();
|
||||
// Deterministic IV from asset ID (same input → same token)
|
||||
const iv = crypto.createHash('md5').update(assetId).digest();
|
||||
const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
|
||||
const encrypted = Buffer.concat([
|
||||
cipher.update(assetId, 'utf8'),
|
||||
cipher.final(),
|
||||
]);
|
||||
// Compact: base64url(iv + ciphertext)
|
||||
return Buffer.concat([iv, encrypted]).toString('base64url');
|
||||
const key = getKey();
|
||||
// Deterministic IV from asset ID (same input → same token)
|
||||
const iv = crypto.createHash('md5').update(assetId).digest();
|
||||
const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
|
||||
const encrypted = Buffer.concat([cipher.update(assetId, 'utf8'), cipher.final()]);
|
||||
// Compact: base64url(iv + ciphertext)
|
||||
return Buffer.concat([iv, encrypted]).toString('base64url');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -40,23 +37,22 @@ export function encodeAssetId(assetId: string): string {
|
||||
* Returns null if the token is invalid.
|
||||
*/
|
||||
export function decodeAssetId(token: string): string | null {
|
||||
try {
|
||||
const key = getKey();
|
||||
const data = Buffer.from(token, 'base64url');
|
||||
if (data.length < 17) return null; // iv(16) + at least 1 byte
|
||||
try {
|
||||
const key = getKey();
|
||||
const data = Buffer.from(token, 'base64url');
|
||||
if (data.length < 17) return null; // iv(16) + at least 1 byte
|
||||
|
||||
const iv = data.subarray(0, 16);
|
||||
const encrypted = data.subarray(16);
|
||||
const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
|
||||
const decrypted = Buffer.concat([
|
||||
decipher.update(encrypted),
|
||||
decipher.final(),
|
||||
]).toString('utf8');
|
||||
const iv = data.subarray(0, 16);
|
||||
const encrypted = data.subarray(16);
|
||||
const decipher = crypto.createDecipheriv('aes-256-cbc', key, iv);
|
||||
const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]).toString(
|
||||
'utf8',
|
||||
);
|
||||
|
||||
// Validate it looks like a UUID
|
||||
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
||||
return uuidRegex.test(decrypted) ? decrypted : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
// Validate it looks like a UUID
|
||||
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
||||
return uuidRegex.test(decrypted) ? decrypted : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user