/*
* 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: '',
grid: '',
list: '',
eye: '',
download: '',
ai: '',
replace: '',
delete: '',
copy: '',
restore: '',
folder: '',
folderPlus: '',
layers: '',
move: '',
rename: '',
chevronRight: '',
chevronLeft: '',
};
// ============================================================
// 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 =
'
' +
'
' +
'
' +
'
' +
'' +
'' +
'
' +
'
';
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 '' + Icons.download + '';
}
return '' + Icons.download + '';
}
// "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 '';
}
if (item.large) {
return '' + escapeHtml(t('media.large_badge')) + '';
}
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 '' +
'' + escapeHtml(mediaDisplayName(item)) + '' + sizeBadgeHtml(item) +
(folder ? '' + escapeHtml(folder) + '' : '') +
'
';
}
function mediaIcon(item) {
if (isAudio(item)) return '';
if (isVideo(item)) return '';
return '';
}
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 '' +
escapeHtml(t('media.ai_badge')) + '';
}
// 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 '';
}
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 '';
}
return '';
}
// ============================================================
// 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 =
'' +
'' +
'' +
'
' +
'
' +
'' +
'
' +
'' +
'
' +
'
' +
'' +
'
' +
'
' +
'
' +
'
' +
'
' +
'' + escapeHtml(t('image.drop_files')) + '' +
'' + escapeHtml(t('image.or')) + '' +
'' +
'' +
'
' +
'
' +
'' +
'
';
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 '' +
'' + (entry.icon || '') + '' +
'' + escapeHtml(entry.label) + '' +
'' + count + '' +
(entry.deletable
? ''
: '') +
'
';
}).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 = '' + escapeHtml(t('image.loading')) + '
';
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 = '' + escapeHtml(t(emptyKey)) + '
';
gridEl.innerHTML = empty;
listBody.innerHTML = empty;
}
})
.catch(function (err) {
gridEl.innerHTML = '' + escapeHtml(err.message) + '
';
});
}
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 = '';
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 = '' + escapeHtml(msg) + '
';
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') +
'' +
'' +
'' +
'' +
'
'
:
mediaThumbHtml(image, 'rj-imgmgr-thumb', true) +
mediaNameHtml(image, 'rj-imgmgr-name') +
'' +
'' +
mediaDownloadActionHtml(image) +
'' +
'' +
'' +
(isImage(image) ? '' : '') +
aiLabelActionHtml(image) +
'' +
'
';
if (state.mode !== 'trash' && state.isPicker) {
item.insertAdjacentHTML('afterbegin', '');
}
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 = '' + escapeHtml(msg) + '
';
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'
? '' +
mediaThumbHtml(image, 'rj-imgmgr-list-thumb', true) +
mediaNameHtml(image, 'rj-imgmgr-list-name') +
'' + escapeHtml(image.size || '-') + '
' +
'' + escapeHtml(image.dateFormatted || '-') + '
' +
'' +
'' +
'' +
'' +
'
'
:
'' +
mediaThumbHtml(image, 'rj-imgmgr-list-thumb', true) +
mediaNameHtml(image, 'rj-imgmgr-list-name') +
'' + escapeHtml(image.size || '-') + '
' +
'' + escapeHtml(image.dateFormatted || '-') + '
' +
'' +
'' +
mediaDownloadActionHtml(image) +
'' +
'' +
'' +
(isImage(image) ? '' : '') +
aiLabelActionHtml(image) +
'' +
'
';
if (state.mode !== 'trash' && state.isPicker) {
row.querySelector('div:first-child').outerHTML = '';
}
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 =
'' +
'' +
'' +
'
' +
'' +
'' +
'
' +
'' +
'
';
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 =
'' +
'
' +
'
' +
'
' +
'
' +
'
' +
'
';
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 = '
';
} else if (isAudio(item)) {
stage.innerHTML = '';
} else if (isVideo(item)) {
stage.innerHTML = '';
} else {
stage.innerHTML = '';
}
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 =
'' +
'' +
'' +
'
' +
'
' + escapeHtml(t('image.replacing')) + '
' +
'
' +
'
' +
'
' +
'
' +
'
' +
'
' +
'
' +
'
' +
'
' +
'' +
'
';
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 =
'' +
'' +
'' +
'
' +
'
' +
'
' +
'
' +
'
' +
'' +
'
';
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 = '' +
state.folders.map(function(folder) {
return '';
}).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 =
'' +
'' +
'' +
'
' +
'
' +
'
' +
'
' +
'
' + escapeHtml(t('media.original_hint')) + '
' +
'
' +
'' +
'
';
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 =
'' +
'' +
'' +
'
' +
'
' +
'
' +
'
' +
'
' +
'
' + escapeHtml(t('media.rename_extension_hint')) + '
' +
'
' +
'
' +
'' +
'
';
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 '';
};
var dialog = document.createElement('div');
dialog.id = 'rj-imgmgr-ai-label';
dialog.className = 'rj-imgmgr-move';
dialog.innerHTML =
'' +
'' +
'' +
'
' +
'
' +
'
' +
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')) +
'
' +
'
' + escapeHtml(t('media.ai_hint')) + '
' +
'
' +
'' +
'
';
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 =
'' + escapeHtml(t('media.rename_references_found', { count: count })) + '' +
'' + 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 ? '' + escapeHtml(first.snippet) + '' : '';
return '- ' + escapeHtml(ref.file + line) + '' + snippet + '
';
}).join('') + '
' +
'' + escapeHtml(t('media.rename_references_confirm')) + '
' +
'';
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();
},
};
})();