From 7e3cb29c5719b94f91e6130165edea81209b72dc Mon Sep 17 00:00:00 2001 From: Maurice Date: Mon, 13 Apr 2026 21:06:15 +0200 Subject: [PATCH] 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 --- client/src/i18n/translations/ar.ts | 2 + client/src/i18n/translations/br.ts | 2 + client/src/i18n/translations/cs.ts | 2 + client/src/i18n/translations/de.ts | 2 + client/src/i18n/translations/en.ts | 2 + client/src/i18n/translations/es.ts | 2 + client/src/i18n/translations/fr.ts | 2 + client/src/i18n/translations/hu.ts | 2 + client/src/i18n/translations/it.ts | 2 + client/src/i18n/translations/nl.ts | 2 + client/src/i18n/translations/pl.ts | 2 + client/src/i18n/translations/ru.ts | 2 + client/src/i18n/translations/zh.ts | 2 + client/src/i18n/translations/zhTw.ts | 2 + client/src/pages/JourneyDetailPage.tsx | 38 ++++++++++++++++++- server/src/routes/memories/immich.ts | 8 ++++ server/src/routes/memories/synology.ts | 6 +++ server/src/services/memories/immichService.ts | 26 +++++++++++++ .../src/services/memories/synologyService.ts | 30 +++++++++++++++ 19 files changed, 135 insertions(+), 1 deletion(-) diff --git a/client/src/i18n/translations/ar.ts b/client/src/i18n/translations/ar.ts index 31ae8002..ecd2a81a 100644 --- a/client/src/i18n/translations/ar.ts +++ b/client/src/i18n/translations/ar.ts @@ -1546,6 +1546,8 @@ const ar: Record = { 'journey.picker.selected': 'محدد', 'journey.picker.addTo': 'إضافة إلى', 'journey.picker.newGallery': 'معرض جديد', + 'journey.picker.selectAll': 'تحديد الكل', + 'journey.picker.deselectAll': 'إلغاء تحديد الكل', 'journey.picker.noAlbums': 'لم يتم العثور على ألبومات', 'journey.picker.selectDate': 'اختر تاريخ', 'journey.picker.search': 'بحث', diff --git a/client/src/i18n/translations/br.ts b/client/src/i18n/translations/br.ts index dd67826b..6ca98f8a 100644 --- a/client/src/i18n/translations/br.ts +++ b/client/src/i18n/translations/br.ts @@ -2020,6 +2020,8 @@ const br: Record = { 'journey.picker.selected': 'selecionados', 'journey.picker.addTo': 'Adicionar a', 'journey.picker.newGallery': 'Nova galeria', + 'journey.picker.selectAll': 'Selecionar tudo', + 'journey.picker.deselectAll': 'Desmarcar tudo', 'journey.picker.noAlbums': 'Nenhum álbum encontrado', 'journey.picker.selectDate': 'Selecionar data', 'journey.picker.search': 'Pesquisar', diff --git a/client/src/i18n/translations/cs.ts b/client/src/i18n/translations/cs.ts index 691d945d..46d69844 100644 --- a/client/src/i18n/translations/cs.ts +++ b/client/src/i18n/translations/cs.ts @@ -2022,6 +2022,8 @@ const cs: Record = { 'journey.picker.selected': 'vybráno', 'journey.picker.addTo': 'Přidat do', 'journey.picker.newGallery': 'Nová galerie', + 'journey.picker.selectAll': 'Vybrat vše', + 'journey.picker.deselectAll': 'Zrušit výběr', 'journey.picker.noAlbums': 'Žádná alba nenalezena', 'journey.picker.selectDate': 'Vyberte datum', 'journey.picker.search': 'Hledat', diff --git a/client/src/i18n/translations/de.ts b/client/src/i18n/translations/de.ts index 48704326..74f30956 100644 --- a/client/src/i18n/translations/de.ts +++ b/client/src/i18n/translations/de.ts @@ -2010,6 +2010,8 @@ const de: Record = { 'journey.picker.selected': 'ausgewählt', 'journey.picker.addTo': 'Hinzufügen zu', 'journey.picker.newGallery': 'Neue Galerie', + 'journey.picker.selectAll': 'Alle auswählen', + 'journey.picker.deselectAll': 'Alle abwählen', 'journey.picker.noAlbums': 'Keine Alben gefunden', 'journey.picker.selectDate': 'Datum wählen', 'journey.picker.search': 'Suchen', diff --git a/client/src/i18n/translations/en.ts b/client/src/i18n/translations/en.ts index 472b9ce3..f4de4d1d 100644 --- a/client/src/i18n/translations/en.ts +++ b/client/src/i18n/translations/en.ts @@ -2037,6 +2037,8 @@ const en: Record = { 'journey.picker.selected': 'selected', 'journey.picker.addTo': 'Add to', 'journey.picker.newGallery': 'New Gallery', + 'journey.picker.selectAll': 'Select all', + 'journey.picker.deselectAll': 'Deselect all', 'journey.picker.noAlbums': 'No albums found', 'journey.picker.selectDate': 'Select date', 'journey.picker.search': 'Search', diff --git a/client/src/i18n/translations/es.ts b/client/src/i18n/translations/es.ts index a176c833..da021080 100644 --- a/client/src/i18n/translations/es.ts +++ b/client/src/i18n/translations/es.ts @@ -2024,6 +2024,8 @@ const es: Record = { 'journey.picker.selected': 'seleccionados', 'journey.picker.addTo': 'Añadir a', 'journey.picker.newGallery': 'Nueva galería', + 'journey.picker.selectAll': 'Seleccionar todo', + 'journey.picker.deselectAll': 'Deseleccionar todo', 'journey.picker.noAlbums': 'No se encontraron álbumes', 'journey.picker.selectDate': 'Seleccionar fecha', 'journey.picker.search': 'Buscar', diff --git a/client/src/i18n/translations/fr.ts b/client/src/i18n/translations/fr.ts index ca9c1d91..69fd54e9 100644 --- a/client/src/i18n/translations/fr.ts +++ b/client/src/i18n/translations/fr.ts @@ -2018,6 +2018,8 @@ const fr: Record = { 'journey.picker.selected': 'sélectionnés', 'journey.picker.addTo': 'Ajouter à', 'journey.picker.newGallery': 'Nouvelle galerie', + 'journey.picker.selectAll': 'Tout sélectionner', + 'journey.picker.deselectAll': 'Tout désélectionner', 'journey.picker.noAlbums': 'Aucun album trouvé', 'journey.picker.selectDate': 'Sélectionner une date', 'journey.picker.search': 'Rechercher', diff --git a/client/src/i18n/translations/hu.ts b/client/src/i18n/translations/hu.ts index 89d9d4df..5afa97b0 100644 --- a/client/src/i18n/translations/hu.ts +++ b/client/src/i18n/translations/hu.ts @@ -2019,6 +2019,8 @@ const hu: Record = { 'journey.picker.selected': 'kiválasztva', 'journey.picker.addTo': 'Hozzáadás', 'journey.picker.newGallery': 'Új galéria', + 'journey.picker.selectAll': 'Összes kijelölése', + 'journey.picker.deselectAll': 'Összes kijelölés törlése', 'journey.picker.noAlbums': 'Nem található album', 'journey.picker.selectDate': 'Dátum választása', 'journey.picker.search': 'Keresés', diff --git a/client/src/i18n/translations/it.ts b/client/src/i18n/translations/it.ts index 7e1d1423..f1d69b6f 100644 --- a/client/src/i18n/translations/it.ts +++ b/client/src/i18n/translations/it.ts @@ -2019,6 +2019,8 @@ const it: Record = { 'journey.picker.selected': 'selezionati', 'journey.picker.addTo': 'Aggiungi a', 'journey.picker.newGallery': 'Nuova galleria', + 'journey.picker.selectAll': 'Seleziona tutto', + 'journey.picker.deselectAll': 'Deseleziona tutto', 'journey.picker.noAlbums': 'Nessun album trovato', 'journey.picker.selectDate': 'Seleziona data', 'journey.picker.search': 'Cerca', diff --git a/client/src/i18n/translations/nl.ts b/client/src/i18n/translations/nl.ts index 25981226..71d7463f 100644 --- a/client/src/i18n/translations/nl.ts +++ b/client/src/i18n/translations/nl.ts @@ -2018,6 +2018,8 @@ const nl: Record = { 'journey.picker.selected': 'geselecteerd', 'journey.picker.addTo': 'Toevoegen aan', 'journey.picker.newGallery': 'Nieuwe galerij', + 'journey.picker.selectAll': 'Alles selecteren', + 'journey.picker.deselectAll': 'Alles deselecteren', 'journey.picker.noAlbums': 'Geen albums gevonden', 'journey.picker.selectDate': 'Selecteer datum', 'journey.picker.search': 'Zoeken', diff --git a/client/src/i18n/translations/pl.ts b/client/src/i18n/translations/pl.ts index ef99e1e6..ebc04162 100644 --- a/client/src/i18n/translations/pl.ts +++ b/client/src/i18n/translations/pl.ts @@ -2014,6 +2014,8 @@ const pl: Record = { 'journey.picker.selected': 'wybranych', 'journey.picker.addTo': 'Dodaj do', 'journey.picker.newGallery': 'Nowa galeria', + 'journey.picker.selectAll': 'Zaznacz wszystko', + 'journey.picker.deselectAll': 'Odznacz wszystko', 'journey.picker.noAlbums': 'Nie znaleziono albumów', 'journey.picker.selectDate': 'Wybierz datę', 'journey.picker.search': 'Szukaj', diff --git a/client/src/i18n/translations/ru.ts b/client/src/i18n/translations/ru.ts index f5b49338..e6cd476d 100644 --- a/client/src/i18n/translations/ru.ts +++ b/client/src/i18n/translations/ru.ts @@ -2018,6 +2018,8 @@ const ru: Record = { 'journey.picker.selected': 'выбрано', 'journey.picker.addTo': 'Добавить в', 'journey.picker.newGallery': 'Новая галерея', + 'journey.picker.selectAll': 'Выбрать все', + 'journey.picker.deselectAll': 'Снять выбор', 'journey.picker.noAlbums': 'Альбомы не найдены', 'journey.picker.selectDate': 'Выберите дату', 'journey.picker.search': 'Поиск', diff --git a/client/src/i18n/translations/zh.ts b/client/src/i18n/translations/zh.ts index 2ec537a5..e2c4d6f6 100644 --- a/client/src/i18n/translations/zh.ts +++ b/client/src/i18n/translations/zh.ts @@ -2018,6 +2018,8 @@ const zh: Record = { 'journey.picker.selected': '已选择', 'journey.picker.addTo': '添加到', 'journey.picker.newGallery': '新相册', + 'journey.picker.selectAll': '全选', + 'journey.picker.deselectAll': '取消全选', 'journey.picker.noAlbums': '未找到相册', 'journey.picker.selectDate': '选择日期', 'journey.picker.search': '搜索', diff --git a/client/src/i18n/translations/zhTw.ts b/client/src/i18n/translations/zhTw.ts index 248f33a1..b1970112 100644 --- a/client/src/i18n/translations/zhTw.ts +++ b/client/src/i18n/translations/zhTw.ts @@ -1979,6 +1979,8 @@ const zhTw: Record = { 'journey.picker.selected': '已選擇', 'journey.picker.addTo': '新增至', 'journey.picker.newGallery': '新相簿', + 'journey.picker.selectAll': '全選', + 'journey.picker.deselectAll': '取消全選', 'journey.picker.noAlbums': '未找到相簿', 'journey.picker.selectDate': '選擇日期', 'journey.picker.search': '搜尋', diff --git a/client/src/pages/JourneyDetailPage.tsx b/client/src/pages/JourneyDetailPage.tsx index eee2e222..a4fda122 100644 --- a/client/src/pages/JourneyDetailPage.tsx +++ b/client/src/pages/JourneyDetailPage.tsx @@ -1398,6 +1398,15 @@ function ProviderPicker({ provider, userId, entries, trips, existingAssetIds, on setLoading(false) } + const loadAlbumPhotos = async (albumId: string) => { + setLoading(true) + try { + const res = await fetch(`/api/integrations/memories/${provider}/albums/${albumId}/photos`, { credentials: 'include' }) + if (res.ok) setPhotos((await res.json()).assets || []) + } catch {} + setLoading(false) + } + const loadAlbums = async () => { try { const res = await fetch(`/api/integrations/memories/${provider}/albums`, { credentials: 'include' }) @@ -1511,7 +1520,7 @@ function ProviderPicker({ provider, userId, entries, trips, existingAssetIds, on {albums.map((a: any) => ( + ) + })()} {loading ? (
diff --git a/server/src/routes/memories/immich.ts b/server/src/routes/memories/immich.ts index 27e631e5..0d2509c2 100644 --- a/server/src/routes/memories/immich.ts +++ b/server/src/routes/memories/immich.ts @@ -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; diff --git a/server/src/routes/memories/synology.ts b/server/src/routes/memories/synology.ts index 30acf2ee..196dd984 100644 --- a/server/src/routes/memories/synology.ts +++ b/server/src/routes/memories/synology.ts @@ -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; diff --git a/server/src/services/memories/immichService.ts b/server/src/services/memories/immichService.ts index 047c732e..31f3c609 100644 --- a/server/src/services/memories/immichService.ts +++ b/server/src/services/memories/immichService.ts @@ -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 diff --git a/server/src/services/memories/synologyService.ts b/server/src/services/memories/synologyService.ts index d15ea3f0..4df4e0a9 100644 --- a/server/src/services/memories/synologyService.ts +++ b/server/src/services/memories/synologyService.ts @@ -452,6 +452,36 @@ export async function listSynologyAlbums(userId: number): Promise> { + 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; + 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> { const response = getAlbumIdFromLink(tripId, linkId, userId); if (!response.success) return response as ServiceResult;