2405 lines
120 KiB
JavaScript
2405 lines
120 KiB
JavaScript
/*
|
|
* Raiju Image Manager — Unified Component
|
|
*
|
|
* Shared between the frontend inline editor and the backend dashboard.
|
|
* Consumers initialize it once via NbImageManager.init({...}) and then
|
|
* call NbImageManager.open(callback, currentPath, { types: ['image'] }) to pick media.
|
|
*
|
|
* Depends on css/image-manager.css and css/raiju-admin-tokens.css.
|
|
*/
|
|
|
|
(function () {
|
|
'use strict';
|
|
|
|
// ============================================================
|
|
// CONFIGURATION (set via init())
|
|
// ============================================================
|
|
var config = {
|
|
apiUrl: 'api.php',
|
|
csrfToken: '',
|
|
t: function (key) { return key; },
|
|
showToast: function (msg) { console.log(msg); },
|
|
showConfirm: null, // optional; falls back to window.confirm
|
|
canGenerateImages: function () { return false; },
|
|
openImageGenerator: null,
|
|
itemsPerPage: 25,
|
|
};
|
|
|
|
// ============================================================
|
|
// STATE
|
|
// ============================================================
|
|
var state = {
|
|
data: [],
|
|
filtered: [],
|
|
selectedPath: null,
|
|
selectedPaths: [],
|
|
revealSelection: false,
|
|
multiplePicker: false,
|
|
callback: null,
|
|
isPicker: false,
|
|
allowedTypes: ['image'],
|
|
activeType: 'image',
|
|
view: 'grid',
|
|
mode: 'library',
|
|
sort: { field: 'date', dir: 'desc' },
|
|
search: '',
|
|
folders: [],
|
|
folderFilter: 'all',
|
|
expandedFolders: {},
|
|
lightboxItem: null,
|
|
page: 1,
|
|
};
|
|
|
|
var replaceTarget = null;
|
|
var moveTarget = null;
|
|
var renameTarget = null;
|
|
var pendingRename = null;
|
|
var aiLabelTarget = null;
|
|
var previousFocus = null;
|
|
// Upload limits from the server (list-media): what PHP lets through and what Raiju allows
|
|
var uploadLimits = null;
|
|
var mediaTypes = {
|
|
image: {
|
|
labelKey: 'media.type_images',
|
|
uploadLabelKey: 'image.upload',
|
|
formatsKey: 'image.formats_hint',
|
|
accept: '.jpg,.jpeg,.png,.webp,.gif,.svg,image/*',
|
|
extensions: ['jpg', 'jpeg', 'png', 'webp', 'gif', 'svg'],
|
|
maxSize: 5 * 1024 * 1024,
|
|
},
|
|
audio: {
|
|
labelKey: 'media.type_audio',
|
|
uploadLabelKey: 'audio.upload',
|
|
formatsKey: 'audio.formats_hint',
|
|
accept: '.mp3,.wav,.ogg,.m4a,.aac,.flac,audio/*',
|
|
extensions: ['mp3', 'wav', 'ogg', 'm4a', 'aac', 'flac'],
|
|
maxSize: 50 * 1024 * 1024,
|
|
},
|
|
video: {
|
|
labelKey: 'media.type_video',
|
|
uploadLabelKey: 'media.upload',
|
|
formatsKey: 'media.video_formats_hint',
|
|
accept: '.mp4,.webm,.mov,.m4v,video/*',
|
|
extensions: ['mp4', 'webm', 'mov', 'm4v'],
|
|
maxSize: 250 * 1024 * 1024,
|
|
},
|
|
document: {
|
|
labelKey: 'media.type_documents',
|
|
uploadLabelKey: 'media.upload',
|
|
formatsKey: 'media.document_formats_hint',
|
|
accept: '.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt,.rtf,application/pdf,text/plain',
|
|
extensions: ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'rtf'],
|
|
maxSize: 50 * 1024 * 1024,
|
|
},
|
|
};
|
|
|
|
// ============================================================
|
|
// SVG ICONS
|
|
// ============================================================
|
|
var Icons = {
|
|
upload: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>',
|
|
grid: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M3 3h8v8H3zM13 3h8v8h-8zM3 13h8v8H3zM13 13h8v8h-8z"/></svg>',
|
|
list: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M3 5h18v2H3zM3 11h18v2H3zM3 17h18v2H3z"/></svg>',
|
|
eye: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>',
|
|
download: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>',
|
|
ai: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2l1.6 5.2L19 9l-5.4 1.8L12 16l-1.6-5.2L5 9l5.4-1.8L12 2z"/><path d="M19 14l.8 2.7L22 17.5l-2.2.8L19 21l-.8-2.7-2.2-.8 2.2-.8L19 14z"/><path d="M5 13l.7 2.1L8 16l-2.3.9L5 19l-.7-2.1L2 16l2.3-.9L5 13z"/></svg>',
|
|
replace: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></svg>',
|
|
delete: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>',
|
|
copy: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>',
|
|
restore: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/></svg>',
|
|
folder: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6.5A2.5 2.5 0 0 1 5.5 4H10l2 2.5h6.5A2.5 2.5 0 0 1 21 9v8.5a2.5 2.5 0 0 1-2.5 2.5h-13A2.5 2.5 0 0 1 3 17.5z"/></svg>',
|
|
folderPlus: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6.5A2.5 2.5 0 0 1 5.5 4H10l2 2.5h6.5A2.5 2.5 0 0 1 21 9v8.5a2.5 2.5 0 0 1-2.5 2.5h-13A2.5 2.5 0 0 1 3 17.5z"/><path d="M12 11v6M9 14h6"/></svg>',
|
|
layers: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round"><path d="M12 2 2 7l10 5 10-5z"/><path d="m2 12 10 5 10-5M2 17l10 5 10-5"/></svg>',
|
|
move: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 7h5l2 2h11v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><path d="M13 12h5M16 9l3 3-3 3"/></svg>',
|
|
rename: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 20h9"/><path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4z"/></svg>',
|
|
chevronRight: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m9 18 6-6-6-6"/></svg>',
|
|
chevronLeft: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m15 18-6-6 6-6"/></svg>',
|
|
};
|
|
|
|
// ============================================================
|
|
// HELPERS
|
|
// ============================================================
|
|
function escapeHtml(str) {
|
|
var div = document.createElement('div');
|
|
div.textContent = str == null ? '' : String(str);
|
|
return div.innerHTML;
|
|
}
|
|
|
|
function t(key, params) {
|
|
var s = config.t(key, params);
|
|
if (params && typeof s === 'string') {
|
|
for (var k in params) {
|
|
s = s.replace('{' + k + '}', params[k]);
|
|
}
|
|
}
|
|
return s;
|
|
}
|
|
|
|
function clampItemsPerPage(value) {
|
|
var parsed = parseInt(value, 10);
|
|
if (!Number.isFinite(parsed)) return 25;
|
|
return Math.max(10, Math.min(500, parsed));
|
|
}
|
|
|
|
function getItemsPerPage() {
|
|
return clampItemsPerPage(config.itemsPerPage);
|
|
}
|
|
|
|
function paginationIcon(direction) {
|
|
return direction === 'prev' ? Icons.chevronLeft : Icons.chevronRight;
|
|
}
|
|
|
|
function mediaSummaryText(total, from, to) {
|
|
if (total <= 0) return t('lists.summary_empty');
|
|
return t('lists.summary_range', { from: from, to: to, total: total });
|
|
}
|
|
|
|
function confirmAction(title, message, onYes) {
|
|
if (typeof config.showConfirm === 'function') {
|
|
config.showConfirm(title, message, onYes);
|
|
} else {
|
|
showFallbackConfirm(title, message, onYes);
|
|
}
|
|
}
|
|
|
|
function showFallbackConfirm(title, message, onYes) {
|
|
var overlay = document.createElement('div');
|
|
overlay.className = 'raiju-dialog-overlay';
|
|
overlay.innerHTML =
|
|
'<div class="raiju-dialog" role="dialog" aria-modal="true">' +
|
|
'<h3></h3>' +
|
|
'<p></p>' +
|
|
'<div class="raiju-dialog__actions">' +
|
|
'<button type="button" class="raiju-dialog__btn raiju-dialog__btn--secondary" data-action="cancel">Abbrechen</button>' +
|
|
'<button type="button" class="raiju-dialog__btn raiju-dialog__btn--primary" data-action="confirm">Bestätigen</button>' +
|
|
'</div>' +
|
|
'</div>';
|
|
overlay.querySelector('h3').textContent = title || 'Bestätigen';
|
|
overlay.querySelector('p').textContent = message || '';
|
|
var style = document.createElement('style');
|
|
style.textContent = '.raiju-dialog-overlay{position:fixed;inset:0;z-index:100000;display:flex;align-items:center;justify-content:center;background:rgba(17,24,39,.55);padding:20px;font-family:var(--rj-font-sans,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif)}.raiju-dialog{width:min(420px,100%);background:#fff;border-radius:10px;box-shadow:0 18px 60px rgba(15,23,42,.25);padding:22px}.raiju-dialog h3{margin:0 0 10px;font-family:inherit;font-size:20px;letter-spacing:normal}.raiju-dialog p{margin:0 0 18px;color:#374151;line-height:1.45}.raiju-dialog__actions{display:flex;gap:10px;justify-content:flex-end}.raiju-dialog__btn{border:1px solid #d1d5db;border-radius:6px;cursor:pointer;font:inherit;padding:9px 14px}.raiju-dialog__btn--primary{background:#5aa6a6;border-color:#5aa6a6;color:#fff}.raiju-dialog__btn--secondary{background:#fff;color:#111827}';
|
|
overlay.appendChild(style);
|
|
function close() {
|
|
overlay.remove();
|
|
}
|
|
overlay.addEventListener('click', function(event) {
|
|
var action = event.target && event.target.getAttribute('data-action');
|
|
if (action === 'cancel' || event.target === overlay) close();
|
|
if (action === 'confirm') {
|
|
close();
|
|
onYes();
|
|
}
|
|
});
|
|
document.body.appendChild(overlay);
|
|
overlay.querySelector('[data-action="confirm"]').focus();
|
|
}
|
|
|
|
function getActiveUploadType() {
|
|
if (state.activeType && state.activeType !== 'all') return state.activeType;
|
|
return state.allowedTypes[0] || 'image';
|
|
}
|
|
|
|
function mediaTypeLabel(type) {
|
|
return t((mediaTypes[type] || mediaTypes.image).labelKey);
|
|
}
|
|
|
|
function isImageOnlyPicker() {
|
|
return state.allowedTypes.length === 1 && state.allowedTypes[0] === 'image';
|
|
}
|
|
|
|
function normalizeTypes(types) {
|
|
var input = Array.isArray(types) ? types : [types || 'image'];
|
|
var normalized = input.filter(function (type) { return !!mediaTypes[type]; });
|
|
return normalized.length ? normalized : ['image'];
|
|
}
|
|
|
|
function isImage(item) { return (item.type || 'image') === 'image'; }
|
|
function isAudio(item) { return item.type === 'audio'; }
|
|
function isVideo(item) { return item.type === 'video'; }
|
|
function isDocument(item) { return item.type === 'document'; }
|
|
|
|
function mediaDisplayName(item) {
|
|
return item.basename || item.name || item.path || t('media.file');
|
|
}
|
|
|
|
function mediaDownloadUrl(item) {
|
|
var path = item && item.path ? String(item.path) : '';
|
|
if (!path) return '#';
|
|
return path.charAt(0) === '/' ? '..' + path : path;
|
|
}
|
|
|
|
function mediaDownloadActionHtml(item) {
|
|
if (!isImage(item)) return '';
|
|
// With a stored original the download hands out the original, not the web version
|
|
if (item.original && item.original.path) {
|
|
var label = t('media.download_original', { size: item.original.size || '' });
|
|
return '<a class="rj-imgmgr-action-btn rj-imgmgr-action-btn--on" data-action="download" href="' + escapeHtml(mediaDownloadUrl(item.original)) + '" download="' + escapeHtml(item.original.name ? item.original.name.split('/').pop() : mediaDisplayName(item)) + '" title="' + escapeHtml(label) + '" aria-label="' + escapeHtml(label) + '">' + Icons.download + '</a>';
|
|
}
|
|
return '<a class="rj-imgmgr-action-btn" data-action="download" href="' + escapeHtml(mediaDownloadUrl(item)) + '" download="' + escapeHtml(mediaDisplayName(item)) + '" title="' + escapeHtml(t('media.download_file')) + '" aria-label="' + escapeHtml(t('media.download_file')) + '">' + Icons.download + '</a>';
|
|
}
|
|
|
|
// "Original" when a web version replaced the upload, "large" for a file that is big for a page
|
|
function sizeBadgeHtml(item) {
|
|
if (!item || !isImage(item)) return '';
|
|
if (item.original) {
|
|
// A button: it opens the original's details (view, download, copy path)
|
|
var title = t('media.original_title', { size: item.original.size || '', width: item.original.width || '?', height: item.original.height || '?' });
|
|
return '<button type="button" class="rj-imgmgr-size-badge rj-imgmgr-size-badge--original" data-action="original" title="' + escapeHtml(title) + '" aria-label="' + escapeHtml(title) + '">' + escapeHtml(t('media.original_badge')) + '</button>';
|
|
}
|
|
if (item.large) {
|
|
return '<span class="rj-imgmgr-size-badge rj-imgmgr-size-badge--large" title="' + escapeHtml(t('media.large_title', { size: item.size || '' })) + '">' + escapeHtml(t('media.large_badge')) + '</span>';
|
|
}
|
|
return '';
|
|
}
|
|
|
|
function canOpenImageGenerator() {
|
|
try {
|
|
return typeof config.openImageGenerator === 'function'
|
|
&& typeof config.canGenerateImages === 'function'
|
|
&& config.canGenerateImages() === true;
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function mediaNameHtml(item, className) {
|
|
var folder = item.folder || '';
|
|
return '<div class="' + className + '" title="' + escapeHtml(item.name || item.path || '') + '">' +
|
|
'<span class="rj-imgmgr-file-name">' + escapeHtml(mediaDisplayName(item)) + '</span>' + sizeBadgeHtml(item) +
|
|
(folder ? '<span class="rj-imgmgr-folder-name">' + escapeHtml(folder) + '</span>' : '') +
|
|
'</div>';
|
|
}
|
|
|
|
function mediaIcon(item) {
|
|
if (isAudio(item)) return '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 3v10.5A4 4 0 1 0 14 17V7h4V3h-6z"/></svg>';
|
|
if (isVideo(item)) return '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M4 5h12a2 2 0 0 1 2 2v1.2l4-2.2v12l-4-2.2V17a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2z"/></svg>';
|
|
return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/><path d="M8 13h8M8 17h8M8 9h2"/></svg>';
|
|
}
|
|
|
|
function detectTransparency(image) {
|
|
if (!image) return;
|
|
var surface = image.closest('.rj-imgmgr-thumb-btn, .rj-imgmgr-lightbox-stage');
|
|
if (!surface) return;
|
|
|
|
var source = (image.currentSrc || image.src || '').split(/[?#]/)[0].toLowerCase();
|
|
if (/\.jpe?g$/.test(source)) {
|
|
surface.classList.remove('rj-imgmgr-has-transparency');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
var naturalWidth = image.naturalWidth || 1;
|
|
var naturalHeight = image.naturalHeight || 1;
|
|
var scale = Math.min(1, 256 / Math.max(naturalWidth, naturalHeight));
|
|
var canvas = document.createElement('canvas');
|
|
canvas.width = Math.max(1, Math.round(naturalWidth * scale));
|
|
canvas.height = Math.max(1, Math.round(naturalHeight * scale));
|
|
var context = canvas.getContext('2d', { willReadFrequently: true });
|
|
if (!context) return;
|
|
context.drawImage(image, 0, 0, canvas.width, canvas.height);
|
|
var pixels = context.getImageData(0, 0, canvas.width, canvas.height).data;
|
|
var hasTransparency = false;
|
|
for (var i = 3; i < pixels.length; i += 4) {
|
|
if (pixels[i] < 255) {
|
|
hasTransparency = true;
|
|
break;
|
|
}
|
|
}
|
|
surface.classList.toggle('rj-imgmgr-has-transparency', hasTransparency);
|
|
} catch (e) {
|
|
// If a browser cannot inspect the bitmap, keep the neutral default.
|
|
surface.classList.remove('rj-imgmgr-has-transparency');
|
|
}
|
|
}
|
|
|
|
// Versioned preview URL: a replaced file keeps its name, so without the
|
|
// modification time the browser would show the cached old image
|
|
function previewSrc(item) {
|
|
var path = item && item.path ? String(item.path) : '';
|
|
if (!path || !item.modified) return path;
|
|
return path + (path.indexOf('?') === -1 ? '?' : '&') + 'v=' + encodeURIComponent(String(item.modified));
|
|
}
|
|
|
|
// AI disclosure label (EU AI Act, Art. 50) for images and videos
|
|
function canAiLabel(item) {
|
|
return !!item && (isImage(item) || item.type === 'video');
|
|
}
|
|
|
|
function aiLabelText(kind) {
|
|
return t(kind === 'modified' ? 'media.ai_modified' : 'media.ai_generated');
|
|
}
|
|
|
|
function aiBadgeHtml(item) {
|
|
if (!item || !item.aiLabel) return '';
|
|
return '<span class="rj-imgmgr-ai-badge rj-imgmgr-ai-badge--' + escapeHtml(item.aiLabel) + '" title="' + escapeHtml(aiLabelText(item.aiLabel)) + '">' +
|
|
escapeHtml(t('media.ai_badge')) + '</span>';
|
|
}
|
|
|
|
// Tell the page about label changes, so the inline editor updates labels next to images
|
|
function announceAiLabel(type, name, label) {
|
|
if (!name) return;
|
|
document.dispatchEvent(new CustomEvent('raiju:ai-label-change', {
|
|
detail: { type: type || 'image', name: name, label: label || '' }
|
|
}));
|
|
}
|
|
|
|
function aiLabelActionHtml(item) {
|
|
if (!canAiLabel(item)) return '';
|
|
var title = t('media.ai_label') + (item.aiLabel ? ': ' + aiLabelText(item.aiLabel) : '');
|
|
return '<button type="button" class="rj-imgmgr-action-btn' + (item.aiLabel ? ' rj-imgmgr-action-btn--on' : '') + '" data-action="ai-label" title="' + escapeHtml(title) + '" aria-label="' + escapeHtml(title) + '">' + Icons.ai + '</button>';
|
|
}
|
|
|
|
function mediaThumbHtml(item, className, previewAction) {
|
|
var actionAttr = previewAction ? ' data-action="preview"' : '';
|
|
var previewAttr = ' data-preview-path="' + escapeHtml(item.path || '') + '" data-preview-name="' + escapeHtml(mediaDisplayName(item)) + '" onclick="window.NbImageManager && NbImageManager.preview(this.dataset.previewPath, this.dataset.previewName); return false;"';
|
|
if (isImage(item)) {
|
|
return '<button type="button" class="' + className + ' rj-imgmgr-thumb-btn rj-imgmgr-thumb-btn--image"' + actionAttr + previewAttr + ' aria-label="' + escapeHtml(t('image_preview')) + '">' +
|
|
'<span class="rj-imgmgr-thumb-surface"><img src="' + escapeHtml(previewSrc(item)) + '" alt="" loading="lazy" onload="window.NbImageManager && NbImageManager.detectTransparency(this)"></span>' +
|
|
aiBadgeHtml(item) +
|
|
'</button>';
|
|
}
|
|
return '<button type="button" class="' + className + ' rj-imgmgr-thumb-btn rj-imgmgr-media-icon rj-imgmgr-media-icon--' + escapeHtml(item.type || 'document') + '"' + actionAttr + previewAttr + ' aria-label="' + escapeHtml(t('image_preview')) + '">' + mediaIcon(item) + '</button>';
|
|
}
|
|
|
|
// ============================================================
|
|
// MODAL CREATION
|
|
// ============================================================
|
|
function createModal() {
|
|
if (document.getElementById('rj-imgmgr-modal')) return;
|
|
|
|
var modal = document.createElement('div');
|
|
modal.id = 'rj-imgmgr-modal';
|
|
modal.className = 'rj-imgmgr-modal';
|
|
modal.innerHTML =
|
|
'<div class="rj-imgmgr-backdrop" aria-hidden="true"></div>' +
|
|
'<div class="rj-imgmgr-dialog" role="dialog" aria-modal="true" aria-labelledby="rj-imgmgr-title">' +
|
|
'<div class="rj-imgmgr-header">' +
|
|
'<h3 id="rj-imgmgr-title">' + escapeHtml(t('media_manager')) + '</h3>' +
|
|
'<button type="button" class="rj-imgmgr-close" aria-label="Close">×</button>' +
|
|
'</div>' +
|
|
'<div class="rj-imgmgr-toolbar">' +
|
|
'<span class="rj-imgmgr-formats">' + escapeHtml(t('image.formats_hint')) + '</span>' +
|
|
'<div class="rj-imgmgr-type-toggle"></div>' +
|
|
'<div class="rj-imgmgr-mode-toggle">' +
|
|
'<button type="button" class="rj-imgmgr-mode-btn rj-imgmgr-mode-btn--active" data-mode="library" aria-pressed="true">' + escapeHtml(t('image.library')) + '</button>' +
|
|
'<button type="button" class="rj-imgmgr-mode-btn" data-mode="trash" aria-pressed="false">' + escapeHtml(t('image.trash')) + '</button>' +
|
|
'</div>' +
|
|
'<button type="button" class="rj-imgmgr-btn rj-imgmgr-btn--secondary rj-imgmgr-empty-trash-btn" data-action="empty-trash" hidden>' + escapeHtml(t('image.empty_trash')) + '</button>' +
|
|
'<span class="rj-imgmgr-spacer"></span>' +
|
|
'<input type="text" class="rj-imgmgr-search" aria-label="' + escapeHtml(t('image.search')) + '" placeholder="' + escapeHtml(t('image.search')) + '">' +
|
|
'<div class="rj-imgmgr-view-toggle">' +
|
|
'<button type="button" class="rj-imgmgr-view-btn rj-imgmgr-view-btn--active" data-view="grid" title="' + escapeHtml(t('image.grid_view')) + '" aria-label="' + escapeHtml(t('image.grid_view')) + '" aria-pressed="true">' + Icons.grid + '</button>' +
|
|
'<button type="button" class="rj-imgmgr-view-btn" data-view="list" title="' + escapeHtml(t('image.list_view')) + '" aria-label="' + escapeHtml(t('image.list_view')) + '" aria-pressed="false">' + Icons.list + '</button>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
'<div class="rj-imgmgr-body">' +
|
|
'<aside class="rj-imgmgr-sidebar" aria-label="' + escapeHtml(t('media.folder')) + '">' +
|
|
'<div class="rj-imgmgr-sidebar-head">' +
|
|
'<span class="rj-imgmgr-sidebar-title">' + escapeHtml(t('media.folders')) + '</span>' +
|
|
'<button type="button" class="rj-imgmgr-action-btn rj-imgmgr-folder-create" title="' + escapeHtml(t('media.folder_create')) + '" aria-label="' + escapeHtml(t('media.folder_create')) + '">' + Icons.folderPlus + '</button>' +
|
|
'</div>' +
|
|
'<div class="rj-imgmgr-folder-list" role="listbox" aria-label="' + escapeHtml(t('media.folders')) + '"></div>' +
|
|
'</aside>' +
|
|
'<div class="rj-imgmgr-content">' +
|
|
'<div class="rj-imgmgr-pagination rj-imgmgr-pagination--top"></div>' +
|
|
'<div class="rj-imgmgr-grid" role="list"></div>' +
|
|
'<div class="rj-imgmgr-list">' +
|
|
'<div class="rj-imgmgr-list-header">' +
|
|
'<div class="rj-imgmgr-list-header-col"></div>' +
|
|
'<div class="rj-imgmgr-list-header-col"></div>' +
|
|
'<div class="rj-imgmgr-list-header-col sortable" data-sort="name">' + escapeHtml(t('image.col_filename')) + '</div>' +
|
|
'<div class="rj-imgmgr-list-header-col sortable" data-sort="size">' + escapeHtml(t('image.col_size')) + '</div>' +
|
|
'<div class="rj-imgmgr-list-header-col sortable" data-sort="date">' + escapeHtml(t('image.col_date')) + '</div>' +
|
|
'<div class="rj-imgmgr-list-header-col"></div>' +
|
|
'</div>' +
|
|
'<div class="rj-imgmgr-list-body"></div>' +
|
|
'</div>' +
|
|
'<div class="rj-imgmgr-pagination rj-imgmgr-pagination--bottom"></div>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
'<div class="rj-imgmgr-dropzone">' +
|
|
'<button type="button" class="rj-imgmgr-ai-generator-btn" data-action="ai-generator" hidden>' +
|
|
Icons.ai + '<span>' + escapeHtml(t('ai.image_generator')) + '</span>' +
|
|
'</button>' +
|
|
'<div class="rj-imgmgr-dropzone-upload">' +
|
|
'<span class="rj-imgmgr-dropzone-text">' + escapeHtml(t('image.drop_files')) + '</span>' +
|
|
'<span class="rj-imgmgr-dropzone-or">' + escapeHtml(t('image.or')) + '</span>' +
|
|
'<select class="rj-imgmgr-upload-type"></select>' +
|
|
'<label class="rj-imgmgr-upload-btn">' +
|
|
Icons.upload + ' <span>' + escapeHtml(t('image.upload')) + '</span>' +
|
|
'<input type="file" class="rj-imgmgr-upload-input" accept=".jpg,.jpeg,.png,.webp" multiple>' +
|
|
'</label>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
'<div class="rj-imgmgr-footer">' +
|
|
'<div class="rj-imgmgr-selection-info"></div>' +
|
|
'<div class="rj-imgmgr-footer-actions">' +
|
|
'<button type="button" class="rj-imgmgr-btn rj-imgmgr-btn--secondary" data-action="cancel">' + escapeHtml(t('close')) + '</button>' +
|
|
'<button type="button" class="rj-imgmgr-btn rj-imgmgr-btn--primary" data-action="confirm" disabled>' + escapeHtml(t('image.select')) + '</button>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
'</div>';
|
|
|
|
document.body.appendChild(modal);
|
|
|
|
// Event wiring
|
|
modal.querySelector('.rj-imgmgr-backdrop').addEventListener('click', close);
|
|
modal.querySelector('.rj-imgmgr-close').addEventListener('click', close);
|
|
modal.querySelector('[data-action="cancel"]').addEventListener('click', close);
|
|
modal.querySelector('[data-action="confirm"]').addEventListener('click', confirmSelection);
|
|
modal.querySelector('[data-action="empty-trash"]').addEventListener('click', emptyImageTrash);
|
|
|
|
modal.querySelector('.rj-imgmgr-upload-input').addEventListener('change', handleUpload);
|
|
modal.querySelector('.rj-imgmgr-ai-generator-btn').addEventListener('click', function () {
|
|
if (!canOpenImageGenerator()) return;
|
|
config.openImageGenerator('', 'auto');
|
|
});
|
|
modal.addEventListener('click', function (e) {
|
|
var pageBtn = e.target.closest && e.target.closest('.rj-imgmgr-pagination-btn[data-page]');
|
|
if (pageBtn && modal.contains(pageBtn)) {
|
|
e.preventDefault();
|
|
setMediaPage(pageBtn.dataset.page);
|
|
return;
|
|
}
|
|
var thumb = e.target.closest && e.target.closest('.rj-imgmgr-thumb-btn');
|
|
if (!thumb || !modal.contains(thumb)) return;
|
|
var itemEl = thumb.closest('.rj-imgmgr-item, .rj-imgmgr-row');
|
|
var path = itemEl ? itemEl.dataset.path : '';
|
|
var image = state.data.find(function (entry) { return entry.path === path; });
|
|
if (!image) return;
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
openLightbox(image);
|
|
}, true);
|
|
modal.querySelector('.rj-imgmgr-folder-create').addEventListener('click', createMediaFolder);
|
|
var folderListEl = modal.querySelector('.rj-imgmgr-folder-list');
|
|
folderListEl.addEventListener('click', function(e) {
|
|
var entry = e.target.closest('.rj-imgmgr-folder-item');
|
|
if (!entry || !modal.contains(entry)) return;
|
|
var deleteBtn = e.target.closest('.rj-imgmgr-folder-item-delete');
|
|
if (deleteBtn) {
|
|
e.stopPropagation();
|
|
deleteMediaFolder(entry.getAttribute('data-folder') || '');
|
|
return;
|
|
}
|
|
// Root folder uses an empty-string value, which is falsy — read the
|
|
// attribute directly so "Main folder" is not coerced to "all".
|
|
state.folderFilter = entry.getAttribute('data-folder') || '';
|
|
state.page = 1;
|
|
filterAndRender();
|
|
updateFolderUI();
|
|
});
|
|
folderListEl.addEventListener('keydown', function(e) {
|
|
if (e.key !== 'Enter' && e.key !== ' ') return;
|
|
var entry = e.target.closest('.rj-imgmgr-folder-item');
|
|
if (!entry) return;
|
|
e.preventDefault();
|
|
state.folderFilter = entry.getAttribute('data-folder') || '';
|
|
state.page = 1;
|
|
filterAndRender();
|
|
updateFolderUI();
|
|
});
|
|
|
|
var dropzone = modal.querySelector('.rj-imgmgr-dropzone');
|
|
['dragenter', 'dragover'].forEach(function (evt) {
|
|
dropzone.addEventListener(evt, function (e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
dropzone.classList.add('rj-imgmgr-dropzone--active');
|
|
});
|
|
});
|
|
['dragleave', 'drop'].forEach(function (evt) {
|
|
dropzone.addEventListener(evt, function (e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
dropzone.classList.remove('rj-imgmgr-dropzone--active');
|
|
});
|
|
});
|
|
dropzone.addEventListener('drop', function (e) {
|
|
var files = e.dataTransfer && e.dataTransfer.files;
|
|
uploadFiles(files);
|
|
});
|
|
|
|
modal.querySelector('.rj-imgmgr-search').addEventListener('input', function (e) {
|
|
state.search = e.target.value.toLowerCase().trim();
|
|
state.page = 1;
|
|
filterAndRender();
|
|
});
|
|
|
|
modal.querySelectorAll('.rj-imgmgr-view-btn').forEach(function (btn) {
|
|
btn.addEventListener('click', function () {
|
|
switchView(btn.dataset.view);
|
|
});
|
|
});
|
|
|
|
modal.querySelectorAll('.rj-imgmgr-mode-btn').forEach(function (btn) {
|
|
btn.addEventListener('click', function () {
|
|
switchMode(btn.dataset.mode);
|
|
});
|
|
});
|
|
|
|
modal.querySelector('.rj-imgmgr-upload-type').addEventListener('change', function (e) {
|
|
state.activeType = e.target.value;
|
|
state.page = 1;
|
|
updateTypeUI();
|
|
loadImages();
|
|
});
|
|
|
|
modal.querySelectorAll('.rj-imgmgr-list-header-col.sortable').forEach(function (col) {
|
|
col.addEventListener('click', function () {
|
|
sort(col.dataset.sort);
|
|
});
|
|
});
|
|
|
|
// ESC key closes the top-most dialog (replace first, then manager itself).
|
|
// Uses capture phase to intercept before any outer modal's keydown handler runs.
|
|
document.addEventListener('keydown', function (e) {
|
|
if (e.key !== 'Escape') return;
|
|
var replaceDialog = document.getElementById('rj-imgmgr-replace');
|
|
var moveDialog = document.getElementById('rj-imgmgr-move');
|
|
var renameDialog = document.getElementById('rj-imgmgr-rename');
|
|
var aiLabelDialog = document.getElementById('rj-imgmgr-ai-label');
|
|
var lightbox = document.getElementById('rj-imgmgr-lightbox');
|
|
if (replaceDialog && replaceDialog.classList.contains('active')) {
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
closeReplaceDialog();
|
|
} else if (moveDialog && moveDialog.classList.contains('active')) {
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
closeMoveDialog();
|
|
} else if (renameDialog && renameDialog.classList.contains('active')) {
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
closeRenameDialog();
|
|
} else if (aiLabelDialog && aiLabelDialog.classList.contains('active')) {
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
closeAiLabelDialog();
|
|
} else if (lightbox && lightbox.classList.contains('active')) {
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
closeLightbox();
|
|
} else if (modal.classList.contains('active')) {
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
close();
|
|
}
|
|
}, true); // capture phase
|
|
|
|
createLightbox();
|
|
createReplaceDialog();
|
|
createMoveDialog();
|
|
createRenameDialog();
|
|
}
|
|
|
|
// ============================================================
|
|
// OPEN / CLOSE
|
|
// ============================================================
|
|
function open(callback, currentPath, options) {
|
|
options = options || {};
|
|
createModal();
|
|
var modal = document.getElementById('rj-imgmgr-modal');
|
|
if (modal.parentElement !== document.body) {
|
|
document.body.appendChild(modal);
|
|
}
|
|
modal.classList.remove('rj-imgmgr-modal--embedded');
|
|
state.callback = typeof callback === 'function' ? callback : null;
|
|
state.isPicker = typeof callback === 'function';
|
|
state.multiplePicker = !!options.multiple;
|
|
state.allowedTypes = normalizeTypes(options.types || (state.isPicker ? ['image'] : ['image', 'audio', 'video', 'document']));
|
|
state.activeType = options.type && state.allowedTypes.indexOf(options.type) !== -1
|
|
? options.type
|
|
: (state.allowedTypes.length > 1 && !state.isPicker ? 'all' : state.allowedTypes[0]);
|
|
state.selectedPaths = state.multiplePicker && Array.isArray(currentPath)
|
|
? currentPath.map(normalizeImagePath).filter(Boolean)
|
|
: [];
|
|
state.selectedPath = state.multiplePicker ? null : normalizeImagePath(currentPath);
|
|
state.revealSelection = !!(state.selectedPath || state.selectedPaths.length);
|
|
state.search = '';
|
|
state.mode = 'library';
|
|
state.folderFilter = 'all';
|
|
state.page = 1;
|
|
|
|
modal.querySelector('.rj-imgmgr-search').value = '';
|
|
renderTypeControls();
|
|
updateModeUI();
|
|
updateTypeUI();
|
|
|
|
loadImages();
|
|
updateSelectionUI();
|
|
|
|
modal.classList.add('active');
|
|
document.body.style.overflow = 'hidden';
|
|
previousFocus = document.activeElement;
|
|
setTimeout(function () {
|
|
var search = modal.querySelector('.rj-imgmgr-search');
|
|
if (search) search.focus();
|
|
}, 0);
|
|
}
|
|
|
|
function mount(container, options) {
|
|
if (!container) return;
|
|
options = options || {};
|
|
createModal();
|
|
var modal = document.getElementById('rj-imgmgr-modal');
|
|
if (modal.parentElement !== container) {
|
|
container.appendChild(modal);
|
|
}
|
|
modal.classList.add('rj-imgmgr-modal--embedded');
|
|
|
|
state.callback = null;
|
|
state.multiplePicker = false;
|
|
state.isPicker = false;
|
|
state.allowedTypes = normalizeTypes(options.types || ['image', 'audio', 'video', 'document']);
|
|
state.activeType = options.type && state.allowedTypes.indexOf(options.type) !== -1
|
|
? options.type
|
|
: (state.allowedTypes.length > 1 ? 'all' : state.allowedTypes[0]);
|
|
state.selectedPath = null;
|
|
state.search = '';
|
|
state.mode = 'library';
|
|
state.folderFilter = 'all';
|
|
state.page = 1;
|
|
|
|
modal.querySelector('.rj-imgmgr-search').value = '';
|
|
renderTypeControls();
|
|
updateModeUI();
|
|
updateTypeUI();
|
|
|
|
loadImages();
|
|
updateSelectionUI();
|
|
|
|
modal.classList.add('active');
|
|
}
|
|
|
|
function normalizeImagePath(path) {
|
|
if (!path) return null;
|
|
path = String(path).trim();
|
|
if (!path) return null;
|
|
// Strip leading "../" segments and ensure leading "/" so we match state.data paths.
|
|
path = path.replace(/^(\.\.\/)+/, '/');
|
|
if (path.charAt(0) !== '/') path = '/' + path;
|
|
return path;
|
|
}
|
|
|
|
function close() {
|
|
var modal = document.getElementById('rj-imgmgr-modal');
|
|
if (modal) modal.classList.remove('active');
|
|
document.body.style.overflow = '';
|
|
state.callback = null;
|
|
if (previousFocus && document.contains(previousFocus)) previousFocus.focus();
|
|
previousFocus = null;
|
|
}
|
|
|
|
function confirmSelection() {
|
|
if (typeof state.callback === 'function') {
|
|
if (state.multiplePicker && state.selectedPaths.length) {
|
|
state.callback(state.selectedPaths.slice());
|
|
} else if (state.selectedPath) {
|
|
state.callback(state.selectedPath);
|
|
}
|
|
}
|
|
close();
|
|
}
|
|
|
|
function renderTypeControls() {
|
|
var modal = document.getElementById('rj-imgmgr-modal');
|
|
if (!modal) return;
|
|
|
|
var typeToggle = modal.querySelector('.rj-imgmgr-type-toggle');
|
|
var uploadType = modal.querySelector('.rj-imgmgr-upload-type');
|
|
var types = state.allowedTypes.slice();
|
|
|
|
typeToggle.innerHTML = '';
|
|
if (types.length > 1 && !state.isPicker) {
|
|
var allBtn = document.createElement('button');
|
|
allBtn.type = 'button';
|
|
allBtn.className = 'rj-imgmgr-type-btn';
|
|
allBtn.dataset.type = 'all';
|
|
allBtn.textContent = t('media.type_all');
|
|
typeToggle.appendChild(allBtn);
|
|
}
|
|
|
|
types.forEach(function (type) {
|
|
var btn = document.createElement('button');
|
|
btn.type = 'button';
|
|
btn.className = 'rj-imgmgr-type-btn';
|
|
btn.dataset.type = type;
|
|
btn.textContent = mediaTypeLabel(type);
|
|
typeToggle.appendChild(btn);
|
|
});
|
|
|
|
typeToggle.querySelectorAll('.rj-imgmgr-type-btn').forEach(function (btn) {
|
|
btn.addEventListener('click', function () {
|
|
state.activeType = btn.dataset.type;
|
|
state.page = 1;
|
|
updateTypeUI();
|
|
loadImages();
|
|
});
|
|
});
|
|
|
|
uploadType.innerHTML = '';
|
|
types.forEach(function (type) {
|
|
var option = document.createElement('option');
|
|
option.value = type;
|
|
option.textContent = mediaTypeLabel(type);
|
|
uploadType.appendChild(option);
|
|
});
|
|
}
|
|
|
|
function updateTypeUI() {
|
|
var modal = document.getElementById('rj-imgmgr-modal');
|
|
if (!modal) return;
|
|
|
|
var uploadType = modal.querySelector('.rj-imgmgr-upload-type');
|
|
var uploadInput = modal.querySelector('.rj-imgmgr-upload-input');
|
|
var uploadLabel = modal.querySelector('.rj-imgmgr-upload-btn span');
|
|
var confirmBtn = modal.querySelector('[data-action="confirm"]');
|
|
var formats = modal.querySelector('.rj-imgmgr-formats');
|
|
var uploadTypeValue = getActiveUploadType();
|
|
var typeConfig = mediaTypes[uploadTypeValue] || mediaTypes.image;
|
|
|
|
modal.querySelectorAll('.rj-imgmgr-type-btn').forEach(function (btn) {
|
|
btn.classList.toggle('rj-imgmgr-type-btn--active', btn.dataset.type === state.activeType);
|
|
});
|
|
|
|
uploadType.hidden = state.allowedTypes.length <= 1;
|
|
uploadType.value = uploadTypeValue;
|
|
uploadInput.accept = typeConfig.accept;
|
|
uploadLabel.textContent = t(typeConfig.uploadLabelKey);
|
|
if (confirmBtn) {
|
|
confirmBtn.textContent = t(uploadTypeValue === 'image' ? 'image.select' : (uploadTypeValue === 'audio' ? 'audio.select' : 'media.select_file'));
|
|
}
|
|
formats.textContent = t(typeConfig.formatsKey);
|
|
updateLimitHint();
|
|
var title = modal.querySelector('.rj-imgmgr-header h3');
|
|
if (title) title.textContent = t(isImageOnlyPicker() ? 'image_manager' : 'media_manager');
|
|
updateGeneratorButton();
|
|
updateFolderUI();
|
|
}
|
|
|
|
function megabytes(bytes) {
|
|
var mb = bytes / 1048576;
|
|
return mb >= 1 && Math.abs(mb - Math.round(mb)) < 0.05 ? String(Math.round(mb)) : String(Math.round(mb * 10) / 10);
|
|
}
|
|
|
|
// "37 KB" below a megabyte, "1.4 MB" above
|
|
function fileSize(bytes) {
|
|
return bytes < 1048576 ? Math.max(1, Math.round(bytes / 1024)) + ' KB' : megabytes(bytes) + ' MB';
|
|
}
|
|
|
|
// The limit that applies to the chosen type: Raiju's, or the server's when that is lower
|
|
function effectiveLimit(type) {
|
|
var typeConfig = mediaTypes[type] || mediaTypes.image;
|
|
var serverMax = uploadLimits && uploadLimits.serverMaxBytes ? uploadLimits.serverMaxBytes : null;
|
|
return { max: typeConfig.maxSize, serverMax: serverMax, serverBound: serverMax !== null && serverMax < typeConfig.maxSize };
|
|
}
|
|
|
|
function updateLimitHint() {
|
|
var modal = document.getElementById('rj-imgmgr-modal');
|
|
if (!modal) return;
|
|
var formats = modal.querySelector('.rj-imgmgr-formats');
|
|
if (!formats) return;
|
|
var type = getActiveUploadType();
|
|
var limit = effectiveLimit(type);
|
|
var text = t((mediaTypes[type] || mediaTypes.image).formatsKey) + ' · ' + t('media.upload_limit', { max: megabytes(limit.serverBound ? limit.serverMax : limit.max) });
|
|
if (limit.serverBound) text += ' (' + t('media.server_limit', { max: megabytes(limit.serverMax) }) + ')';
|
|
formats.textContent = text;
|
|
formats.classList.toggle('rj-imgmgr-formats--server-bound', limit.serverBound);
|
|
}
|
|
|
|
function updateGeneratorButton() {
|
|
var modal = document.getElementById('rj-imgmgr-modal');
|
|
if (!modal) return;
|
|
var button = modal.querySelector('.rj-imgmgr-ai-generator-btn');
|
|
var dropzone = modal.querySelector('.rj-imgmgr-dropzone');
|
|
if (!button || !dropzone) return;
|
|
var visible = !state.isPicker && state.mode !== 'trash' && getActiveUploadType() === 'image' && canOpenImageGenerator();
|
|
button.hidden = !visible;
|
|
dropzone.classList.toggle('rj-imgmgr-dropzone--with-ai', visible);
|
|
}
|
|
|
|
function folderItemCount(folderValue) {
|
|
return state.data.reduce(function(total, item) {
|
|
return total + ((item.folder || '') === folderValue ? 1 : 0);
|
|
}, 0);
|
|
}
|
|
|
|
function updateFolderUI() {
|
|
var modal = document.getElementById('rj-imgmgr-modal');
|
|
if (!modal) return;
|
|
var sidebar = modal.querySelector('.rj-imgmgr-sidebar');
|
|
var list = modal.querySelector('.rj-imgmgr-folder-list');
|
|
if (!sidebar || !list) return;
|
|
|
|
// The folder sidebar is a library-only concept; hide it in trash mode.
|
|
var isLibrary = state.mode === 'library';
|
|
sidebar.hidden = !isLibrary;
|
|
if (!isLibrary) return;
|
|
|
|
var current = state.folderFilter || 'all';
|
|
if (current !== 'all' && current !== '' && state.folders.indexOf(current) === -1) {
|
|
current = 'all';
|
|
state.folderFilter = current;
|
|
}
|
|
|
|
var entries = [
|
|
{ value: 'all', label: t('media.folder_all'), icon: Icons.layers || Icons.folder, deletable: false },
|
|
{ value: '', label: t('media.folder_root'), icon: Icons.folder, deletable: false }
|
|
].concat(state.folders.map(function(folder) {
|
|
return { value: folder, label: folder, icon: Icons.folder, deletable: true };
|
|
}));
|
|
|
|
list.innerHTML = entries.map(function(entry) {
|
|
var active = entry.value === current;
|
|
var count = entry.value === 'all' ? state.data.length : folderItemCount(entry.value);
|
|
return '<div class="rj-imgmgr-folder-item' + (active ? ' is-active' : '') + '"' +
|
|
' role="option" tabindex="0" aria-selected="' + (active ? 'true' : 'false') + '"' +
|
|
' data-folder="' + escapeHtml(entry.value) + '">' +
|
|
'<span class="rj-imgmgr-folder-item-icon">' + (entry.icon || '') + '</span>' +
|
|
'<span class="rj-imgmgr-folder-item-label">' + escapeHtml(entry.label) + '</span>' +
|
|
'<span class="rj-imgmgr-folder-item-count">' + count + '</span>' +
|
|
(entry.deletable
|
|
? '<button type="button" class="rj-imgmgr-folder-item-delete" title="' + escapeHtml(t('media.folder_delete')) + '" aria-label="' + escapeHtml(t('media.folder_delete')) + '">' + Icons.delete + '</button>'
|
|
: '') +
|
|
'</div>';
|
|
}).join('');
|
|
}
|
|
|
|
// ============================================================
|
|
// LOAD / RENDER
|
|
// ============================================================
|
|
function loadImages() {
|
|
var modal = document.getElementById('rj-imgmgr-modal');
|
|
var gridEl = modal.querySelector('.rj-imgmgr-grid');
|
|
var listBody = modal.querySelector('.rj-imgmgr-list-body');
|
|
var typeParam = state.activeType === 'all' ? state.allowedTypes.join(',') : state.activeType;
|
|
var url = config.apiUrl + '?action=list-media&type=' + encodeURIComponent(typeParam) + '&trash=' + (state.mode === 'trash' ? '1' : '0') + '&_=' + Date.now();
|
|
|
|
gridEl.innerHTML = '<p class="rj-imgmgr-loading">' + escapeHtml(t('image.loading')) + '</p>';
|
|
listBody.innerHTML = '';
|
|
|
|
fetch(url)
|
|
.then(function (r) { return r.json(); })
|
|
.then(function (result) {
|
|
var items = result.success && result.data
|
|
? (Array.isArray(result.data) ? result.data : (result.data.items || []))
|
|
: [];
|
|
state.folders = result.success && result.data && Array.isArray(result.data.folders) ? result.data.folders : [];
|
|
if (result.success && result.data && result.data.limits) {
|
|
uploadLimits = result.data.limits;
|
|
if (uploadLimits.imageMaxBytes) mediaTypes.image.maxSize = uploadLimits.imageMaxBytes;
|
|
}
|
|
if (result.success && result.data && Array.isArray(result.data.types)) {
|
|
result.data.types.forEach(function (typeInfo) {
|
|
if (mediaTypes[typeInfo.type] && typeInfo.maxSize) mediaTypes[typeInfo.type].maxSize = typeInfo.maxSize;
|
|
});
|
|
}
|
|
updateLimitHint();
|
|
if (items.length > 0) {
|
|
state.data = items.map(function (img) {
|
|
return Object.assign({}, img, {
|
|
type: img.type || 'image',
|
|
folder: img.folder || '',
|
|
basename: img.basename || img.name || '',
|
|
path: img.path.indexOf('api.php?') === 0 ? img.path : img.path.replace(/^\.\.\//, '/')
|
|
});
|
|
});
|
|
updateFolderUI();
|
|
updateSortHeaderClasses();
|
|
applySortOrder();
|
|
filterAndRender();
|
|
if (state.revealSelection) revealSelection();
|
|
} else {
|
|
state.data = [];
|
|
state.filtered = [];
|
|
state.page = 1;
|
|
updateFolderUI();
|
|
renderPagination();
|
|
var emptyKey = state.mode === 'trash' ? (isImageOnlyPicker() ? 'image.trash_empty' : 'media.trash_empty') : 'media.no_files';
|
|
var empty = '<p class="rj-imgmgr-empty">' + escapeHtml(t(emptyKey)) + '</p>';
|
|
gridEl.innerHTML = empty;
|
|
listBody.innerHTML = empty;
|
|
}
|
|
})
|
|
.catch(function (err) {
|
|
gridEl.innerHTML = '<p class="rj-imgmgr-error">' + escapeHtml(err.message) + '</p>';
|
|
});
|
|
}
|
|
|
|
function updateSortHeaderClasses() {
|
|
var modal = document.getElementById('rj-imgmgr-modal');
|
|
if (!modal) return;
|
|
modal.querySelectorAll('.rj-imgmgr-list-header-col.sortable').forEach(function (col) {
|
|
col.classList.remove('sorted-asc', 'sorted-desc');
|
|
if (col.dataset.sort === state.sort.field) {
|
|
col.classList.add(state.sort.dir === 'asc' ? 'sorted-asc' : 'sorted-desc');
|
|
}
|
|
});
|
|
}
|
|
|
|
function applySortOrder() {
|
|
state.data.sort(function (a, b) {
|
|
var va, vb;
|
|
switch (state.sort.field) {
|
|
case 'name': va = (a.name || '').toLowerCase(); vb = (b.name || '').toLowerCase(); break;
|
|
case 'size': va = a.sizeBytes || 0; vb = b.sizeBytes || 0; break;
|
|
case 'date': va = a.modified || 0; vb = b.modified || 0; break;
|
|
default: return 0;
|
|
}
|
|
if (va < vb) return state.sort.dir === 'asc' ? -1 : 1;
|
|
if (va > vb) return state.sort.dir === 'asc' ? 1 : -1;
|
|
return 0;
|
|
});
|
|
}
|
|
|
|
function filterAndRender() {
|
|
state.filtered = state.data.filter(function (img) {
|
|
var folder = img.folder || '';
|
|
var matchesFolder = state.folderFilter === 'all' || folder === state.folderFilter;
|
|
var matchesSearch = !state.search || ((img.name || '') + ' ' + (img.basename || '') + ' ' + folder).toLowerCase().indexOf(state.search) !== -1;
|
|
return matchesFolder && matchesSearch;
|
|
});
|
|
var pageSize = getItemsPerPage();
|
|
var pages = Math.max(1, Math.ceil(state.filtered.length / pageSize));
|
|
state.page = Math.max(1, Math.min(state.page || 1, pages));
|
|
render();
|
|
}
|
|
|
|
// Picker opened with the current file: show the page that holds it and scroll it into view (once per open)
|
|
function revealSelection() {
|
|
state.revealSelection = false;
|
|
var path = state.multiplePicker ? state.selectedPaths[0] : state.selectedPath;
|
|
var index = path ? state.filtered.findIndex(function (entry) { return entry.path === path; }) : -1;
|
|
if (index === -1) return;
|
|
var page = Math.floor(index / getItemsPerPage()) + 1;
|
|
if (page !== state.page) {
|
|
state.page = page;
|
|
render();
|
|
}
|
|
var modal = document.getElementById('rj-imgmgr-modal');
|
|
var selected = modal && modal.querySelector('.rj-imgmgr-item.selected, .rj-imgmgr-row.selected');
|
|
if (selected) selected.scrollIntoView({ block: 'nearest' });
|
|
}
|
|
|
|
function render() {
|
|
renderPagination();
|
|
if (state.view === 'grid') renderGrid();
|
|
else renderList();
|
|
}
|
|
|
|
function currentPageItems() {
|
|
var pageSize = getItemsPerPage();
|
|
var current = Math.max(1, state.page || 1);
|
|
var start = (current - 1) * pageSize;
|
|
return state.filtered.slice(start, start + pageSize);
|
|
}
|
|
|
|
function renderPagination() {
|
|
var modal = document.getElementById('rj-imgmgr-modal');
|
|
if (!modal) return;
|
|
var pageSize = getItemsPerPage();
|
|
var total = state.filtered.length;
|
|
var pageCount = Math.max(1, Math.ceil(total / pageSize));
|
|
var current = Math.max(1, Math.min(state.page || 1, pageCount));
|
|
state.page = current;
|
|
var from = total === 0 ? 0 : ((current - 1) * pageSize) + 1;
|
|
var to = Math.min(total, current * pageSize);
|
|
var summary = mediaSummaryText(total, from, to);
|
|
var showPager = total > pageSize;
|
|
var html = '<div class="rj-imgmgr-pagination-inner">' +
|
|
'<span class="rj-imgmgr-pagination-summary">' + escapeHtml(summary) + '</span>';
|
|
|
|
if (showPager) {
|
|
var nav = [];
|
|
var addPage = function(page) {
|
|
nav.push('<button type="button" class="rj-imgmgr-pagination-btn' + (page === current ? ' is-active' : '') + '" data-page="' + page + '" aria-current="' + (page === current ? 'page' : 'false') + '">' + page + '</button>');
|
|
};
|
|
nav.push('<button type="button" class="rj-imgmgr-pagination-btn rj-imgmgr-pagination-btn--arrow" data-page="' + Math.max(1, current - 1) + '" aria-label="' + escapeHtml(t('lists.previous_page')) + '"' + (current === 1 ? ' disabled' : '') + '>' + paginationIcon('prev') + '</button>');
|
|
addPage(1);
|
|
if (current > 3) nav.push('<span class="rj-imgmgr-pagination-ellipsis">...</span>');
|
|
for (var page = Math.max(2, current - 1); page <= Math.min(pageCount - 1, current + 1); page += 1) {
|
|
addPage(page);
|
|
}
|
|
if (current < pageCount - 2) nav.push('<span class="rj-imgmgr-pagination-ellipsis">...</span>');
|
|
if (pageCount > 1) addPage(pageCount);
|
|
nav.push('<button type="button" class="rj-imgmgr-pagination-btn rj-imgmgr-pagination-btn--arrow" data-page="' + Math.min(pageCount, current + 1) + '" aria-label="' + escapeHtml(t('lists.next_page')) + '"' + (current === pageCount ? ' disabled' : '') + '>' + paginationIcon('next') + '</button>');
|
|
html += '<nav class="rj-imgmgr-pagination-nav" aria-label="' + escapeHtml(t('lists.pagination')) + '">' + nav.join('') + '</nav>';
|
|
}
|
|
|
|
html += '</div>';
|
|
modal.querySelectorAll('.rj-imgmgr-pagination').forEach(function (footer) {
|
|
var isTop = footer.classList.contains('rj-imgmgr-pagination--top');
|
|
footer.hidden = isTop && !showPager;
|
|
footer.innerHTML = isTop && !showPager ? '' : html;
|
|
});
|
|
}
|
|
|
|
function setMediaPage(page) {
|
|
var pageSize = getItemsPerPage();
|
|
var pages = Math.max(1, Math.ceil(state.filtered.length / pageSize));
|
|
state.page = Math.max(1, Math.min(parseInt(page, 10) || 1, pages));
|
|
render();
|
|
}
|
|
|
|
function renderGrid() {
|
|
var modal = document.getElementById('rj-imgmgr-modal');
|
|
var gridEl = modal.querySelector('.rj-imgmgr-grid');
|
|
|
|
if (state.filtered.length === 0) {
|
|
var msg = state.search
|
|
? t('image.no_search_results', { term: state.search })
|
|
: t('media.no_files');
|
|
gridEl.innerHTML = '<p class="rj-imgmgr-empty">' + escapeHtml(msg) + '</p>';
|
|
return;
|
|
}
|
|
|
|
gridEl.innerHTML = '';
|
|
currentPageItems().forEach(function (image) {
|
|
var item = document.createElement('div');
|
|
var isSelected = state.isPicker && isSelectedPath(image.path);
|
|
item.className = 'rj-imgmgr-item' + (isSelected ? ' selected' : '');
|
|
item.dataset.path = image.path;
|
|
item.setAttribute('role', 'listitem');
|
|
item.setAttribute('aria-label', image.name || image.path || t('media.file'));
|
|
item.innerHTML = state.mode === 'trash'
|
|
? mediaThumbHtml(image, 'rj-imgmgr-thumb', true) +
|
|
mediaNameHtml(image, 'rj-imgmgr-name') +
|
|
'<div class="rj-imgmgr-actions">' +
|
|
'<button type="button" class="rj-imgmgr-action-btn" data-action="preview" title="' + escapeHtml(t('image_preview')) + '" aria-label="' + escapeHtml(t('image_preview')) + '">' + Icons.eye + '</button>' +
|
|
'<button type="button" class="rj-imgmgr-action-btn" data-action="restore" title="' + escapeHtml(t('image.restore')) + '" aria-label="' + escapeHtml(t('image.restore')) + '">' + Icons.restore + '</button>' +
|
|
'<button type="button" class="rj-imgmgr-action-btn rj-imgmgr-action-btn--danger" data-action="delete-permanent" title="' + escapeHtml(t('image.delete_permanently')) + '" aria-label="' + escapeHtml(t('image.delete_permanently')) + '">' + Icons.delete + '</button>' +
|
|
'</div>'
|
|
:
|
|
mediaThumbHtml(image, 'rj-imgmgr-thumb', true) +
|
|
mediaNameHtml(image, 'rj-imgmgr-name') +
|
|
'<div class="rj-imgmgr-actions">' +
|
|
'<button type="button" class="rj-imgmgr-action-btn" data-action="preview" title="' + escapeHtml(t('image_preview')) + '" aria-label="' + escapeHtml(t('image_preview')) + '">' + Icons.eye + '</button>' +
|
|
mediaDownloadActionHtml(image) +
|
|
'<button type="button" class="rj-imgmgr-action-btn" data-action="copy" title="' + escapeHtml(t('image.copy_path')) + '" aria-label="' + escapeHtml(t('image.copy_path')) + '">' + Icons.copy + '</button>' +
|
|
'<button type="button" class="rj-imgmgr-action-btn" data-action="move" title="' + escapeHtml(t('media.move_file')) + '" aria-label="' + escapeHtml(t('media.move_file')) + '">' + Icons.move + '</button>' +
|
|
'<button type="button" class="rj-imgmgr-action-btn" data-action="rename" title="' + escapeHtml(t('media.rename_file')) + '" aria-label="' + escapeHtml(t('media.rename_file')) + '">' + Icons.rename + '</button>' +
|
|
(isImage(image) ? '<button type="button" class="rj-imgmgr-action-btn" data-action="replace" title="' + escapeHtml(t('image.replace')) + '" aria-label="' + escapeHtml(t('image.replace')) + '">' + Icons.replace + '</button>' : '') +
|
|
aiLabelActionHtml(image) +
|
|
'<button type="button" class="rj-imgmgr-action-btn rj-imgmgr-action-btn--danger" data-action="delete" title="' + escapeHtml(t('delete')) + '" aria-label="' + escapeHtml(t('delete')) + '">' + Icons.delete + '</button>' +
|
|
'</div>';
|
|
if (state.mode !== 'trash' && state.isPicker) {
|
|
item.insertAdjacentHTML('afterbegin', '<div class="rj-imgmgr-check' + (isSelected ? ' checked' : '') + '"></div>');
|
|
}
|
|
gridEl.appendChild(item);
|
|
attachItemEvents(item, image);
|
|
});
|
|
}
|
|
|
|
function renderList() {
|
|
var modal = document.getElementById('rj-imgmgr-modal');
|
|
var listBody = modal.querySelector('.rj-imgmgr-list-body');
|
|
|
|
if (state.filtered.length === 0) {
|
|
var msg = state.search
|
|
? t('image.no_search_results', { term: state.search })
|
|
: t('media.no_files');
|
|
listBody.innerHTML = '<p class="rj-imgmgr-empty">' + escapeHtml(msg) + '</p>';
|
|
return;
|
|
}
|
|
|
|
listBody.innerHTML = '';
|
|
currentPageItems().forEach(function (image) {
|
|
appendMediaRow(listBody, image);
|
|
});
|
|
}
|
|
|
|
function appendMediaRow(listBody, image) {
|
|
var row = document.createElement('div');
|
|
var isSelected = state.isPicker && isSelectedPath(image.path);
|
|
row.className = 'rj-imgmgr-row' + (isSelected ? ' selected' : '');
|
|
if (image.folder) row.classList.add('rj-imgmgr-row--child');
|
|
row.dataset.path = image.path;
|
|
row.setAttribute('aria-label', image.name || image.path || t('media.file'));
|
|
row.innerHTML = state.mode === 'trash'
|
|
? '<div></div>' +
|
|
mediaThumbHtml(image, 'rj-imgmgr-list-thumb', true) +
|
|
mediaNameHtml(image, 'rj-imgmgr-list-name') +
|
|
'<div class="rj-imgmgr-list-size">' + escapeHtml(image.size || '-') + '</div>' +
|
|
'<div class="rj-imgmgr-list-date">' + escapeHtml(image.dateFormatted || '-') + '</div>' +
|
|
'<div class="rj-imgmgr-list-actions">' +
|
|
'<button type="button" class="rj-imgmgr-action-btn" data-action="preview" title="' + escapeHtml(t('image_preview')) + '" aria-label="' + escapeHtml(t('image_preview')) + '">' + Icons.eye + '</button>' +
|
|
'<button type="button" class="rj-imgmgr-action-btn" data-action="restore" title="' + escapeHtml(t('image.restore')) + '" aria-label="' + escapeHtml(t('image.restore')) + '">' + Icons.restore + '</button>' +
|
|
'<button type="button" class="rj-imgmgr-action-btn rj-imgmgr-action-btn--danger" data-action="delete-permanent" title="' + escapeHtml(t('image.delete_permanently')) + '" aria-label="' + escapeHtml(t('image.delete_permanently')) + '">' + Icons.delete + '</button>' +
|
|
'</div>'
|
|
:
|
|
'<div></div>' +
|
|
mediaThumbHtml(image, 'rj-imgmgr-list-thumb', true) +
|
|
mediaNameHtml(image, 'rj-imgmgr-list-name') +
|
|
'<div class="rj-imgmgr-list-size">' + escapeHtml(image.size || '-') + '</div>' +
|
|
'<div class="rj-imgmgr-list-date">' + escapeHtml(image.dateFormatted || '-') + '</div>' +
|
|
'<div class="rj-imgmgr-list-actions">' +
|
|
'<button type="button" class="rj-imgmgr-action-btn" data-action="preview" title="' + escapeHtml(t('image_preview')) + '" aria-label="' + escapeHtml(t('image_preview')) + '">' + Icons.eye + '</button>' +
|
|
mediaDownloadActionHtml(image) +
|
|
'<button type="button" class="rj-imgmgr-action-btn" data-action="copy" title="' + escapeHtml(t('image.copy_path')) + '" aria-label="' + escapeHtml(t('image.copy_path')) + '">' + Icons.copy + '</button>' +
|
|
'<button type="button" class="rj-imgmgr-action-btn" data-action="move" title="' + escapeHtml(t('media.move_file')) + '" aria-label="' + escapeHtml(t('media.move_file')) + '">' + Icons.move + '</button>' +
|
|
'<button type="button" class="rj-imgmgr-action-btn" data-action="rename" title="' + escapeHtml(t('media.rename_file')) + '" aria-label="' + escapeHtml(t('media.rename_file')) + '">' + Icons.rename + '</button>' +
|
|
(isImage(image) ? '<button type="button" class="rj-imgmgr-action-btn" data-action="replace" title="' + escapeHtml(t('image.replace')) + '" aria-label="' + escapeHtml(t('image.replace')) + '">' + Icons.replace + '</button>' : '') +
|
|
aiLabelActionHtml(image) +
|
|
'<button type="button" class="rj-imgmgr-action-btn rj-imgmgr-action-btn--danger" data-action="delete" title="' + escapeHtml(t('delete')) + '" aria-label="' + escapeHtml(t('delete')) + '">' + Icons.delete + '</button>' +
|
|
'</div>';
|
|
if (state.mode !== 'trash' && state.isPicker) {
|
|
row.querySelector('div:first-child').outerHTML = '<div class="rj-imgmgr-check rj-imgmgr-check--list' + (isSelected ? ' checked' : '') + '"></div>';
|
|
}
|
|
listBody.appendChild(row);
|
|
attachItemEvents(row, image);
|
|
}
|
|
|
|
function attachItemEvents(element, image) {
|
|
var check = element.querySelector('.rj-imgmgr-check');
|
|
if (check) {
|
|
check.addEventListener('click', function (e) {
|
|
e.stopPropagation();
|
|
toggleSelection(image.path);
|
|
});
|
|
}
|
|
|
|
var previewThumb = element.querySelector('.rj-imgmgr-thumb-btn');
|
|
if (previewThumb) {
|
|
previewThumb.addEventListener('click', function (e) {
|
|
e.stopPropagation();
|
|
openLightbox(image);
|
|
});
|
|
}
|
|
|
|
element.addEventListener('click', function (e) {
|
|
var actionEl = e.target.closest('[data-action]');
|
|
if (actionEl) {
|
|
e.stopPropagation();
|
|
var action = actionEl.dataset.action;
|
|
if (action === 'preview') openLightbox(image);
|
|
else if (action === 'download') return;
|
|
else if (action === 'copy') copyPath(image.path);
|
|
else if (action === 'move') openMoveDialog(image);
|
|
else if (action === 'rename') openRenameDialog(image);
|
|
else if (action === 'ai-label') openAiLabelDialog(image);
|
|
else if (action === 'original') openOriginalDialog(image);
|
|
else if (action === 'replace') openReplaceDialog(image.name, image.path);
|
|
else if (action === 'delete') deleteMedia(image);
|
|
else if (action === 'restore') restoreMedia(image);
|
|
else if (action === 'delete-permanent') deleteMediaPermanently(image);
|
|
return;
|
|
}
|
|
// Click elsewhere on item = toggle selection
|
|
if (state.mode !== 'trash' && state.isPicker) toggleSelection(image.path);
|
|
});
|
|
}
|
|
|
|
function isSelectedPath(path) {
|
|
return state.multiplePicker
|
|
? state.selectedPaths.indexOf(path) !== -1
|
|
: state.selectedPath === path;
|
|
}
|
|
|
|
function toggleSelection(path) {
|
|
if (state.multiplePicker) {
|
|
var idx = state.selectedPaths.indexOf(path);
|
|
if (idx === -1) {
|
|
state.selectedPaths.push(path);
|
|
} else {
|
|
state.selectedPaths.splice(idx, 1);
|
|
}
|
|
} else {
|
|
state.selectedPath = state.selectedPath === path ? null : path;
|
|
}
|
|
updateSelectionUI();
|
|
}
|
|
|
|
function updateSelectionUI() {
|
|
var modal = document.getElementById('rj-imgmgr-modal');
|
|
if (!modal) return;
|
|
|
|
modal.querySelectorAll('.rj-imgmgr-item, .rj-imgmgr-row').forEach(function (item) {
|
|
var path = item.dataset.path;
|
|
var isSelected = isSelectedPath(path);
|
|
item.classList.toggle('selected', isSelected);
|
|
var check = item.querySelector('.rj-imgmgr-check');
|
|
if (check) check.classList.toggle('checked', isSelected);
|
|
});
|
|
|
|
var info = modal.querySelector('.rj-imgmgr-selection-info');
|
|
var confirmBtn = modal.querySelector('[data-action="confirm"]');
|
|
|
|
if (!state.isPicker) {
|
|
info.textContent = '';
|
|
info.classList.remove('has-selection');
|
|
confirmBtn.hidden = true;
|
|
confirmBtn.disabled = true;
|
|
return;
|
|
}
|
|
|
|
if (state.mode === 'trash') {
|
|
info.textContent = t('image.trash_hint');
|
|
info.classList.remove('has-selection');
|
|
confirmBtn.hidden = true;
|
|
confirmBtn.disabled = true;
|
|
return;
|
|
}
|
|
|
|
confirmBtn.hidden = false;
|
|
var selectedCount = state.multiplePicker ? state.selectedPaths.length : (state.selectedPath ? 1 : 0);
|
|
if (selectedCount) {
|
|
info.textContent = state.multiplePicker
|
|
? t('media.selection_count', { count: selectedCount })
|
|
: state.selectedPath;
|
|
info.classList.add('has-selection');
|
|
confirmBtn.disabled = false;
|
|
} else {
|
|
info.textContent = t(isImageOnlyPicker() ? 'image.no_selection' : 'media.no_selection');
|
|
info.classList.remove('has-selection');
|
|
confirmBtn.disabled = true;
|
|
}
|
|
}
|
|
|
|
function updateModeUI() {
|
|
var modal = document.getElementById('rj-imgmgr-modal');
|
|
if (!modal) return;
|
|
|
|
modal.querySelectorAll('.rj-imgmgr-mode-btn').forEach(function (btn) {
|
|
btn.classList.toggle('rj-imgmgr-mode-btn--active', btn.dataset.mode === state.mode);
|
|
btn.setAttribute('aria-pressed', btn.dataset.mode === state.mode ? 'true' : 'false');
|
|
});
|
|
|
|
var dropzone = modal.querySelector('.rj-imgmgr-dropzone');
|
|
var emptyBtn = modal.querySelector('.rj-imgmgr-empty-trash-btn');
|
|
if (dropzone) dropzone.hidden = state.mode === 'trash';
|
|
if (emptyBtn) emptyBtn.hidden = state.mode !== 'trash';
|
|
updateGeneratorButton();
|
|
|
|
if (state.mode === 'trash') {
|
|
state.selectedPath = null;
|
|
state.folderFilter = 'all';
|
|
}
|
|
|
|
updateFolderUI();
|
|
updateSelectionUI();
|
|
}
|
|
|
|
// ============================================================
|
|
// VIEW / SORT
|
|
// ============================================================
|
|
function switchMode(mode) {
|
|
if (mode !== 'library' && mode !== 'trash') return;
|
|
if (state.mode === mode) return;
|
|
|
|
state.mode = mode;
|
|
state.search = '';
|
|
state.page = 1;
|
|
var modal = document.getElementById('rj-imgmgr-modal');
|
|
modal.querySelector('.rj-imgmgr-search').value = '';
|
|
updateModeUI();
|
|
loadImages();
|
|
}
|
|
|
|
function switchView(view) {
|
|
state.view = view;
|
|
var modal = document.getElementById('rj-imgmgr-modal');
|
|
modal.querySelectorAll('.rj-imgmgr-view-btn').forEach(function (btn) {
|
|
btn.classList.toggle('rj-imgmgr-view-btn--active', btn.dataset.view === view);
|
|
btn.setAttribute('aria-pressed', btn.dataset.view === view ? 'true' : 'false');
|
|
});
|
|
var gridEl = modal.querySelector('.rj-imgmgr-grid');
|
|
var listEl = modal.querySelector('.rj-imgmgr-list');
|
|
if (view === 'grid') {
|
|
gridEl.classList.remove('hidden');
|
|
listEl.classList.remove('active');
|
|
} else {
|
|
gridEl.classList.add('hidden');
|
|
listEl.classList.add('active');
|
|
}
|
|
render();
|
|
}
|
|
|
|
function sort(field) {
|
|
if (state.sort.field === field) {
|
|
state.sort.dir = state.sort.dir === 'asc' ? 'desc' : 'asc';
|
|
} else {
|
|
state.sort.field = field;
|
|
state.sort.dir = 'asc';
|
|
}
|
|
updateSortHeaderClasses();
|
|
applySortOrder();
|
|
state.page = 1;
|
|
filterAndRender();
|
|
}
|
|
|
|
// ============================================================
|
|
// ACTIONS
|
|
// ============================================================
|
|
function copyPath(path) {
|
|
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
navigator.clipboard.writeText(path).then(function () {
|
|
config.showToast(t('toast.copied'), 'success');
|
|
}).catch(function () {
|
|
fallbackCopy(path);
|
|
});
|
|
} else {
|
|
fallbackCopy(path);
|
|
}
|
|
}
|
|
|
|
function fallbackCopy(text) {
|
|
var ta = document.createElement('textarea');
|
|
ta.value = text;
|
|
ta.style.position = 'fixed';
|
|
ta.style.opacity = '0';
|
|
document.body.appendChild(ta);
|
|
ta.select();
|
|
try { document.execCommand('copy'); config.showToast(t('toast.copied'), 'success'); }
|
|
catch (e) { config.showToast(t('toast.error'), 'error'); }
|
|
document.body.removeChild(ta);
|
|
}
|
|
|
|
function deleteMedia(item) {
|
|
if (!item || !item.name) return;
|
|
var formData = new FormData();
|
|
formData.append('action', 'delete-media');
|
|
formData.append('type', item.type || 'image');
|
|
formData.append('filename', item.name);
|
|
formData.append('csrf_token', config.csrfToken);
|
|
|
|
fetch(config.apiUrl, { method: 'POST', body: formData })
|
|
.then(function (r) { return r.json(); })
|
|
.then(function (result) {
|
|
if (result.success) {
|
|
config.showToast(t('image.trashed'), 'success');
|
|
if (state.selectedPath === item.path || (state.selectedPath && state.selectedPath.indexOf('/' + item.name) !== -1)) {
|
|
state.selectedPath = null;
|
|
}
|
|
state.selectedPaths = state.selectedPaths.filter(function(path) { return path !== item.path; });
|
|
loadImages();
|
|
} else {
|
|
config.showToast(result.message || t('toast.error'), 'error');
|
|
}
|
|
})
|
|
.catch(function (err) {
|
|
config.showToast(err.message || t('toast.error'), 'error');
|
|
});
|
|
}
|
|
|
|
function restoreMedia(item) {
|
|
if (!item || !item.name) return;
|
|
var formData = new FormData();
|
|
formData.append('action', 'restore-media');
|
|
formData.append('type', item.type || 'image');
|
|
formData.append('filename', item.name);
|
|
formData.append('csrf_token', config.csrfToken);
|
|
|
|
fetch(config.apiUrl, { method: 'POST', body: formData })
|
|
.then(function (r) { return r.json(); })
|
|
.then(function (result) {
|
|
if (result.success) {
|
|
config.showToast(t('image.restored'), 'success');
|
|
loadImages();
|
|
} else {
|
|
config.showToast(result.message || t('toast.error'), 'error');
|
|
}
|
|
})
|
|
.catch(function (err) {
|
|
config.showToast(err.message || t('toast.error'), 'error');
|
|
});
|
|
}
|
|
|
|
function deleteMediaPermanently(item) {
|
|
if (!item || !item.name) return;
|
|
confirmAction(
|
|
t('image.delete_permanently'),
|
|
t('image.delete_permanently_confirm', { filename: item.name }),
|
|
function () {
|
|
var formData = new FormData();
|
|
formData.append('action', 'delete-media-trash');
|
|
formData.append('type', item.type || 'image');
|
|
formData.append('filename', item.name);
|
|
formData.append('csrf_token', config.csrfToken);
|
|
|
|
fetch(config.apiUrl, { method: 'POST', body: formData })
|
|
.then(function (r) { return r.json(); })
|
|
.then(function (result) {
|
|
if (result.success) {
|
|
config.showToast(t('image.deleted_permanently'), 'success');
|
|
loadImages();
|
|
} else {
|
|
config.showToast(result.message || t('toast.error'), 'error');
|
|
}
|
|
})
|
|
.catch(function (err) {
|
|
config.showToast(err.message || t('toast.error'), 'error');
|
|
});
|
|
}
|
|
);
|
|
}
|
|
|
|
// In-app name prompt: window.prompt() is unavailable in embedded
|
|
// webviews (Electron/VS Code) and clashes with the UI style anyway.
|
|
function promptFolderName(onSubmit) {
|
|
var existing = document.getElementById('rj-imgmgr-folder-prompt');
|
|
if (existing) existing.remove();
|
|
var dialog = document.createElement('div');
|
|
dialog.id = 'rj-imgmgr-folder-prompt';
|
|
dialog.className = 'rj-imgmgr-move active';
|
|
dialog.innerHTML =
|
|
'<div class="rj-imgmgr-move-backdrop"></div>' +
|
|
'<div class="rj-imgmgr-move-dialog">' +
|
|
'<div class="rj-imgmgr-move-header">' +
|
|
'<h3>' + escapeHtml(t('media.folder_create')) + '</h3>' +
|
|
'<button type="button" class="rj-imgmgr-close" aria-label="Close">×</button>' +
|
|
'</div>' +
|
|
'<div class="rj-imgmgr-move-body">' +
|
|
'<label>' + escapeHtml(t('media.folder_create_prompt')) + '</label>' +
|
|
'<input type="text" class="rj-imgmgr-folder-name-input" maxlength="60" spellcheck="false">' +
|
|
'</div>' +
|
|
'<div class="rj-imgmgr-move-footer">' +
|
|
'<button type="button" class="rj-imgmgr-btn rj-imgmgr-btn--secondary" data-action="cancel">' + escapeHtml(t('cancel')) + '</button>' +
|
|
'<button type="button" class="rj-imgmgr-btn rj-imgmgr-btn--primary" data-action="submit">' + escapeHtml(t('media.folder_create')) + '</button>' +
|
|
'</div>' +
|
|
'</div>';
|
|
document.body.appendChild(dialog);
|
|
var input = dialog.querySelector('.rj-imgmgr-folder-name-input');
|
|
var close = function () { dialog.remove(); };
|
|
var submit = function () {
|
|
var name = input.value.trim();
|
|
if (!name) { input.focus(); return; }
|
|
close();
|
|
onSubmit(name);
|
|
};
|
|
dialog.querySelector('.rj-imgmgr-move-backdrop').addEventListener('click', close);
|
|
dialog.querySelector('.rj-imgmgr-close').addEventListener('click', close);
|
|
dialog.querySelector('[data-action="cancel"]').addEventListener('click', close);
|
|
dialog.querySelector('[data-action="submit"]').addEventListener('click', submit);
|
|
input.addEventListener('keydown', function (event) {
|
|
if (event.key === 'Enter') { event.preventDefault(); submit(); }
|
|
if (event.key === 'Escape') { event.stopPropagation(); close(); }
|
|
});
|
|
input.focus();
|
|
}
|
|
|
|
function createMediaFolder() {
|
|
if (state.mode !== 'library') return;
|
|
promptFolderName(submitCreateMediaFolder);
|
|
}
|
|
|
|
function submitCreateMediaFolder(name) {
|
|
var formData = new FormData();
|
|
formData.append('action', 'create-media-folder');
|
|
formData.append('type', getActiveUploadType());
|
|
formData.append('folder', name);
|
|
formData.append('csrf_token', config.csrfToken);
|
|
|
|
fetch(config.apiUrl, { method: 'POST', body: formData })
|
|
.then(function (r) { return r.json(); })
|
|
.then(function (result) {
|
|
if (result.success) {
|
|
config.showToast(t('media.folder_created'), 'success');
|
|
state.folderFilter = result.data && result.data.folder ? result.data.folder : name;
|
|
state.expandedFolders[state.folderFilter] = true;
|
|
loadImages();
|
|
} else {
|
|
config.showToast(result.message || t('toast.error'), 'error');
|
|
}
|
|
})
|
|
.catch(function (err) {
|
|
config.showToast(err.message || t('toast.error'), 'error');
|
|
});
|
|
}
|
|
|
|
function deleteMediaFolder(folder) {
|
|
folder = (folder || '').trim();
|
|
if (state.mode !== 'library' || !folder || folder === 'all') return;
|
|
confirmAction(
|
|
t('media.folder_delete'),
|
|
t('media.folder_delete_confirm', { folder: folder }),
|
|
function () {
|
|
var formData = new FormData();
|
|
formData.append('action', 'delete-media-folder');
|
|
formData.append('type', getActiveUploadType());
|
|
formData.append('folder', folder);
|
|
formData.append('csrf_token', config.csrfToken);
|
|
|
|
fetch(config.apiUrl, { method: 'POST', body: formData })
|
|
.then(function (r) { return r.json(); })
|
|
.then(function (result) {
|
|
if (result.success) {
|
|
config.showToast(t('media.folder_deleted'), 'success');
|
|
delete state.expandedFolders[folder];
|
|
if (state.folderFilter === folder) state.folderFilter = 'all';
|
|
loadImages();
|
|
} else {
|
|
config.showToast(result.message || t('toast.error'), 'error');
|
|
}
|
|
})
|
|
.catch(function (err) {
|
|
config.showToast(err.message || t('toast.error'), 'error');
|
|
});
|
|
}
|
|
);
|
|
}
|
|
|
|
function moveMedia(item, target) {
|
|
if (!item || !item.name) return;
|
|
target = (target || '').trim();
|
|
if (target === (item.folder || '')) return;
|
|
var formData = new FormData();
|
|
formData.append('action', 'move-media');
|
|
formData.append('type', item.type || 'image');
|
|
formData.append('filename', item.name);
|
|
formData.append('folder', target);
|
|
formData.append('csrf_token', config.csrfToken);
|
|
|
|
fetch(config.apiUrl, { method: 'POST', body: formData })
|
|
.then(function (r) { return r.json(); })
|
|
.then(function (result) {
|
|
if (result.success) {
|
|
config.showToast(t('media.file_moved'), 'success');
|
|
closeMoveDialog();
|
|
if (target) state.expandedFolders[target] = true;
|
|
loadImages();
|
|
} else {
|
|
config.showToast(result.message || t('toast.error'), 'error');
|
|
}
|
|
})
|
|
.catch(function (err) {
|
|
config.showToast(err.message || t('toast.error'), 'error');
|
|
});
|
|
}
|
|
|
|
function emptyImageTrash() {
|
|
if (!state.data.length) {
|
|
config.showToast(t(isImageOnlyPicker() ? 'image.trash_empty' : 'media.trash_empty'), 'info');
|
|
return;
|
|
}
|
|
|
|
confirmAction(
|
|
t('image.empty_trash'),
|
|
t('image.empty_trash_confirm'),
|
|
function () {
|
|
var formData = new FormData();
|
|
formData.append('action', 'empty-media-trash');
|
|
formData.append('type', state.activeType === 'all' ? state.allowedTypes.join(',') : state.activeType);
|
|
formData.append('csrf_token', config.csrfToken);
|
|
|
|
fetch(config.apiUrl, { method: 'POST', body: formData })
|
|
.then(function (r) { return r.json(); })
|
|
.then(function (result) {
|
|
if (result.success) {
|
|
config.showToast(t('image.trash_emptied'), 'success');
|
|
loadImages();
|
|
} else {
|
|
config.showToast(result.message || t('toast.error'), 'error');
|
|
}
|
|
})
|
|
.catch(function (err) {
|
|
config.showToast(err.message || t('toast.error'), 'error');
|
|
});
|
|
}
|
|
);
|
|
}
|
|
|
|
function handleUpload(e) {
|
|
uploadFiles(e.target.files);
|
|
e.target.value = '';
|
|
}
|
|
|
|
function uploadFiles(files) {
|
|
var queue = Array.prototype.slice.call(files || []).filter(Boolean);
|
|
if (queue.length === 0) return;
|
|
|
|
if (queue.length === 1) {
|
|
uploadFile(queue[0]);
|
|
return;
|
|
}
|
|
|
|
var uploaded = 0;
|
|
var total = queue.length;
|
|
config.showToast(t('media.uploading_multiple'), 'info');
|
|
|
|
queue.reduce(function (chain, file) {
|
|
return chain.then(function () {
|
|
return uploadFile(file, {
|
|
deferReload: true,
|
|
quietProgress: true,
|
|
quietSuccess: true
|
|
}).then(function (success) {
|
|
if (success) uploaded += 1;
|
|
});
|
|
});
|
|
}, Promise.resolve()).then(function () {
|
|
if (uploaded > 0) {
|
|
config.showToast(t(uploaded === total ? 'media.uploaded_multiple' : 'media.upload_partial', {
|
|
uploaded: uploaded,
|
|
total: total,
|
|
count: uploaded
|
|
}), uploaded === total ? 'success' : 'info');
|
|
loadImages();
|
|
setTimeout(updateSelectionUI, 300);
|
|
}
|
|
});
|
|
}
|
|
|
|
function uploadFile(file, options) {
|
|
options = options || {};
|
|
if (!file) return Promise.resolve(false);
|
|
|
|
var type = getActiveUploadType();
|
|
var typeConfig = mediaTypes[type] || mediaTypes.image;
|
|
var ext = (file.name.split('.').pop() || '').toLowerCase();
|
|
if (typeConfig.extensions.indexOf(ext) === -1) {
|
|
config.showToast(t(type === 'image' ? 'image.format_error' : 'media.format_error'), 'error');
|
|
return Promise.resolve(false);
|
|
}
|
|
var limit = effectiveLimit(type);
|
|
if (limit.serverMax !== null && file.size > limit.serverMax && limit.serverBound) {
|
|
config.showToast(t('media.server_limit_error', { name: file.name, max: megabytes(limit.serverMax) }), 'error');
|
|
return Promise.resolve(false);
|
|
}
|
|
if (file.size > typeConfig.maxSize) {
|
|
config.showToast(t('media.size_limit_error', { name: file.name, max: megabytes(typeConfig.maxSize) }), 'error');
|
|
return Promise.resolve(false);
|
|
}
|
|
|
|
var formData = new FormData();
|
|
formData.append('action', 'upload-media');
|
|
formData.append('type', type);
|
|
formData.append('file', file);
|
|
if (state.mode === 'library' && state.folderFilter && state.folderFilter !== 'all') {
|
|
formData.append('folder', state.folderFilter);
|
|
}
|
|
formData.append('csrf_token', config.csrfToken);
|
|
|
|
if (!options.quietProgress) {
|
|
config.showToast(t('image.uploading'), 'info');
|
|
}
|
|
|
|
return fetch(config.apiUrl, { method: 'POST', body: formData })
|
|
.then(function (r) { return r.json(); })
|
|
.then(function (result) {
|
|
if (result.success) {
|
|
if (!options.quietSuccess) {
|
|
config.showToast(t('image.uploaded'), 'success');
|
|
}
|
|
if (result.data && result.data.webVersion) {
|
|
var wv = result.data.webVersion;
|
|
config.showToast(t('media.web_version_created', { width: wv.width, height: wv.height, size: fileSize(wv.bytes) }), 'info');
|
|
}
|
|
if (result.data && result.data.aiLabel) {
|
|
config.showToast(t('media.ai_detected'), 'info');
|
|
announceAiLabel(result.data.type || type, result.data.name, result.data.aiLabel);
|
|
}
|
|
state.sort = { field: 'date', dir: 'desc' };
|
|
if (state.isPicker) {
|
|
state.activeType = type;
|
|
}
|
|
if (!options.deferReload) {
|
|
loadImages();
|
|
}
|
|
if (result.data && result.data.path) {
|
|
var uploadedPath = result.data.path.replace(/^\.\.\//, '/');
|
|
if (state.multiplePicker) {
|
|
if (state.selectedPaths.indexOf(uploadedPath) === -1) state.selectedPaths.push(uploadedPath);
|
|
} else {
|
|
state.selectedPath = uploadedPath;
|
|
}
|
|
if (!options.deferReload) {
|
|
setTimeout(updateSelectionUI, 300);
|
|
}
|
|
}
|
|
return true;
|
|
} else {
|
|
config.showToast(result.message || t('toast.error'), 'error');
|
|
return false;
|
|
}
|
|
})
|
|
.catch(function (err) {
|
|
config.showToast(err.message || t('toast.error'), 'error');
|
|
return false;
|
|
});
|
|
}
|
|
|
|
// ============================================================
|
|
// LIGHTBOX
|
|
// ============================================================
|
|
function createLightbox() {
|
|
if (document.getElementById('rj-imgmgr-lightbox')) return;
|
|
|
|
var lb = document.createElement('div');
|
|
lb.id = 'rj-imgmgr-lightbox';
|
|
lb.className = 'rj-imgmgr-lightbox';
|
|
lb.innerHTML =
|
|
'<div class="rj-imgmgr-lightbox-content">' +
|
|
'<button type="button" class="rj-imgmgr-lightbox-close" aria-label="Close">×</button>' +
|
|
'<button type="button" class="rj-imgmgr-lightbox-nav rj-imgmgr-lightbox-nav--prev" data-action="lightbox-prev" aria-label="' + escapeHtml(t('media.previous_image')) + '">' + Icons.chevronLeft + '</button>' +
|
|
'<button type="button" class="rj-imgmgr-lightbox-nav rj-imgmgr-lightbox-nav--next" data-action="lightbox-next" aria-label="' + escapeHtml(t('media.next_image')) + '">' + Icons.chevronRight + '</button>' +
|
|
'<div class="rj-imgmgr-lightbox-stage"></div>' +
|
|
'<div class="rj-imgmgr-lightbox-info"></div>' +
|
|
'</div>';
|
|
document.body.appendChild(lb);
|
|
|
|
lb.addEventListener('click', function (e) {
|
|
if (e.target === lb) closeLightbox();
|
|
});
|
|
lb.querySelector('.rj-imgmgr-lightbox-close').addEventListener('click', closeLightbox);
|
|
lb.querySelector('[data-action="lightbox-prev"]').addEventListener('click', function(e) {
|
|
e.stopPropagation();
|
|
showAdjacentLightboxImage(-1);
|
|
});
|
|
lb.querySelector('[data-action="lightbox-next"]').addEventListener('click', function(e) {
|
|
e.stopPropagation();
|
|
showAdjacentLightboxImage(1);
|
|
});
|
|
|
|
document.addEventListener('keydown', function (e) {
|
|
if (e.key === 'Escape' && lb.classList.contains('active')) closeLightbox();
|
|
if (lb.classList.contains('active') && e.key === 'ArrowLeft') {
|
|
e.preventDefault();
|
|
showAdjacentLightboxImage(-1);
|
|
}
|
|
if (lb.classList.contains('active') && e.key === 'ArrowRight') {
|
|
e.preventDefault();
|
|
showAdjacentLightboxImage(1);
|
|
}
|
|
});
|
|
}
|
|
|
|
function openLightbox(itemOrPath, name) {
|
|
var item = typeof itemOrPath === 'object'
|
|
? itemOrPath
|
|
: { type: 'image', path: itemOrPath, name: name || '' };
|
|
state.lightboxItem = item;
|
|
var lb = document.getElementById('rj-imgmgr-lightbox');
|
|
if (!lb) { createLightbox(); lb = document.getElementById('rj-imgmgr-lightbox'); }
|
|
var stage = lb.querySelector('.rj-imgmgr-lightbox-stage');
|
|
var label = mediaDisplayName(item);
|
|
stage.classList.toggle('rj-imgmgr-lightbox-stage--image', isImage(item));
|
|
stage.classList.remove('rj-imgmgr-has-transparency');
|
|
if (isImage(item)) {
|
|
stage.innerHTML = '<img alt="" src="' + escapeHtml(previewSrc(item)) + '" onload="window.NbImageManager && NbImageManager.detectTransparency(this)">';
|
|
} else if (isAudio(item)) {
|
|
stage.innerHTML = '<div class="rj-imgmgr-lightbox-media rj-imgmgr-lightbox-media--audio">' +
|
|
mediaIcon(item) +
|
|
'<audio controls src="' + escapeHtml(item.path) + '"></audio>' +
|
|
'</div>';
|
|
} else if (isVideo(item)) {
|
|
stage.innerHTML = '<video class="rj-imgmgr-lightbox-video" controls src="' + escapeHtml(item.path) + '"></video>';
|
|
} else {
|
|
stage.innerHTML = '<div class="rj-imgmgr-lightbox-media rj-imgmgr-lightbox-media--document">' +
|
|
mediaIcon(item) +
|
|
'<a href="' + escapeHtml(item.path) + '" target="_blank" rel="noopener">' + escapeHtml(t('media.open_file')) + '</a>' +
|
|
'</div>';
|
|
}
|
|
lb.querySelector('.rj-imgmgr-lightbox-info').textContent = label;
|
|
updateLightboxNav();
|
|
lb.classList.add('active');
|
|
}
|
|
|
|
function lightboxImages() {
|
|
var items = (state.filtered && state.filtered.length ? state.filtered : state.data).filter(isImage);
|
|
return items.length ? items : (state.lightboxItem && isImage(state.lightboxItem) ? [state.lightboxItem] : []);
|
|
}
|
|
|
|
function updateLightboxNav() {
|
|
var lb = document.getElementById('rj-imgmgr-lightbox');
|
|
if (!lb) return;
|
|
var items = lightboxImages();
|
|
var canNavigate = items.length > 1 && state.lightboxItem && isImage(state.lightboxItem);
|
|
lb.querySelectorAll('.rj-imgmgr-lightbox-nav').forEach(function(btn) {
|
|
btn.hidden = !canNavigate;
|
|
});
|
|
}
|
|
|
|
function showAdjacentLightboxImage(direction) {
|
|
var items = lightboxImages();
|
|
if (items.length <= 1 || !state.lightboxItem) return;
|
|
var index = items.findIndex(function(item) { return item.path === state.lightboxItem.path; });
|
|
if (index === -1) index = 0;
|
|
var nextIndex = (index + direction + items.length) % items.length;
|
|
openLightbox(items[nextIndex]);
|
|
}
|
|
|
|
function closeLightbox() {
|
|
var lb = document.getElementById('rj-imgmgr-lightbox');
|
|
if (lb) {
|
|
lb.querySelectorAll('audio, video').forEach(function (media) { media.pause(); });
|
|
lb.classList.remove('active');
|
|
}
|
|
state.lightboxItem = null;
|
|
}
|
|
|
|
// ============================================================
|
|
// REPLACE DIALOG
|
|
// ============================================================
|
|
function createReplaceDialog() {
|
|
if (document.getElementById('rj-imgmgr-replace')) return;
|
|
|
|
var dialog = document.createElement('div');
|
|
dialog.id = 'rj-imgmgr-replace';
|
|
dialog.className = 'rj-imgmgr-replace';
|
|
dialog.innerHTML =
|
|
'<div class="rj-imgmgr-replace-backdrop"></div>' +
|
|
'<div class="rj-imgmgr-replace-dialog">' +
|
|
'<div class="rj-imgmgr-replace-header">' +
|
|
'<h3>' + escapeHtml(t('image.replace')) + '</h3>' +
|
|
'<button type="button" class="rj-imgmgr-close" aria-label="Close">×</button>' +
|
|
'</div>' +
|
|
'<div class="rj-imgmgr-replace-body">' +
|
|
'<p class="rj-imgmgr-replace-intro">' + escapeHtml(t('image.replacing')) + ' <strong class="rj-imgmgr-replace-target"></strong></p>' +
|
|
'<div class="rj-imgmgr-replace-options">' +
|
|
'<label class="rj-imgmgr-replace-option selected" data-option="replace">' +
|
|
'<input type="radio" name="rj-replace-option" value="replace" checked>' +
|
|
'<div class="rj-imgmgr-replace-option-content">' +
|
|
'<div class="rj-imgmgr-replace-option-title">' + escapeHtml(t('image.overwrite_file')) + '</div>' +
|
|
'<div class="rj-imgmgr-replace-option-desc">' + escapeHtml(t('image.overwrite_desc')) + '</div>' +
|
|
'</div>' +
|
|
'</label>' +
|
|
'<label class="rj-imgmgr-replace-option" data-option="new">' +
|
|
'<input type="radio" name="rj-replace-option" value="new">' +
|
|
'<div class="rj-imgmgr-replace-option-content">' +
|
|
'<div class="rj-imgmgr-replace-option-title">' + escapeHtml(t('image.save_new_name')) + '</div>' +
|
|
'<div class="rj-imgmgr-replace-option-desc">' + escapeHtml(t('image.save_new_desc')) + '</div>' +
|
|
'</div>' +
|
|
'</label>' +
|
|
'</div>' +
|
|
'<div class="rj-imgmgr-replace-file">' +
|
|
'<label>' + Icons.upload + ' <span>' + escapeHtml(t('image.choose_file')) + '</span>' +
|
|
'<input type="file" accept=".jpg,.jpeg,.png,.webp">' +
|
|
'</label>' +
|
|
'<div class="rj-imgmgr-replace-filename"></div>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
'<div class="rj-imgmgr-replace-footer">' +
|
|
'<button type="button" class="rj-imgmgr-btn rj-imgmgr-btn--secondary" data-action="cancel">' + escapeHtml(t('cancel')) + '</button>' +
|
|
'<button type="button" class="rj-imgmgr-btn rj-imgmgr-btn--primary" data-action="submit" disabled>' + escapeHtml(t('image.upload')) + '</button>' +
|
|
'</div>' +
|
|
'</div>';
|
|
document.body.appendChild(dialog);
|
|
|
|
dialog.querySelector('.rj-imgmgr-replace-backdrop').addEventListener('click', closeReplaceDialog);
|
|
dialog.querySelector('.rj-imgmgr-close').addEventListener('click', closeReplaceDialog);
|
|
dialog.querySelector('[data-action="cancel"]').addEventListener('click', closeReplaceDialog);
|
|
dialog.querySelector('[data-action="submit"]').addEventListener('click', handleReplaceSubmit);
|
|
|
|
dialog.querySelectorAll('.rj-imgmgr-replace-option').forEach(function (opt) {
|
|
opt.addEventListener('click', function () {
|
|
dialog.querySelectorAll('.rj-imgmgr-replace-option').forEach(function (o) {
|
|
o.classList.remove('selected');
|
|
});
|
|
opt.classList.add('selected');
|
|
opt.querySelector('input[type="radio"]').checked = true;
|
|
});
|
|
});
|
|
|
|
var fileInput = dialog.querySelector('input[type="file"]');
|
|
fileInput.addEventListener('change', function (e) {
|
|
var file = e.target.files[0];
|
|
var filenameEl = dialog.querySelector('.rj-imgmgr-replace-filename');
|
|
var submitBtn = dialog.querySelector('[data-action="submit"]');
|
|
if (file) {
|
|
filenameEl.textContent = file.name;
|
|
submitBtn.disabled = false;
|
|
} else {
|
|
filenameEl.textContent = '';
|
|
submitBtn.disabled = true;
|
|
}
|
|
});
|
|
}
|
|
|
|
function openReplaceDialog(filename, filepath) {
|
|
replaceTarget = { name: filename, path: filepath };
|
|
var dialog = document.getElementById('rj-imgmgr-replace');
|
|
if (!dialog) { createReplaceDialog(); dialog = document.getElementById('rj-imgmgr-replace'); }
|
|
|
|
dialog.querySelector('.rj-imgmgr-replace-target').textContent = filename;
|
|
dialog.querySelector('input[type="file"]').value = '';
|
|
dialog.querySelector('.rj-imgmgr-replace-filename').textContent = '';
|
|
dialog.querySelector('[data-action="submit"]').disabled = true;
|
|
|
|
dialog.querySelectorAll('.rj-imgmgr-replace-option').forEach(function (o, i) {
|
|
o.classList.toggle('selected', i === 0);
|
|
});
|
|
dialog.querySelector('input[name="rj-replace-option"][value="replace"]').checked = true;
|
|
|
|
dialog.classList.add('active');
|
|
}
|
|
|
|
function closeReplaceDialog() {
|
|
var dialog = document.getElementById('rj-imgmgr-replace');
|
|
if (dialog) dialog.classList.remove('active');
|
|
replaceTarget = null;
|
|
}
|
|
|
|
function handleReplaceSubmit() {
|
|
var dialog = document.getElementById('rj-imgmgr-replace');
|
|
var fileInput = dialog.querySelector('input[type="file"]');
|
|
var file = fileInput.files[0];
|
|
if (!file || !replaceTarget) return;
|
|
|
|
var allowed = ['image/jpeg', 'image/png', 'image/webp'];
|
|
if (allowed.indexOf(file.type) === -1) {
|
|
config.showToast(t('image.format_error'), 'error');
|
|
return;
|
|
}
|
|
if (file.size > mediaTypes.image.maxSize) {
|
|
config.showToast(t('media.size_limit_error', { name: file.name, max: megabytes(mediaTypes.image.maxSize) }), 'error');
|
|
return;
|
|
}
|
|
|
|
var option = dialog.querySelector('input[name="rj-replace-option"]:checked').value;
|
|
var targetFilename = replaceTarget.name;
|
|
|
|
if (option === 'new') {
|
|
targetFilename = file.name;
|
|
var existing = state.data.find(function (img) {
|
|
return (img.name || '').toLowerCase() === targetFilename.toLowerCase();
|
|
});
|
|
if (existing) {
|
|
config.showToast(t('image.exists', { filename: targetFilename }), 'error');
|
|
return;
|
|
}
|
|
}
|
|
|
|
var formData = new FormData();
|
|
formData.append('action', 'upload-image');
|
|
formData.append('image', file);
|
|
formData.append('filename', targetFilename);
|
|
formData.append('replace', option === 'replace' ? '1' : '0');
|
|
formData.append('csrf_token', config.csrfToken);
|
|
|
|
config.showToast(t('image.uploading'), 'info');
|
|
|
|
fetch(config.apiUrl, { method: 'POST', body: formData })
|
|
.then(function (r) { return r.json(); })
|
|
.then(function (result) {
|
|
if (result.success) {
|
|
config.showToast(option === 'replace' ? t('image.replaced') : t('image.uploaded'), 'success');
|
|
if (result.data) announceAiLabel('image', result.data.name, result.data.aiLabel);
|
|
closeReplaceDialog();
|
|
state.sort = { field: 'date', dir: 'desc' };
|
|
loadImages();
|
|
} else {
|
|
config.showToast(result.message || t('toast.error'), 'error');
|
|
}
|
|
})
|
|
.catch(function (err) {
|
|
config.showToast(err.message || t('toast.error'), 'error');
|
|
});
|
|
}
|
|
|
|
// ============================================================
|
|
// MOVE DIALOG
|
|
// ============================================================
|
|
function createMoveDialog() {
|
|
if (document.getElementById('rj-imgmgr-move')) return;
|
|
|
|
var dialog = document.createElement('div');
|
|
dialog.id = 'rj-imgmgr-move';
|
|
dialog.className = 'rj-imgmgr-move';
|
|
dialog.innerHTML =
|
|
'<div class="rj-imgmgr-move-backdrop"></div>' +
|
|
'<div class="rj-imgmgr-move-dialog">' +
|
|
'<div class="rj-imgmgr-move-header">' +
|
|
'<h3>' + escapeHtml(t('media.move_file')) + '</h3>' +
|
|
'<button type="button" class="rj-imgmgr-close" aria-label="Close">×</button>' +
|
|
'</div>' +
|
|
'<div class="rj-imgmgr-move-body">' +
|
|
'<p class="rj-imgmgr-move-filename"></p>' +
|
|
'<label>' + escapeHtml(t('media.folder')) + '</label>' +
|
|
'<select class="rj-imgmgr-move-select"></select>' +
|
|
'</div>' +
|
|
'<div class="rj-imgmgr-move-footer">' +
|
|
'<button type="button" class="rj-imgmgr-btn rj-imgmgr-btn--secondary" data-action="cancel">' + escapeHtml(t('cancel')) + '</button>' +
|
|
'<button type="button" class="rj-imgmgr-btn rj-imgmgr-btn--primary" data-action="submit">' + escapeHtml(t('media.move_file')) + '</button>' +
|
|
'</div>' +
|
|
'</div>';
|
|
document.body.appendChild(dialog);
|
|
|
|
dialog.querySelector('.rj-imgmgr-move-backdrop').addEventListener('click', closeMoveDialog);
|
|
dialog.querySelector('.rj-imgmgr-close').addEventListener('click', closeMoveDialog);
|
|
dialog.querySelector('[data-action="cancel"]').addEventListener('click', closeMoveDialog);
|
|
dialog.querySelector('[data-action="submit"]').addEventListener('click', function () {
|
|
var select = dialog.querySelector('.rj-imgmgr-move-select');
|
|
moveMedia(moveTarget, select ? select.value : '');
|
|
});
|
|
}
|
|
|
|
function openMoveDialog(item) {
|
|
moveTarget = item;
|
|
var dialog = document.getElementById('rj-imgmgr-move');
|
|
if (!dialog) { createMoveDialog(); dialog = document.getElementById('rj-imgmgr-move'); }
|
|
dialog.querySelector('.rj-imgmgr-move-filename').textContent = mediaDisplayName(item);
|
|
var select = dialog.querySelector('.rj-imgmgr-move-select');
|
|
select.innerHTML = '<option value="">' + escapeHtml(t('media.folder_root')) + '</option>' +
|
|
state.folders.map(function(folder) {
|
|
return '<option value="' + escapeHtml(folder) + '">' + escapeHtml(folder) + '</option>';
|
|
}).join('');
|
|
select.value = item.folder || '';
|
|
dialog.classList.add('active');
|
|
select.focus();
|
|
}
|
|
|
|
function closeMoveDialog() {
|
|
var dialog = document.getElementById('rj-imgmgr-move');
|
|
if (dialog) dialog.classList.remove('active');
|
|
moveTarget = null;
|
|
}
|
|
|
|
// ============================================================
|
|
// ORIGINAL DIALOG: where the uploaded file went, and what to do with it
|
|
// ============================================================
|
|
var originalTarget = null;
|
|
|
|
function createOriginalDialog() {
|
|
if (document.getElementById('rj-imgmgr-original')) return;
|
|
var dialog = document.createElement('div');
|
|
dialog.id = 'rj-imgmgr-original';
|
|
dialog.className = 'rj-imgmgr-move';
|
|
dialog.innerHTML =
|
|
'<div class="rj-imgmgr-move-backdrop"></div>' +
|
|
'<div class="rj-imgmgr-move-dialog rj-imgmgr-original-dialog" role="dialog" aria-modal="true" aria-labelledby="rj-imgmgr-original-title">' +
|
|
'<div class="rj-imgmgr-move-header">' +
|
|
'<h3 id="rj-imgmgr-original-title">' + escapeHtml(t('media.original_dialog_title')) + '</h3>' +
|
|
'<button type="button" class="rj-imgmgr-close" aria-label="Close">×</button>' +
|
|
'</div>' +
|
|
'<div class="rj-imgmgr-move-body">' +
|
|
'<p class="rj-imgmgr-move-filename"></p>' +
|
|
'<p class="rj-imgmgr-original-info"></p>' +
|
|
'<p class="rj-imgmgr-original-path"><code></code></p>' +
|
|
'<p class="rj-imgmgr-original-hint">' + escapeHtml(t('media.original_hint')) + '</p>' +
|
|
'</div>' +
|
|
'<div class="rj-imgmgr-move-footer rj-imgmgr-original-footer">' +
|
|
'<button type="button" class="rj-imgmgr-btn rj-imgmgr-btn--secondary" data-action="view">' + escapeHtml(t('media.original_view')) + '</button>' +
|
|
'<button type="button" class="rj-imgmgr-btn rj-imgmgr-btn--secondary" data-action="copy">' + escapeHtml(t('media.original_copy_path')) + '</button>' +
|
|
'<a class="rj-imgmgr-btn rj-imgmgr-btn--primary" data-action="download" href="#" download>' + escapeHtml(t('media.download_file')) + '</a>' +
|
|
'</div>' +
|
|
'</div>';
|
|
document.body.appendChild(dialog);
|
|
dialog.querySelector('.rj-imgmgr-move-backdrop').addEventListener('click', closeOriginalDialog);
|
|
dialog.querySelector('.rj-imgmgr-close').addEventListener('click', closeOriginalDialog);
|
|
dialog.querySelector('[data-action="view"]').addEventListener('click', function () {
|
|
if (!originalTarget) return;
|
|
var original = originalTarget.original;
|
|
closeOriginalDialog();
|
|
openLightbox(mediaDownloadUrl(original), original.name ? original.name.split('/').pop() : mediaDisplayName(originalTarget));
|
|
});
|
|
dialog.querySelector('[data-action="copy"]').addEventListener('click', function () {
|
|
if (originalTarget) copyPath(String(originalTarget.original.path || '').replace(/^\.\.\//, '/'));
|
|
});
|
|
}
|
|
|
|
function openOriginalDialog(item) {
|
|
if (!item || !item.original) return;
|
|
originalTarget = item;
|
|
var dialog = document.getElementById('rj-imgmgr-original');
|
|
if (!dialog) { createOriginalDialog(); dialog = document.getElementById('rj-imgmgr-original'); }
|
|
var original = item.original;
|
|
var originalName = original.name ? original.name.split('/').pop() : mediaDisplayName(item);
|
|
dialog.querySelector('.rj-imgmgr-move-filename').textContent = originalName;
|
|
dialog.querySelector('.rj-imgmgr-original-info').textContent = t('media.original_info', { size: original.size || '', width: original.width || '?', height: original.height || '?', web: item.size || '' });
|
|
dialog.querySelector('.rj-imgmgr-original-path code').textContent = String(original.path || '').replace(/^\.\.\//, '/');
|
|
var link = dialog.querySelector('[data-action="download"]');
|
|
link.href = mediaDownloadUrl(original);
|
|
link.setAttribute('download', originalName);
|
|
dialog.classList.add('active');
|
|
dialog.querySelector('[data-action="download"]').focus();
|
|
}
|
|
|
|
function closeOriginalDialog() {
|
|
var dialog = document.getElementById('rj-imgmgr-original');
|
|
if (dialog) dialog.classList.remove('active');
|
|
originalTarget = null;
|
|
}
|
|
|
|
// ============================================================
|
|
// RENAME DIALOG
|
|
// ============================================================
|
|
function createRenameDialog() {
|
|
if (document.getElementById('rj-imgmgr-rename')) return;
|
|
|
|
var dialog = document.createElement('div');
|
|
dialog.id = 'rj-imgmgr-rename';
|
|
dialog.className = 'rj-imgmgr-move';
|
|
dialog.innerHTML =
|
|
'<div class="rj-imgmgr-move-backdrop"></div>' +
|
|
'<div class="rj-imgmgr-move-dialog rj-imgmgr-rename-dialog">' +
|
|
'<div class="rj-imgmgr-move-header">' +
|
|
'<h3>' + escapeHtml(t('media.rename_file')) + '</h3>' +
|
|
'<button type="button" class="rj-imgmgr-close" aria-label="Close">×</button>' +
|
|
'</div>' +
|
|
'<div class="rj-imgmgr-move-body">' +
|
|
'<p class="rj-imgmgr-move-filename"></p>' +
|
|
'<label>' + escapeHtml(t('media.rename_new_name')) + '</label>' +
|
|
'<input type="text" class="rj-imgmgr-rename-input" maxlength="180" spellcheck="false">' +
|
|
'<label class="rj-imgmgr-rename-check">' +
|
|
'<input type="checkbox" class="rj-imgmgr-rename-scan" checked> ' +
|
|
'<span>' + escapeHtml(t('media.rename_scan_references')) + '</span>' +
|
|
'</label>' +
|
|
'<p class="rj-imgmgr-rename-hint">' + escapeHtml(t('media.rename_extension_hint')) + '</p>' +
|
|
'<div class="rj-imgmgr-rename-references" hidden></div>' +
|
|
'</div>' +
|
|
'<div class="rj-imgmgr-move-footer">' +
|
|
'<button type="button" class="rj-imgmgr-btn rj-imgmgr-btn--secondary" data-action="cancel">' + escapeHtml(t('cancel')) + '</button>' +
|
|
'<button type="button" class="rj-imgmgr-btn rj-imgmgr-btn--primary" data-action="submit">' + escapeHtml(t('media.rename_file')) + '</button>' +
|
|
'</div>' +
|
|
'</div>';
|
|
document.body.appendChild(dialog);
|
|
|
|
dialog.querySelector('.rj-imgmgr-move-backdrop').addEventListener('click', closeRenameDialog);
|
|
dialog.querySelector('.rj-imgmgr-close').addEventListener('click', closeRenameDialog);
|
|
dialog.querySelector('[data-action="cancel"]').addEventListener('click', closeRenameDialog);
|
|
dialog.querySelector('[data-action="submit"]').addEventListener('click', function () {
|
|
submitRename(false);
|
|
});
|
|
dialog.querySelector('.rj-imgmgr-rename-input').addEventListener('keydown', function (event) {
|
|
if (event.key === 'Enter') {
|
|
event.preventDefault();
|
|
submitRename(false);
|
|
}
|
|
});
|
|
}
|
|
|
|
function openRenameDialog(item) {
|
|
if (!item || !item.name || state.mode !== 'library') return;
|
|
renameTarget = item;
|
|
pendingRename = null;
|
|
var dialog = document.getElementById('rj-imgmgr-rename');
|
|
if (!dialog) { createRenameDialog(); dialog = document.getElementById('rj-imgmgr-rename'); }
|
|
dialog.querySelector('.rj-imgmgr-move-filename').textContent = item.name;
|
|
dialog.querySelector('.rj-imgmgr-rename-input').value = mediaDisplayName(item);
|
|
dialog.querySelector('.rj-imgmgr-rename-scan').checked = true;
|
|
var refs = dialog.querySelector('.rj-imgmgr-rename-references');
|
|
refs.hidden = true;
|
|
refs.innerHTML = '';
|
|
dialog.classList.add('active');
|
|
dialog.querySelector('.rj-imgmgr-rename-input').focus();
|
|
dialog.querySelector('.rj-imgmgr-rename-input').select();
|
|
}
|
|
|
|
function closeRenameDialog() {
|
|
var dialog = document.getElementById('rj-imgmgr-rename');
|
|
if (dialog) dialog.classList.remove('active');
|
|
renameTarget = null;
|
|
pendingRename = null;
|
|
}
|
|
|
|
// ============================================================
|
|
// AI LABEL DIALOG
|
|
// ============================================================
|
|
function createAiLabelDialog() {
|
|
if (document.getElementById('rj-imgmgr-ai-label')) return;
|
|
|
|
var option = function (value, title, desc) {
|
|
return '<label class="rj-imgmgr-replace-option" data-option="' + value + '">' +
|
|
'<input type="radio" name="rj-imgmgr-ai-label" value="' + value + '">' +
|
|
'<div class="rj-imgmgr-replace-option-content">' +
|
|
'<div class="rj-imgmgr-replace-option-title">' + escapeHtml(title) + '</div>' +
|
|
(desc ? '<div class="rj-imgmgr-replace-option-desc">' + escapeHtml(desc) + '</div>' : '') +
|
|
'</div>' +
|
|
'</label>';
|
|
};
|
|
var dialog = document.createElement('div');
|
|
dialog.id = 'rj-imgmgr-ai-label';
|
|
dialog.className = 'rj-imgmgr-move';
|
|
dialog.innerHTML =
|
|
'<div class="rj-imgmgr-move-backdrop"></div>' +
|
|
'<div class="rj-imgmgr-move-dialog rj-imgmgr-ai-dialog" role="dialog" aria-modal="true" aria-labelledby="rj-imgmgr-ai-title">' +
|
|
'<div class="rj-imgmgr-move-header">' +
|
|
'<h3 id="rj-imgmgr-ai-title">' + escapeHtml(t('media.ai_label')) + '</h3>' +
|
|
'<button type="button" class="rj-imgmgr-close" aria-label="' + escapeHtml(t('close')) + '">×</button>' +
|
|
'</div>' +
|
|
'<div class="rj-imgmgr-move-body">' +
|
|
'<p class="rj-imgmgr-move-filename"></p>' +
|
|
'<div class="rj-imgmgr-replace-options">' +
|
|
option('', t('media.ai_none'), '') +
|
|
option('generated', t('media.ai_generated'), t('media.ai_generated_desc')) +
|
|
option('modified', t('media.ai_modified'), t('media.ai_modified_desc')) +
|
|
'</div>' +
|
|
'<p class="rj-imgmgr-ai-hint">' + escapeHtml(t('media.ai_hint')) + '</p>' +
|
|
'</div>' +
|
|
'<div class="rj-imgmgr-move-footer">' +
|
|
'<button type="button" class="rj-imgmgr-btn rj-imgmgr-btn--secondary" data-action="cancel">' + escapeHtml(t('cancel')) + '</button>' +
|
|
'<button type="button" class="rj-imgmgr-btn rj-imgmgr-btn--primary" data-action="submit">' + escapeHtml(t('save')) + '</button>' +
|
|
'</div>' +
|
|
'</div>';
|
|
document.body.appendChild(dialog);
|
|
|
|
dialog.querySelector('.rj-imgmgr-move-backdrop').addEventListener('click', closeAiLabelDialog);
|
|
dialog.querySelector('.rj-imgmgr-close').addEventListener('click', closeAiLabelDialog);
|
|
dialog.querySelector('[data-action="cancel"]').addEventListener('click', closeAiLabelDialog);
|
|
dialog.querySelector('[data-action="submit"]').addEventListener('click', submitAiLabel);
|
|
dialog.querySelectorAll('input[name="rj-imgmgr-ai-label"]').forEach(function (input) {
|
|
input.addEventListener('change', function () { selectAiLabelOption(input.value); });
|
|
});
|
|
}
|
|
|
|
function selectAiLabelOption(value) {
|
|
var dialog = document.getElementById('rj-imgmgr-ai-label');
|
|
if (!dialog) return;
|
|
dialog.querySelectorAll('.rj-imgmgr-replace-option').forEach(function (option) {
|
|
var active = option.dataset.option === value;
|
|
option.classList.toggle('selected', active);
|
|
option.querySelector('input').checked = active;
|
|
});
|
|
}
|
|
|
|
function openAiLabelDialog(item) {
|
|
if (!canAiLabel(item) || !item.name || state.mode !== 'library') return;
|
|
aiLabelTarget = item;
|
|
var dialog = document.getElementById('rj-imgmgr-ai-label');
|
|
if (!dialog) { createAiLabelDialog(); dialog = document.getElementById('rj-imgmgr-ai-label'); }
|
|
dialog.querySelector('.rj-imgmgr-move-filename').textContent = item.name;
|
|
selectAiLabelOption(item.aiLabel || '');
|
|
dialog.classList.add('active');
|
|
var checked = dialog.querySelector('input[name="rj-imgmgr-ai-label"]:checked');
|
|
if (checked) checked.focus();
|
|
}
|
|
|
|
function closeAiLabelDialog() {
|
|
var dialog = document.getElementById('rj-imgmgr-ai-label');
|
|
if (dialog) dialog.classList.remove('active');
|
|
aiLabelTarget = null;
|
|
}
|
|
|
|
function submitAiLabel() {
|
|
if (!aiLabelTarget || !aiLabelTarget.name) return;
|
|
var dialog = document.getElementById('rj-imgmgr-ai-label');
|
|
var checked = dialog.querySelector('input[name="rj-imgmgr-ai-label"]:checked');
|
|
var submit = dialog.querySelector('[data-action="submit"]');
|
|
var formData = new FormData();
|
|
formData.append('action', 'set-media-ai-label');
|
|
formData.append('type', aiLabelTarget.type || 'image');
|
|
formData.append('filename', aiLabelTarget.name);
|
|
formData.append('label', checked ? checked.value : '');
|
|
formData.append('csrf_token', config.csrfToken);
|
|
var target = aiLabelTarget;
|
|
|
|
submit.disabled = true;
|
|
fetch(config.apiUrl, { method: 'POST', body: formData })
|
|
.then(function (r) { return r.json(); })
|
|
.then(function (result) {
|
|
if (result.success) {
|
|
config.showToast(t('media.ai_saved'), 'success');
|
|
announceAiLabel(target.type || 'image', target.name, result.data ? result.data.aiLabel : (checked ? checked.value : ''));
|
|
closeAiLabelDialog();
|
|
loadImages();
|
|
return;
|
|
}
|
|
config.showToast(result.message || t('toast.error'), 'error');
|
|
})
|
|
.catch(function (err) {
|
|
config.showToast(err.message || t('toast.error'), 'error');
|
|
})
|
|
.finally(function () {
|
|
submit.disabled = false;
|
|
});
|
|
}
|
|
|
|
function renderRenameReferences(references) {
|
|
var dialog = document.getElementById('rj-imgmgr-rename');
|
|
if (!dialog) return;
|
|
var box = dialog.querySelector('.rj-imgmgr-rename-references');
|
|
references = Array.isArray(references) ? references : [];
|
|
if (!references.length) {
|
|
box.hidden = true;
|
|
box.innerHTML = '';
|
|
return;
|
|
}
|
|
var count = references.reduce(function(total, item) {
|
|
return total + (parseInt(item.count || 0, 10) || 0);
|
|
}, 0);
|
|
box.hidden = false;
|
|
box.innerHTML =
|
|
'<strong>' + escapeHtml(t('media.rename_references_found', { count: count })) + '</strong>' +
|
|
'<ul>' + references.slice(0, 10).map(function(ref) {
|
|
var first = ref.matches && ref.matches[0] ? ref.matches[0] : null;
|
|
var line = first && first.line ? ':' + first.line : '';
|
|
var snippet = first && first.snippet ? '<small>' + escapeHtml(first.snippet) + '</small>' : '';
|
|
return '<li><span>' + escapeHtml(ref.file + line) + '</span>' + snippet + '</li>';
|
|
}).join('') + '</ul>' +
|
|
'<p>' + escapeHtml(t('media.rename_references_confirm')) + '</p>' +
|
|
'<button type="button" class="rj-imgmgr-btn rj-imgmgr-btn--primary" data-action="confirm-rename">' + escapeHtml(t('media.rename_confirm_anyway')) + '</button>';
|
|
box.querySelector('[data-action="confirm-rename"]').addEventListener('click', function () {
|
|
submitRename(true);
|
|
});
|
|
}
|
|
|
|
function submitRename(confirmReferences) {
|
|
if (!renameTarget || !renameTarget.name) return;
|
|
var dialog = document.getElementById('rj-imgmgr-rename');
|
|
var input = dialog.querySelector('.rj-imgmgr-rename-input');
|
|
var submit = dialog.querySelector('[data-action="submit"]');
|
|
var newName = (input.value || '').trim();
|
|
if (!newName) {
|
|
input.focus();
|
|
return;
|
|
}
|
|
|
|
var formData = new FormData();
|
|
formData.append('action', 'rename-media');
|
|
formData.append('type', renameTarget.type || 'image');
|
|
formData.append('filename', renameTarget.name);
|
|
formData.append('newName', newName);
|
|
formData.append('scanReferences', dialog.querySelector('.rj-imgmgr-rename-scan').checked ? '1' : '0');
|
|
formData.append('confirmReferences', confirmReferences ? '1' : '0');
|
|
formData.append('csrf_token', config.csrfToken);
|
|
|
|
submit.disabled = true;
|
|
fetch(config.apiUrl, { method: 'POST', body: formData })
|
|
.then(function (r) { return r.json(); })
|
|
.then(function (result) {
|
|
if (result.success) {
|
|
var oldPath = renameTarget.path;
|
|
config.showToast(t('media.file_renamed'), 'success');
|
|
if (state.selectedPath === renameTarget.path && result.data && result.data.path) {
|
|
state.selectedPath = result.data.path.replace(/^\.\.\//, '/');
|
|
}
|
|
state.selectedPaths = state.selectedPaths.map(function(path) {
|
|
return path === oldPath && result.data && result.data.path
|
|
? result.data.path.replace(/^\.\.\//, '/')
|
|
: path;
|
|
});
|
|
closeRenameDialog();
|
|
loadImages();
|
|
return;
|
|
}
|
|
if (result.data && result.data.requiresConfirmation) {
|
|
pendingRename = result.data;
|
|
renderRenameReferences(result.data.references || []);
|
|
return;
|
|
}
|
|
config.showToast(result.message || t('toast.error'), 'error');
|
|
})
|
|
.catch(function (err) {
|
|
config.showToast(err.message || t('toast.error'), 'error');
|
|
})
|
|
.finally(function () {
|
|
submit.disabled = false;
|
|
});
|
|
}
|
|
|
|
// ============================================================
|
|
// PUBLIC API
|
|
// ============================================================
|
|
window.NbImageManager = {
|
|
init: function (options) {
|
|
Object.assign(config, options || {});
|
|
},
|
|
open: open,
|
|
mount: mount,
|
|
preview: function (path, name) {
|
|
var item = state.data.find(function (entry) { return entry.path === path; });
|
|
openLightbox(item || path, name || path);
|
|
},
|
|
detectTransparency: detectTransparency,
|
|
close: close,
|
|
confirmSelection: confirmSelection,
|
|
refresh: function () {
|
|
if (!document.getElementById('rj-imgmgr-modal')) return;
|
|
updateModeUI();
|
|
updateTypeUI();
|
|
filterAndRender();
|
|
},
|
|
};
|
|
})();
|