From 60975c88abec0322524878df23ac9c6d9b7e61c1 Mon Sep 17 00:00:00 2001 From: Daniel Date: Thu, 24 Sep 2026 11:47:48 +0200 Subject: [PATCH] 1.2 --- README.md | 22 ++++++-- main.js | 15 ++++-- package.json | 2 +- preload.js | 2 +- renderer/app.js | 129 ++++++++++++++++++++++++++++++++------------ renderer/index.html | 18 +++++-- renderer/style.css | 5 +- 7 files changed, 145 insertions(+), 48 deletions(-) diff --git a/README.md b/README.md index e5ff759..9f67569 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,8 @@ liegen ausschließlich im Arbeitsspeicher). ## Starten - chmod +x Lichttisch-1.0.0-x86_64.AppImage - ./Lichttisch-1.0.0-x86_64.AppImage + chmod +x Lichttisch-1.2.0-x86_64.AppImage + ./Lichttisch-1.2.0-x86_64.AppImage Falls ein Fehler zu FUSE erscheint (z. B. auf Ubuntu 22.04+): @@ -30,6 +30,22 @@ Systems (GNOME Keyring / KWallet) verschlüsselt in - Ordner: durch deine Ordner blättern - Alben: Alben aus der Nextcloud-Fotos-App (inklusive geteilter Alben) +## Sortierung und Filter + +Oben in der Werkzeugleiste wählst du, wonach sortiert wird: + +- Aufnahmedatum aus den Bild-Metadaten (EXIF), verfügbar ab Nextcloud 28. + Bei älteren Servern schaltet die Zeitleiste automatisch aufs Änderungsdatum + um und zeigt einen Hinweis. +- Änderungsdatum der Datei, so wie die Nextcloud es als „Geändert“ anzeigt. + Bei Uploads über den Browser ist das der Zeitpunkt des Hochladens. + +In Ordnern und Alben gibt es beides jeweils aufsteigend und absteigend sowie +die Sortierung nach Name. + +„DNG ausblenden“ in der Werkzeugleiste versteckt Rohdaten im DNG-Format in +allen Ansichten. Die Einstellung bleibt gespeichert. + ## Tastatur Im Betrachter: ← / → blättern, Esc schließen, F Vollbild, O Originaldatei @@ -40,4 +56,4 @@ F5 neu laden. npm install npm start # zum Ausprobieren - npm run dist # erzeugt dist/Lichttisch-1.0.0-x86_64.AppImage + npm run dist # erzeugt dist/Lichttisch-1.2.0-x86_64.AppImage diff --git a/main.js b/main.js index 2ec81e1..1df3209 100644 --- a/main.js +++ b/main.js @@ -238,7 +238,10 @@ async function listFolder(relPath) { return items; } -async function searchMedia(offset) { +// order: 'taken' = Aufnahmedatum aus den Metadaten (Nextcloud 28+), +// 'mtime' = Änderungs-/Upload-Datum der Datei +async function searchMedia(offset, order = 'taken') { + const orderProp = order === 'taken' ? '' : ''; const body = ` @@ -250,7 +253,7 @@ async function searchMedia(offset) { video/% - + ${orderProp} ${PAGE_SIZE}${Number(offset) || 0} `; @@ -259,7 +262,11 @@ async function searchMedia(offset) { headers: { 'Content-Type': 'text/xml; charset=utf-8' }, body, }); - if (res.status !== 207) throw new Error(`Die Suche nach Fotos ist fehlgeschlagen (Status ${res.status}).`); + if (res.status !== 207) { + // Ältere Server kennen das Aufnahmedatum nicht und lehnen die Sortierung ab + if (order === 'taken') return { unsupported: true, items: [], done: false }; + throw new Error(`Die Suche nach Fotos ist fehlgeschlagen (Status ${res.status}).`); + } const items = parseMultistatus(await res.text()).map(toItem).filter((i) => !i.isDir); return { items, done: items.length < PAGE_SIZE }; } @@ -396,7 +403,7 @@ handle('logout', () => { }); handle('folder', (p) => listFolder(p)); -handle('timeline', (offset) => searchMedia(offset)); +handle('timeline', (offset, order) => searchMedia(offset, order)); handle('albums', () => listAlbums()); handle('album', (href) => listAlbum(href)); diff --git a/package.json b/package.json index 9a911ce..66ed844 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "lichttisch", "productName": "Lichttisch", - "version": "1.0.0", + "version": "1.2.0", "description": "Fotos aus der Nextcloud per WebDAV ansehen, ohne sie herunterzuladen", "main": "main.js", "author": { diff --git a/preload.js b/preload.js index ca126a4..767a125 100644 --- a/preload.js +++ b/preload.js @@ -14,7 +14,7 @@ contextBridge.exposeInMainWorld('nc', { loginCancel: () => call('login-cancel'), logout: () => call('logout'), folder: (path) => call('folder', path), - timeline: (offset) => call('timeline', offset), + timeline: (offset, order) => call('timeline', offset, order), albums: () => call('albums'), album: (href) => call('album', href), openInNextcloud: (fileId) => call('open-in-nextcloud', fileId), diff --git a/renderer/app.js b/renderer/app.js index e99f44b..f0750b8 100644 --- a/renderer/app.js +++ b/renderer/app.js @@ -47,11 +47,13 @@ function thumbUrl(item) { const state = { source: store.get('source', 'timeline'), path: store.get('path', ''), - sort: store.get('sort', 'name'), + sort: store.get('sort', 'new'), + timelineOrder: store.get('timelineOrder', 'taken'), + hideDng: store.get('hideDng', true), zoom: store.get('zoom', 180), album: null, // { href, name } wenn ein Album offen ist media: [], // aktuell sichtbare Bilder/Videos (für den Betrachter) - timeline: { offset: 0, done: false, loading: false, lastMonth: null, grid: null }, + timeline: { offset: 0, done: false, loading: false }, loadToken: 0, }; @@ -193,6 +195,7 @@ async function load() { state.media = []; renderCrumbs(); $('sort').hidden = !(state.source === 'folder' || (state.source === 'albums' && state.album)); + $('tl-order').hidden = state.source !== 'timeline'; try { if (state.source === 'folder') { @@ -210,7 +213,7 @@ async function load() { renderAlbums(res); } } else { - startTimeline(token); + startTimeline(token, state.timelineOrder); } } catch (e) { if (token !== state.loadToken) return; @@ -220,13 +223,22 @@ async function load() { } } +// DNG-Dateien (Rohdaten) optional ausblenden +function isDng(item) { + return /\.dng$/i.test(item.name) || /dng/i.test(item.mime); +} +const isHidden = (item) => state.hideDng && isDng(item); +const visibleMedia = (items) => items.filter((i) => (i.kind === 'image' || i.kind === 'video') && !isHidden(i)); + function sortItems(items) { const s = state.sort; - const by = (i) => i.taken || i.mtime; + const by = photoDate; const arr = [...items]; if (s === 'name') arr.sort((a, b) => collator.compare(a.name, b.name)); if (s === 'new') arr.sort((a, b) => by(b) - by(a)); if (s === 'old') arr.sort((a, b) => by(a) - by(b)); + if (s === 'mnew') arr.sort((a, b) => b.mtime - a.mtime); + if (s === 'mold') arr.sort((a, b) => a.mtime - b.mtime); return arr; } @@ -269,11 +281,16 @@ function folderTile(item) { return b; } +function hiddenNote(items) { + const n = items.filter(isHidden).length; + return n ? `${n} DNG-${n === 1 ? 'Datei ist' : 'Dateien sind'} ausgeblendet.` : null; +} + function renderMediaList(items, emptyText) { - const media = sortItems(items.filter((i) => i.kind === 'image' || i.kind === 'video')); + const media = sortItems(visibleMedia(items)); state.media = media; const content = $('content'); - if (!media.length) { content.replaceChildren(notice(emptyText)); return; } + if (!media.length) { content.replaceChildren(notice(emptyText, hiddenNote(items))); return; } const grid = el('div', 'grid'); media.forEach((m, i) => grid.append(mediaTile(m, i))); content.replaceChildren(grid); @@ -281,7 +298,7 @@ function renderMediaList(items, emptyText) { function renderFolder(items) { const dirs = items.filter((i) => i.isDir).sort((a, b) => collator.compare(a.name, b.name)); - const media = sortItems(items.filter((i) => i.kind === 'image' || i.kind === 'video')); + const media = sortItems(visibleMedia(items)); state.media = media; const content = $('content'); content.replaceChildren(); @@ -297,7 +314,7 @@ function renderFolder(items) { content.append(g); } if (!dirs.length && !media.length) { - content.append(notice('Hier liegen keine Fotos.', state.path ? 'Über den Pfad oben kommst du zurück.' : null)); + content.append(notice('Hier liegen keine Fotos.', hiddenNote(items) || (state.path ? 'Über den Pfad oben kommst du zurück.' : null))); } } @@ -339,10 +356,16 @@ function renderAlbums({ available, albums }) { let timelineObserver = null; -function startTimeline(token) { - state.timeline = { offset: 0, done: false, loading: false, lastMonth: null, grid: null, token }; +function startTimeline(token, order = 'taken', fellBack = false) { + $('tl-order').value = order; + state.timeline = { offset: 0, done: false, loading: false, token, order, months: new Map(), skipped: 0 }; + state.media = []; const content = $('content'); content.replaceChildren(); + if (fellBack) { + content.append(el('p', 'hint-line', + 'Dein Server liefert keine Aufnahmedaten, darum ist nach dem Änderungsdatum der Dateien sortiert. Aufnahmedaten gibt es ab Nextcloud 28.')); + } const sentinel = el('div', 'sentinel'); const more = el('div', 'loading-more', 'Lade Fotos …'); content.append(sentinel, more); @@ -355,6 +378,27 @@ function startTimeline(token) { loadTimelinePage(); } +// Datum, nach dem ein Foto einsortiert wird: Aufnahmedatum, sonst Dateidatum +const photoDate = (item) => item.taken || item.mtime; + +function monthSection(t, date, sentinel) { + const d = new Date(date); + const key = `${d.getFullYear()}-${d.getMonth()}`; + let m = t.months.get(key); + if (!m) { + const sec = el('section', 'month'); + const h = el('h2', null, monthFmt.format(d)); + const countEl = el('small'); + h.append(countEl); + const grid = el('div', 'grid'); + sec.append(h, grid); + sentinel.before(sec); + m = { grid, countEl, count: 0 }; + t.months.set(key, m); + } + return m; +} + async function loadTimelinePage() { const t = state.timeline; if (t.loading || t.done || t.token !== state.loadToken) return; @@ -363,33 +407,36 @@ async function loadTimelinePage() { const sentinel = content.querySelector('.sentinel'); const more = content.querySelector('.loading-more'); try { - const { items, done } = await window.nc.timeline(t.offset); + const res = await window.nc.timeline(t.offset, t.order); if (t.token !== state.loadToken) return; - t.offset += items.length; - t.done = done; - for (const item of items) { - const d = new Date(item.mtime); - const key = `${d.getFullYear()}-${d.getMonth()}`; - if (key !== t.lastMonth) { - t.lastMonth = key; - const sec = el('section', 'month'); - const h = el('h2', null, monthFmt.format(d)); - t.countEl = el('small'); - t.count = 0; - h.append(t.countEl); - t.grid = el('div', 'grid'); - sec.append(h, t.grid); - content.insertBefore(sec, sentinel); - } + + // Server kann nicht nach Aufnahmedatum sortieren, oder kein einziges Foto + // hat eines: auf Änderungsdatum umschalten + const noMetadata = t.order === 'taken' && t.offset === 0 && res.items.length && !res.items.some((i) => i.taken); + if (res.unsupported || noMetadata) { + t.loading = false; + startTimeline(t.token, 'mtime', true); + return; + } + + t.offset += res.items.length; + t.done = res.done; + for (const item of res.items) { + if (isHidden(item)) { t.skipped++; continue; } + const m = monthSection(t, t.order === 'taken' ? photoDate(item) : item.mtime, sentinel); const index = state.media.length; state.media.push(item); - t.grid.append(mediaTile(item, index)); - t.count++; - t.countEl.textContent = `${t.count} ${t.count === 1 ? 'Foto' : 'Fotos'}`; + m.grid.append(mediaTile(item, index)); + m.count++; + m.countEl.textContent = `${m.count} ${m.count === 1 ? 'Foto' : 'Fotos'}`; } - if (done) { + if (t.done) { more.remove(); - if (!state.media.length) content.replaceChildren(notice('Keine Fotos gefunden.', 'In deiner Nextcloud liegen noch keine Bilder oder Videos.')); + if (!state.media.length) { + content.replaceChildren(t.skipped + ? notice('Keine Fotos sichtbar.', 'Alle gefundenen Dateien sind DNG-Dateien. Schalte „DNG ausblenden“ oben aus, um sie zu sehen.') + : notice('Keine Fotos gefunden.', 'In deiner Nextcloud liegen noch keine Bilder oder Videos.')); + } } } catch (e) { if (t.token !== state.loadToken) return; @@ -406,6 +453,13 @@ async function loadTimelinePage() { // ------------------------------------------------------------------ Werkzeugleiste +$('tl-order').value = state.timelineOrder; +$('tl-order').addEventListener('change', () => { + state.timelineOrder = $('tl-order').value; + store.set('timelineOrder', state.timelineOrder); + load(); +}); + $('sort').value = state.sort; $('sort').addEventListener('change', () => { state.sort = $('sort').value; @@ -413,6 +467,13 @@ $('sort').addEventListener('change', () => { load(); }); +$('hide-dng').checked = state.hideDng; +$('hide-dng').addEventListener('change', () => { + state.hideDng = $('hide-dng').checked; + store.set('hideDng', state.hideDng); + load(); +}); + function applyZoom() { document.documentElement.style.setProperty('--thumb', `${state.zoom}px`); } @@ -474,9 +535,9 @@ function showCurrent() { msg.hidden = true; $('v-name').textContent = item.name; - const when = item.taken || item.mtime; const meta = [`${viewer.index + 1} von ${state.media.length}`]; - if (when) meta.push(dateFmt.format(when)); + if (item.taken) meta.push(`Aufgenommen ${dateFmt.format(item.taken)}`); + if (item.mtime) meta.push(`Geändert ${dateFmt.format(item.mtime)}`); if (item.size) meta.push(fmtSize(item.size)); $('v-meta').replaceChildren(...meta.map((m) => el('span', null, m))); $('v-prev').disabled = viewer.index === 0; diff --git a/renderer/index.html b/renderer/index.html index 33a5681..e90babc 100644 --- a/renderer/index.html +++ b/renderer/index.html @@ -79,11 +79,21 @@
- + + + +