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:
@@ -1546,6 +1546,8 @@ const ar: Record<string, string | { name: string; category: string }[]> = {
|
||||
'journey.picker.selected': 'محدد',
|
||||
'journey.picker.addTo': 'إضافة إلى',
|
||||
'journey.picker.newGallery': 'معرض جديد',
|
||||
'journey.picker.selectAll': 'تحديد الكل',
|
||||
'journey.picker.deselectAll': 'إلغاء تحديد الكل',
|
||||
'journey.picker.noAlbums': 'لم يتم العثور على ألبومات',
|
||||
'journey.picker.selectDate': 'اختر تاريخ',
|
||||
'journey.picker.search': 'بحث',
|
||||
|
||||
@@ -2020,6 +2020,8 @@ const br: Record<string, string | { name: string; category: string }[]> = {
|
||||
'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',
|
||||
|
||||
@@ -2022,6 +2022,8 @@ const cs: Record<string, string | { name: string; category: string }[]> = {
|
||||
'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',
|
||||
|
||||
@@ -2010,6 +2010,8 @@ const de: Record<string, string | { name: string; category: string }[]> = {
|
||||
'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',
|
||||
|
||||
@@ -2037,6 +2037,8 @@ const en: Record<string, string | { name: string; category: string }[]> = {
|
||||
'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',
|
||||
|
||||
@@ -2024,6 +2024,8 @@ const es: Record<string, string> = {
|
||||
'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',
|
||||
|
||||
@@ -2018,6 +2018,8 @@ const fr: Record<string, string> = {
|
||||
'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',
|
||||
|
||||
@@ -2019,6 +2019,8 @@ const hu: Record<string, string | { name: string; category: string }[]> = {
|
||||
'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',
|
||||
|
||||
@@ -2019,6 +2019,8 @@ const it: Record<string, string | { name: string; category: string }[]> = {
|
||||
'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',
|
||||
|
||||
@@ -2018,6 +2018,8 @@ const nl: Record<string, string> = {
|
||||
'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',
|
||||
|
||||
@@ -2014,6 +2014,8 @@ const pl: Record<string, string | { name: string; category: string }[]> = {
|
||||
'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',
|
||||
|
||||
@@ -2018,6 +2018,8 @@ const ru: Record<string, string> = {
|
||||
'journey.picker.selected': 'выбрано',
|
||||
'journey.picker.addTo': 'Добавить в',
|
||||
'journey.picker.newGallery': 'Новая галерея',
|
||||
'journey.picker.selectAll': 'Выбрать все',
|
||||
'journey.picker.deselectAll': 'Снять выбор',
|
||||
'journey.picker.noAlbums': 'Альбомы не найдены',
|
||||
'journey.picker.selectDate': 'Выберите дату',
|
||||
'journey.picker.search': 'Поиск',
|
||||
|
||||
@@ -2018,6 +2018,8 @@ const zh: Record<string, string> = {
|
||||
'journey.picker.selected': '已选择',
|
||||
'journey.picker.addTo': '添加到',
|
||||
'journey.picker.newGallery': '新相册',
|
||||
'journey.picker.selectAll': '全选',
|
||||
'journey.picker.deselectAll': '取消全选',
|
||||
'journey.picker.noAlbums': '未找到相册',
|
||||
'journey.picker.selectDate': '选择日期',
|
||||
'journey.picker.search': '搜索',
|
||||
|
||||
@@ -1979,6 +1979,8 @@ const zhTw: Record<string, string> = {
|
||||
'journey.picker.selected': '已選擇',
|
||||
'journey.picker.addTo': '新增至',
|
||||
'journey.picker.newGallery': '新相簿',
|
||||
'journey.picker.selectAll': '全選',
|
||||
'journey.picker.deselectAll': '取消全選',
|
||||
'journey.picker.noAlbums': '未找到相簿',
|
||||
'journey.picker.selectDate': '選擇日期',
|
||||
'journey.picker.search': '搜尋',
|
||||
|
||||
@@ -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) => (
|
||||
<button
|
||||
key={a.id}
|
||||
onClick={() => { setSelectedAlbum(a.id); searchPhotos(a.startDate || '2000-01-01', a.endDate || '2099-01-01') }}
|
||||
onClick={() => { setSelectedAlbum(a.id); loadAlbumPhotos(a.id) }}
|
||||
className={`px-2.5 py-1 rounded-lg text-[11px] font-medium whitespace-nowrap flex-shrink-0 border ${
|
||||
selectedAlbum === a.id
|
||||
? 'bg-zinc-900 dark:bg-white text-white dark:text-zinc-900 border-zinc-900 dark:border-white'
|
||||
@@ -1577,6 +1586,33 @@ function ProviderPicker({ provider, userId, entries, trips, existingAssetIds, on
|
||||
|
||||
{/* Photo grid */}
|
||||
<div className="flex-1 overflow-y-auto p-4">
|
||||
{/* Select all toggle */}
|
||||
{!loading && photos.length > 0 && (() => {
|
||||
const selectable = photos.filter((a: any) => !existingAssetIds.has(a.id))
|
||||
const allSelected = selectable.length > 0 && selectable.every((a: any) => selected.has(a.id))
|
||||
if (selectable.length === 0) return null
|
||||
return (
|
||||
<button
|
||||
onClick={() => {
|
||||
if (allSelected) {
|
||||
setSelected(new Set())
|
||||
} else {
|
||||
setSelected(new Set(selectable.map((a: any) => a.id)))
|
||||
}
|
||||
}}
|
||||
className="mb-3 inline-flex items-center gap-1.5 px-2.5 py-1 rounded-lg text-[11px] font-medium border border-zinc-200 dark:border-zinc-700 text-zinc-500 dark:text-zinc-400 hover:bg-zinc-50 dark:hover:bg-zinc-800"
|
||||
>
|
||||
<div className={`w-3.5 h-3.5 rounded border flex items-center justify-center ${
|
||||
allSelected
|
||||
? 'bg-zinc-900 dark:bg-white border-zinc-900 dark:border-white'
|
||||
: 'border-zinc-300 dark:border-zinc-600'
|
||||
}`}>
|
||||
{allSelected && <Check size={9} className="text-white dark:text-zinc-900" strokeWidth={3} />}
|
||||
</div>
|
||||
{allSelected ? t('journey.picker.deselectAll') : t('journey.picker.selectAll')} ({selectable.length})
|
||||
</button>
|
||||
)
|
||||
})()}
|
||||
{loading ? (
|
||||
<div className="flex justify-center py-12">
|
||||
<div className="w-6 h-6 border-2 border-zinc-300 border-t-zinc-900 rounded-full animate-spin" />
|
||||
|
||||
@@ -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