Add status filtering and segmented progress v0.3
This commit is contained in:
1 parent
e2f5c91138
commit
cbf0daf062
7 files changed
+123
-28
No files matched your search
+61
-15
@@ -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));
|
||||
|
||||
Reference in new issue
Block a user