1.2
This commit is contained in:
+95
-34
@@ -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;
|
||||
|
||||
+14
-4
@@ -79,11 +79,21 @@
|
||||
<header class="toolbar">
|
||||
<nav id="crumbs" class="crumbs" aria-label="Pfad"></nav>
|
||||
<div class="tools">
|
||||
<select id="sort" title="Sortierung">
|
||||
<option value="name">Nach Name</option>
|
||||
<option value="new">Neueste zuerst</option>
|
||||
<option value="old">Älteste zuerst</option>
|
||||
<select id="tl-order" title="Wonach die Zeitleiste sortiert wird">
|
||||
<option value="taken">Nach Aufnahmedatum</option>
|
||||
<option value="mtime">Nach Änderungsdatum</option>
|
||||
</select>
|
||||
<select id="sort" title="Sortierung">
|
||||
<option value="new">Aufnahmedatum, neueste zuerst</option>
|
||||
<option value="old">Aufnahmedatum, älteste zuerst</option>
|
||||
<option value="mnew">Änderungsdatum, neueste zuerst</option>
|
||||
<option value="mold">Änderungsdatum, älteste zuerst</option>
|
||||
<option value="name">Nach Name</option>
|
||||
</select>
|
||||
<label class="check tool-check" title="Rohdaten im DNG-Format nicht anzeigen">
|
||||
<input id="hide-dng" type="checkbox">
|
||||
<span>DNG ausblenden</span>
|
||||
</label>
|
||||
<label class="zoom" title="Vorschaugröße">
|
||||
<span class="zoom-dot small" aria-hidden="true"></span>
|
||||
<input id="zoom" type="range" min="110" max="360" step="10" value="180" aria-label="Vorschaugröße">
|
||||
|
||||
+4
-1
@@ -159,7 +159,10 @@ details button { justify-self: start; }
|
||||
.crumbs button:last-child:hover { background: none; }
|
||||
.crumbs .sep { color: var(--line); }
|
||||
|
||||
.tools { display: flex; align-items: center; gap: 1rem; }
|
||||
.tools { display: flex; align-items: center; gap: 1.2rem; }
|
||||
.tool-check { font-size: 0.9rem; white-space: nowrap; cursor: pointer; }
|
||||
.tool-check:hover { color: var(--text); }
|
||||
.hint-line { margin: 0 0 1.4rem; color: var(--muted); font-size: 0.9rem; max-width: 60ch; }
|
||||
.zoom { display: flex; align-items: center; gap: 0.5rem; }
|
||||
.zoom input { width: 7rem; accent-color: var(--safelight); }
|
||||
.zoom-dot { display: block; border: 1.5px solid var(--muted); border-radius: 2px; }
|
||||
|
||||
Reference in New Issue
Block a user