feat(photos): modern confirm dialog + correct lightbox favorite state
- Add Modal.confirmDialog() (Promise<boolean>, built on openPanel so it inherits the overlay, animation, focus-trap and Escape handling) and use it to replace native confirm() in the photos batch-delete and lightbox single-delete flows. - The lightbox now reflects the real favorite state when an item opens (previously the star always started empty) and toggles favorites through the favorites module so its cache stays in sync. - Add photos.delete_* i18n keys (English; other locales fall back to en). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JW6ghFMDtnRYuYNzZhb47M
This commit is contained in:
@@ -436,6 +436,47 @@ const Modal = {
|
|||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
this.overlay.classList.add('active');
|
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<boolean>} 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)
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
* Photo grid grouped by day/month/year, with infinite scroll and multi-select.
|
* 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 { getCsrfHeaders } from '../../core/csrf.js';
|
||||||
import { i18n } from '../../core/i18n.js';
|
import { i18n } from '../../core/i18n.js';
|
||||||
import { thumbnail } from '../thumbnail.js';
|
import { thumbnail } from '../thumbnail.js';
|
||||||
@@ -665,7 +666,13 @@ const photosView = {
|
|||||||
const bar_delete = /** @type {HTMLButtonElement} */ (bar.querySelector('#photos-sel-delete'));
|
const bar_delete = /** @type {HTMLButtonElement} */ (bar.querySelector('#photos-sel-delete'));
|
||||||
if (bar_delete) {
|
if (bar_delete) {
|
||||||
bar_delete.onclick = async () => {
|
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.
|
// One batch request per chunk instead of one DELETE per photo.
|
||||||
// The photos view is files-only, so every id is a file id.
|
// The photos view is files-only, so every id is a file id.
|
||||||
|
|||||||
@@ -10,7 +10,9 @@
|
|||||||
* original streams in only on demand via the toolbar expand button.
|
* original streams in only on demand via the toolbar expand button.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { Modal } from '../../components/modal.js';
|
||||||
import { getCsrfHeaders } from '../../core/csrf.js';
|
import { getCsrfHeaders } from '../../core/csrf.js';
|
||||||
|
import { i18n } from '../../core/i18n.js';
|
||||||
import { favorites } from '../library/favorites.js';
|
import { favorites } from '../library/favorites.js';
|
||||||
|
|
||||||
/** @import {FileItem, FileMetadata} from '../../core/types.js' */
|
/** @import {FileItem, FileMetadata} from '../../core/types.js' */
|
||||||
@@ -182,6 +184,15 @@ export const photosLightbox = {
|
|||||||
filename.textContent = item.name;
|
filename.textContent = item.name;
|
||||||
counter.textContent = `${this.index + 1} / ${this.items.length}`;
|
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
|
// Format date
|
||||||
const ts = (item.sort_date || item.created_at) * 1000;
|
const ts = (item.sort_date || item.created_at) * 1000;
|
||||||
const dateStr = new Date(ts).toLocaleDateString(undefined, {
|
const dateStr = new Date(ts).toLocaleDateString(undefined, {
|
||||||
@@ -317,23 +328,25 @@ export const photosLightbox = {
|
|||||||
a.remove();
|
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() {
|
async _toggleFavorite() {
|
||||||
const item = this.items[this.index];
|
const item = this.items[this.index];
|
||||||
if (!item || !favorites) return;
|
if (!item) return;
|
||||||
|
const isFav = favorites.isFavorite(item.id, 'file');
|
||||||
try {
|
try {
|
||||||
await fetch(`/api/favorites/file/${item.id}`, {
|
if (isFav) {
|
||||||
method: 'POST',
|
await favorites.removeFromFavorites(item.id, 'file', item.name);
|
||||||
credentials: 'include',
|
} else {
|
||||||
headers: this._headers()
|
await favorites.addToFavorites(item.id, item.name, 'file', null);
|
||||||
});
|
}
|
||||||
const btn = this._overlay.querySelector('.lb-favorite');
|
const btn = this._overlay?.querySelector('.lb-favorite');
|
||||||
if (btn) {
|
if (btn) {
|
||||||
btn.classList.toggle('active');
|
const nowFav = !isFav;
|
||||||
|
btn.classList.toggle('active', nowFav);
|
||||||
const icon = btn.querySelector('i');
|
const icon = btn.querySelector('i');
|
||||||
if (icon) {
|
if (icon) icon.className = nowFav ? 'fas fa-star' : 'far fa-star';
|
||||||
icon.className = btn.classList.contains('active') ? 'fas fa-star' : 'far fa-star';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Favorite toggle failed:', err);
|
console.error('Favorite toggle failed:', err);
|
||||||
@@ -344,7 +357,13 @@ export const photosLightbox = {
|
|||||||
async _delete() {
|
async _delete() {
|
||||||
const item = this.items[this.index];
|
const item = this.items[this.index];
|
||||||
if (!item) return;
|
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 {
|
try {
|
||||||
await fetch(`/api/files/${item.id}`, {
|
await fetch(`/api/files/${item.id}`, {
|
||||||
|
|||||||
@@ -68,7 +68,10 @@
|
|||||||
"items_selected": "selected",
|
"items_selected": "selected",
|
||||||
"view_daily": "Day",
|
"view_daily": "Day",
|
||||||
"view_monthly": "Month",
|
"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": {
|
"music": {
|
||||||
"create_playlist": "Create Playlist",
|
"create_playlist": "Create Playlist",
|
||||||
|
|||||||
Reference in New Issue
Block a user