mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-19 13:21:46 +00:00
fix(journey): album photos, select-all, heading/hr fixes, dark mode
- Load actual album photos instead of date-range search fallback (new GET /albums/:id/photos for Immich + Synology) - Add select all / deselect all toggle in photo picker - Normalize Markdown headings to plain text in journal stories - Fix setext headings (---) rendering as hr instead of h2 - Add remark-breaks for proper line break rendering - Fix pros/cons dark mode gradient backgrounds - i18n: selectAll/deselectAll in 14 languages
This commit is contained in:
@@ -13,6 +13,7 @@ import {
|
||||
searchPhotos,
|
||||
streamImmichAsset,
|
||||
listAlbums,
|
||||
getAlbumPhotos,
|
||||
syncAlbumAssets,
|
||||
getAssetInfo,
|
||||
isValidAssetId,
|
||||
@@ -113,6 +114,13 @@ router.get('/albums', authenticate, async (req: Request, res: Response) => {
|
||||
res.json({ albums: result.albums });
|
||||
});
|
||||
|
||||
router.get('/albums/:albumId/photos', authenticate, async (req: Request, res: Response) => {
|
||||
const authReq = req as AuthRequest;
|
||||
const result = await getAlbumPhotos(authReq.user.id, req.params.albumId);
|
||||
if (result.error) return res.status(result.status!).json({ error: result.error });
|
||||
res.json({ assets: result.assets });
|
||||
});
|
||||
|
||||
router.post('/trips/:tripId/album-links/:linkId/sync', authenticate, async (req: Request, res: Response) => {
|
||||
const authReq = req as AuthRequest;
|
||||
const { tripId, linkId } = req.params;
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
getSynologyStatus,
|
||||
testSynologyConnection,
|
||||
listSynologyAlbums,
|
||||
getSynologyAlbumPhotos,
|
||||
syncSynologyAlbumLink,
|
||||
searchSynologyPhotos,
|
||||
getSynologyAssetInfo,
|
||||
@@ -77,6 +78,11 @@ router.get('/albums', authenticate, async (req: Request, res: Response) => {
|
||||
handleServiceResult(res, await listSynologyAlbums(authReq.user.id));
|
||||
});
|
||||
|
||||
router.get('/albums/:albumId/photos', authenticate, async (req: Request, res: Response) => {
|
||||
const authReq = req as AuthRequest;
|
||||
handleServiceResult(res, await getSynologyAlbumPhotos(authReq.user.id, req.params.albumId));
|
||||
});
|
||||
|
||||
router.post('/trips/:tripId/album-links/:linkId/sync', authenticate, async (req: Request, res: Response) => {
|
||||
const authReq = req as AuthRequest;
|
||||
const { tripId, linkId } = req.params;
|
||||
|
||||
@@ -285,6 +285,32 @@ export async function listAlbums(
|
||||
}
|
||||
}
|
||||
|
||||
export async function getAlbumPhotos(
|
||||
userId: number,
|
||||
albumId: string,
|
||||
): Promise<{ assets?: any[]; error?: string; status?: number }> {
|
||||
const creds = getImmichCredentials(userId);
|
||||
if (!creds) return { error: 'Immich not configured', status: 400 };
|
||||
|
||||
try {
|
||||
const resp = await safeFetch(`${creds.immich_url}/api/albums/${albumId}`, {
|
||||
headers: { 'x-api-key': creds.immich_api_key, 'Accept': 'application/json' },
|
||||
signal: AbortSignal.timeout(15000) as any,
|
||||
});
|
||||
if (!resp.ok) return { error: 'Failed to fetch album', status: resp.status };
|
||||
const albumData = await resp.json() as { assets?: any[] };
|
||||
const assets = (albumData.assets || []).filter((a: any) => a.type === 'IMAGE').map((a: any) => ({
|
||||
id: a.id,
|
||||
takenAt: a.fileCreatedAt || a.createdAt,
|
||||
city: a.exifInfo?.city || null,
|
||||
country: a.exifInfo?.country || null,
|
||||
}));
|
||||
return { assets };
|
||||
} catch {
|
||||
return { error: 'Could not reach Immich', status: 502 };
|
||||
}
|
||||
}
|
||||
|
||||
export function listAlbumLinks(tripId: string) {
|
||||
return db.prepare(`
|
||||
SELECT tal.*, u.username
|
||||
|
||||
@@ -452,6 +452,36 @@ export async function listSynologyAlbums(userId: number): Promise<ServiceResult<
|
||||
}
|
||||
|
||||
|
||||
export async function getSynologyAlbumPhotos(userId: number, albumId: string): Promise<ServiceResult<AssetsList>> {
|
||||
const allItems: SynologyPhotoItem[] = [];
|
||||
const pageSize = 1000;
|
||||
let offset = 0;
|
||||
|
||||
while (true) {
|
||||
const result = await _requestSynologyApi<{ list: SynologyPhotoItem[] }>(userId, {
|
||||
api: 'SYNO.Foto.Browse.Item',
|
||||
method: 'list',
|
||||
version: 1,
|
||||
album_id: Number(albumId),
|
||||
offset,
|
||||
limit: pageSize,
|
||||
additional: ['thumbnail'],
|
||||
});
|
||||
if (!result.success) return result as ServiceResult<AssetsList>;
|
||||
const items = result.data.list || [];
|
||||
allItems.push(...items);
|
||||
if (items.length < pageSize) break;
|
||||
offset += pageSize;
|
||||
}
|
||||
|
||||
const assets = allItems.map(item => ({
|
||||
id: String(item.additional?.thumbnail?.cache_key || item.id || ''),
|
||||
takenAt: item.time ? new Date(item.time * 1000).toISOString() : '',
|
||||
})).filter(a => a.id);
|
||||
|
||||
return success({ assets, total: assets.length, hasMore: false });
|
||||
}
|
||||
|
||||
export async function syncSynologyAlbumLink(userId: number, tripId: string, linkId: string, sid: string): Promise<ServiceResult<SyncAlbumResult>> {
|
||||
const response = getAlbumIdFromLink(tripId, linkId, userId);
|
||||
if (!response.success) return response as ServiceResult<SyncAlbumResult>;
|
||||
|
||||
Reference in New Issue
Block a user