Files
newznab-tmux/resources/js/csp-safe.js
T
2025-10-26 22:12:10 +01:00

4366 lines
162 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// CSP-Safe JavaScript - All inline event handlers moved here
// This file handles all onclick, onchange, and other inline event handlers
document.addEventListener('DOMContentLoaded', function() {
initEventDelegation();
initToastNotifications();
initNfoModal();
initConfirmDialogs();
initSelectRedirects();
initLogoutForms();
initSeasonSwitcher();
initBinaryBlacklist();
initImageModal();
initTabSwitcher();
initPreviewModal();
initRegexManagement();
initMediainfoAndFilelist();
initCartFunctionality();
initAdminMenu();
initSidebarToggle();
initDropdownMenus();
initImageFallbacks();
initProfileTabs();
initAdminUserEdit();
initMobileEnhancements();
initAdminDashboardCharts();
initAdminGroups();
initTinyMCE();
initAdminSpecificFeatures();
// Initialize page-specific functionality from inline scripts
initMyMovies();
initAuthPages();
initProfileEdit();
initDetailsPageImageModal();
initAddToCart();
initMoviesLayoutToggle();
});
// Event delegation for dynamically added elements
function initEventDelegation() {
document.addEventListener('click', function(e) {
// Handle toast close buttons
if (e.target.classList.contains('toast-close') || e.target.closest('.toast-close')) {
const toast = e.target.closest('.toast-notification');
if (toast) {
toast.remove();
}
}
// Handle NFO modal close
if (e.target.hasAttribute('data-close-nfo-modal') || e.target.closest('[data-close-nfo-modal]')) {
e.preventDefault();
closeNfoModal();
}
// Handle NFO modal open
if (e.target.hasAttribute('data-open-nfo') || e.target.closest('[data-open-nfo]')) {
e.preventDefault();
const guid = e.target.getAttribute('data-open-nfo') || e.target.closest('[data-open-nfo]').getAttribute('data-open-nfo');
if (guid) {
openNfoModal(guid);
}
}
// Handle logout
if (e.target.hasAttribute('data-logout') || e.target.closest('[data-logout]')) {
e.preventDefault();
const logoutForm = document.getElementById('logout-form');
if (logoutForm) {
logoutForm.submit();
}
}
// Handle confirm delete
if (e.target.hasAttribute('data-confirm-delete') || e.target.closest('[data-confirm-delete]')) {
const confirmed = confirm('Are you sure you want to delete this item?');
if (!confirmed) {
e.preventDefault();
}
}
// Handle confirm action
if (e.target.hasAttribute('data-confirm') || e.target.closest('[data-confirm]')) {
const message = e.target.getAttribute('data-confirm') || e.target.closest('[data-confirm]').getAttribute('data-confirm');
const confirmed = confirm(message);
if (!confirmed) {
e.preventDefault();
}
}
// Handle download NZB buttons
if (e.target.classList.contains('download-nzb') || e.target.closest('.download-nzb')) {
if (typeof showToast === 'function') {
showToast('Downloading NZB...', 'success');
}
}
// Handle season switcher
if (e.target.hasAttribute('data-season') || e.target.closest('[data-season]')) {
e.preventDefault();
const seasonNumber = e.target.getAttribute('data-season') || e.target.closest('[data-season]').getAttribute('data-season');
switchSeason(seasonNumber);
}
// Handle binary blacklist delete
if (e.target.hasAttribute('data-delete-blacklist') || e.target.closest('[data-delete-blacklist]')) {
const id = e.target.getAttribute('data-delete-blacklist') || e.target.closest('[data-delete-blacklist]').getAttribute('data-delete-blacklist');
const confirmed = confirm('Are you sure? This will delete the blacklist from this list.');
if (confirmed && typeof ajax_binaryblacklist_delete === 'function') {
ajax_binaryblacklist_delete(id);
}
e.preventDefault();
}
// Handle admin groups management actions
const actionTarget = e.target.closest('[data-action]');
if (actionTarget) {
const action = actionTarget.dataset.action;
const groupId = actionTarget.dataset.groupId;
const status = actionTarget.dataset.status;
switch(action) {
case 'show-reset-modal':
showResetAllModal();
break;
case 'hide-reset-modal':
hideResetAllModal();
break;
case 'show-purge-modal':
showPurgeAllModal();
break;
case 'hide-purge-modal':
hidePurgeAllModal();
break;
case 'toggle-group-status':
ajax_group_status(groupId, status);
break;
case 'toggle-backfill':
ajax_backfill_status(groupId, status);
break;
case 'reset-group':
ajax_group_reset(groupId);
break;
case 'delete-group':
confirmGroupDelete(groupId);
break;
case 'purge-group':
confirmGroupPurge(groupId);
break;
case 'reset-all':
ajax_group_reset_all();
break;
case 'purge-all':
ajax_group_purge_all();
break;
}
}
});
// Handle select redirects
document.addEventListener('change', function(e) {
if (e.target.hasAttribute('data-redirect-on-change')) {
const url = e.target.value;
if (url && url !== '#') {
window.location.href = url;
}
}
});
}
// Toast Notifications
function initToastNotifications() {
// showToast is defined globally for backward compatibility
window.showToast = function(message, type) {
type = type || 'success';
let container = document.getElementById('toast-container');
if (!container) {
// Create container if it doesn't exist
container = document.createElement('div');
container.id = 'toast-container';
container.className = 'fixed top-4 right-4 z-50';
document.body.appendChild(container);
}
const toast = document.createElement('div');
toast.className = 'toast-notification ' + type;
const iconClass = type === 'success' ? 'fa-check-circle' :
type === 'error' ? 'fa-exclamation-circle' :
'fa-info-circle';
toast.innerHTML =
'<span class="toast-icon"><i class="fas ' + iconClass + '"></i></span>' +
'<span class="toast-message">' + escapeHtml(message) + '</span>' +
'<button class="toast-close" type="button" aria-label="Close">×</button>';
container.appendChild(toast);
// Auto-remove after 5 seconds
setTimeout(function() {
toast.classList.add('removing');
setTimeout(function() {
toast.remove();
}, 300);
}, 5000);
};
}
// NFO Modal
function initNfoModal() {
window.openNfoModal = function(guid) {
const modal = document.getElementById('nfoModal');
const content = document.getElementById('nfoContent');
if (!modal || !content) return;
// Show modal
modal.classList.remove('hidden');
// Reset content with loading message
content.innerHTML = '<div class="flex items-center justify-center py-8"><i class="fas fa-spinner fa-spin text-2xl mr-2"></i><span>Loading NFO...</span></div>';
// Fetch NFO content
const baseUrl = document.querySelector('meta[name="app-url"]')?.content || '';
fetch(baseUrl + '/nfo/' + guid + '?modal=1')
.then(response => {
if (!response.ok) {
throw new Error('NFO not found');
}
return response.text();
})
.then(html => {
// Extract the NFO content from the response
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const nfoText = doc.querySelector('pre')?.textContent || doc.body.textContent;
content.textContent = nfoText;
})
.catch(error => {
content.innerHTML = '<div class="text-center py-8 text-red-400"><i class="fas fa-exclamation-triangle text-2xl mr-2"></i><span>Error loading NFO file</span></div>';
console.error('Error loading NFO:', error);
});
};
window.closeNfoModal = function() {
const modal = document.getElementById('nfoModal');
if (modal) {
modal.classList.add('hidden');
}
};
// Close modal on Escape key
document.addEventListener('keydown', function(event) {
if (event.key === 'Escape') {
const modal = document.getElementById('nfoModal');
if (modal && !modal.classList.contains('hidden')) {
closeNfoModal();
}
}
});
// Add click handlers for NFO badges
document.addEventListener('click', function(event) {
if (event.target.closest('.nfo-badge')) {
event.preventDefault();
const badge = event.target.closest('.nfo-badge');
const guid = badge.getAttribute('data-guid');
if (guid) {
openNfoModal(guid);
}
}
});
}
// Confirm dialogs
function initConfirmDialogs() {
window.confirmDelete = function(id) {
if (confirm('Are you sure you want to delete this item?')) {
// If there's a form with this ID, submit it
const form = document.getElementById('delete-form-' + id);
if (form) {
form.submit();
}
return true;
}
return false;
};
}
// Select redirects
function initSelectRedirects() {
// Already handled in event delegation
}
// Logout forms
function initLogoutForms() {
// Already handled in event delegation
}
// Season switcher for series
function initSeasonSwitcher() {
window.switchSeason = function(seasonNumber) {
// Hide all season containers
document.querySelectorAll('[data-season-container]').forEach(function(container) {
container.style.display = 'none';
});
// Show selected season
const selectedSeason = document.querySelector('[data-season-container="' + seasonNumber + '"]');
if (selectedSeason) {
selectedSeason.style.display = 'block';
}
// Update button states
document.querySelectorAll('[data-season]').forEach(function(btn) {
btn.classList.remove('active', 'bg-blue-600', 'text-white');
btn.classList.add('bg-gray-200', 'text-gray-700');
});
const activeBtn = document.querySelector('[data-season="' + seasonNumber + '"]');
if (activeBtn) {
activeBtn.classList.remove('bg-gray-200', 'text-gray-700');
activeBtn.classList.add('active', 'bg-blue-600', 'text-white');
}
};
}
// Binary blacklist
function initBinaryBlacklist() {
// Placeholder - actual implementation depends on existing ajax function
window.ajax_binaryblacklist_delete = window.ajax_binaryblacklist_delete || function(id) {
console.log('Delete blacklist:', id);
};
}
// Image modal
function initImageModal() {
// Handle image modal if needed
document.querySelectorAll('[data-open-image-modal]').forEach(function(element) {
element.addEventListener('click', function(e) {
e.preventDefault();
const imageUrl = this.getAttribute('data-open-image-modal');
openImageModal(imageUrl);
});
});
window.openImageModal = function(imageUrl) {
const modal = document.getElementById('imageModal');
if (modal) {
const img = modal.querySelector('img');
if (img) {
img.src = imageUrl;
}
modal.classList.remove('hidden');
modal.classList.add('flex');
}
};
window.closeImageModal = function() {
const modal = document.getElementById('imageModal');
if (modal) {
modal.classList.add('hidden');
modal.classList.remove('flex');
}
};
}
// Tab switcher for profile and other pages
function initTabSwitcher() {
document.querySelectorAll('[data-tab-trigger]').forEach(function(trigger) {
trigger.addEventListener('click', function(e) {
e.preventDefault();
const tabId = this.getAttribute('data-tab-trigger');
// Hide all tabs
document.querySelectorAll('.tab-content').forEach(function(tab) {
tab.style.display = 'none';
});
// Show selected tab
const selectedTab = document.getElementById(tabId);
if (selectedTab) {
selectedTab.style.display = 'block';
}
// Update active state
document.querySelectorAll('[data-tab-trigger]').forEach(function(t) {
t.classList.remove('active', 'border-blue-500', 'text-blue-600');
t.classList.add('border-transparent', 'text-gray-500');
});
this.classList.remove('border-transparent', 'text-gray-500');
this.classList.add('active', 'border-blue-500', 'text-blue-600');
});
});
}
// Preview Modal for Browse Page
function initPreviewModal() {
window.closePreviewModal = function() {
const modal = document.getElementById('previewModal');
if (modal) {
modal.style.display = 'none';
modal.classList.add('hidden');
const img = document.getElementById('previewImage');
if (img) img.src = '';
}
};
window.showPreviewImage = function(guid, type = 'preview') {
const modal = document.getElementById('previewModal');
const img = document.getElementById('previewImage');
const error = document.getElementById('previewError');
const title = document.getElementById('previewTitle');
if (!modal || !img || !error || !title) return;
modal.style.display = 'flex';
modal.classList.remove('hidden');
title.textContent = type === 'sample' ? 'Sample Image' : 'Preview Image';
const imageUrl = '/covers/' + type + '/' + guid + '_thumb.jpg';
img.src = imageUrl;
error.classList.add('hidden');
img.style.display = 'block';
img.onerror = function() {
error.textContent = (type === 'sample' ? 'Sample' : 'Preview') + ' image not available';
error.classList.remove('hidden');
img.style.display = 'none';
};
img.onload = function() {
img.style.display = 'block';
};
};
// Event listeners for preview badges
document.querySelectorAll('.preview-badge').forEach(function(badge) {
badge.addEventListener('click', function() {
const guid = this.getAttribute('data-guid');
if (guid) {
window.showPreviewImage(guid, 'preview');
}
});
});
// Event listeners for sample badges
document.querySelectorAll('.sample-badge').forEach(function(badge) {
badge.addEventListener('click', function() {
const guid = this.getAttribute('data-guid');
if (guid) {
window.showPreviewImage(guid, 'sample');
}
});
});
// Event listeners for NFO badges
document.querySelectorAll('.nfo-badge').forEach(function(badge) {
badge.addEventListener('click', function() {
const guid = this.getAttribute('data-guid');
if (guid) {
fetch('/getnfo/' + guid)
.then(response => response.text())
.then(data => {
const nfoContent = document.getElementById('nfo-content');
if (nfoContent) {
nfoContent.textContent = data;
const nfoModal = document.getElementById('nfo-modal');
if (nfoModal) {
nfoModal.classList.remove('hidden');
nfoModal.classList.add('flex');
}
}
})
.catch(error => {
console.error('Error fetching NFO:', error);
});
}
});
});
// Close preview modal on background click
const previewModal = document.getElementById('previewModal');
if (previewModal) {
previewModal.addEventListener('click', function(e) {
if (e.target === this) {
window.closePreviewModal();
}
});
// Event listener for close button (using onclick attribute)
const closeButton = previewModal.querySelector('button[onclick*="closePreviewModal"]');
if (closeButton) {
closeButton.addEventListener('click', function(e) {
e.stopPropagation();
window.closePreviewModal();
});
}
// Also handle any data-close-preview buttons
const dataCloseButton = previewModal.querySelector('[data-close-preview]');
if (dataCloseButton) {
dataCloseButton.addEventListener('click', function(e) {
e.stopPropagation();
window.closePreviewModal();
});
}
}
// Close modal on Escape key
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
window.closePreviewModal();
}
});
}
// Regex Management Functions
function initRegexManagement() {
window.deleteRegex = function(id, deleteUrl) {
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content;
fetch(deleteUrl + '?id=' + id, {
method: 'GET',
headers: {
'X-Requested-With': 'XMLHttpRequest',
'X-CSRF-TOKEN': csrfToken
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
const row = document.getElementById('row-' + id);
if (row) {
row.style.transition = 'opacity 0.3s';
row.style.opacity = '0';
setTimeout(() => row.remove(), 300);
}
showMessage('Regex deleted successfully', 'success');
} else {
showMessage('Error deleting regex', 'error');
}
})
.catch(error => {
showMessage('Error deleting regex', 'error');
console.error('Error:', error);
});
};
window.showMessage = function(message, type = 'success') {
const messageDiv = document.getElementById('message');
if (!messageDiv) return;
const bgColor = type === 'success' ? 'bg-green-50 dark:bg-green-900 border-green-200 dark:border-green-700' : 'bg-red-50 dark:bg-red-900 border-red-200 dark:border-red-700';
const textColor = type === 'success' ? 'text-green-800 dark:text-green-200' : 'text-red-800 dark:text-red-200';
const icon = type === 'success' ? 'check-circle' : 'exclamation-circle';
const messageEl = document.createElement('div');
messageEl.className = 'mt-4 p-4 border rounded-lg ' + bgColor;
messageEl.innerHTML = `
<div class="flex items-center justify-between">
<p class="${textColor}">
<i class="fa fa-${icon} mr-2"></i>${escapeHtml(message)}
</p>
<button type="button" class="${textColor} hover:opacity-75 close-message">
<i class="fa fa-times"></i>
</button>
</div>
`;
messageDiv.appendChild(messageEl);
// Add close handler
messageEl.querySelector('.close-message').addEventListener('click', function() {
messageEl.remove();
});
// Auto-remove after 5 seconds
setTimeout(() => {
messageEl.remove();
}, 5000);
};
// Handle delete buttons with proper confirmation
document.addEventListener('click', function(e) {
if (e.target.hasAttribute('data-delete-regex') || e.target.closest('[data-delete-regex]')) {
const element = e.target.hasAttribute('data-delete-regex') ? e.target : e.target.closest('[data-delete-regex]');
const id = element.getAttribute('data-delete-regex');
const deleteUrl = element.getAttribute('data-delete-url');
if (confirm('Are you sure you want to delete this regex? This action cannot be undone.')) {
deleteRegex(id, deleteUrl);
}
e.preventDefault();
}
});
}
// Mediainfo and Filelist Modals
function initMediainfoAndFilelist() {
window.closeMediainfoModal = function() {
const modal = document.getElementById('mediainfoModal');
if (modal) {
modal.style.display = 'none';
}
};
window.closeFilelistModal = function() {
const modal = document.getElementById('filelistModal');
if (modal) {
modal.style.display = 'none';
}
};
window.formatFileSize = function(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i];
};
window.showFilelist = function(guid) {
let modal = document.getElementById('filelistModal');
if (modal && modal.parentElement !== document.body) {
document.body.appendChild(modal);
}
const content = document.getElementById('filelistContent');
modal.style.display = 'flex';
modal.style.position = 'fixed';
modal.style.top = '0';
modal.style.left = '0';
modal.style.right = '0';
modal.style.bottom = '0';
modal.style.zIndex = '99999';
modal.style.backgroundColor = 'rgba(0, 0, 0, 0.75)';
content.innerHTML = `
<div class="text-center py-8">
<i class="fas fa-spinner fa-spin text-3xl text-green-600"></i>
<p class="text-gray-600 dark:text-gray-400 mt-2">Loading file list...</p>
</div>
`;
const apiUrl = '/api/release/' + guid + '/filelist';
fetch(apiUrl)
.then(response => {
if (!response.ok) {
throw new Error('Failed to load file list');
}
return response.json();
})
.then(data => {
if (!data.files || data.files.length === 0) {
content.innerHTML = '<p class="text-center text-gray-500 dark:text-gray-400 py-8">No files available</p>';
return;
}
let html = '<div class="space-y-4">';
html += `
<div class="bg-gradient-to-r from-green-50 to-teal-50 dark:from-green-900 dark:to-teal-900 rounded-lg p-4 mb-4">
<h4 class="text-md font-semibold text-gray-800 dark:text-gray-200 mb-2">${escapeHtml(data.release.searchname)}</h4>
<p class="text-sm text-gray-600 dark:text-gray-400">Total Files: ${data.total}</p>
</div>
`;
html += `
<div class="overflow-hidden rounded-lg border border-gray-200 dark:border-gray-700">
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead class="bg-gray-100 dark:bg-gray-800">
<tr>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">File Name</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">Size</th>
</tr>
</thead>
<tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
`;
data.files.forEach((file, index) => {
const rowClass = index % 2 === 0 ? 'bg-white dark:bg-gray-800' : 'bg-gray-50 dark:bg-gray-900';
const fileName = file.title || file.name || 'Unknown';
const fileSize = file.size ? formatFileSize(file.size) : 'N/A';
html += `
<tr class="${rowClass} hover:bg-gray-100 dark:hover:bg-gray-700">
<td class="px-4 py-3 text-sm text-gray-900 dark:text-gray-100 break-all">${escapeHtml(fileName)}</td>
<td class="px-4 py-3 text-sm text-gray-600 dark:text-gray-400 whitespace-nowrap">${fileSize}</td>
</tr>
`;
});
html += `
</tbody>
</table>
</div>
`;
html += '</div>';
content.innerHTML = html;
})
.catch(error => {
content.innerHTML = `
<div class="text-center py-8">
<i class="fas fa-exclamation-circle text-3xl text-red-600"></i>
<p class="text-red-600 mt-2">${escapeHtml(error.message)}</p>
</div>
`;
});
};
window.showMediainfo = function(releaseId) {
let modal = document.getElementById('mediainfoModal');
if (modal && modal.parentElement !== document.body) {
document.body.appendChild(modal);
}
const content = document.getElementById('mediainfoContent');
modal.style.display = 'flex';
modal.style.position = 'fixed';
modal.style.top = '0';
modal.style.left = '0';
modal.style.right = '0';
modal.style.bottom = '0';
modal.style.zIndex = '99999';
modal.style.backgroundColor = 'rgba(0, 0, 0, 0.75)';
content.innerHTML = `
<div class="text-center py-8">
<i class="fas fa-spinner fa-spin text-3xl text-blue-600"></i>
<p class="text-gray-600 dark:text-gray-400 mt-2">Loading media information...</p>
</div>
`;
const apiUrl = '/api/release/' + releaseId + '/mediainfo';
fetch(apiUrl)
.then(response => {
if (!response.ok) {
throw new Error('Failed to load media information');
}
return response.json();
})
.then(data => {
if (!data.video && !data.audio && !data.subs) {
content.innerHTML = '<p class="text-center text-gray-500 dark:text-gray-400 py-8">No media information available</p>';
return;
}
let html = '<div class="space-y-6">';
// Video information
if (data.video) {
html += `
<div class="bg-gradient-to-r from-blue-50 to-indigo-50 dark:from-blue-900 dark:to-indigo-900 rounded-lg p-4">
<h4 class="text-md font-semibold text-gray-800 dark:text-gray-200 mb-3 flex items-center">
<i class="fas fa-video mr-2 text-blue-600 dark:text-blue-400"></i> Video Information
</h4>
<dl class="grid grid-cols-2 gap-3">
`;
if (data.video.containerformat) {
html += `
<div>
<dt class="text-xs font-medium text-gray-600 dark:text-gray-400">Container</dt>
<dd class="text-sm text-gray-900 dark:text-gray-100">${escapeHtml(data.video.containerformat)}</dd>
</div>
`;
}
if (data.video.videocodec) {
html += `
<div>
<dt class="text-xs font-medium text-gray-600 dark:text-gray-400">Codec</dt>
<dd class="text-sm text-gray-900 dark:text-gray-100">${escapeHtml(data.video.videocodec)}</dd>
</div>
`;
}
if (data.video.videowidth && data.video.videoheight) {
html += `
<div>
<dt class="text-xs font-medium text-gray-600 dark:text-gray-400">Resolution</dt>
<dd class="text-sm text-gray-900 dark:text-gray-100">${data.video.videowidth}x${data.video.videoheight}</dd>
</div>
`;
}
if (data.video.videoaspect) {
html += `
<div>
<dt class="text-xs font-medium text-gray-600 dark:text-gray-400">Aspect Ratio</dt>
<dd class="text-sm text-gray-900 dark:text-gray-100">${escapeHtml(data.video.videoaspect)}</dd>
</div>
`;
}
if (data.video.videoframerate) {
html += `
<div>
<dt class="text-xs font-medium text-gray-600 dark:text-gray-400">Frame Rate</dt>
<dd class="text-sm text-gray-900 dark:text-gray-100">${escapeHtml(data.video.videoframerate)} fps</dd>
</div>
`;
}
if (data.video.videoduration) {
const durationMs = parseInt(data.video.videoduration);
if (!isNaN(durationMs) && durationMs > 0) {
const minutes = Math.round(durationMs / 1000 / 60);
html += `
<div>
<dt class="text-xs font-medium text-gray-600 dark:text-gray-400">Duration</dt>
<dd class="text-sm text-gray-900 dark:text-gray-100">${minutes} minutes</dd>
</div>
`;
}
}
html += '</dl></div>';
}
// Audio information
if (data.audio && data.audio.length > 0) {
html += `
<div class="bg-gradient-to-r from-green-50 to-teal-50 dark:from-green-900 dark:to-teal-900 rounded-lg p-4">
<h4 class="text-md font-semibold text-gray-800 dark:text-gray-200 mb-3 flex items-center">
<i class="fas fa-volume-up mr-2 text-green-600 dark:text-green-400"></i> Audio Information
</h4>
`;
data.audio.forEach((audio, index) => {
if (index > 0) html += '<hr class="my-3 border-gray-200 dark:border-gray-700">';
html += '<dl class="grid grid-cols-2 gap-3">';
if (audio.audioformat) {
html += `
<div>
<dt class="text-xs font-medium text-gray-600 dark:text-gray-400">Format</dt>
<dd class="text-sm text-gray-900 dark:text-gray-100">${escapeHtml(audio.audioformat)}</dd>
</div>
`;
}
if (audio.audiochannels) {
html += `
<div>
<dt class="text-xs font-medium text-gray-600 dark:text-gray-400">Channels</dt>
<dd class="text-sm text-gray-900 dark:text-gray-100">${escapeHtml(audio.audiochannels)}</dd>
</div>
`;
}
if (audio.audiobitrate) {
html += `
<div>
<dt class="text-xs font-medium text-gray-600 dark:text-gray-400">Bit Rate</dt>
<dd class="text-sm text-gray-900 dark:text-gray-100">${escapeHtml(audio.audiobitrate)}</dd>
</div>
`;
}
if (audio.audiolanguage) {
html += `
<div>
<dt class="text-xs font-medium text-gray-600 dark:text-gray-400">Language</dt>
<dd class="text-sm text-gray-900 dark:text-gray-100">${escapeHtml(audio.audiolanguage)}</dd>
</div>
`;
}
if (audio.audiosamplerate) {
html += `
<div>
<dt class="text-xs font-medium text-gray-600 dark:text-gray-400">Sample Rate</dt>
<dd class="text-sm text-gray-900 dark:text-gray-100">${escapeHtml(audio.audiosamplerate)}</dd>
</div>
`;
}
html += '</dl>';
});
html += '</div>';
}
// Subtitle information
if (data.subs) {
html += `
<div class="bg-gradient-to-r from-purple-50 to-pink-50 dark:from-purple-900 dark:to-pink-900 rounded-lg p-4">
<h4 class="text-md font-semibold text-gray-800 dark:text-gray-200 mb-3 flex items-center">
<i class="fas fa-closed-captioning mr-2 text-purple-600 dark:text-purple-400"></i> Subtitles
</h4>
<p class="text-sm text-gray-900 dark:text-gray-100">${escapeHtml(data.subs)}</p>
</div>
`;
}
html += '</div>';
content.innerHTML = html;
})
.catch(error => {
content.innerHTML = `
<div class="text-center py-8">
<i class="fas fa-exclamation-circle text-3xl text-red-600"></i>
<p class="text-red-600 mt-2">${escapeHtml(error.message)}</p>
</div>
`;
});
};
// Event handlers for badges and modals
document.addEventListener('click', function(e) {
// Preview badge
const previewBadge = e.target.closest('.preview-badge');
if (previewBadge) {
e.preventDefault();
const guid = previewBadge.dataset.guid;
if (typeof showPreviewImage === 'function') {
showPreviewImage(guid, 'preview');
}
}
// Sample badge
const sampleBadge = e.target.closest('.sample-badge');
if (sampleBadge) {
e.preventDefault();
const guid = sampleBadge.dataset.guid;
if (typeof showPreviewImage === 'function') {
showPreviewImage(guid, 'sample');
}
}
// Mediainfo badge
const mediainfoBadge = e.target.closest('.mediainfo-badge');
if (mediainfoBadge) {
e.preventDefault();
const releaseId = mediainfoBadge.dataset.releaseId;
showMediainfo(releaseId);
}
// File list badge
const filelistBadge = e.target.closest('.filelist-badge');
if (filelistBadge) {
e.preventDefault();
const guid = filelistBadge.dataset.guid;
showFilelist(guid);
}
});
// Close modals on background click
const previewModal = document.getElementById('previewModal');
if (previewModal) {
previewModal.addEventListener('click', function(e) {
if (e.target === this && typeof closePreviewModal === 'function') {
closePreviewModal();
}
});
}
const mediainfoModal = document.getElementById('mediainfoModal');
if (mediainfoModal) {
mediainfoModal.addEventListener('click', function(e) {
// Close if clicking the backdrop OR the close button
if (e.target === this || e.target.closest('[onclick*="closeMediainfoModal"]')) {
closeMediainfoModal();
}
});
}
const filelistModal = document.getElementById('filelistModal');
if (filelistModal) {
filelistModal.addEventListener('click', function(e) {
// Close if clicking the backdrop OR the close button
if (e.target === this || e.target.closest('[onclick*="closeFilelistModal"]')) {
closeFilelistModal();
}
});
}
// Close modals on ESC key
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
if (typeof closePreviewModal === 'function') closePreviewModal();
closeMediainfoModal();
closeFilelistModal();
}
});
}
// Cart and Multi-select functionality
function initCartFunctionality() {
// Global showConfirmation function for cart page
window.showConfirmation = function(message, onConfirm) {
const modal = document.createElement('div');
modal.className = 'confirmation-modal';
modal.innerHTML = `
<div class="confirmation-modal-content">
<div class="confirmation-modal-header">
<div class="confirmation-modal-icon">
<i class="fa fa-exclamation-triangle"></i>
</div>
<h3 class="confirmation-modal-title">Confirm Deletion</h3>
</div>
<div class="confirmation-modal-body">
${message}
</div>
<div class="confirmation-modal-footer">
<button class="btn-cancel">Cancel</button>
<button class="btn-confirm">Delete</button>
</div>
</div>
`;
document.body.appendChild(modal);
const cancelBtn = modal.querySelector('.btn-cancel');
const confirmBtn = modal.querySelector('.btn-confirm');
function closeModal() {
modal.style.animation = 'fadeOut 0.2s ease-out';
setTimeout(() => modal.remove(), 200);
}
cancelBtn.addEventListener('click', closeModal);
modal.addEventListener('click', function(e) {
if (e.target === modal) {
closeModal();
}
});
confirmBtn.addEventListener('click', function() {
closeModal();
onConfirm();
});
// Focus on cancel button by default
setTimeout(() => cancelBtn.focus(), 100);
// ESC key to close
const escHandler = function(e) {
if (e.key === 'Escape') {
closeModal();
document.removeEventListener('keydown', escHandler);
}
};
document.addEventListener('keydown', escHandler);
};
// Cart page specific functionality
const checkAll = document.getElementById('check-all');
const checkboxes = document.querySelectorAll('.cart-checkbox');
if (checkAll && checkboxes.length > 0) {
// Function to update the check-all checkbox state
function updateCheckAllState() {
const checkedCount = Array.from(checkboxes).filter(cb => cb.checked).length;
checkAll.checked = checkedCount === checkboxes.length;
checkAll.indeterminate = checkedCount > 0 && checkedCount < checkboxes.length;
}
// Handle check-all checkbox change
checkAll.addEventListener('change', function() {
const isChecked = this.checked;
checkboxes.forEach(checkbox => {
checkbox.checked = isChecked;
});
});
// Handle individual checkbox changes
checkboxes.forEach(checkbox => {
checkbox.addEventListener('change', updateCheckAllState);
});
// Initialize the check-all state on page load
updateCheckAllState();
// Download selected
document.querySelectorAll('.nzb_multi_operations_download_cart').forEach(btn => {
btn.addEventListener('click', function(e) {
e.preventDefault();
const selected = Array.from(checkboxes)
.filter(cb => cb.checked)
.map(cb => cb.value);
if (selected.length === 0) {
if (typeof showToast === 'function') {
showToast('Please select at least one item', 'error');
} else {
alert('Please select at least one item');
}
return;
}
// Download all selected NZBs
selected.forEach(guid => {
window.open('/getnzb?id=' + guid, '_blank');
});
if (typeof showToast === 'function') {
showToast('Downloading ' + selected.length + ' item(s)', 'success');
}
});
});
// Delete selected
document.querySelectorAll('.nzb_multi_operations_cartdelete').forEach(btn => {
btn.addEventListener('click', function(e) {
e.preventDefault();
const selected = Array.from(checkboxes)
.filter(cb => cb.checked)
.map(cb => cb.value);
if (selected.length === 0) {
if (typeof showToast === 'function') {
showToast('Please select at least one item', 'error');
} else {
alert('Please select at least one item');
}
return;
}
showConfirmation(
`Are you sure you want to delete <strong>${selected.length}</strong> item(s) from your cart?`,
function() {
// Delete via AJAX
fetch('/cart/delete/' + selected.join(','), {
method: 'POST',
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
'Content-Type': 'application/json',
}
})
.then(response => {
if (response.ok) {
if (typeof showToast === 'function') {
showToast('Items deleted successfully', 'success');
}
setTimeout(() => window.location.reload(), 1000);
} else {
if (typeof showToast === 'function') {
showToast('Failed to delete items', 'error');
} else {
alert('Failed to delete items');
}
}
})
.catch(error => {
console.error('Error:', error);
if (typeof showToast === 'function') {
showToast('Failed to delete items', 'error');
} else {
alert('Failed to delete items');
}
});
}
);
});
});
// Individual delete confirmation
document.querySelectorAll('.cart-delete-link').forEach(function(link) {
link.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
const releaseName = this.getAttribute('data-release-name');
const deleteUrl = this.getAttribute('data-delete-url');
showConfirmation(
`Are you sure you want to remove <strong>${releaseName}</strong> from your cart?`,
function() {
if (typeof showToast === 'function') {
showToast('Removing item from cart...', 'info');
}
setTimeout(() => {
window.location.href = deleteUrl;
}, 500);
}
);
});
});
}
// Select all checkbox functionality
const selectAllCheckbox = document.getElementById('chkSelectAll');
if (selectAllCheckbox) {
selectAllCheckbox.addEventListener('change', function() {
const checkboxes = document.querySelectorAll('.chkRelease');
checkboxes.forEach(checkbox => checkbox.checked = this.checked);
});
}
// Multi-operations download
const multiDownloadBtn = document.querySelector('.nzb_multi_operations_download');
if (multiDownloadBtn) {
multiDownloadBtn.addEventListener('click', function() {
const selected = Array.from(document.querySelectorAll('.chkRelease:checked')).map(cb => cb.value);
if (selected.length === 0) {
if (typeof showToast === 'function') {
showToast('Please select at least one release', 'error');
}
return;
}
// If only one release is selected, download it directly
if (selected.length === 1) {
window.location.href = '/getnzb/' + selected[0];
if (typeof showToast === 'function') {
showToast('Downloading NZB...', 'success');
}
} else {
// For multiple releases, download as zip
const guids = selected.join(',');
window.location.href = '/getnzb?id=' + encodeURIComponent(guids) + '&zip=1';
if (typeof showToast === 'function') {
showToast(`Downloading ${selected.length} NZBs as zip file...`, 'success');
}
}
});
}
// Multi-operations cart
const multiCartBtn = document.querySelector('.nzb_multi_operations_cart');
if (multiCartBtn) {
multiCartBtn.addEventListener('click', function() {
const selected = Array.from(document.querySelectorAll('.chkRelease:checked')).map(cb => cb.value);
if (selected.length === 0) {
if (typeof showToast === 'function') {
showToast('Please select at least one release', 'error');
}
return;
}
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content;
fetch('/cart/add', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': csrfToken,
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify({ id: selected.join(',') })
})
.then(response => response.json())
.then(data => {
if (data.success) {
if (typeof showToast === 'function') {
showToast(data.message || `Added ${selected.length} item${selected.length > 1 ? 's' : ''} to cart`, 'success');
}
} else {
if (typeof showToast === 'function') {
showToast('Failed to add items to cart', 'error');
}
}
})
.catch(error => {
console.error('Error adding to cart:', error);
if (typeof showToast === 'function') {
showToast('An error occurred', 'error');
}
});
});
}
// Individual add to cart buttons
document.addEventListener('click', function(e) {
const cartBtn = e.target.closest('.add-to-cart');
if (cartBtn) {
e.preventDefault();
const guid = cartBtn.dataset.guid;
const iconElement = cartBtn.querySelector('.icon_cart');
if (!guid) {
console.error('No GUID found for cart item');
return;
}
// Prevent double-clicking
if (iconElement && iconElement.classList.contains('icon_cart_clicked')) {
return;
}
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content;
// Send AJAX request to add item to cart
fetch('/cart/add', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': csrfToken,
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify({ id: guid })
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Visual feedback
if (iconElement) {
iconElement.classList.remove('fa-shopping-basket');
iconElement.classList.add('fa-check', 'icon_cart_clicked');
// Reset icon after 2 seconds
setTimeout(() => {
iconElement.classList.remove('fa-check', 'icon_cart_clicked');
iconElement.classList.add('fa-shopping-basket');
}, 2000);
}
// Show success notification
if (typeof showToast === 'function') {
showToast('Added to cart successfully!', 'success');
}
} else {
if (typeof showToast === 'function') {
showToast('Failed to add item to cart', 'error');
}
}
})
.catch(error => {
console.error('Error adding to cart:', error);
if (typeof showToast === 'function') {
showToast('An error occurred', 'error');
}
});
}
});
}
// Admin Menu Toggle
function initAdminMenu() {
window.toggleAdminSubmenu = function(id) {
const submenu = document.getElementById(id);
const icon = document.getElementById(id + '-icon');
if (submenu) {
submenu.classList.toggle('hidden');
}
if (icon) {
icon.classList.toggle('rotate-180');
}
};
// Add click handlers for admin menu buttons
document.addEventListener('click', function(e) {
const menuButton = e.target.closest('[data-toggle-submenu]');
if (menuButton) {
const menuId = menuButton.getAttribute('data-toggle-submenu');
if (menuId) {
toggleAdminSubmenu(menuId);
}
}
});
// Theme management with system preference support for admin panel
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const themeToggle = document.getElementById('theme-toggle');
if (themeToggle) {
// Get initial theme preference
const metaTheme = document.querySelector('meta[name="theme-preference"]');
const isAuthenticated = document.querySelector('meta[name="user-authenticated"]');
let currentTheme = metaTheme ? metaTheme.content : 'light';
if (!isAuthenticated || isAuthenticated.content !== 'true') {
currentTheme = localStorage.getItem('theme') || 'light';
}
// Function to apply theme
function applyTheme(themePreference) {
const html = document.documentElement;
if (themePreference === 'system') {
if (mediaQuery.matches) {
html.classList.add('dark');
} else {
html.classList.remove('dark');
}
} else if (themePreference === 'dark') {
html.classList.add('dark');
} else {
html.classList.remove('dark');
}
}
// Apply initial theme
applyTheme(currentTheme);
// Listen for OS theme changes
mediaQuery.addEventListener('change', () => {
const userThemePreference = metaTheme ? metaTheme.content : localStorage.getItem('theme') || 'light';
if (userThemePreference === 'system') {
applyTheme('system');
}
});
// Theme toggle click handler - cycles through light -> dark -> system
themeToggle.addEventListener('click', function() {
let nextTheme;
// Cycle through: light -> dark -> system -> light
if (currentTheme === 'light') {
nextTheme = 'dark';
} else if (currentTheme === 'dark') {
nextTheme = 'system';
} else {
nextTheme = 'light';
}
applyTheme(nextTheme);
// Update button title
const titles = {
'light': 'Theme: Light',
'dark': 'Theme: Dark',
'system': 'Theme: System (Auto)'
};
this.setAttribute('title', titles[nextTheme]);
// Save theme preference
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content;
const updateThemeUrl = document.querySelector('meta[name="update-theme-url"]')?.content;
if (isAuthenticated && isAuthenticated.content === 'true' && updateThemeUrl && csrfToken) {
// Save to backend for authenticated users
fetch(updateThemeUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': csrfToken
},
body: JSON.stringify({ theme_preference: nextTheme })
}).then(response => response.json())
.then(data => {
if (data.success) {
currentTheme = nextTheme;
if (metaTheme) {
metaTheme.content = nextTheme;
}
}
})
.catch(error => console.error('Error updating theme:', error));
} else {
// Save to localStorage for guests
localStorage.setItem('theme', nextTheme);
currentTheme = nextTheme;
}
});
}
}
// Sidebar Toggle functionality for regular user sidebar
function initSidebarToggle() {
const sidebarToggles = document.querySelectorAll('.sidebar-toggle');
sidebarToggles.forEach(toggle => {
toggle.addEventListener('click', function() {
const targetId = this.getAttribute('data-target');
const submenu = document.getElementById(targetId);
const chevron = this.querySelector('.fa-chevron-down');
if (submenu) {
submenu.classList.toggle('hidden');
if (chevron) {
chevron.classList.toggle('rotate-180');
}
}
});
});
// Handle logout link in sidebar
const sidebarLogoutLink = document.querySelector('[data-logout]');
const sidebarLogoutForm = document.getElementById('sidebar-logout-form');
if (sidebarLogoutLink && sidebarLogoutForm) {
sidebarLogoutLink.addEventListener('click', function(e) {
e.preventDefault();
sidebarLogoutForm.submit();
});
}
}
// Dropdown Menus for Header Navigation
function initDropdownMenus() {
document.querySelectorAll('.dropdown-toggle').forEach(function(toggle) {
toggle.addEventListener('click', function(e) {
e.preventDefault();
const menu = this.nextElementSibling;
if (menu && menu.classList.contains('dropdown-menu')) {
// Close all other dropdowns
document.querySelectorAll('.dropdown-menu').forEach(function(m) {
if (m !== menu) {
m.classList.remove('show');
m.style.display = 'none';
}
});
// Toggle this dropdown
menu.classList.toggle('show');
menu.style.display = menu.classList.contains('show') ? 'block' : 'none';
}
});
});
// Close dropdowns when clicking outside
document.addEventListener('click', function(e) {
if (!e.target.closest('.dropdown-toggle') && !e.target.closest('.dropdown-menu')) {
document.querySelectorAll('.dropdown-menu').forEach(function(menu) {
menu.classList.remove('show');
menu.style.display = 'none';
});
}
});
}
// Image Fallbacks
function initImageFallbacks() {
// Handle images with data-fallback-src attribute
document.querySelectorAll('img[data-fallback-src]').forEach(function(img) {
img.addEventListener('error', function() {
const fallbackSrc = this.getAttribute('data-fallback-src');
if (fallbackSrc && this.src !== fallbackSrc) {
this.src = fallbackSrc;
}
});
});
}
// Profile Page Tab Switching
function initProfileTabs() {
const tabLinks = document.querySelectorAll('.tab-link');
const tabContents = document.querySelectorAll('.tab-content');
if (tabLinks.length === 0 || tabContents.length === 0) {
return; // Not on a page with tabs
}
tabLinks.forEach(link => {
link.addEventListener('click', function(e) {
e.preventDefault();
const targetId = this.getAttribute('href').substring(1);
// Update active states on tab links
tabLinks.forEach(l => {
l.classList.remove('bg-blue-50', 'text-blue-700', 'font-medium');
l.classList.add('text-gray-700');
l.classList.add('dark:text-gray-300');
});
this.classList.add('bg-blue-50', 'text-blue-700', 'font-medium');
this.classList.remove('text-gray-700', 'dark:text-gray-300');
// Hide all tab contents
tabContents.forEach(content => {
content.style.display = 'none';
});
// Show selected tab content
const targetContent = document.getElementById(targetId);
if (targetContent) {
targetContent.style.display = 'block';
}
// Update URL hash without scrolling
history.pushState(null, null, '#' + targetId);
});
});
// Handle initial hash
const hash = window.location.hash.substring(1);
if (hash && document.getElementById(hash)) {
const link = document.querySelector(`a[href="#${hash}"]`);
if (link) {
link.click();
}
}
}
// Add to Cart function (standalone for backward compatibility)
window.addToCart = function(guid) {
if (!guid) {
console.error('No GUID provided to addToCart');
return;
}
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content;
fetch('/cart/add', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': csrfToken,
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify({ id: guid })
})
.then(response => response.json())
.then(data => {
if (data.success) {
if (typeof showToast === 'function') {
showToast('Added to cart successfully!', 'success');
}
} else {
if (typeof showToast === 'function') {
showToast('Failed to add item to cart', 'error');
}
}
})
.catch(error => {
console.error('Error adding to cart:', error);
if (typeof showToast === 'function') {
showToast('An error occurred', 'error');
}
});
};
// Admin User Edit - Role Expiry DateTime Picker
function initAdminUserEdit() {
// Only run if we're on the admin user edit page
if (!document.getElementById('rolechangedate')) {
return;
}
initializeDateTimePicker();
setupTypeToSelect();
setupExpiryDateHandlers();
}
// Initialize the datetime picker with existing values
function initializeDateTimePicker() {
const hiddenInput = document.getElementById('rolechangedate');
if (!hiddenInput) return;
if (hiddenInput.value) {
const date = new Date(hiddenInput.value);
if (!isNaN(date.getTime())) {
document.getElementById('expiry_year').value = date.getFullYear().toString();
document.getElementById('expiry_month').value = String(date.getMonth() + 1).padStart(2, '0');
document.getElementById('expiry_day').value = String(date.getDate()).padStart(2, '0');
document.getElementById('expiry_hour').value = String(date.getHours()).padStart(2, '0');
document.getElementById('expiry_minute').value = String(date.getMinutes()).padStart(2, '0');
updateDateTimePreview();
}
}
// Add change listeners to all selectors
['expiry_year', 'expiry_month', 'expiry_day', 'expiry_hour', 'expiry_minute'].forEach(id => {
const element = document.getElementById(id);
if (element) {
element.addEventListener('change', updateDateTime);
}
});
}
// Setup type-to-select functionality for all dropdowns
function setupTypeToSelect() {
const selects = ['expiry_year', 'expiry_month', 'expiry_day', 'expiry_hour', 'expiry_minute'];
selects.forEach(selectId => {
const select = document.getElementById(selectId);
if (!select) return;
let typedValue = '';
let typingTimer;
select.addEventListener('keypress', function(e) {
clearTimeout(typingTimer);
// Add typed character
typedValue += e.key;
// Find matching option
const options = Array.from(this.options);
const match = options.find(opt =>
opt.value.startsWith(typedValue) ||
opt.text.toLowerCase().startsWith(typedValue.toLowerCase())
);
if (match) {
this.value = match.value;
updateDateTime();
// Visual feedback
this.classList.add('ring-2', 'ring-green-500', 'dark:ring-green-400');
setTimeout(() => {
this.classList.remove('ring-2', 'ring-green-500', 'dark:ring-green-400');
}, 300);
}
// Clear typed value after 1 second
typingTimer = setTimeout(() => {
typedValue = '';
}, 1000);
});
});
}
// Update the hidden input and preview when any selector changes
function updateDateTime() {
const year = document.getElementById('expiry_year')?.value;
const month = document.getElementById('expiry_month')?.value;
const day = document.getElementById('expiry_day')?.value;
const hour = document.getElementById('expiry_hour')?.value;
const minute = document.getElementById('expiry_minute')?.value;
if (year && month && day && hour && minute) {
const dateTimeStr = `${year}-${month}-${day}T${hour}:${minute}:00`;
document.getElementById('rolechangedate').value = dateTimeStr;
updateDateTimePreview();
// Show success flash on all filled selectors
['expiry_year', 'expiry_month', 'expiry_day', 'expiry_hour', 'expiry_minute'].forEach(id => {
const el = document.getElementById(id);
if (el && el.value) {
el.classList.add('border-green-500', 'dark:border-green-400');
setTimeout(() => {
el.classList.remove('border-green-500', 'dark:border-green-400');
}, 500);
}
});
} else {
const preview = document.getElementById('datetime_preview');
if (preview) {
preview.classList.add('hidden');
}
}
}
// Update the datetime preview display
function updateDateTimePreview() {
const hiddenInput = document.getElementById('rolechangedate');
const preview = document.getElementById('datetime_preview');
const display = document.getElementById('datetime_display');
if (!hiddenInput || !preview || !display) return;
if (hiddenInput.value) {
const date = new Date(hiddenInput.value);
const options = {
weekday: 'short',
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
};
display.textContent = date.toLocaleDateString('en-US', options);
preview.classList.remove('hidden');
// Check if date is in past or expiring soon
const now = new Date();
const diff = date - now;
if (diff < 0) {
display.classList.add('text-red-600', 'dark:text-red-400');
display.classList.remove('text-blue-600', 'dark:text-blue-400', 'text-yellow-600', 'dark:text-yellow-400');
} else if (diff < 7 * 24 * 60 * 60 * 1000) {
display.classList.add('text-yellow-600', 'dark:text-yellow-400');
display.classList.remove('text-blue-600', 'dark:text-blue-400', 'text-red-600', 'dark:text-red-400');
} else {
display.classList.add('text-blue-600', 'dark:text-blue-400');
display.classList.remove('text-red-600', 'dark:text-red-400', 'text-yellow-600', 'dark:text-yellow-400');
}
} else {
preview.classList.add('hidden');
}
}
// Setup event handlers for expiry date quick actions
function setupExpiryDateHandlers() {
// Event delegation for all expiry date buttons
document.addEventListener('click', function(e) {
const target = e.target.closest('[data-expiry-action]');
if (!target) return;
e.preventDefault();
const action = target.getAttribute('data-expiry-action');
const days = parseInt(target.getAttribute('data-days') || '0', 10);
const hours = parseInt(target.getAttribute('data-hours') || '0', 10);
if (action === 'set') {
setExpiryDateTime(days, hours);
} else if (action === 'end-of-day') {
setEndOfDay();
} else if (action === 'clear') {
clearExpiryDate();
}
});
}
// Function to set expiry date and time by adding days and hours (stackable)
function setExpiryDateTime(days, hours) {
let baseDate;
// Check if there's already a selected datetime
const year = document.getElementById('expiry_year')?.value;
const month = document.getElementById('expiry_month')?.value;
const day = document.getElementById('expiry_day')?.value;
const hour = document.getElementById('expiry_hour')?.value;
const minute = document.getElementById('expiry_minute')?.value;
if (year && month && day && hour && minute) {
// Use existing selected datetime as base
baseDate = new Date(year, parseInt(month) - 1, day, hour, minute);
showExpiryToast('Added ' + (days > 0 ? days + ' day' + (days !== 1 ? 's' : '') : '') + (days > 0 && hours > 0 ? ' and ' : '') + (hours > 0 ? hours + ' hour' + (hours !== 1 ? 's' : '') : ''), 'info');
} else {
// Start from current time
baseDate = new Date();
showExpiryToast('Expiry date set to ' + formatDateTimeForDisplay(baseDate), 'success');
}
// Add the days and hours
baseDate.setDate(baseDate.getDate() + days);
baseDate.setHours(baseDate.getHours() + hours);
// Update all selectors
document.getElementById('expiry_year').value = baseDate.getFullYear().toString();
document.getElementById('expiry_month').value = String(baseDate.getMonth() + 1).padStart(2, '0');
document.getElementById('expiry_day').value = String(baseDate.getDate()).padStart(2, '0');
document.getElementById('expiry_hour').value = String(baseDate.getHours()).padStart(2, '0');
document.getElementById('expiry_minute').value = String(baseDate.getMinutes()).padStart(2, '0');
updateDateTime();
// Add animated visual feedback
['expiry_year', 'expiry_month', 'expiry_day', 'expiry_hour', 'expiry_minute'].forEach(id => {
const el = document.getElementById(id);
if (el) {
el.classList.add('ring-2', 'ring-green-500', 'dark:ring-green-400', 'scale-105');
setTimeout(() => {
el.classList.remove('ring-2', 'ring-green-500', 'dark:ring-green-400', 'scale-105');
}, 600);
}
});
}
// Function to set expiry to end of current day (23:59)
function setEndOfDay() {
const endOfDay = new Date();
endOfDay.setHours(23, 59, 0, 0);
document.getElementById('expiry_year').value = endOfDay.getFullYear().toString();
document.getElementById('expiry_month').value = String(endOfDay.getMonth() + 1).padStart(2, '0');
document.getElementById('expiry_day').value = String(endOfDay.getDate()).padStart(2, '0');
document.getElementById('expiry_hour').value = '23';
document.getElementById('expiry_minute').value = '55';
updateDateTime();
['expiry_year', 'expiry_month', 'expiry_day', 'expiry_hour', 'expiry_minute'].forEach(id => {
const el = document.getElementById(id);
if (el) {
el.classList.add('ring-2', 'ring-indigo-500', 'dark:ring-indigo-400', 'scale-105');
setTimeout(() => {
el.classList.remove('ring-2', 'ring-indigo-500', 'dark:ring-indigo-400', 'scale-105');
}, 600);
}
});
showExpiryToast('Expiry set to end of today (23:59)', 'info');
}
// Function to clear expiry date
function clearExpiryDate() {
document.getElementById('expiry_year').value = '';
document.getElementById('expiry_month').value = '';
document.getElementById('expiry_day').value = '';
document.getElementById('expiry_hour').value = '';
document.getElementById('expiry_minute').value = '';
document.getElementById('rolechangedate').value = '';
const preview = document.getElementById('datetime_preview');
if (preview) {
preview.classList.add('hidden');
}
// Add a visual feedback with gray pulse
['expiry_year', 'expiry_month', 'expiry_day', 'expiry_hour', 'expiry_minute'].forEach(id => {
const el = document.getElementById(id);
if (el) {
el.classList.add('ring-2', 'ring-gray-500', 'dark:ring-gray-400', 'scale-105');
setTimeout(() => {
el.classList.remove('ring-2', 'ring-gray-500', 'dark:ring-gray-400', 'scale-105');
}, 600);
}
});
showExpiryToast('Expiry date cleared - role is now permanent', 'info');
}
// Format date for display
function formatDateTimeForDisplay(date) {
const options = {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
};
return date.toLocaleDateString('en-US', options);
}
// Show toast notification specific to expiry date changes
function showExpiryToast(message, type) {
type = type || 'success';
// Remove existing toast if any
const existingToast = document.getElementById('expiryToast');
if (existingToast) {
existingToast.remove();
}
// Create toast
const toast = document.createElement('div');
toast.id = 'expiryToast';
toast.className = 'fixed bottom-4 right-4 px-4 py-3 rounded-lg shadow-lg flex items-center gap-2 z-50 transform transition-all duration-300 translate-y-0 opacity-100';
if (type === 'success') {
toast.classList.add('bg-green-500', 'dark:bg-green-600', 'text-white');
toast.innerHTML = '<i class="fa fa-check-circle"></i><span>' + escapeHtml(message) + '</span>';
} else if (type === 'info') {
toast.classList.add('bg-blue-500', 'dark:bg-blue-600', 'text-white');
toast.innerHTML = '<i class="fa fa-info-circle"></i><span>' + escapeHtml(message) + '</span>';
}
document.body.appendChild(toast);
// Animate in
setTimeout(() => {
toast.style.transform = 'translateY(0)';
toast.style.opacity = '1';
}, 10);
// Remove after 3 seconds with fade out
setTimeout(() => {
toast.style.transform = 'translateY(20px)';
toast.style.opacity = '0';
setTimeout(() => toast.remove(), 300);
}, 3000);
}
// Utility function to escape HTML
function escapeHtml(text) {
// Handle non-string values
if (text === null || text === undefined) {
return '';
}
// Convert to string if it's not already
if (typeof text !== 'string') {
text = String(text);
}
const map = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
};
return text.replace(/[&<>"']/g, function(m) { return map[m]; });
}
// TinyMCE Initialization for Admin Content Pages
function initTinyMCE() {
// Only initialize if TinyMCE editor element exists (check for #body or .tinymce-editor)
if (!document.getElementById('body') && !document.querySelector('.tinymce-editor')) {
return;
}
// Function to dynamically load TinyMCE from CDN
function loadTinyMCEScript() {
return new Promise(function(resolve, reject) {
// Check if already loaded
if (typeof tinymce !== 'undefined') {
resolve();
return;
}
// Get API key from textarea data attribute or create meta tag
const bodyTextarea = document.getElementById('body');
const tinymceEditors = document.querySelectorAll('.tinymce-editor');
let apiKey = 'no-api-key';
// Try to get from existing meta tag first
let apiKeyMeta = document.querySelector('meta[name="tinymce-api-key"]');
if (!apiKeyMeta) {
// Create meta tag dynamically if it doesn't exist
// Get API key from window config or use default
if (window.NNTmuxConfig && window.NNTmuxConfig.tinymceApiKey) {
apiKey = window.NNTmuxConfig.tinymceApiKey;
} else if (bodyTextarea && bodyTextarea.dataset.tinymceApiKey) {
apiKey = bodyTextarea.dataset.tinymceApiKey;
} else if (tinymceEditors.length > 0) {
// Check if any tinymce-editor has the API key
for (let i = 0; i < tinymceEditors.length; i++) {
if (tinymceEditors[i].dataset.tinymceApiKey) {
apiKey = tinymceEditors[i].dataset.tinymceApiKey;
break;
}
}
}
// Create and append meta tag
apiKeyMeta = document.createElement('meta');
apiKeyMeta.setAttribute('name', 'tinymce-api-key');
apiKeyMeta.setAttribute('content', apiKey);
document.head.appendChild(apiKeyMeta);
} else {
apiKey = apiKeyMeta.content;
}
// Create script element
const script = document.createElement('script');
script.src = 'https://cdn.tiny.cloud/1/' + apiKey + '/tinymce/8/tinymce.min.js';
script.referrerPolicy = 'origin';
script.onload = function() {
console.log('TinyMCE script loaded from CDN');
resolve();
};
script.onerror = function() {
console.error('Failed to load TinyMCE script from CDN');
reject(new Error('Failed to load TinyMCE'));
};
// Append to head
document.head.appendChild(script);
});
}
// Function to detect if dark mode is active
function isDarkMode() {
return document.documentElement.classList.contains('dark') ||
(window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches);
}
// Function to get TinyMCE config
function getTinyMCEConfig() {
const darkMode = isDarkMode();
const apiKeyMeta = document.querySelector('meta[name="tinymce-api-key"]');
const apiKey = apiKeyMeta ? apiKeyMeta.content : 'no-api-key';
return {
selector: '#body, .tinymce-editor',
height: 500,
menubar: true,
skin: darkMode ? 'oxide-dark' : 'oxide',
content_css: darkMode ? 'dark' : 'default',
plugins: [
'advlist', 'autolink', 'lists', 'link', 'image', 'charmap', 'preview',
'anchor', 'searchreplace', 'visualblocks', 'code', 'fullscreen',
'insertdatetime', 'media', 'table', 'help', 'wordcount', 'emoticons'
],
toolbar: 'undo redo | blocks fontfamily fontsize | ' +
'bold italic underline strikethrough | forecolor backcolor | ' +
'alignleft aligncenter alignright alignjustify | ' +
'bullist numlist outdent indent | ' +
'link image media table emoticons | ' +
'removeformat code fullscreen | help',
toolbar_mode: 'sliding',
content_style: 'body { font-family: Helvetica, Arial, sans-serif; font-size: 14px; line-height: 1.6; }',
branding: false,
promotion: false,
resize: true,
statusbar: true,
elementpath: true,
automatic_uploads: false,
file_picker_types: 'image',
font_family_formats: 'Arial=arial,helvetica,sans-serif; Courier New=courier new,courier,monospace; Georgia=georgia,palatino,serif; Tahoma=tahoma,arial,helvetica,sans-serif; Times New Roman=times new roman,times,serif; Verdana=verdana,geneva,sans-serif',
font_size_formats: '8pt 10pt 12pt 14pt 16pt 18pt 24pt 36pt 48pt',
autolink_pattern: /^(https?:\/\/|www\.|(?!www\.)[a-z0-9\-]+\.[a-z]{2,13})/i,
link_default_protocol: 'https',
link_assume_external_targets: true,
link_target_list: [
{title: 'None', value: ''},
{title: 'New window', value: '_blank'},
{title: 'Same window', value: '_self'}
],
block_formats: 'Paragraph=p; Heading 1=h1; Heading 2=h2; Heading 3=h3; Heading 4=h4; Heading 5=h5; Heading 6=h6; Preformatted=pre; Blockquote=blockquote',
valid_elements: '*[*]',
extended_valid_elements: '*[*]',
valid_children: '+body[style]',
paste_as_text: false,
paste_block_drop: false,
paste_data_images: true,
image_advtab: true,
image_caption: true,
image_description: true,
image_dimensions: true,
image_title: true,
table_default_attributes: {
border: '1'
},
table_default_styles: {
'border-collapse': 'collapse',
'width': '100%'
},
emoticons_database: 'emojis',
setup: function(editor) {
// Auto-save on content change
editor.on('change', function() {
editor.save();
});
// Auto-save on blur (when editor loses focus)
editor.on('blur', function() {
editor.save();
});
// Auto-save on keyup (for continuous saving)
editor.on('keyup', function() {
editor.save();
});
// Save before form submission
editor.on('submit', function() {
editor.save();
});
}
};
}
// Function to actually initialize TinyMCE once it's loaded
function doInitTinyMCE() {
// Initialize TinyMCE
tinymce.init(getTinyMCEConfig()).then(function(editors) {
if (editors && editors.length > 0) {
console.log('TinyMCE initialized successfully for ' + editors.length + ' editor(s)');
// Add form submission handler to sync content for all editors
editors.forEach(function(editor) {
const textarea = document.getElementById(editor.id);
if (textarea && textarea.form) {
// Remove any existing listeners to avoid duplicates
const form = textarea.form;
if (!form.hasAttribute('data-tinymce-handler')) {
form.addEventListener('submit', function(e) {
// Sync all TinyMCE editors before form submission
tinymce.triggerSave();
console.log('TinyMCE content synced to textareas before form submission');
});
form.setAttribute('data-tinymce-handler', 'true');
}
}
});
}
}).catch(function(error) {
console.error('TinyMCE initialization failed:', error);
});
// Watch for theme changes and reinitialize TinyMCE
const observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.attributeName === 'class') {
// Get all active TinyMCE editors
const allEditors = tinymce.editors;
if (allEditors && allEditors.length > 0) {
// Save content from all editors
const editorContents = {};
allEditors.forEach(function(editor) {
editorContents[editor.id] = editor.getContent();
});
// Remove all editors
tinymce.remove();
// Reinitialize with new theme
tinymce.init(getTinyMCEConfig()).then(function(editors) {
// Restore content to each editor
if (editors && editors.length > 0) {
editors.forEach(function(editor) {
if (editorContents[editor.id]) {
editor.setContent(editorContents[editor.id]);
}
});
}
});
}
}
});
});
// Start observing theme changes on the html element
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ['class']
});
}
// Load TinyMCE script dynamically, then initialize
loadTinyMCEScript()
.then(function() {
console.log('TinyMCE available, initializing editor...');
doInitTinyMCE();
})
.catch(function(error) {
console.error('Failed to load TinyMCE:', error);
// Show error message to user for all TinyMCE textareas
const textareas = document.querySelectorAll('#body, .tinymce-editor');
textareas.forEach(function(textarea) {
if (textarea && textarea.parentElement) {
const errorDiv = document.createElement('div');
errorDiv.className = 'mt-2 p-3 bg-red-50 dark:bg-red-900 border border-red-200 dark:border-red-700 text-red-800 dark:text-red-200 rounded';
errorDiv.innerHTML = '<i class="fas fa-exclamation-triangle mr-2"></i>TinyMCE editor failed to load. Please refresh the page or check your internet connection.';
textarea.parentElement.insertBefore(errorDiv, textarea.nextSibling);
}
});
});
}
// Mobile Enhancements
function initMobileEnhancements() {
// Create mobile sidebar overlay
createMobileSidebarOverlay();
// Enhanced mobile sidebar toggle
initMobileSidebarToggle();
// Mobile menu toggle in header
initMobileHeaderMenu();
// Touch-friendly improvements
initTouchEnhancements();
// Mobile table responsiveness
initMobileTableEnhancements();
// Prevent zoom on double tap for buttons
preventDoubleTapZoom();
// Optimize modals for mobile
optimizeModalsForMobile();
}
// Create overlay for mobile sidebar
function createMobileSidebarOverlay() {
if (document.querySelector('.mobile-sidebar-overlay')) return;
const overlay = document.createElement('div');
overlay.className = 'mobile-sidebar-overlay';
overlay.addEventListener('click', function() {
closeMobileSidebar();
});
document.body.appendChild(overlay);
}
// Mobile sidebar toggle functionality
function initMobileSidebarToggle() {
const mobileToggle = document.getElementById('mobile-sidebar-toggle');
const sidebar = document.getElementById('sidebar');
if (mobileToggle && sidebar) {
mobileToggle.addEventListener('click', function(e) {
e.stopPropagation();
toggleMobileSidebar();
});
}
}
function toggleMobileSidebar() {
const sidebar = document.getElementById('sidebar');
const overlay = document.querySelector('.mobile-sidebar-overlay');
if (sidebar && overlay) {
sidebar.classList.toggle('mobile-open');
overlay.classList.toggle('active');
// Prevent body scroll when sidebar is open
if (sidebar.classList.contains('mobile-open')) {
document.body.style.overflow = 'hidden';
} else {
document.body.style.overflow = '';
}
}
}
function closeMobileSidebar() {
const sidebar = document.getElementById('sidebar');
const overlay = document.querySelector('.mobile-sidebar-overlay');
if (sidebar && overlay) {
sidebar.classList.remove('mobile-open');
overlay.classList.remove('active');
document.body.style.overflow = '';
}
}
// Mobile header menu toggle
function initMobileHeaderMenu() {
// Mobile search toggle
const mobileSearchToggle = document.getElementById('mobile-search-toggle');
const mobileSearchForm = document.getElementById('mobile-search-form');
if (mobileSearchToggle && mobileSearchForm) {
mobileSearchToggle.addEventListener('click', function(e) {
e.stopPropagation();
if (mobileSearchForm.classList.contains('hidden')) {
mobileSearchForm.classList.remove('hidden');
// Close mobile menu if open
const mobileMenu = document.getElementById('mobile-menu');
if (mobileMenu) {
mobileMenu.style.display = 'none';
}
} else {
mobileSearchForm.classList.add('hidden');
}
});
// Close mobile search when clicking outside
document.addEventListener('click', function(e) {
if (!mobileSearchForm.contains(e.target) &&
!mobileSearchToggle.contains(e.target) &&
!mobileSearchForm.classList.contains('hidden')) {
mobileSearchForm.classList.add('hidden');
}
});
}
const mobileMenuToggle = document.getElementById('mobile-menu-toggle');
if (mobileMenuToggle) {
mobileMenuToggle.addEventListener('click', function(e) {
e.stopPropagation();
// Close mobile search if open
if (mobileSearchForm && !mobileSearchForm.classList.contains('hidden')) {
mobileSearchForm.classList.add('hidden');
}
const desktopNav = document.querySelector('.hidden.md\\:flex');
if (desktopNav) {
// Create mobile menu if it doesn't exist
let mobileMenu = document.getElementById('mobile-menu');
if (!mobileMenu) {
mobileMenu = document.createElement('div');
mobileMenu.id = 'mobile-menu';
mobileMenu.className = 'md:hidden absolute top-16 left-0 right-0 bg-gray-800 dark:bg-gray-950 shadow-lg z-50 max-h-[80vh] overflow-y-auto';
mobileMenu.style.display = 'none';
// Clone navigation content
const navClone = desktopNav.cloneNode(true);
navClone.classList.remove('hidden', 'md:flex', 'md:items-center', 'md:space-x-1', 'flex-1');
navClone.classList.add('flex', 'flex-col', 'space-y-2', 'p-4');
// Update dropdown styles for mobile
navClone.querySelectorAll('.dropdown-container').forEach(dropdown => {
dropdown.classList.remove('relative');
dropdown.classList.add('w-full');
const button = dropdown.querySelector('.dropdown-toggle');
if (button) {
button.classList.add('w-full', 'justify-between');
}
const menu = dropdown.querySelector('.dropdown-menu');
if (menu) {
menu.classList.remove('absolute', 'left-0', 'top-full', 'w-48');
menu.classList.add('relative', 'w-full', 'mt-2');
}
});
mobileMenu.appendChild(navClone);
mobileMenuToggle.parentElement.parentElement.appendChild(mobileMenu);
}
// Toggle menu visibility
if (mobileMenu.style.display === 'none') {
mobileMenu.style.display = 'block';
mobileMenuToggle.querySelector('i').classList.replace('fa-bars', 'fa-times');
} else {
mobileMenu.style.display = 'none';
mobileMenuToggle.querySelector('i').classList.replace('fa-times', 'fa-bars');
}
}
});
}
// Close mobile menu when clicking outside
document.addEventListener('click', function(e) {
const mobileMenu = document.getElementById('mobile-menu');
const mobileMenuToggle = document.getElementById('mobile-menu-toggle');
if (mobileMenu && mobileMenuToggle &&
mobileMenu.style.display === 'block' &&
!mobileMenu.contains(e.target) &&
!mobileMenuToggle.contains(e.target)) {
mobileMenu.style.display = 'none';
mobileMenuToggle.querySelector('i').classList.replace('fa-times', 'fa-bars');
}
});
}
// Touch enhancements for better mobile interaction
function initTouchEnhancements() {
// Add touch feedback to all buttons
document.addEventListener('touchstart', function(e) {
const btn = e.target.closest('.btn, button, a[class*="btn"]');
if (btn) {
btn.style.opacity = '0.7';
}
}, { passive: true });
document.addEventListener('touchend', function(e) {
const btn = e.target.closest('.btn, button, a[class*="btn"]');
if (btn) {
setTimeout(() => {
btn.style.opacity = '';
}, 100);
}
}, { passive: true });
// Improve dropdown touch handling
document.querySelectorAll('.dropdown-toggle').forEach(toggle => {
let touchStartY = 0;
toggle.addEventListener('touchstart', function(e) {
touchStartY = e.touches[0].clientY;
}, { passive: true });
toggle.addEventListener('touchend', function(e) {
const touchEndY = e.changedTouches[0].clientY;
const touchDiff = Math.abs(touchEndY - touchStartY);
// Only trigger if it's a tap, not a scroll
if (touchDiff < 10) {
e.preventDefault();
this.click();
}
});
});
}
// Mobile table enhancements
function initMobileTableEnhancements() {
// Add data labels to table cells for mobile view
const tables = document.querySelectorAll('table');
tables.forEach(table => {
const headers = Array.from(table.querySelectorAll('thead th')).map(th => th.textContent.trim());
table.querySelectorAll('tbody tr').forEach(row => {
Array.from(row.querySelectorAll('td')).forEach((cell, index) => {
if (headers[index] && !cell.hasAttribute('data-label')) {
cell.setAttribute('data-label', headers[index]);
}
});
});
// Add responsive wrapper if not present
if (!table.parentElement.classList.contains('table-responsive')) {
const wrapper = document.createElement('div');
wrapper.className = 'table-responsive';
table.parentNode.insertBefore(wrapper, table);
wrapper.appendChild(table);
}
});
}
// Prevent double-tap zoom on buttons
function preventDoubleTapZoom() {
let lastTouchEnd = 0;
document.addEventListener('touchend', function(e) {
const now = Date.now();
if (now - lastTouchEnd <= 300) {
const target = e.target.closest('button, a, .btn, input[type="submit"]');
if (target) {
e.preventDefault();
}
}
lastTouchEnd = now;
}, { passive: false });
}
// Optimize modals for mobile devices
function optimizeModalsForMobile() {
// Ensure modals are properly sized on mobile
const observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
mutation.addedNodes.forEach(function(node) {
if (node.nodeType === 1) {
const modal = node.querySelector('.modal-content') ||
(node.classList && node.classList.contains('modal-content') ? node : null);
if (modal && window.innerWidth < 768) {
modal.style.maxHeight = '90vh';
modal.style.overflowY = 'auto';
modal.style.margin = '0.5rem';
modal.style.width = 'calc(100% - 1rem)';
}
}
});
});
});
observer.observe(document.body, { childList: true, subtree: true });
// Handle existing modals
document.querySelectorAll('.modal-content').forEach(modal => {
if (window.innerWidth < 768) {
modal.style.maxHeight = '90vh';
modal.style.overflowY = 'auto';
modal.style.margin = '0.5rem';
modal.style.width = 'calc(100% - 1rem)';
}
});
// Close sidebar when opening modal
document.addEventListener('click', function(e) {
if (e.target.closest('[data-open-nfo], [data-open-image-modal], .mediainfo-badge, .filelist-badge')) {
closeMobileSidebar();
}
});
}
// Admin Dashboard Charts Initialization
function initAdminDashboardCharts() {
// Check if Chart.js is loaded and if we're on the admin dashboard
if (typeof Chart === 'undefined') {
return;
}
// Check for dark mode
const isDarkMode = document.documentElement.classList.contains('dark');
const textColor = isDarkMode ? '#e5e7eb' : '#374151';
const gridColor = isDarkMode ? '#374151' : '#e5e7eb';
// Store chart instances for updates
window.adminCharts = {
cpu24h: null,
ram24h: null,
cpu30d: null,
ram30d: null,
downloadsMinute: null,
apiHitsMinute: null
};
// Downloads Chart (Hourly)
const downloadsCtx = document.getElementById('downloadsChart');
if (downloadsCtx) {
const downloadsData = JSON.parse(downloadsCtx.getAttribute('data-chart-data') || '[]');
new Chart(downloadsCtx, {
type: 'bar',
data: {
labels: downloadsData.map(d => d.time || d.date),
datasets: [{
label: 'Downloads',
data: downloadsData.map(d => d.count),
backgroundColor: 'rgba(34, 197, 94, 0.7)',
borderColor: 'rgba(34, 197, 94, 1)',
borderWidth: 2,
borderRadius: 6,
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
},
tooltip: {
backgroundColor: isDarkMode ? '#1f2937' : '#ffffff',
titleColor: textColor,
bodyColor: textColor,
borderColor: gridColor,
borderWidth: 1,
padding: 12,
displayColors: false,
callbacks: {
label: function(context) {
return 'Downloads: ' + context.parsed.y.toLocaleString();
}
}
}
},
scales: {
y: {
beginAtZero: true,
ticks: {
color: textColor,
precision: 0
},
grid: {
color: gridColor,
drawBorder: false
}
},
x: {
ticks: {
color: textColor,
maxRotation: 45,
minRotation: 45,
maxTicksLimit: 24
},
grid: {
display: false
}
}
}
}
});
}
// API Hits Chart (Hourly)
const apiHitsCtx = document.getElementById('apiHitsChart');
if (apiHitsCtx) {
const apiHitsData = JSON.parse(apiHitsCtx.getAttribute('data-chart-data') || '[]');
new Chart(apiHitsCtx, {
type: 'line',
data: {
labels: apiHitsData.map(d => d.time || d.date),
datasets: [{
label: 'API Hits',
data: apiHitsData.map(d => d.count),
backgroundColor: 'rgba(147, 51, 234, 0.1)',
borderColor: 'rgba(147, 51, 234, 1)',
borderWidth: 3,
fill: true,
tension: 0.4,
pointRadius: 4,
pointHoverRadius: 6,
pointBackgroundColor: 'rgba(147, 51, 234, 1)',
pointBorderColor: '#ffffff',
pointBorderWidth: 2,
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
},
tooltip: {
backgroundColor: isDarkMode ? '#1f2937' : '#ffffff',
titleColor: textColor,
bodyColor: textColor,
borderColor: gridColor,
borderWidth: 1,
padding: 12,
displayColors: false,
callbacks: {
label: function(context) {
return 'API Hits: ' + context.parsed.y.toLocaleString();
}
}
}
},
scales: {
y: {
beginAtZero: true,
ticks: {
color: textColor,
precision: 0
},
grid: {
color: gridColor,
drawBorder: false
}
},
x: {
ticks: {
color: textColor,
maxRotation: 45,
minRotation: 45,
maxTicksLimit: 24
},
grid: {
color: gridColor,
drawBorder: false
}
}
}
}
});
}
// Downloads Per Minute Chart
const downloadsMinuteCtx = document.getElementById('downloadsMinuteChart');
if (downloadsMinuteCtx) {
const downloadsMinuteData = JSON.parse(downloadsMinuteCtx.getAttribute('data-chart-data') || '[]');
window.adminCharts.downloadsMinute = new Chart(downloadsMinuteCtx, {
type: 'line',
data: {
labels: downloadsMinuteData.map(d => d.time),
datasets: [{
label: 'Downloads',
data: downloadsMinuteData.map(d => d.count),
backgroundColor: 'rgba(34, 197, 94, 0.1)',
borderColor: 'rgba(34, 197, 94, 1)',
borderWidth: 2,
fill: true,
tension: 0.4,
pointRadius: 2,
pointHoverRadius: 4,
pointBackgroundColor: 'rgba(34, 197, 94, 1)',
pointBorderColor: '#ffffff',
pointBorderWidth: 1,
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
},
tooltip: {
backgroundColor: isDarkMode ? '#1f2937' : '#ffffff',
titleColor: textColor,
bodyColor: textColor,
borderColor: gridColor,
borderWidth: 1,
padding: 12,
displayColors: false,
callbacks: {
label: function(context) {
return 'Downloads: ' + context.parsed.y.toLocaleString();
}
}
}
},
scales: {
y: {
beginAtZero: true,
ticks: {
color: textColor,
precision: 0
},
grid: {
color: gridColor,
drawBorder: false
}
},
x: {
ticks: {
color: textColor,
maxRotation: 45,
minRotation: 45,
maxTicksLimit: 12
},
grid: {
color: gridColor,
drawBorder: false
}
}
}
}
});
}
// API Hits Per Minute Chart
const apiHitsMinuteCtx = document.getElementById('apiHitsMinuteChart');
if (apiHitsMinuteCtx) {
const apiHitsMinuteData = JSON.parse(apiHitsMinuteCtx.getAttribute('data-chart-data') || '[]');
window.adminCharts.apiHitsMinute = new Chart(apiHitsMinuteCtx, {
type: 'line',
data: {
labels: apiHitsMinuteData.map(d => d.time),
datasets: [{
label: 'API Hits',
data: apiHitsMinuteData.map(d => d.count),
backgroundColor: 'rgba(147, 51, 234, 0.1)',
borderColor: 'rgba(147, 51, 234, 1)',
borderWidth: 2,
fill: true,
tension: 0.4,
pointRadius: 2,
pointHoverRadius: 4,
pointBackgroundColor: 'rgba(147, 51, 234, 1)',
pointBorderColor: '#ffffff',
pointBorderWidth: 1,
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
},
tooltip: {
backgroundColor: isDarkMode ? '#1f2937' : '#ffffff',
titleColor: textColor,
bodyColor: textColor,
borderColor: gridColor,
borderWidth: 1,
padding: 12,
displayColors: false,
callbacks: {
label: function(context) {
return 'API Hits: ' + context.parsed.y.toLocaleString();
}
}
}
},
scales: {
y: {
beginAtZero: true,
ticks: {
color: textColor,
precision: 0
},
grid: {
color: gridColor,
drawBorder: false
}
},
x: {
ticks: {
color: textColor,
maxRotation: 45,
minRotation: 45,
maxTicksLimit: 12
},
grid: {
color: gridColor,
drawBorder: false
}
}
}
}
});
}
// CPU Usage 24 Hour Historical Chart (Line)
const cpuHistory24hCtx = document.getElementById('cpuHistory24hChart');
if (cpuHistory24hCtx) {
const cpuHistory = JSON.parse(cpuHistory24hCtx.getAttribute('data-history') || '[]');
const cpuLabel = cpuHistory24hCtx.getAttribute('data-chart-label') || 'CPU Usage %';
window.adminCharts.cpu24h = new Chart(cpuHistory24hCtx, {
type: 'line',
data: {
labels: cpuHistory.map(d => d.time),
datasets: [{
label: cpuLabel,
data: cpuHistory.map(d => d.value),
backgroundColor: 'rgba(249, 115, 22, 0.1)',
borderColor: 'rgba(249, 115, 22, 1)',
borderWidth: 3,
fill: true,
tension: 0.4,
pointRadius: 4,
pointHoverRadius: 6,
pointBackgroundColor: 'rgba(249, 115, 22, 1)',
pointBorderColor: '#ffffff',
pointBorderWidth: 2,
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
},
tooltip: {
backgroundColor: isDarkMode ? '#1f2937' : '#ffffff',
titleColor: textColor,
bodyColor: textColor,
borderColor: gridColor,
borderWidth: 1,
padding: 12,
displayColors: false,
callbacks: {
label: function(context) {
return 'CPU: ' + context.parsed.y.toFixed(1) + '%';
}
}
}
},
scales: {
y: {
beginAtZero: true,
max: 100,
ticks: {
color: textColor,
callback: function(value) {
return value + '%';
}
},
grid: {
color: gridColor,
drawBorder: false
}
},
x: {
ticks: {
color: textColor,
maxRotation: 45,
minRotation: 45
},
grid: {
color: gridColor,
drawBorder: false
}
}
}
}
});
}
// RAM Usage 24 Hour Historical Chart (Line)
const ramHistory24hCtx = document.getElementById('ramHistory24hChart');
if (ramHistory24hCtx) {
const ramHistory = JSON.parse(ramHistory24hCtx.getAttribute('data-history') || '[]');
const ramLabel = ramHistory24hCtx.getAttribute('data-chart-label') || 'RAM Usage %';
window.adminCharts.ram24h = new Chart(ramHistory24hCtx, {
type: 'line',
data: {
labels: ramHistory.map(d => d.time),
datasets: [{
label: ramLabel,
data: ramHistory.map(d => d.value),
backgroundColor: 'rgba(6, 182, 212, 0.1)',
borderColor: 'rgba(6, 182, 212, 1)',
borderWidth: 3,
fill: true,
tension: 0.4,
pointRadius: 4,
pointHoverRadius: 6,
pointBackgroundColor: 'rgba(6, 182, 212, 1)',
pointBorderColor: '#ffffff',
pointBorderWidth: 2,
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
},
tooltip: {
backgroundColor: isDarkMode ? '#1f2937' : '#ffffff',
titleColor: textColor,
bodyColor: textColor,
borderColor: gridColor,
borderWidth: 1,
padding: 12,
displayColors: false,
callbacks: {
label: function(context) {
return 'RAM: ' + context.parsed.y.toFixed(1) + '%';
}
}
}
},
scales: {
y: {
beginAtZero: true,
max: 100,
ticks: {
color: textColor,
callback: function(value) {
return value + '%';
}
},
grid: {
color: gridColor,
drawBorder: false
}
},
x: {
ticks: {
color: textColor,
maxRotation: 45,
minRotation: 45
},
grid: {
color: gridColor,
drawBorder: false
}
}
}
}
});
}
// CPU Usage 30 Day Historical Chart (Line)
const cpuHistory30dCtx = document.getElementById('cpuHistory30dChart');
if (cpuHistory30dCtx) {
const cpuHistory = JSON.parse(cpuHistory30dCtx.getAttribute('data-history') || '[]');
const cpuLabel = cpuHistory30dCtx.getAttribute('data-chart-label') || 'CPU Usage %';
window.adminCharts.cpu30d = new Chart(cpuHistory30dCtx, {
type: 'line',
data: {
labels: cpuHistory.map(d => d.time),
datasets: [{
label: cpuLabel,
data: cpuHistory.map(d => d.value),
backgroundColor: 'rgba(249, 115, 22, 0.1)',
borderColor: 'rgba(249, 115, 22, 1)',
borderWidth: 3,
fill: true,
tension: 0.4,
pointRadius: 3,
pointHoverRadius: 5,
pointBackgroundColor: 'rgba(249, 115, 22, 1)',
pointBorderColor: '#ffffff',
pointBorderWidth: 2,
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
},
tooltip: {
backgroundColor: isDarkMode ? '#1f2937' : '#ffffff',
titleColor: textColor,
bodyColor: textColor,
borderColor: gridColor,
borderWidth: 1,
padding: 12,
displayColors: false,
callbacks: {
label: function(context) {
return 'CPU: ' + context.parsed.y.toFixed(1) + '%';
}
}
}
},
scales: {
y: {
beginAtZero: true,
max: 100,
ticks: {
color: textColor,
callback: function(value) {
return value + '%';
}
},
grid: {
color: gridColor,
drawBorder: false
}
},
x: {
ticks: {
color: textColor,
maxRotation: 45,
minRotation: 45
},
grid: {
color: gridColor,
drawBorder: false
}
}
}
}
});
}
// RAM Usage 30 Day Historical Chart (Line)
const ramHistory30dCtx = document.getElementById('ramHistory30dChart');
if (ramHistory30dCtx) {
const ramHistory = JSON.parse(ramHistory30dCtx.getAttribute('data-history') || '[]');
const ramLabel = ramHistory30dCtx.getAttribute('data-chart-label') || 'RAM Usage %';
window.adminCharts.ram30d = new Chart(ramHistory30dCtx, {
type: 'line',
data: {
labels: ramHistory.map(d => d.time),
datasets: [{
label: ramLabel,
data: ramHistory.map(d => d.value),
backgroundColor: 'rgba(6, 182, 212, 0.1)',
borderColor: 'rgba(6, 182, 212, 1)',
borderWidth: 3,
fill: true,
tension: 0.4,
pointRadius: 3,
pointHoverRadius: 5,
pointBackgroundColor: 'rgba(6, 182, 212, 1)',
pointBorderColor: '#ffffff',
pointBorderWidth: 2,
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
},
tooltip: {
backgroundColor: isDarkMode ? '#1f2937' : '#ffffff',
titleColor: textColor,
bodyColor: textColor,
borderColor: gridColor,
borderWidth: 1,
padding: 12,
displayColors: false,
callbacks: {
label: function(context) {
return 'RAM: ' + context.parsed.y.toFixed(1) + '%';
}
}
}
},
scales: {
y: {
beginAtZero: true,
max: 100,
ticks: {
color: textColor,
callback: function(value) {
return value + '%';
}
},
grid: {
color: gridColor,
drawBorder: false
}
},
x: {
ticks: {
color: textColor,
maxRotation: 45,
minRotation: 45
},
grid: {
color: gridColor,
drawBorder: false
}
}
}
}
});
}
// Start auto-refresh for system metrics (every 60 seconds)
startSystemMetricsAutoRefresh();
}
// Auto-refresh system metrics every minute
function startSystemMetricsAutoRefresh() {
// Check if we're on the admin dashboard with system metrics
if (!document.getElementById('cpuHistory24hChart')) {
return;
}
// Update current metrics display
function updateCurrentMetrics() {
fetch('/admin/api/system-metrics/current')
.then(response => response.json())
.then(data => {
// Update CPU current usage
const cpuCurrentElem = document.querySelector('[data-metric="cpu-current"]');
if (cpuCurrentElem) {
cpuCurrentElem.textContent = data.cpu.current.toFixed(1) + '%';
}
// Update CPU load averages
const load1minElem = document.querySelector('[data-metric="cpu-load-1min"]');
const load5minElem = document.querySelector('[data-metric="cpu-load-5min"]');
const load15minElem = document.querySelector('[data-metric="cpu-load-15min"]');
if (load1minElem) load1minElem.textContent = data.cpu.load_average['1min'];
if (load5minElem) load5minElem.textContent = data.cpu.load_average['5min'];
if (load15minElem) load15minElem.textContent = data.cpu.load_average['15min'];
// Update RAM current usage
const ramCurrentElem = document.querySelector('[data-metric="ram-current"]');
if (ramCurrentElem) {
ramCurrentElem.textContent = data.ram.percentage.toFixed(1) + '%';
}
const ramDetailsElem = document.querySelector('[data-metric="ram-details"]');
if (ramDetailsElem) {
ramDetailsElem.textContent = `${data.ram.used.toFixed(2)} GB / ${data.ram.total.toFixed(2)} GB`;
}
// Update last updated timestamp
const timestampElem = document.querySelector('[data-metric="last-updated"]');
if (timestampElem) {
const now = new Date();
timestampElem.textContent = now.toLocaleTimeString();
}
})
.catch(error => {
console.error('Error fetching current metrics:', error);
});
}
// Update historical charts
function updateHistoricalCharts() {
fetch('/admin/api/system-metrics/historical')
.then(response => response.json())
.then(data => {
// Update CPU 24h chart
if (window.adminCharts && window.adminCharts.cpu24h) {
window.adminCharts.cpu24h.data.labels = data.cpu.history_24h.map(d => d.time);
window.adminCharts.cpu24h.data.datasets[0].data = data.cpu.history_24h.map(d => d.value);
window.adminCharts.cpu24h.update('none'); // 'none' for no animation
}
// Update RAM 24h chart
if (window.adminCharts && window.adminCharts.ram24h) {
window.adminCharts.ram24h.data.labels = data.ram.history_24h.map(d => d.time);
window.adminCharts.ram24h.data.datasets[0].data = data.ram.history_24h.map(d => d.value);
window.adminCharts.ram24h.update('none');
}
// Update CPU 30d chart
if (window.adminCharts && window.adminCharts.cpu30d) {
window.adminCharts.cpu30d.data.labels = data.cpu.history_30d.map(d => d.time);
window.adminCharts.cpu30d.data.datasets[0].data = data.cpu.history_30d.map(d => d.value);
window.adminCharts.cpu30d.update('none');
}
// Update RAM 30d chart
if (window.adminCharts && window.adminCharts.ram30d) {
window.adminCharts.ram30d.data.labels = data.ram.history_30d.map(d => d.time);
window.adminCharts.ram30d.data.datasets[0].data = data.ram.history_30d.map(d => d.value);
window.adminCharts.ram30d.update('none');
}
})
.catch(error => {
console.error('Error fetching historical metrics:', error);
});
}
// Update user activity minute charts
function updateUserActivityMinutes() {
fetch('/admin/api/user-activity/minutes')
.then(response => response.json())
.then(data => {
// Update Downloads Per Minute chart
if (window.adminCharts && window.adminCharts.downloadsMinute) {
window.adminCharts.downloadsMinute.data.labels = data.downloads.map(d => d.time);
window.adminCharts.downloadsMinute.data.datasets[0].data = data.downloads.map(d => d.count);
window.adminCharts.downloadsMinute.update('none');
}
// Update API Hits Per Minute chart
if (window.adminCharts && window.adminCharts.apiHitsMinute) {
window.adminCharts.apiHitsMinute.data.labels = data.api_hits.map(d => d.time);
window.adminCharts.apiHitsMinute.data.datasets[0].data = data.api_hits.map(d => d.count);
window.adminCharts.apiHitsMinute.update('none');
}
})
.catch(error => {
console.error('Error fetching user activity minutes:', error);
});
}
// Initial update
updateCurrentMetrics();
// Update current metrics every 60 seconds (1 minute)
setInterval(updateCurrentMetrics, 60000);
// Update historical charts every 5 minutes (to reduce load)
setInterval(updateHistoricalCharts, 300000);
// Update user activity minute charts every 60 seconds (1 minute)
if (window.adminCharts && (window.adminCharts.downloadsMinute || window.adminCharts.apiHitsMinute)) {
setInterval(updateUserActivityMinutes, 60000);
}
}
// Admin Groups Management
function initAdminGroups() {
// Group Edit Form Validation
const groupEditForm = document.getElementById('groupForm');
if (groupEditForm) {
groupEditForm.addEventListener('submit', function(event) {
const firstRecord = document.getElementById('first_record');
const lastRecord = document.getElementById('last_record');
if (!firstRecord || !lastRecord) return;
const firstVal = parseInt(firstRecord.value);
const lastVal = parseInt(lastRecord.value);
if (firstVal > lastVal && lastVal > 0) {
event.preventDefault();
alert('First record ID cannot be greater than last record ID');
return false;
}
});
}
// Group Bulk Form Validation
const groupBulkForm = document.getElementById('groupBulkForm');
if (groupBulkForm) {
groupBulkForm.addEventListener('submit', function(event) {
const groupfilter = document.getElementById('groupfilter');
if (!groupfilter) return;
const filterValue = groupfilter.value.trim();
if (filterValue === '') {
event.preventDefault();
alert('Please enter a group pattern');
return false;
}
// Simple regex validation
try {
new RegExp(filterValue);
} catch(e) {
event.preventDefault();
alert('Invalid regex pattern: ' + e.message);
return false;
}
});
}
}
function showResetAllModal() {
const modal = document.getElementById('resetAllModal');
if (modal) {
modal.classList.remove('hidden');
}
}
function hideResetAllModal() {
const modal = document.getElementById('resetAllModal');
if (modal) {
modal.classList.add('hidden');
}
}
function showPurgeAllModal() {
const modal = document.getElementById('purgeAllModal');
if (modal) {
modal.classList.remove('hidden');
}
}
function hidePurgeAllModal() {
const modal = document.getElementById('purgeAllModal');
if (modal) {
modal.classList.add('hidden');
}
}
function ajax_group_status(id, status) {
const ajaxUrl = document.querySelector('[data-ajax-url]')?.dataset.ajaxUrl;
const csrfToken = document.querySelector('[data-csrf-token]')?.dataset.csrfToken;
if (!ajaxUrl || !csrfToken) return;
// Ensure status is an integer
const statusInt = parseInt(status);
fetch(ajaxUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': csrfToken
},
body: JSON.stringify({
action: 'toggle_group_active_status',
group_id: parseInt(id),
group_status: statusInt
})
})
.then(response => response.text())
.then(data => {
// Update succeeded, regenerate the button with the new status
const groupCell = document.getElementById('group-' + id);
if (groupCell) {
// The status parameter is what we're setting it TO (not toggling)
// status = 1 means we're activating, status = 0 means we're deactivating
const isActive = statusInt === 1;
// Generate new button HTML with proper data attributes
const buttonHTML = isActive
? `<button type="button"
data-action="toggle-group-status"
data-group-id="${id}"
data-status="0"
class="inline-flex items-center px-3 py-1 text-xs font-semibold rounded-full bg-green-100 text-green-800 hover:bg-green-200">
<i class="fa fa-check-circle mr-1"></i>Active
</button>`
: `<button type="button"
data-action="toggle-group-status"
data-group-id="${id}"
data-status="1"
class="inline-flex items-center px-3 py-1 text-xs font-semibold rounded-full bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-gray-200 hover:bg-gray-200">
<i class="fa fa-times-circle mr-1"></i>Inactive
</button>`;
groupCell.innerHTML = buttonHTML;
// Show success message
if (typeof showToast === 'function') {
showToast(isActive ? 'Group activated successfully' : 'Group deactivated successfully', 'success');
}
}
})
.catch(error => {
console.error('Error toggling group status:', error);
if (typeof showToast === 'function') {
showToast('Error updating group status', 'error');
}
});
}
function ajax_backfill_status(id, status) {
const ajaxUrl = document.querySelector('[data-ajax-url]')?.dataset.ajaxUrl;
const csrfToken = document.querySelector('[data-csrf-token]')?.dataset.csrfToken;
if (!ajaxUrl || !csrfToken) return;
// Ensure status is an integer
const statusInt = parseInt(status);
fetch(ajaxUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': csrfToken
},
body: JSON.stringify({
action: 'toggle_group_backfill',
group_id: parseInt(id),
backfill: statusInt
})
})
.then(response => response.text())
.then(data => {
// Update succeeded, regenerate the button with the new status
const backfillCell = document.getElementById('backfill-' + id);
if (backfillCell) {
// The status parameter is what we're setting it TO (not toggling)
// status = 1 means we're enabling, status = 0 means we're disabling
const isEnabled = statusInt === 1;
// Generate new button HTML with proper data attributes
const buttonHTML = isEnabled
? `<button type="button"
data-action="toggle-backfill"
data-group-id="${id}"
data-status="0"
class="inline-flex items-center px-3 py-1 text-xs font-semibold rounded-full bg-blue-100 text-blue-800 hover:bg-blue-200">
<i class="fa fa-check-circle mr-1"></i>Enabled
</button>`
: `<button type="button"
data-action="toggle-backfill"
data-group-id="${id}"
data-status="1"
class="inline-flex items-center px-3 py-1 text-xs font-semibold rounded-full bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-gray-200 hover:bg-gray-200">
<i class="fa fa-times-circle mr-1"></i>Disabled
</button>`;
backfillCell.innerHTML = buttonHTML;
// Show success message
if (typeof showToast === 'function') {
showToast(isEnabled ? 'Backfill enabled successfully' : 'Backfill disabled successfully', 'success');
}
}
})
.catch(error => {
console.error('Error toggling backfill status:', error);
if (typeof showToast === 'function') {
showToast('Error updating backfill status', 'error');
}
});
}
function ajax_group_reset(id) {
if (!confirm('Are you sure you want to reset this group?')) {
return;
}
const ajaxUrl = document.querySelector('[data-ajax-url]')?.dataset.ajaxUrl;
const csrfToken = document.querySelector('[data-csrf-token]')?.dataset.csrfToken;
if (!ajaxUrl || !csrfToken) return;
fetch(ajaxUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': csrfToken
},
body: JSON.stringify({
action: 'reset_group',
group_id: id
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
location.reload();
}
})
.catch(error => console.error('Error resetting group:', error));
}
function confirmGroupDelete(id) {
if (!confirm('Are you sure you want to delete this group?')) {
return;
}
const ajaxUrl = document.querySelector('[data-ajax-url]')?.dataset.ajaxUrl;
const csrfToken = document.querySelector('[data-csrf-token]')?.dataset.csrfToken;
if (!ajaxUrl || !csrfToken) return;
fetch(ajaxUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': csrfToken
},
body: JSON.stringify({
action: 'delete_group',
group_id: id
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
const row = document.getElementById('grouprow-' + id);
if (row) {
row.classList.add('fade-out');
setTimeout(() => row.remove(), 300);
}
}
})
.catch(error => console.error('Error deleting group:', error));
}
function confirmGroupPurge(id) {
if (!confirm('Are you sure you want to purge this group? This will delete all releases and binaries!')) {
return;
}
const ajaxUrl = document.querySelector('[data-ajax-url]')?.dataset.ajaxUrl;
const csrfToken = document.querySelector('[data-csrf-token]')?.dataset.csrfToken;
if (!ajaxUrl || !csrfToken) return;
fetch(ajaxUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': csrfToken
},
body: JSON.stringify({
action: 'purge_group',
group_id: id
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
location.reload();
}
})
.catch(error => console.error('Error purging group:', error));
}
function ajax_group_reset_all() {
const ajaxUrl = document.querySelector('[data-ajax-url]')?.dataset.ajaxUrl;
const csrfToken = document.querySelector('[data-csrf-token]')?.dataset.csrfToken;
if (!ajaxUrl || !csrfToken) return;
fetch(ajaxUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': csrfToken
},
body: JSON.stringify({
action: 'reset_all_groups'
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
hideResetAllModal();
location.reload();
}
})
.catch(error => console.error('Error resetting all groups:', error));
}
function ajax_group_purge_all() {
const ajaxUrl = document.querySelector('[data-ajax-url]')?.dataset.ajaxUrl;
const csrfToken = document.querySelector('[data-csrf-token]')?.dataset.csrfToken;
if (!ajaxUrl || !csrfToken) return;
fetch(ajaxUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': csrfToken
},
body: JSON.stringify({
action: 'purge_all_groups'
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
hidePurgeAllModal();
location.reload();
}
})
.catch(error => console.error('Error purging all groups:', error));
}
// ========================================
// Admin-specific functions
// ========================================
// Admin Categories - Delete confirmation
window.confirmDelete = window.confirmDelete || function(id) {
const modal = document.getElementById('deleteModal');
const deleteLink = document.getElementById('confirmDeleteLink');
if (modal && deleteLink) {
deleteLink.href = window.location.origin + '/admin/category-delete?id=' + id;
modal.classList.remove('hidden');
}
};
window.closeDeleteModal = function() {
const modal = document.getElementById('deleteModal');
if (modal) {
modal.classList.add('hidden');
}
};
// Admin Console/Books - Image preview on file upload
function initAdminImagePreview() {
// Console cover preview
const consoleCoverInput = document.getElementById('cover');
if (consoleCoverInput) {
consoleCoverInput.addEventListener('change', function(e) {
if (e.target.files && e.target.files[0]) {
const reader = new FileReader();
reader.onload = function(event) {
const imgs = document.querySelectorAll('img[alt]');
imgs.forEach(img => {
if (img.closest('form')?.contains(consoleCoverInput)) {
img.src = event.target.result;
}
});
};
reader.readAsDataURL(e.target.files[0]);
}
});
}
}
// Admin Users - Verify user modal
window.showVerifyModal = function(event, form) {
event.preventDefault();
window.currentVerifyForm = form;
const modal = document.getElementById('verifyUserModal');
if (modal) {
modal.classList.remove('hidden');
}
};
window.hideVerifyModal = function() {
const modal = document.getElementById('verifyUserModal');
if (modal) {
modal.classList.add('hidden');
}
window.currentVerifyForm = null;
};
window.submitVerifyForm = function() {
if (window.currentVerifyForm) {
window.currentVerifyForm.submit();
}
};
// Admin Invitations - Select all checkboxes
function initAdminInvitationsSelectAll() {
const selectAllCheckbox = document.getElementById('select_all');
if (selectAllCheckbox) {
selectAllCheckbox.addEventListener('change', function(e) {
document.querySelectorAll('.invitation-checkbox').forEach(cb => {
cb.checked = e.target.checked;
});
});
}
}
// Admin Invitations - Copy to clipboard
window.copyToClipboard = function(text) {
navigator.clipboard.writeText(text).then(function() {
if (typeof showToast === 'function') {
showToast('Link copied to clipboard!', 'success');
} else {
alert('Link copied to clipboard!');
}
}, function(err) {
console.error('Could not copy text: ', err);
alert('Failed to copy to clipboard');
});
};
// Admin Blacklist - Delete via AJAX
window.ajax_binaryblacklist_delete = function(id) {
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content;
fetch(window.location.origin + '/admin/binaryblacklist-delete?id=' + id, {
method: 'GET',
headers: {
'X-Requested-With': 'XMLHttpRequest',
'X-CSRF-TOKEN': csrfToken
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
const row = document.getElementById('row-' + id);
if (row) {
row.style.transition = 'opacity 0.3s';
row.style.opacity = '0';
setTimeout(() => row.remove(), 300);
}
if (typeof showToast === 'function') {
showToast('Blacklist entry deleted successfully', 'success');
}
} else {
if (typeof showToast === 'function') {
showToast('Error deleting blacklist entry', 'error');
}
}
})
.catch(error => {
console.error('Error:', error);
if (typeof showToast === 'function') {
showToast('Error deleting blacklist entry', 'error');
}
});
};
// Admin Comments - Delete modal
window.openDeleteModal = function(commentId, commentText) {
const modal = document.getElementById('deleteModal');
const form = document.getElementById('deleteForm');
const preview = document.getElementById('deleteCommentPreview');
if (modal && form && preview) {
// Set the form action
form.action = window.location.origin + '/admin/comments-delete/' + commentId;
// Set the comment preview
preview.textContent = '"' + commentText.substring(0, 100) + (commentText.length >= 100 ? '..."' : '"');
// Show the modal
modal.classList.remove('hidden');
modal.classList.add('flex');
// Prevent body scroll
document.body.style.overflow = 'hidden';
}
};
window.closeDeleteModal = window.closeDeleteModal || function() {
const modal = document.getElementById('deleteModal');
if (modal) {
// Hide the modal
modal.classList.add('hidden');
modal.classList.remove('flex');
// Restore body scroll
document.body.style.overflow = '';
}
};
// Admin Regex Forms - Validation
function initAdminRegexFormValidation() {
// Release naming regex form
const releaseNamingForm = document.getElementById('regexForm');
if (releaseNamingForm && window.location.pathname.includes('release-naming')) {
releaseNamingForm.addEventListener('submit', function(event) {
const groupRegex = document.getElementById('group_regex');
const regex = document.getElementById('regex');
const ordinal = document.getElementById('ordinal');
if (!groupRegex?.value.trim() || !regex?.value.trim() || !ordinal?.value) {
event.preventDefault();
alert('Please fill in all required fields.');
return false;
}
});
}
// Category regex form
if (releaseNamingForm && window.location.pathname.includes('category')) {
releaseNamingForm.addEventListener('submit', function(event) {
const groupRegex = document.getElementById('group_regex');
const regex = document.getElementById('regex');
const ordinal = document.getElementById('ordinal');
if (!groupRegex?.value.trim() || !regex?.value.trim() || !ordinal?.value) {
event.preventDefault();
alert('Please fill in all required fields.');
return false;
}
});
}
// Collection regex form
if (releaseNamingForm && window.location.pathname.includes('collection')) {
releaseNamingForm.addEventListener('submit', function(event) {
const groupRegex = document.getElementById('group_regex');
const regex = document.getElementById('regex');
const ordinal = document.getElementById('ordinal');
if (!groupRegex?.value.trim() || !regex?.value.trim() || !ordinal?.value) {
event.preventDefault();
alert('Please fill in all required fields.');
return false;
}
});
}
// Blacklist form
const blacklistForm = document.getElementById('blacklistForm');
if (blacklistForm) {
blacklistForm.addEventListener('submit', function(event) {
const groupname = document.getElementById('groupname');
const regex = document.getElementById('regex');
if (!groupname?.value.trim() || !regex?.value.trim()) {
event.preventDefault();
alert('Please fill in all required fields.');
return false;
}
});
}
}
// Admin Tmux - Select color highlighting
function initAdminTmuxSelectColors() {
const tmuxForm = document.getElementById('tmuxForm');
if (!tmuxForm) return;
// Function to update select colors based on value
function updateSelectColor(select) {
select.classList.remove('select-yes', 'select-no', 'select-other');
const value = select.value.toLowerCase();
if (value === '1' || value === 'true' || value === 'yes') {
select.classList.add('select-yes');
} else if (value === '0' || value === 'false' || value === 'no') {
select.classList.add('select-no');
} else {
select.classList.add('select-other');
}
}
// Apply to all select elements in the form
const selects = tmuxForm.querySelectorAll('select');
selects.forEach(select => {
// Initial color
updateSelectColor(select);
// Update on change
select.addEventListener('change', function() {
updateSelectColor(this);
});
});
}
// Initialize all admin-specific features
function initAdminSpecificFeatures() {
initAdminImagePreview();
initAdminInvitationsSelectAll();
initAdminRegexFormValidation();
initAdminTmuxSelectColors();
initAdminDeletedUsers();
// Close delete modal when clicking outside
const deleteModal = document.getElementById('deleteModal');
if (deleteModal) {
deleteModal.addEventListener('click', function(e) {
if (e.target === this) {
closeDeleteModal();
}
});
}
// Close verify modal when clicking outside
const verifyUserModal = document.getElementById('verifyUserModal');
if (verifyUserModal) {
verifyUserModal.addEventListener('click', function(event) {
if (event.target === this) {
hideVerifyModal();
}
});
}
}
// Admin Deleted Users page functionality
function initAdminDeletedUsers() {
// Select all checkbox
const selectAllCheckbox = document.getElementById('selectAll');
if (selectAllCheckbox) {
selectAllCheckbox.addEventListener('change', function() {
const checkboxes = document.querySelectorAll('.user-checkbox');
checkboxes.forEach(checkbox => {
checkbox.checked = this.checked;
});
});
}
// Bulk action confirmation
window.confirmBulkAction = function() {
const action = document.getElementById('bulkAction')?.value;
const checkedBoxes = document.querySelectorAll('.user-checkbox:checked');
const validationError = document.getElementById('validationError');
const validationErrorMessage = document.getElementById('validationErrorMessage');
if (!action) {
if (validationError && validationErrorMessage) {
validationErrorMessage.textContent = 'Please select an action from the dropdown.';
validationError.classList.remove('hidden');
}
return false;
}
if (checkedBoxes.length === 0) {
if (validationError && validationErrorMessage) {
validationErrorMessage.textContent = 'Please select at least one user.';
validationError.classList.remove('hidden');
}
return false;
}
// Hide validation error if showing
if (validationError) {
validationError.classList.add('hidden');
}
const count = checkedBoxes.length;
const actionText = action === 'restore' ? 'restore' : 'permanently delete';
return confirm(`Are you sure you want to ${actionText} ${count} user${count > 1 ? 's' : ''}?`);
};
// Individual action confirmations
document.querySelectorAll('.inline-form button[data-confirm]').forEach(button => {
button.addEventListener('click', function(e) {
const message = this.getAttribute('data-confirm');
if (!confirm(message)) {
e.preventDefault();
}
});
});
}
// My Movies page functionality
function initMyMovies() {
// Confirm before removing movies
document.querySelectorAll('.confirm_action').forEach(element => {
element.addEventListener('click', function(e) {
if (!confirm('Are you sure you want to remove this movie from your watchlist?')) {
e.preventDefault();
}
});
});
// Image fallback for movie covers
document.querySelectorAll('img[data-fallback-src]').forEach(img => {
img.addEventListener('error', function() {
const fallback = this.getAttribute('data-fallback-src');
if (fallback && this.src !== fallback) {
this.src = fallback;
}
});
});
// Initialize Bootstrap tooltips if they exist
if (typeof bootstrap !== 'undefined' && bootstrap.Tooltip) {
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
const tooltipList = [...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl));
}
}
// Auth pages functionality
function initAuthPages() {
// Auto-hide success messages after 5 seconds on login page
if (window.location.pathname.includes('/login')) {
setTimeout(function() {
const alerts = document.querySelectorAll('.bg-green-50, .bg-blue-50');
alerts.forEach(function(alert) {
alert.style.transition = 'opacity 0.5s ease-out';
alert.style.opacity = '0';
setTimeout(() => alert.remove(), 500);
});
}, 5000);
}
// 2FA verification - Auto-submit when 6 digits are entered
const otpInput = document.getElementById('one_time_password');
if (otpInput) {
otpInput.addEventListener('input', function(e) {
// Remove non-numeric characters
this.value = this.value.replace(/[^0-9]/g, '');
// Auto-submit when 6 digits are entered
if (this.value.length === 6) {
// Small delay to show the complete code before submitting
setTimeout(() => {
this.form.submit();
}, 300);
}
});
// Focus on input when page loads
otpInput.focus();
}
}
// Profile edit page functionality
function initProfileEdit() {
// 2FA Disable Form Toggle
const toggleBtn = document.getElementById('toggle-disable-2fa-btn');
const cancelBtn = document.getElementById('cancel-disable-2fa-btn');
const formContainer = document.getElementById('disable-2fa-form-container');
if (toggleBtn && formContainer) {
toggleBtn.addEventListener('click', function() {
formContainer.style.display = formContainer.style.display === 'none' ? 'block' : 'none';
});
}
if (cancelBtn && formContainer) {
cancelBtn.addEventListener('click', function() {
formContainer.style.display = 'none';
// Clear password field
const passwordInput = document.getElementById('disable_2fa_password');
if (passwordInput) {
passwordInput.value = '';
}
});
}
// Theme preference instant preview
const themeRadios = document.querySelectorAll('input[name="theme_preference"]');
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
function applyTheme(themeValue) {
const html = document.documentElement;
if (themeValue === 'system') {
// Use system preference
if (mediaQuery.matches) {
html.classList.add('dark');
} else {
html.classList.remove('dark');
}
} else if (themeValue === 'dark') {
html.classList.add('dark');
} else {
html.classList.remove('dark');
}
}
themeRadios.forEach(radio => {
radio.addEventListener('change', function() {
applyTheme(this.value);
});
});
// Listen to system preference changes if 'system' is selected
mediaQuery.addEventListener('change', function(e) {
const selectedTheme = document.querySelector('input[name="theme_preference"]:checked')?.value;
if (selectedTheme === 'system') {
applyTheme('system');
}
});
}
// Details page image modal functionality
function initDetailsPageImageModal() {
window.openImageModal = function(imageUrl, title) {
const modal = document.getElementById('imageModal');
const modalImage = document.getElementById('imageModalImage');
const modalTitle = document.getElementById('imageModalTitle');
if (modal && modalImage) {
modalImage.src = imageUrl;
if (modalTitle) {
modalTitle.textContent = title || '';
}
modal.classList.remove('hidden');
modal.classList.add('flex');
// Prevent body scroll when modal is open
document.body.style.overflow = 'hidden';
}
};
window.closeImageModal = function() {
const modal = document.getElementById('imageModal');
if (modal) {
modal.classList.add('hidden');
modal.classList.remove('flex');
// Restore body scroll
document.body.style.overflow = '';
}
};
// Close modal when clicking outside the image
const imageModal = document.getElementById('imageModal');
if (imageModal) {
imageModal.addEventListener('click', function(e) {
if (e.target === this) {
closeImageModal();
}
});
}
// Close modal with Escape key
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
const modal = document.getElementById('imageModal');
if (modal && !modal.classList.contains('hidden')) {
closeImageModal();
}
}
});
}
// Add to cart functionality for movie/browse pages
function initAddToCart() {
window.addToCart = function(guid) {
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content;
const baseUrl = document.querySelector('meta[name="app-url"]')?.content || '';
fetch(baseUrl + '/cart/add', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': csrfToken,
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify({ id: guid })
})
.then(response => response.json())
.then(data => {
if (data.success) {
if (typeof showToast === 'function') {
showToast('Added to cart successfully!', 'success');
}
} else {
if (typeof showToast === 'function') {
showToast('Failed to add to cart', 'error');
}
}
})
.catch(error => {
console.error('Error:', error);
if (typeof showToast === 'function') {
showToast('An error occurred', 'error');
}
});
};
}
// Note: Main DOMContentLoaded event listener is already defined at the top of the file
// Just adding the new initializations to the existing initialization list above
// Show validation error message for deleted users page
window.showValidationError = function(message) {
const errorDiv = document.getElementById('validationError');
const errorMessage = document.getElementById('validationErrorMessage');
if (errorDiv && errorMessage) {
errorMessage.textContent = message;
errorDiv.classList.remove('hidden');
// Scroll to the error message
errorDiv.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
// Auto-hide after 5 seconds
setTimeout(() => {
errorDiv.classList.add('hidden');
}, 5000);
}
};
// Hide validation error
window.hideValidationError = function() {
const errorDiv = document.getElementById('validationError');
if (errorDiv) {
errorDiv.classList.add('hidden');
}
};
// Confirm bulk action for deleted users
window.confirmBulkAction = function(event) {
const action = document.getElementById('bulkAction')?.value;
const checkedBoxes = document.querySelectorAll('.user-checkbox:checked');
if (!action) {
event.preventDefault();
showValidationError('Please select an action from the dropdown.');
return false;
}
if (checkedBoxes.length === 0) {
event.preventDefault();
showValidationError('Please select at least one user to perform this action.');
return false;
}
// Show confirmation dialog with appropriate message
let confirmMessage = '';
if (action === 'delete') {
confirmMessage = `Are you sure you want to PERMANENTLY delete ${checkedBoxes.length} user(s)?\n\nThis action cannot be undone!`;
} else if (action === 'restore') {
confirmMessage = `Are you sure you want to restore ${checkedBoxes.length} user(s)?`;
}
if (!confirm(confirmMessage)) {
event.preventDefault();
return false;
}
return true;
};
// Movies Layout Toggle Functionality
function initMoviesLayoutToggle() {
const layoutToggle = document.getElementById('layoutToggle');
const layoutToggleText = document.getElementById('layoutToggleText');
const moviesGrid = document.getElementById('moviesGrid');
if (!layoutToggle || !layoutToggleText || !moviesGrid) {
return; // Not on movies page
}
// Get saved layout preference from data attribute (from database)
// 1 = 1-column, 2 = 2-columns
let currentLayout = parseInt(moviesGrid.dataset.userLayout) || 2;
// Apply the saved layout on page load
applyLayout(currentLayout);
// Toggle layout on button click
layoutToggle.addEventListener('click', function() {
if (currentLayout === 2) {
currentLayout = 1;
} else {
currentLayout = 2;
}
// Save preference to database via AJAX
fetch('/movies/update-layout', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content
},
body: JSON.stringify({
layout: currentLayout
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Apply layout after successful save
applyLayout(currentLayout);
}
})
.catch(error => {
console.error('Error saving layout preference:', error);
// Still apply layout even if save failed
applyLayout(currentLayout);
});
});
function applyLayout(layout) {
// Get all movie poster images and placeholder divs
const posterImages = moviesGrid.querySelectorAll('img[alt], .bg-gray-200.dark\\:bg-gray-700');
// Get all release card containers
const releaseCardContainers = moviesGrid.querySelectorAll('.release-card-container');
if (layout === 1) {
// 1-column layout: larger images (w-48 h-72, original size)
moviesGrid.classList.remove('lg:grid-cols-2');
moviesGrid.classList.add('grid-cols-1');
layoutToggleText.textContent = '1 Column';
layoutToggle.querySelector('i').className = 'fas fa-th-list mr-2';
// Update image sizes to larger
posterImages.forEach(el => {
el.classList.remove('w-32', 'h-48');
el.classList.add('w-48', 'h-72');
});
// Update release card layout for 1-column view - side by side on all screens
releaseCardContainers.forEach(container => {
// Change container to flex-row with items on sides
container.classList.remove('space-y-2');
container.classList.add('flex', 'flex-row', 'items-start', 'justify-between', 'gap-3');
// Make info wrapper take available space
const infoWrapper = container.querySelector('.release-info-wrapper');
if (infoWrapper) {
infoWrapper.classList.add('flex-1', 'min-w-0');
}
// Make actions wrapper stay on the right in a horizontal row
const actionsWrapper = container.querySelector('.release-actions');
if (actionsWrapper) {
actionsWrapper.classList.remove('flex-wrap');
actionsWrapper.classList.add('flex-shrink-0', 'flex-row', 'items-center');
}
});
} else {
// 2-column layout: smaller images (w-32 h-48)
moviesGrid.classList.remove('grid-cols-1');
moviesGrid.classList.add('lg:grid-cols-2');
layoutToggleText.textContent = '2 Columns';
layoutToggle.querySelector('i').className = 'fas fa-th-large mr-2';
// Update image sizes to smaller
posterImages.forEach(el => {
el.classList.remove('w-48', 'h-72');
el.classList.add('w-32', 'h-48');
});
// Update release card layout for 2-column view - stacked vertically
releaseCardContainers.forEach(container => {
// Change back to stacked layout
container.classList.add('space-y-2');
container.classList.remove('flex', 'flex-row', 'items-start', 'justify-between', 'gap-3');
// Remove special styling from info wrapper
const infoWrapper = container.querySelector('.release-info-wrapper');
if (infoWrapper) {
infoWrapper.classList.remove('flex-1', 'min-w-0');
}
// Restore normal flex-wrap for actions wrapper
const actionsWrapper = container.querySelector('.release-actions');
if (actionsWrapper) {
actionsWrapper.classList.add('flex-wrap', 'items-center');
actionsWrapper.classList.remove('flex-shrink-0', 'flex-row');
}
});
}
}
}