diff --git a/static/js/components/modal.js b/static/js/components/modal.js index 6293d47a..63e0e487 100644 --- a/static/js/components/modal.js +++ b/static/js/components/modal.js @@ -436,6 +436,47 @@ const Modal = { requestAnimationFrame(() => { this.overlay.classList.add('active'); }); + }, + + /** + * Confirmation dialog (replacement for window.confirm()). + * Built on openPanel, so it inherits the overlay, animation, focus-trap, + * Escape and click-outside handling. + * @param {Object} options + * @param {string} options.title + * @param {string} options.message + * @param {string} [options.confirmText] + * @param {string} [options.cancelText] + * @param {string} [options.icon] - Font Awesome class, default 'fa-circle-question' + * @returns {Promise} true if confirmed, false otherwise + */ + confirmDialog({ title, message, confirmText = null, cancelText = null, icon = 'fa-circle-question' }) { + return new Promise((resolve) => { + if (!this.overlay) { + resolve(false); + return; + } + const content = document.createElement('p'); + content.className = 'modal-confirm-message'; + content.textContent = message; + + let settled = false; + const done = (/** @type {boolean} */ value) => { + if (settled) return; + settled = true; + resolve(value); + }; + + this.openPanel({ + title, + icon, + content, + confirmText: confirmText ?? i18n.t('actions.confirm'), + cancelText: cancelText ?? i18n.t('actions.cancel'), + onConfirm: () => done(true), + onCancel: () => done(false) + }); + }); } }; diff --git a/static/js/features/library/photos.js b/static/js/features/library/photos.js index 9c7b2c7f..437de6ff 100644 --- a/static/js/features/library/photos.js +++ b/static/js/features/library/photos.js @@ -3,6 +3,7 @@ * Photo grid grouped by day/month/year, with infinite scroll and multi-select. */ +import { Modal } from '../../components/modal.js'; import { getCsrfHeaders } from '../../core/csrf.js'; import { i18n } from '../../core/i18n.js'; import { thumbnail } from '../thumbnail.js'; @@ -665,7 +666,13 @@ const photosView = { const bar_delete = /** @type {HTMLButtonElement} */ (bar.querySelector('#photos-sel-delete')); if (bar_delete) { bar_delete.onclick = async () => { - if (!confirm('Delete selected items?')) return; + const ok = await Modal.confirmDialog({ + title: i18n.t('photos.delete_title'), + message: i18n.t('photos.delete_selected_confirm'), + confirmText: i18n.t('actions.delete'), + icon: 'fa-trash' + }); + if (!ok) return; // One batch request per chunk instead of one DELETE per photo. // The photos view is files-only, so every id is a file id. diff --git a/static/js/features/library/photosLightbox.js b/static/js/features/library/photosLightbox.js index 705bace5..8875f6f7 100644 --- a/static/js/features/library/photosLightbox.js +++ b/static/js/features/library/photosLightbox.js @@ -10,7 +10,9 @@ * original streams in only on demand via the toolbar expand button. */ +import { Modal } from '../../components/modal.js'; import { getCsrfHeaders } from '../../core/csrf.js'; +import { i18n } from '../../core/i18n.js'; import { favorites } from '../library/favorites.js'; /** @import {FileItem, FileMetadata} from '../../core/types.js' */ @@ -182,6 +184,15 @@ export const photosLightbox = { filename.textContent = item.name; counter.textContent = `${this.index + 1} / ${this.items.length}`; + // Reflect the current favorite state on the toolbar star. + const favBtn = this._overlay.querySelector('.lb-favorite'); + if (favBtn) { + const isFav = favorites.isFavorite(item.id, 'file'); + favBtn.classList.toggle('active', isFav); + const favIcon = favBtn.querySelector('i'); + if (favIcon) favIcon.className = isFav ? 'fas fa-star' : 'far fa-star'; + } + // Format date const ts = (item.sort_date || item.created_at) * 1000; const dateStr = new Date(ts).toLocaleDateString(undefined, { @@ -317,23 +328,25 @@ export const photosLightbox = { a.remove(); }, - /** Toggle favorite on current item */ + /** Toggle favorite on current item (via the favorites module so its + * cache stays in sync — the lightbox can then show the right initial + * star next time the item is opened). */ async _toggleFavorite() { const item = this.items[this.index]; - if (!item || !favorites) return; + if (!item) return; + const isFav = favorites.isFavorite(item.id, 'file'); try { - await fetch(`/api/favorites/file/${item.id}`, { - method: 'POST', - credentials: 'include', - headers: this._headers() - }); - const btn = this._overlay.querySelector('.lb-favorite'); + if (isFav) { + await favorites.removeFromFavorites(item.id, 'file', item.name); + } else { + await favorites.addToFavorites(item.id, item.name, 'file', null); + } + const btn = this._overlay?.querySelector('.lb-favorite'); if (btn) { - btn.classList.toggle('active'); + const nowFav = !isFav; + btn.classList.toggle('active', nowFav); const icon = btn.querySelector('i'); - if (icon) { - icon.className = btn.classList.contains('active') ? 'fas fa-star' : 'far fa-star'; - } + if (icon) icon.className = nowFav ? 'fas fa-star' : 'far fa-star'; } } catch (err) { console.error('Favorite toggle failed:', err); @@ -344,7 +357,13 @@ export const photosLightbox = { async _delete() { const item = this.items[this.index]; if (!item) return; - if (!confirm(`Delete ${item.name}?`)) return; + const ok = await Modal.confirmDialog({ + title: i18n.t('photos.delete_title'), + message: i18n.t('photos.delete_one_confirm', { name: item.name }), + confirmText: i18n.t('actions.delete'), + icon: 'fa-trash' + }); + if (!ok) return; try { await fetch(`/api/files/${item.id}`, { diff --git a/static/locales/en.json b/static/locales/en.json index c829bf57..13b40de5 100644 --- a/static/locales/en.json +++ b/static/locales/en.json @@ -68,7 +68,10 @@ "items_selected": "selected", "view_daily": "Day", "view_monthly": "Month", - "view_yearly": "Year" + "view_yearly": "Year", + "delete_title": "Move to Trash", + "delete_selected_confirm": "Move the selected items to Trash?", + "delete_one_confirm": "Move \"{{name}}\" to Trash?" }, "music": { "create_playlist": "Create Playlist",