Add status filtering and segmented progress v0.3

This commit is contained in:
dmo committed 2026-07-23 01:13:15 +02:00
1 parent e2f5c91138
commit cbf0daf062
7 files changed
+123 -28

No files matched your search

+61 -15
View File
@@ -20,9 +20,13 @@ const elements = {
folderName: $('#folder-name'),
folderAccess: $('#folder-access'),
progressCount: $('#progress-count'),
progressBar: $('#progress-bar'),
progressSaved: $('#progress-saved'),
progressSkipped: $('#progress-skipped'),
progressFailed: $('#progress-failed'),
photoCount: $('#photo-count'),
filter: $('#filter-input'),
statusFilter: $('#status-filter'),
sortSelect: $('#sort-select'),
fileList: $('#file-list'),
viewerEmpty: $('#viewer-empty'),
viewerContent: $('#viewer-content'),
@@ -163,6 +167,8 @@ async function scanFolder() {
elements.folderName.textContent = state.directoryHandle.name;
elements.folderAccess.textContent = 'Read/write access granted';
elements.filter.disabled = !photos.length;
elements.statusFilter.disabled = !photos.length;
elements.sortSelect.disabled = !photos.length;
elements.rescan.disabled = false;
elements.photoCount.textContent = String(photos.length);
@@ -202,15 +208,33 @@ function statusSymbol(status) {
return '';
}
function renderFileList() {
const filter = elements.filter.value.trim().toLowerCase();
const matching = state.photos
const statusSortOrder = { pending: 0, failed: 1, skipped: 2, saved: 3 };
function visiblePhotoEntries() {
const filenameFilter = elements.filter.value.trim().toLowerCase();
const statusFilter = elements.statusFilter.value;
const entries = state.photos
.map((photo, index) => ({ photo, index }))
.filter(({ photo }) => !filter || photo.name.toLowerCase().includes(filter));
.filter(({ photo }) => !filenameFilter || photo.name.toLowerCase().includes(filenameFilter))
.filter(({ photo }) => statusFilter === 'all' || photo.status === statusFilter);
if (elements.sortSelect.value === 'status') {
entries.sort((a, b) => {
const statusDifference = statusSortOrder[a.photo.status] - statusSortOrder[b.photo.status];
return statusDifference || naturalCompare(a.photo.name, b.photo.name);
});
} else {
entries.sort((a, b) => naturalCompare(a.photo.name, b.photo.name));
}
return entries;
}
function renderFileList() {
const matching = visiblePhotoEntries();
elements.fileList.innerHTML = '';
if (!matching.length) {
elements.fileList.innerHTML = '<div class="empty-list">No matching photos.</div>';
elements.fileList.innerHTML = '<div class="empty-list">No photos match the current filters.</div>';
return;
}
@@ -330,9 +354,18 @@ async function selectPhoto(index, force = false) {
}
}
function adjacentVisibleIndex(delta) {
const visible = visiblePhotoEntries();
if (!visible.length) return null;
const position = visible.findIndex(({ index }) => index === state.currentIndex);
if (position === -1) return delta > 0 ? visible[0].index : visible[visible.length - 1].index;
const target = position + delta;
return target >= 0 && target < visible.length ? visible[target].index : null;
}
function updateNavigation() {
const hasPhoto = state.currentIndex >= 0;
elements.previous.disabled = !hasPhoto || state.currentIndex === 0 || state.processing;
elements.previous.disabled = !hasPhoto || adjacentVisibleIndex(-1) === null || state.processing;
elements.skip.disabled = !hasPhoto || state.processing;
elements.save.disabled = !hasPhoto || state.processing;
elements.saveNext.disabled = !hasPhoto || state.processing;
@@ -409,6 +442,7 @@ async function savePhoto(moveNext) {
catch (error) { showToast(error.message, true); return; }
const photo = state.photos[state.currentIndex];
const nextVisibleIndex = moveNext ? adjacentVisibleIndex(1) : null;
state.processing = true;
updateNavigation();
setSaveStatus('saving', 'Saving metadata…', photo.name);
@@ -464,8 +498,8 @@ async function savePhoto(moveNext) {
if (oldThumb) URL.revokeObjectURL(oldThumb);
state.thumbUrls.delete(photo.name);
if (moveNext && state.currentIndex < state.photos.length - 1) {
await selectPhoto(state.currentIndex + 1, true);
if (moveNext && nextVisibleIndex !== null) {
await selectPhoto(nextVisibleIndex, true);
} else {
await selectPhoto(state.currentIndex, true);
}
@@ -485,29 +519,39 @@ async function savePhoto(moveNext) {
function skipCurrentPhoto() {
if (state.processing || state.currentIndex < 0) return;
const photo = state.photos[state.currentIndex];
const nextVisibleIndex = adjacentVisibleIndex(1);
captureCurrentValues();
photo.status = 'skipped';
saveFolderState();
updateProgress();
renderFileList();
showToast(`Skipped ${photo.name}`);
if (state.currentIndex < state.photos.length - 1) goRelative(1);
if (nextVisibleIndex !== null) selectPhoto(nextVisibleIndex);
else updatePhotoStatus(photo);
}
function updateProgress() {
const saved = state.photos.filter((photo) => photo.status === 'saved').length;
const skipped = state.photos.filter((photo) => photo.status === 'skipped').length;
const failed = state.photos.filter((photo) => photo.status === 'failed').length;
const pending = state.photos.filter((photo) => photo.status === 'pending').length;
const processed = saved + skipped;
const total = state.photos.length;
const percentage = (count) => total ? `${(count / total) * 100}%` : '0%';
elements.progressCount.textContent = `${processed} / ${total}`;
elements.progressBar.style.width = total ? `${(processed / total) * 100}%` : '0%';
elements.progressCount.title = `${saved} saved, ${skipped} skipped`;
elements.progressSaved.style.width = percentage(saved);
elements.progressSkipped.style.width = percentage(skipped);
elements.progressFailed.style.width = percentage(failed);
elements.progressCount.title = `${saved} saved, ${skipped} skipped, ${failed} failed, ${pending} pending`;
const track = elements.progressSaved.parentElement;
track.setAttribute('aria-valuenow', total ? String(Math.round((processed / total) * 100)) : '0');
track.setAttribute('aria-valuetext', `${saved} saved, ${skipped} skipped, ${failed} failed, ${pending} pending`);
}
function goRelative(delta) {
const target = state.currentIndex + delta;
if (target >= 0 && target < state.photos.length) selectPhoto(target);
const target = adjacentVisibleIndex(delta);
if (target !== null) selectPhoto(target);
}
function copyPreviousValues() {
@@ -527,7 +571,9 @@ function resetView() { state.zoom = 1; state.rotation = 0; applyView(); }
for (const button of elements.openButtons) button.addEventListener('click', openFolder);
elements.rescan.addEventListener('click', scanFolder);
elements.filter.addEventListener('input', renderFileList);
elements.filter.addEventListener('input', () => { renderFileList(); updateNavigation(); });
elements.statusFilter.addEventListener('change', () => { renderFileList(); updateNavigation(); });
elements.sortSelect.addEventListener('change', () => { renderFileList(); updateNavigation(); });
elements.previous.addEventListener('click', () => goRelative(-1));
elements.skip.addEventListener('click', skipCurrentPhoto);
elements.save.addEventListener('click', () => savePhoto(false));