Files
newznab-tmux/resources/js/csp-safe.js
T
2026-02-01 22:16:40 +01:00

6020 lines
227 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
// Utility function to escape HTML
function escapeHtml(text) {
const map = {
'&': '&',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
};
return String(text).replace(/[&<>"']/g, function(m) { return map[m]; });
}
// Shared theme management utilities
const themeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
/**
* Apply theme preference to the document
* @param {string} themePreference - 'light', 'dark', or 'system'
*/
function applyTheme(themePreference) {
const html = document.documentElement;
if (themePreference === 'system') {
if (themeMediaQuery.matches) {
html.classList.add('dark');
} else {
html.classList.remove('dark');
}
} else if (themePreference === 'dark') {
html.classList.add('dark');
} else {
html.classList.remove('dark');
}
}
/**
* Update all theme UI elements to reflect the current theme
* @param {string} themePreference - 'light', 'dark', or 'system'
*/
function updateAllThemeUI(themePreference) {
// Update ALL theme selector radio buttons (user area profile edit page)
// Use a flag to prevent event loops when programmatically updating
window._updatingThemeUI = true;
const allThemeRadios = document.querySelectorAll('input[name="theme_preference"]');
let updatedCount = 0;
allThemeRadios.forEach(radio => {
const wasChecked = radio.checked;
if (radio.value === themePreference) {
radio.checked = true;
if (!wasChecked) updatedCount++;
} else {
radio.checked = false;
}
});
if (updatedCount > 0) {
console.log(`Updated ${updatedCount} theme radio button(s) to ${themePreference}`);
}
// Update theme toggle button title and icon
const themeToggle = document.getElementById('theme-toggle');
if (themeToggle) {
const titles = {
'light': 'Theme: Light',
'dark': 'Theme: Dark',
'system': 'Theme: System (Auto)'
};
themeToggle.setAttribute('title', titles[themePreference] || 'Toggle theme');
// Update icon if it exists
const themeIcon = document.getElementById('theme-icon');
if (themeIcon) {
const icons = {
'light': 'fa-sun',
'dark': 'fa-moon',
'system': 'fa-desktop'
};
themeIcon.classList.remove('fa-sun', 'fa-moon', 'fa-desktop');
themeIcon.classList.add(icons[themePreference] || 'fa-sun');
}
// Update label if it exists
const themeLabel = document.getElementById('theme-label');
if (themeLabel) {
const labels = {
'light': 'Light',
'dark': 'Dark',
'system': 'System'
};
themeLabel.textContent = labels[themePreference] || 'Light';
}
}
// Update visual state of theme options
const themeOptions = document.querySelectorAll('.theme-option');
themeOptions.forEach(option => {
const radio = option.querySelector('.theme-radio') || option.querySelector('input[type="radio"]');
if (radio && radio.checked) {
option.classList.add('theme-option-active');
} else {
option.classList.remove('theme-option-active');
}
});
// Update meta tag
const metaTheme = document.querySelector('meta[name="theme-preference"]');
if (metaTheme) {
metaTheme.content = themePreference;
}
// Clear the flag after a short delay to allow all updates to complete
setTimeout(() => {
window._updatingThemeUI = false;
}, 100);
}
/**
* Save theme preference to backend or localStorage
* @param {string} themePreference - 'light', 'dark', or 'system'
* @returns {Promise} Promise that resolves when theme is saved
*/
function saveThemePreference(themePreference) {
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content;
const updateThemeUrl = document.querySelector('meta[name="update-theme-url"]')?.content;
const isAuthenticated = document.querySelector('meta[name="user-authenticated"]');
if (isAuthenticated && isAuthenticated.content === 'true' && updateThemeUrl && csrfToken) {
return fetch(updateThemeUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': csrfToken
},
body: JSON.stringify({ theme_preference: themePreference })
}).then(response => response.json())
.then(data => {
if (data.success) {
console.log('Theme saved successfully:', themePreference);
// Update meta tag immediately
const metaTheme = document.querySelector('meta[name="theme-preference"]');
if (metaTheme) {
metaTheme.content = themePreference;
console.log('Meta tag updated to:', themePreference);
}
updateAllThemeUI(themePreference);
// Dispatch custom event for theme change
document.dispatchEvent(new CustomEvent('themeChanged', {
detail: { theme: themePreference }
}));
return data;
} else {
console.error('Theme save failed:', data);
}
})
.catch(error => {
console.error('Error updating theme:', error);
throw error;
});
} else {
// Save to localStorage for guests
localStorage.setItem('theme', themePreference);
updateAllThemeUI(themePreference);
document.dispatchEvent(new CustomEvent('themeChanged', {
detail: { theme: themePreference }
}));
return Promise.resolve({ success: true });
}
}
/**
* Initialize theme system - runs once on page load
*/
function initThemeSystem() {
// 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';
}
// Apply initial theme
applyTheme(currentTheme);
updateAllThemeUI(currentTheme);
// Listen for OS theme changes if 'system' is selected
themeMediaQuery.addEventListener('change', () => {
const selectedTheme = metaTheme ? metaTheme.content : localStorage.getItem('theme') || 'light';
if (selectedTheme === 'system') {
applyTheme('system');
}
});
// Listen for theme changes from any source
document.addEventListener('themeChanged', function(e) {
if (e.detail && e.detail.theme) {
applyTheme(e.detail.theme);
updateAllThemeUI(e.detail.theme);
}
});
}
/**
* Toggle password visibility for password input fields
* @param {string} fieldId - The ID of the password field to toggle
*/
window.togglePasswordVisibility = function(fieldId) {
const field = document.getElementById(fieldId);
const icon = document.getElementById(fieldId + '-eye');
if (!field || !icon) return;
if (field.type === 'password') {
field.type = 'text';
icon.classList.remove('fa-eye');
icon.classList.add('fa-eye-slash');
} else {
field.type = 'password';
icon.classList.remove('fa-eye-slash');
icon.classList.add('fa-eye');
}
};
/**
* Initialize password visibility toggles for all password fields with toggle buttons
*/
function initPasswordVisibilityToggles() {
// Add click event listeners to all password toggle buttons
document.querySelectorAll('.password-toggle-btn').forEach(function(button) {
button.addEventListener('click', function(e) {
e.preventDefault();
const fieldId = this.getAttribute('data-field-id');
if (fieldId) {
window.togglePasswordVisibility(fieldId);
}
});
});
}
document.addEventListener('DOMContentLoaded', function() {
// Initialize theme system first
initThemeSystem();
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();
initRecentActivityRefresh();
initContentToggle();
initContentDelete();
initPasswordVisibilityToggles();
initSearchAutocomplete();
// Initialize page-specific functionality from inline scripts
initMyMovies();
initAuthPages();
initProfileEdit();
initDetailsPageImageModal();
initMoviesLayoutToggle();
initProfilePage(); // Initialize profile page (progress bars and charts)
initCopyToClipboard(); // Copy functionality for RSS/Profile pages
initVerifyUserModal(); // Admin user verification modal
initQualityFilter(); // Quality filter for movie releases
initUserListScrollSync(); // Synchronized scrollbars for user list table
});
// Event delegation for dynamically added elements - optimized with early returns
function initEventDelegation() {
// Helper function to find element with attribute, checking both target and closest
function findElementWithAttr(e, attr) {
if (!e || !e.target) return null;
if (e.target.hasAttribute(attr)) return e.target;
return e.target.closest(`[${attr}]`);
}
// Helper function to find element with class
function findElementWithClass(e, className) {
if (!e || !e.target) return null;
if (e.target.classList && e.target.classList.contains(className)) return e.target;
return e.target.closest(`.${className}`);
}
document.addEventListener('click', function(e) {
// Handle toast close buttons
const toastClose = findElementWithClass(e, 'toast-close');
if (toastClose) {
const toast = toastClose.closest('.toast-notification');
if (toast) {
toast.remove();
}
return;
}
// Handle NFO modal close
if (findElementWithAttr(e, 'data-close-nfo-modal')) {
e.preventDefault();
closeNfoModal();
return;
}
// Handle NFO modal open
const nfoOpen = findElementWithAttr(e, 'data-open-nfo');
if (nfoOpen) {
e.preventDefault();
const guid = nfoOpen.getAttribute('data-open-nfo');
if (guid) {
openNfoModal(guid);
}
return;
}
// Handle logout
if (findElementWithAttr(e, 'data-logout')) {
e.preventDefault();
const logoutForm = document.getElementById('logout-form');
if (logoutForm) {
logoutForm.submit();
}
return;
}
// Handle confirm delete - using styled modal
const confirmDelete = findElementWithAttr(e, 'data-confirm-delete');
if (confirmDelete) {
e.preventDefault();
e.stopPropagation();
const form = confirmDelete.closest('form');
showConfirm({
message: 'Are you sure you want to delete this item?',
type: 'danger',
confirmText: 'Delete',
onConfirm: function() {
if (form) {
form.submit();
} else if (confirmDelete.href) {
window.location.href = confirmDelete.href;
}
}
});
return;
}
// Handle confirm action - using styled modal
const confirmAction = findElementWithAttr(e, 'data-confirm');
if (confirmAction) {
e.preventDefault();
e.stopPropagation();
const message = confirmAction.getAttribute('data-confirm');
const form = confirmAction.closest('form');
showConfirm({
message: message,
type: 'danger',
confirmText: 'Delete',
onConfirm: function() {
if (form) {
form.submit();
} else if (confirmAction.href) {
window.location.href = confirmAction.href;
}
}
});
return;
}
// 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 - only for season-tab buttons, not content
const seasonTab = e.target.classList.contains('season-tab') ? e.target : e.target.closest('.season-tab');
if (seasonTab) {
e.preventDefault();
const seasonNumber = seasonTab.getAttribute('data-season');
if (seasonNumber && typeof switchSeason === 'function') {
switchSeason(seasonNumber);
}
return;
}
// 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 confirmation modal close button
if (e.target.hasAttribute('data-close-confirmation-modal') || e.target.closest('[data-close-confirmation-modal]')) {
e.preventDefault();
closeConfirmationModal();
}
// Handle confirmation modal confirm button
if (e.target.hasAttribute('data-confirm-confirmation-modal') || e.target.closest('[data-confirm-confirmation-modal]')) {
e.preventDefault();
confirmConfirmationModal();
}
// 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 'show-reset-selected-modal':
showResetSelectedModal();
break;
case 'hide-reset-selected-modal':
hideResetSelectedModal();
break;
case 'select-all-groups':
toggleSelectAllGroups(actionTarget);
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;
case 'reset-selected':
ajax_group_reset_selected();
break;
}
}
// Handle restore user button (both admin/user-list and admin/deleted-users pages)
const restoreUserBtn = e.target.closest('.restore-user-btn');
if (restoreUserBtn) {
e.preventDefault();
const userId = restoreUserBtn.getAttribute('data-user-id');
const username = restoreUserBtn.getAttribute('data-username') || 'this user';
showConfirm({
title: 'Restore User',
message: `Are you sure you want to restore user "${username}"?`,
type: 'success',
confirmText: 'Restore',
cancelText: 'Cancel',
onConfirm: function() {
const form = document.getElementById('individualActionForm');
if (!form) {
console.error('Individual action form not found');
if (typeof showToast === 'function') {
showToast('Error: Form not found', 'error');
}
return;
}
// Set form action and submit
form.action = `/admin/deleted-users/restore/${userId}`;
form.method = 'POST';
form.submit();
}
});
return;
}
// Handle permanent delete user button (deleted users page only)
const deleteUserBtn = e.target.closest('.delete-user-btn');
if (deleteUserBtn) {
e.preventDefault();
const userId = deleteUserBtn.getAttribute('data-user-id');
const username = deleteUserBtn.getAttribute('data-username') || 'this user';
showConfirm({
title: 'Permanently Delete User',
message: `Are you sure you want to permanently delete user "${username}"? This action cannot be undone and will remove all user data.`,
type: 'danger',
confirmText: 'Delete Permanently',
cancelText: 'Cancel',
onConfirm: function() {
const form = document.getElementById('individualActionForm');
if (!form) {
console.error('Individual action form not found');
if (typeof showToast === 'function') {
showToast('Error: Form not found', 'error');
}
return;
}
// Set form action and submit
form.action = `/admin/deleted-users/permanent-delete/${userId}`;
form.method = 'POST';
form.submit();
}
});
return;
}
// Handle promotion toggle (activate/deactivate)
const promotionToggleBtn = e.target.closest('.promotion-toggle-btn');
if (promotionToggleBtn) {
e.preventDefault();
const promotionName = promotionToggleBtn.getAttribute('data-promotion-name');
const isActive = promotionToggleBtn.getAttribute('data-promotion-active') === '1';
const action = isActive ? 'deactivate' : 'activate';
showConfirm({
title: `${action.charAt(0).toUpperCase() + action.slice(1)} Promotion`,
message: `Are you sure you want to ${action} the promotion "${promotionName}"?`,
type: isActive ? 'warning' : 'success',
confirmText: action.charAt(0).toUpperCase() + action.slice(1),
cancelText: 'Cancel',
onConfirm: function() {
window.location.href = promotionToggleBtn.href;
}
});
return;
}
// Handle promotion delete
const promotionDeleteBtn = e.target.closest('.promotion-delete-btn');
if (promotionDeleteBtn) {
e.preventDefault();
const promotionName = promotionDeleteBtn.getAttribute('data-promotion-name');
const form = promotionDeleteBtn.closest('form');
showConfirm({
title: 'Delete Promotion',
message: `Are you sure you want to delete the promotion "${promotionName}"?`,
details: 'This action cannot be undone.',
type: 'danger',
confirmText: 'Delete',
cancelText: 'Cancel',
onConfirm: function() {
if (form) {
form.submit();
}
}
});
return;
}
});
// 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);
// Add close button listener
const closeBtn = toast.querySelector('.toast-close');
if (closeBtn) {
closeBtn.addEventListener('click', function() {
toast.classList.add('removing');
setTimeout(function() {
toast.remove();
}, 300);
});
}
};
}
// Styled Confirmation Modal (replaces native confirm dialogs)
let confirmationCallback = null;
window.showConfirm = function(options) {
// Options: { message, title, details, confirmText, cancelText, type, onConfirm }
const modal = document.getElementById('confirmationModal');
if (!modal) return Promise.reject('Modal not found');
const defaults = {
title: 'Confirm Action',
message: 'Are you sure you want to proceed?',
details: '',
confirmText: 'Confirm',
cancelText: 'Cancel',
type: 'info', // 'info', 'warning', 'danger', 'success'
onConfirm: null
};
const config = { ...defaults, ...options };
// Set modal content - with null checks
const titleText = document.getElementById('confirmationModalTitleText');
const messageText = document.getElementById('confirmationModalMessage');
const confirmText = document.getElementById('confirmationModalConfirmText');
const cancelText = document.getElementById('confirmationModalCancelText');
if (titleText) titleText.textContent = config.title;
if (messageText) messageText.textContent = config.message;
if (confirmText) confirmText.textContent = config.confirmText;
if (cancelText) cancelText.textContent = config.cancelText;
// Set icon based on type
const icon = document.getElementById('confirmationModalIcon');
const confirmBtn = document.getElementById('confirmationModalConfirmBtn');
// Helper function to set classes (works with both regular elements and SVG)
function setIconClasses(element, classes) {
if (!element) return;
if (element instanceof SVGElement) {
element.setAttribute('class', classes.join(' '));
} else {
element.className = classes.join(' ');
}
}
// Set base icon classes
let iconClasses = ['fas', 'mr-2'];
// Remove all existing classes from button and set base classes
if (confirmBtn) {
confirmBtn.className = '';
confirmBtn.classList.add('px-4', 'py-2', 'text-white', 'rounded-lg', 'transition', 'font-medium');
}
if (config.type === 'danger') {
iconClasses.push('fa-exclamation-triangle', 'text-red-600', 'dark:text-red-400');
if (confirmBtn) {
confirmBtn.classList.add('bg-red-600', 'dark:bg-red-700', 'hover:bg-red-700', 'dark:hover:bg-red-800');
}
} else if (config.type === 'warning') {
iconClasses.push('fa-exclamation-circle', 'text-yellow-600', 'dark:text-yellow-400');
if (confirmBtn) {
confirmBtn.classList.add('bg-yellow-600', 'dark:bg-yellow-700', 'hover:bg-yellow-700', 'dark:hover:bg-yellow-800');
}
} else if (config.type === 'success') {
iconClasses.push('fa-check-circle', 'text-green-600', 'dark:text-green-400');
if (confirmBtn) {
confirmBtn.classList.add('bg-green-600', 'dark:bg-green-700', 'hover:bg-green-700', 'dark:hover:bg-green-800');
}
} else { // info
iconClasses.push('fa-info-circle', 'text-blue-600', 'dark:text-blue-400');
if (confirmBtn) {
confirmBtn.classList.add('bg-blue-600', 'dark:bg-blue-700', 'hover:bg-blue-700', 'dark:hover:bg-blue-800');
}
}
// Apply icon classes
setIconClasses(icon, iconClasses);
// Handle details
const detailsDiv = document.getElementById('confirmationModalDetails');
const detailsText = document.getElementById('confirmationModalDetailsText');
if (config.details && detailsText && detailsDiv) {
detailsText.textContent = config.details;
detailsDiv.classList.remove('hidden');
} else if (detailsDiv) {
detailsDiv.classList.add('hidden');
}
// Store callback for use when confirm is clicked
const storedOnConfirm = config.onConfirm;
// Show modal
modal.classList.remove('hidden');
modal.classList.add('flex');
// Return promise for async/await usage
return new Promise((resolve, reject) => {
confirmationCallback = function(confirmed) {
if (confirmed && storedOnConfirm) {
storedOnConfirm();
}
resolve(confirmed);
};
});
};
window.closeConfirmationModal = function() {
const modal = document.getElementById('confirmationModal');
if (modal) {
modal.classList.add('hidden');
modal.classList.remove('flex');
}
if (confirmationCallback) {
confirmationCallback(false);
confirmationCallback = null;
}
};
window.confirmConfirmationModal = function() {
const modal = document.getElementById('confirmationModal');
if (modal) {
modal.classList.add('hidden');
modal.classList.remove('flex');
}
if (confirmationCallback) {
confirmationCallback(true);
confirmationCallback = null;
}
};
// Enhanced confirm function (backward compatible but with styled modal)
window.confirmStyled = function(message, title = 'Confirm', type = 'info') {
return new Promise((resolve) => {
showConfirm({
message: message,
title: title,
type: type,
onConfirm: () => resolve(true)
}).then(result => {
if (!result) resolve(false);
});
});
};
// 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');
modal.style.display = 'block';
// 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');
modal.style.display = 'none';
}
};
// 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 - optimized
function initSeasonSwitcher() {
window.switchSeason = function(seasonNumber) {
// Hide all season content
document.querySelectorAll('.season-content').forEach(function(content) {
content.classList.add('hidden');
});
// Remove active styling from all tabs
document.querySelectorAll('.season-tab').forEach(function(tab) {
tab.classList.remove('border-blue-500', 'text-blue-600');
tab.classList.add('border-transparent', 'text-gray-500');
// Update badge styling
const badge = tab.querySelector('span');
if (badge) {
badge.classList.remove('bg-blue-100', 'text-blue-800');
badge.classList.add('bg-gray-100', 'text-gray-600');
}
});
// Show selected season content
const selectedContent = document.querySelector('.season-content[data-season="' + seasonNumber + '"]');
if (selectedContent) {
selectedContent.classList.remove('hidden');
}
// Add active styling to selected tab
const selectedTab = document.querySelector('.season-tab[data-season="' + seasonNumber + '"]');
if (selectedTab) {
selectedTab.classList.remove('border-transparent', 'text-gray-500');
selectedTab.classList.add('border-blue-500', 'text-blue-600');
// Update badge styling
const badge = selectedTab.querySelector('span');
if (badge) {
badge.classList.remove('bg-gray-100', 'text-gray-600');
badge.classList.add('bg-blue-100', 'text-blue-800');
}
}
};
}
// 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');
}
});
});
// NFO badge clicks are handled by initNfoModal() which uses event delegation
// and the correct endpoint /nfo/{guid}?modal=1
// 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();
}
// Handle release delete buttons
if (e.target.hasAttribute('data-delete-release') || e.target.closest('[data-delete-release]')) {
const element = e.target.hasAttribute('data-delete-release') ? e.target : e.target.closest('[data-delete-release]');
const id = element.getAttribute('data-delete-release');
const deleteUrl = element.getAttribute('data-delete-url');
showConfirm({
title: 'Delete Release',
message: 'Are you sure you want to delete this release? This action cannot be undone.',
type: 'danger',
confirmText: 'Delete',
onConfirm: function() {
deleteRelease(id, deleteUrl, element);
}
});
e.preventDefault();
}
});
// Delete release function
window.deleteRelease = function(id, deleteUrl, element) {
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content;
if (!csrfToken) {
console.error('CSRF token not found');
if (typeof showToast === 'function') {
showToast('Security token not found. Please refresh the page.', 'error');
}
return;
}
console.log('Deleting release:', id, 'URL:', deleteUrl);
if (typeof showToast === 'function') {
showToast('Deleting release...', 'info');
}
fetch(deleteUrl, {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest',
'X-CSRF-TOKEN': csrfToken,
'Content-Type': 'application/json',
'Accept': 'application/json'
}
})
.then(response => {
console.log('Delete response status:', response.status);
if (response.ok) {
const row = element.closest('tr');
if (row) {
row.style.transition = 'opacity 0.3s';
row.style.opacity = '0';
setTimeout(() => row.remove(), 300);
}
if (typeof showToast === 'function') {
showToast('Release deleted successfully', 'success');
} else {
showMessage('Release deleted successfully', 'success');
}
} else {
return response.text().then(text => {
console.error('Delete failed with status:', response.status, 'Response:', text);
if (typeof showToast === 'function') {
showToast('Error deleting release: ' + response.status, 'error');
} else {
showMessage('Error deleting release', 'error');
}
});
}
})
.catch(error => {
console.error('Error deleting release:', error);
if (typeof showToast === 'function') {
showToast('Error deleting release: ' + error.message, 'error');
} else {
showMessage('Error deleting release', 'error');
}
});
};
}
// 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);
}
// Handle modal close buttons with data attributes
if (e.target.hasAttribute('data-close-preview-modal') || e.target.closest('[data-close-preview-modal]')) {
e.preventDefault();
if (typeof closePreviewModal === 'function') {
closePreviewModal();
}
}
if (e.target.hasAttribute('data-close-mediainfo-modal') || e.target.closest('[data-close-mediainfo-modal]')) {
e.preventDefault();
if (typeof closeMediainfoModal === 'function') {
closeMediainfoModal();
}
}
if (e.target.hasAttribute('data-close-filelist-modal') || e.target.closest('[data-close-filelist-modal]')) {
e.preventDefault();
if (typeof closeFilelistModal === 'function') {
closeFilelistModal();
}
}
});
// 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('[data-close-mediainfo-modal]')) {
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('[data-close-filelist-modal]')) {
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() {
// Handle individual add to cart button clicks (for details page, browse page, etc.)
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;
}
// Send AJAX request to add item to cart
fetch('/cart/add', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
'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);
}
// Update cart count if element exists
const cartCount = document.querySelector('.cart-count');
if (cartCount && data.cartCount) {
cartCount.textContent = data.cartCount;
}
// 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');
}
});
}
});
// 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;
}
// 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');
}
}
});
});
// 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;
}
showConfirm({
title: 'Delete from Cart',
message: `Are you sure you want to delete ${selected.length} item${selected.length > 1 ? 's' : ''} from your cart?`,
type: 'danger',
confirmText: 'Delete',
onConfirm: 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');
showConfirm({
title: 'Remove from Cart',
message: `Are you sure you want to remove "${releaseName}" from your cart?`,
type: 'warning',
confirmText: 'Remove',
onConfirm: 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');
}
});
});
}
// Multi-operations delete (Admin only)
const multiDeleteBtn = document.querySelector('.nzb_multi_operations_delete');
if (multiDeleteBtn) {
console.log('Delete button found and event listener being attached');
multiDeleteBtn.addEventListener('click', function(e) {
e.preventDefault();
console.log('Delete button clicked');
const checkboxes = document.querySelectorAll('.chkRelease:checked');
const selected = Array.from(checkboxes);
console.log('Selected releases:', selected.length);
if (selected.length === 0) {
if (typeof showToast === 'function') {
showToast('Please select at least one release to delete', 'error');
} else {
alert('Please select at least one release to delete');
}
return;
}
const confirmMessage = `Are you sure you want to delete ${selected.length} release${selected.length > 1 ? 's' : ''}? This action cannot be undone.`;
// Use styled confirmation modal
showConfirm({
title: 'Delete Releases',
message: confirmMessage,
type: 'danger',
confirmText: 'Delete',
onConfirm: function() {
console.log('User confirmed deletion');
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content;
if (!csrfToken) {
console.error('CSRF token not found');
if (typeof showToast === 'function') {
showToast('Security token not found. Please refresh the page.', 'error');
} else {
alert('Security token not found. Please refresh the page.');
}
return;
}
let deletedCount = 0;
let errorCount = 0;
if (typeof showToast === 'function') {
showToast(`Deleting ${selected.length} release${selected.length > 1 ? 's' : ''}...`, 'info');
}
// Delete releases one by one
const deletePromises = selected.map(checkbox => {
const guid = checkbox.value;
const row = checkbox.closest('tr');
console.log('Attempting to delete release:', guid);
return fetch('/admin/release-delete/' + guid, {
method: 'POST',
headers: {
'X-CSRF-TOKEN': csrfToken,
'X-Requested-With': 'XMLHttpRequest',
'Content-Type': 'application/json'
}
})
.then(response => {
console.log('Delete response status:', response.status, 'for guid:', guid);
if (response.ok) {
deletedCount++;
// Fade out and remove the row
if (row) {
row.style.transition = 'opacity 0.3s';
row.style.opacity = '0';
setTimeout(() => row.remove(), 300);
}
return true;
} else {
errorCount++;
console.error('Failed to delete release:', guid, 'Status:', response.status);
return response.text().then(text => {
console.error('Error response:', text);
return false;
});
}
})
.catch(error => {
console.error('Error deleting release:', guid, error);
errorCount++;
return false;
});
});
// Wait for all deletes to complete
Promise.all(deletePromises).then(() => {
console.log('All delete operations completed. Deleted:', deletedCount, 'Errors:', errorCount);
// Uncheck the select all checkbox
const selectAllCheckbox = document.getElementById('chkSelectAll');
if (selectAllCheckbox) {
selectAllCheckbox.checked = false;
}
// Show final result
if (deletedCount > 0 && errorCount === 0) {
if (typeof showToast === 'function') {
showToast(`Successfully deleted ${deletedCount} release${deletedCount > 1 ? 's' : ''}`, 'success');
}
} else if (deletedCount > 0 && errorCount > 0) {
if (typeof showToast === 'function') {
showToast(`Deleted ${deletedCount} release${deletedCount > 1 ? 's' : ''}, ${errorCount} failed`, 'error');
}
} else {
if (typeof showToast === 'function') {
showToast('Failed to delete releases', 'error');
}
}
// Reload page if all items on current page were deleted
const remainingRows = document.querySelectorAll('.chkRelease');
if (remainingRows.length === 0) {
setTimeout(() => window.location.reload(), 1500);
}
});
}
});
});
}
}
// 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 toggle button handler - cycles through light -> dark -> system
const themeToggle = document.getElementById('theme-toggle');
if (themeToggle) {
themeToggle.addEventListener('click', function() {
const metaTheme = document.querySelector('meta[name="theme-preference"]');
const currentTheme = metaTheme ? metaTheme.content : localStorage.getItem('theme') || 'light';
let nextTheme;
// Cycle through: light -> dark -> system -> light
if (currentTheme === 'light') {
nextTheme = 'dark';
} else if (currentTheme === 'dark') {
nextTheme = 'system';
} else {
nextTheme = 'light';
}
applyTheme(nextTheme);
saveThemePreference(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() {
const dropdownContainers = document.querySelectorAll('.dropdown-container');
dropdownContainers.forEach(function(container) {
const toggle = container.querySelector('.dropdown-toggle');
const menu = container.querySelector('.dropdown-menu');
if (!toggle || !menu) return;
let closeTimeout;
// Ensure menu is hidden initially
menu.style.display = 'none';
// Toggle on click
toggle.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
const isCurrentlyOpen = menu.style.display === 'block';
// Close all other dropdowns
dropdownContainers.forEach(function(otherContainer) {
if (otherContainer !== container) {
const otherMenu = otherContainer.querySelector('.dropdown-menu');
if (otherMenu) {
otherMenu.style.display = 'none';
}
}
});
// Toggle this dropdown
if (isCurrentlyOpen) {
menu.style.display = 'none';
} else {
menu.style.display = 'block';
}
});
// Keep open on hover over the container
container.addEventListener('mouseenter', function() {
clearTimeout(closeTimeout);
});
// Close after delay when leaving the container
container.addEventListener('mouseleave', function() {
closeTimeout = setTimeout(function() {
menu.style.display = 'none';
}, 300);
});
// Prevent closing when hovering over the menu itself
menu.addEventListener('mouseenter', function() {
clearTimeout(closeTimeout);
});
});
// Close dropdowns when clicking outside
document.addEventListener('click', function(e) {
if (!e.target.closest('.dropdown-container')) {
dropdownContainers.forEach(function(container) {
const menu = container.querySelector('.dropdown-menu');
if (menu) {
menu.style.display = 'none';
}
});
}
});
// Handle nested submenus (e.g., Foreign languages dropdown)
const submenuContainers = document.querySelectorAll('.submenu-container');
submenuContainers.forEach(function(container) {
const submenu = container.querySelector('.submenu');
if (!submenu) return;
let submenuCloseTimeout;
container.addEventListener('mouseenter', function() {
clearTimeout(submenuCloseTimeout);
submenu.style.display = 'block';
});
container.addEventListener('mouseleave', function() {
submenuCloseTimeout = setTimeout(function() {
submenu.style.display = 'none';
}, 200);
});
submenu.addEventListener('mouseenter', function() {
clearTimeout(submenuCloseTimeout);
});
});
// Mobile menu toggle
const mobileMenuToggle = document.getElementById('mobile-menu-toggle');
if (mobileMenuToggle) {
mobileMenuToggle.addEventListener('click', function() {
const desktopNav = document.querySelector('.md\\:flex.md\\:items-center');
if (desktopNav) {
desktopNav.classList.toggle('hidden');
desktopNav.classList.toggle('flex');
}
});
}
// 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.preventDefault();
mobileSearchForm.classList.toggle('hidden');
});
}
}
// 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
}
let chartsInitialized = false;
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';
// Initialize charts when API tab is shown for the first time
if (targetId === 'api' && !chartsInitialized) {
chartsInitialized = true;
// Small delay to ensure the tab is fully visible before rendering charts
setTimeout(() => {
// Wait for Chart.js to be available
let attempts = 0;
const maxAttempts = 20; // Try for up to 2 seconds
const checkChartJs = setInterval(() => {
attempts++;
if (typeof Chart !== 'undefined') {
clearInterval(checkChartJs);
initializeProfileCharts();
} else if (attempts >= maxAttempts) {
clearInterval(checkChartJs);
console.error('Chart.js failed to load within timeout period');
}
}, 100);
}, 50);
}
}
// 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();
}
} else if (hash === '' || hash === 'general') {
// If on general tab or no hash, ensure other tabs are hidden
tabContents.forEach((content, index) => {
if (index !== 0) {
content.style.display = 'none';
}
});
}
}
// 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');
// Update valid days before setting the day value
if (typeof updateValidDays === 'function') {
updateValidDays();
}
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);
}
});
// Add listeners for year and month changes to update valid days
const yearSelect = document.getElementById('expiry_year');
const monthSelect = document.getElementById('expiry_month');
if (yearSelect && monthSelect) {
yearSelect.addEventListener('change', updateValidDays);
monthSelect.addEventListener('change', updateValidDays);
}
}
// Update valid days based on selected month and year
function updateValidDays() {
const yearSelect = document.getElementById('expiry_year');
const monthSelect = document.getElementById('expiry_month');
const daySelect = document.getElementById('expiry_day');
if (!yearSelect || !monthSelect || !daySelect) return;
const year = parseInt(yearSelect.value);
const month = parseInt(monthSelect.value);
if (!year || !month) return;
// Get number of days in the selected month
const daysInMonth = new Date(year, month, 0).getDate();
// Store current selection
const currentDay = parseInt(daySelect.value);
// Clear and rebuild day options
daySelect.innerHTML = '<option value="">--</option>';
for (let d = 1; d <= daysInMonth; d++) {
const option = document.createElement('option');
option.value = String(d).padStart(2, '0');
option.textContent = d;
daySelect.appendChild(option);
}
// Restore selection if still valid
if (currentDay && currentDay <= daysInMonth) {
daySelect.value = String(currentDay).padStart(2, '0');
} else if (currentDay > daysInMonth) {
// If previously selected day is now invalid, clear it and show warning
daySelect.value = '';
// Flash the day selector to indicate it needs attention
daySelect.classList.add('border-yellow-500', 'dark:border-yellow-400', 'bg-yellow-50', 'dark:bg-yellow-900/20');
setTimeout(() => {
daySelect.classList.remove('border-yellow-500', 'dark:border-yellow-400', 'bg-yellow-50', 'dark:bg-yellow-900/20');
}, 1500);
}
}
// 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;
let isAddingTime = false;
// First, check if user has an existing expiry date (from the original user data)
const originalUserExpiry = document.getElementById('original_user_expiry')?.value;
// Check if there's already a selected datetime in the form
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 (user is modifying an already set date)
baseDate = new Date(year, parseInt(month) - 1, day, hour, minute);
isAddingTime = true;
showExpiryToast('Added ' + (days > 0 ? days + ' day' + (days !== 1 ? 's' : '') : '') + (days > 0 && hours > 0 ? ' and ' : '') + (hours > 0 ? hours + ' hour' + (hours !== 1 ? 's' : '') : ''), 'info');
} else if (originalUserExpiry && originalUserExpiry !== '') {
// User has an existing expiry date - add time from that date (proper stacking)
baseDate = new Date(originalUserExpiry);
isAddingTime = true;
const originalDate = formatDateTimeForDisplay(new Date(originalUserExpiry));
showExpiryToast('Adding time from current expiry: ' + originalDate, 'info');
} else {
// No existing date - start from current time
baseDate = new Date();
showExpiryToast('Setting expiry date from now', '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');
// Update valid days before setting the day value
if (typeof updateValidDays === 'function') {
updateValidDays();
}
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');
// Update valid days before setting the day value
if (typeof updateValidDays === 'function') {
updateValidDays();
}
document.getElementById('expiry_day').value = String(endOfDay.getDate()).padStart(2, '0');
document.getElementById('expiry_hour').value = '23';
document.getElementById('expiry_minute').value = '59';
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);
}
// 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 (user area - profile edit page)
const allThemeRadios = document.querySelectorAll('input[name="theme_preference"]');
allThemeRadios.forEach(radio => {
// Skip if event listener already attached (prevent duplicates)
if (radio.dataset.themeListenerAttached === 'true') {
return;
}
radio.dataset.themeListenerAttached = 'true';
radio.addEventListener('change', function() {
// Prevent event loop if we're programmatically updating
if (window._updatingThemeUI) {
return;
}
const selectedTheme = this.value;
applyTheme(selectedTheme);
saveThemePreference(selectedTheme);
});
});
}
function initDetailsPageImageModal() {
// Handle image modal triggers on release details page
const imageModalTriggers = document.querySelectorAll('.image-modal-trigger');
const imageModal = document.getElementById('imageModal');
const imageModalImage = document.getElementById('imageModalImage');
const imageModalTitle = document.getElementById('imageModalTitle');
const closeButtons = document.querySelectorAll('[data-close-image-modal]');
if (!imageModal || !imageModalImage || !imageModalTitle) {
return; // Not on the details page
}
// Add click handlers to image triggers
imageModalTriggers.forEach(function(trigger) {
trigger.addEventListener('click', function(e) {
e.preventDefault();
const imageUrl = this.getAttribute('data-image-url');
const imageTitle = this.getAttribute('data-image-title') || 'Image Preview';
// Use the image URL as-is (keeping _thumb suffix)
imageModalImage.src = imageUrl;
imageModalTitle.textContent = imageTitle;
imageModal.classList.remove('hidden');
imageModal.style.display = 'flex';
});
});
// Add click handler to close button
closeButtons.forEach(function(closeBtn) {
closeBtn.addEventListener('click', function(e) {
e.preventDefault();
imageModal.classList.add('hidden');
imageModal.style.display = 'none';
});
});
// Close modal when clicking on backdrop
imageModal.addEventListener('click', function(e) {
if (e.target === imageModal) {
imageModal.classList.add('hidden');
imageModal.style.display = 'none';
}
});
// Close modal with Escape key
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape' && !imageModal.classList.contains('hidden')) {
imageModal.classList.add('hidden');
imageModal.style.display = 'none';
}
});
}
// Movies page layout toggle functionality
function initMoviesLayoutToggle() {
const toggleButton = document.getElementById('layoutToggle');
const toggleText = document.getElementById('layoutToggleText');
const moviesGrid = document.getElementById('moviesGrid');
if (!toggleButton || !toggleText || !moviesGrid) {
return; // Not on movies page
}
// Get current layout from data attribute
let currentLayout = parseInt(moviesGrid.dataset.userLayout) || 2;
// Apply current layout
applyLayout(currentLayout);
// Handle toggle button click
toggleButton.addEventListener('click', function() {
// Toggle between 1 and 2 column layouts
currentLayout = currentLayout === 2 ? 1 : 2;
// Save preference to server
const csrfToken = document.querySelector('meta[name="csrf-token"]');
if (!csrfToken || !csrfToken.content) {
console.error('CSRF token not found');
applyLayout(currentLayout); // Apply layout anyway
return;
}
fetch('/movies/update-layout', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': csrfToken.content
},
body: JSON.stringify({ layout: currentLayout })
})
.then(response => response.json())
.then(data => {
if (data.success) {
applyLayout(currentLayout);
}
})
.catch(error => {
console.error('Error saving layout preference:', error);
// Apply layout anyway even if save fails
applyLayout(currentLayout);
});
});
function applyLayout(layout) {
const images = moviesGrid.querySelectorAll('img[alt], .bg-gray-200.dark\\:bg-gray-700');
const releaseContainers = moviesGrid.querySelectorAll('.release-card-container');
if (layout === 1) {
// 1 Column Layout
moviesGrid.classList.remove('lg:grid-cols-2');
moviesGrid.classList.add('grid-cols-1');
if (toggleText) toggleText.textContent = '1 Column';
const icon = toggleButton.querySelector('i');
if (icon) icon.className = 'fas fa-th-list mr-2';
// Make images larger
images.forEach(img => {
img.classList.remove('w-32', 'h-48');
img.classList.add('w-48', 'h-72');
});
// Adjust release card containers to horizontal layout
releaseContainers.forEach(container => {
container.classList.remove('space-y-2');
container.classList.add('flex', 'flex-row', 'items-start', 'justify-between', 'gap-3');
const infoWrapper = container.querySelector('.release-info-wrapper');
if (infoWrapper) {
infoWrapper.classList.add('flex-1', 'min-w-0');
}
const actions = container.querySelector('.release-actions');
if (actions) {
actions.classList.remove('flex-wrap');
actions.classList.add('flex-shrink-0', 'flex-row', 'items-center');
}
});
} else {
// 2 Column Layout
moviesGrid.classList.remove('grid-cols-1');
moviesGrid.classList.add('lg:grid-cols-2');
if (toggleText) toggleText.textContent = '2 Columns';
const icon = toggleButton.querySelector('i');
if (icon) icon.className = 'fas fa-th-large mr-2';
// Make images smaller
images.forEach(img => {
img.classList.remove('w-48', 'h-72');
img.classList.add('w-32', 'h-48');
});
// Adjust release card containers to vertical layout
releaseContainers.forEach(container => {
container.classList.add('space-y-2');
container.classList.remove('flex', 'flex-row', 'items-start', 'justify-between', 'gap-3');
const infoWrapper = container.querySelector('.release-info-wrapper');
if (infoWrapper) {
infoWrapper.classList.remove('flex-1', 'min-w-0');
}
const actions = container.querySelector('.release-actions');
if (actions) {
actions.classList.add('flex-wrap', 'items-center');
actions.classList.remove('flex-shrink-0', 'flex-row');
}
});
}
}
}
function initMobileEnhancements() {
// Handle mobile-specific enhancements
const mobileSidebarToggle = document.getElementById('mobile-sidebar-toggle');
if (mobileSidebarToggle) {
mobileSidebarToggle.addEventListener('click', function() {
const sidebar = document.getElementById('sidebar');
if (sidebar) {
sidebar.classList.toggle('hidden');
sidebar.classList.toggle('flex');
}
});
}
}
function initAdminDashboardCharts() {
// Only initialize if Chart.js is available and we're on a page with charts
if (typeof Chart === 'undefined') {
// If Chart.js is not loaded yet, wait for it
let attempts = 0;
const maxAttempts = 30; // Try for up to 3 seconds
const checkChartJs = setInterval(() => {
attempts++;
if (typeof Chart !== 'undefined') {
clearInterval(checkChartJs);
initializeAllDashboardCharts();
} else if (attempts >= maxAttempts) {
clearInterval(checkChartJs);
console.warn('Chart.js not loaded - charts will not be displayed');
}
}, 100);
return;
}
initializeAllDashboardCharts();
}
function initializeAllDashboardCharts() {
// Check if any chart canvas elements exist
const hasUserStatsCharts = document.getElementById('downloadsChart') ||
document.getElementById('apiHitsChart') ||
document.getElementById('downloadsMinuteChart') ||
document.getElementById('apiHitsMinuteChart');
const hasSystemCharts = document.getElementById('cpuHistory24hChart') ||
document.getElementById('ramHistory24hChart') ||
document.getElementById('cpuHistory30dChart') ||
document.getElementById('ramHistory30dChart');
if (!hasUserStatsCharts && !hasSystemCharts) {
return; // Not on admin dashboard or charts not present
}
// Initialize user statistics charts
initializeUserStatCharts();
// Initialize system metrics charts
initializeSystemMetricsCharts();
}
function initializeUserStatCharts() {
// Downloads Chart (Last 7 Days - Hourly)
const downloadsCanvas = document.getElementById('downloadsChart');
if (downloadsCanvas) {
const data = JSON.parse(downloadsCanvas.dataset.chartData || '[]');
if (data.length > 0) {
new Chart(downloadsCanvas, {
type: 'bar',
data: {
labels: data.map(item => item.time),
datasets: [{
label: 'Downloads',
data: data.map(item => item.count),
backgroundColor: 'rgba(34, 197, 94, 0.7)',
borderColor: 'rgba(34, 197, 94, 1)',
borderWidth: 1
}]
},
options: {
responsive: true,
maintainAspectRatio: true,
scales: {
y: {
beginAtZero: true,
ticks: {
precision: 0
}
}
},
plugins: {
legend: {
display: false
}
}
}
});
}
}
// API Hits Chart (Last 7 Days - Hourly)
const apiHitsCanvas = document.getElementById('apiHitsChart');
if (apiHitsCanvas) {
const data = JSON.parse(apiHitsCanvas.dataset.chartData || '[]');
if (data.length > 0) {
new Chart(apiHitsCanvas, {
type: 'line',
data: {
labels: data.map(item => item.time),
datasets: [{
label: 'API Hits',
data: data.map(item => item.count),
backgroundColor: 'rgba(168, 85, 247, 0.2)',
borderColor: 'rgba(168, 85, 247, 1)',
borderWidth: 2,
fill: true,
tension: 0.4
}]
},
options: {
responsive: true,
maintainAspectRatio: true,
scales: {
y: {
beginAtZero: true,
ticks: {
precision: 0
}
}
},
plugins: {
legend: {
display: false
}
}
}
});
}
}
// Downloads Per Minute Chart (Last 60 Minutes)
const downloadsMinuteCanvas = document.getElementById('downloadsMinuteChart');
if (downloadsMinuteCanvas) {
const data = JSON.parse(downloadsMinuteCanvas.dataset.chartData || '[]');
if (data.length > 0) {
new Chart(downloadsMinuteCanvas, {
type: 'line',
data: {
labels: data.map(item => item.time),
datasets: [{
label: 'Downloads',
data: data.map(item => item.count),
backgroundColor: 'rgba(34, 197, 94, 0.2)',
borderColor: 'rgba(34, 197, 94, 1)',
borderWidth: 2,
fill: true,
tension: 0.4
}]
},
options: {
responsive: true,
maintainAspectRatio: true,
scales: {
y: {
beginAtZero: true,
ticks: {
precision: 0
}
}
},
plugins: {
legend: {
display: false
}
}
}
});
}
}
// API Hits Per Minute Chart (Last 60 Minutes)
const apiHitsMinuteCanvas = document.getElementById('apiHitsMinuteChart');
if (apiHitsMinuteCanvas) {
const data = JSON.parse(apiHitsMinuteCanvas.dataset.chartData || '[]');
if (data.length > 0) {
new Chart(apiHitsMinuteCanvas, {
type: 'line',
data: {
labels: data.map(item => item.time),
datasets: [{
label: 'API Hits',
data: data.map(item => item.count),
backgroundColor: 'rgba(168, 85, 247, 0.2)',
borderColor: 'rgba(168, 85, 247, 1)',
borderWidth: 2,
fill: true,
tension: 0.4
}]
},
options: {
responsive: true,
maintainAspectRatio: true,
scales: {
y: {
beginAtZero: true,
ticks: {
precision: 0
}
}
},
plugins: {
legend: {
display: false
}
}
}
});
}
}
}
function initializeSystemMetricsCharts() {
// CPU Usage 24h Chart
const cpuHistory24hCanvas = document.getElementById('cpuHistory24hChart');
if (cpuHistory24hCanvas) {
const history = JSON.parse(cpuHistory24hCanvas.dataset.history || '[]');
if (history.length > 0) {
new Chart(cpuHistory24hCanvas, {
type: 'line',
data: {
labels: history.map(item => item.time),
datasets: [{
label: 'CPU Usage %',
data: history.map(item => item.value),
backgroundColor: 'rgba(249, 115, 22, 0.2)',
borderColor: 'rgba(249, 115, 22, 1)',
borderWidth: 2,
fill: true,
tension: 0.4
}]
},
options: {
responsive: true,
maintainAspectRatio: true,
scales: {
y: {
beginAtZero: true,
max: 100,
ticks: {
callback: function(value) {
return value + '%';
}
}
}
},
plugins: {
legend: {
display: false
}
}
}
});
}
}
// RAM Usage 24h Chart
const ramHistory24hCanvas = document.getElementById('ramHistory24hChart');
if (ramHistory24hCanvas) {
const history = JSON.parse(ramHistory24hCanvas.dataset.history || '[]');
if (history.length > 0) {
new Chart(ramHistory24hCanvas, {
type: 'line',
data: {
labels: history.map(item => item.time),
datasets: [{
label: 'RAM Usage %',
data: history.map(item => item.value),
backgroundColor: 'rgba(6, 182, 212, 0.2)',
borderColor: 'rgba(6, 182, 212, 1)',
borderWidth: 2,
fill: true,
tension: 0.4
}]
},
options: {
responsive: true,
maintainAspectRatio: true,
scales: {
y: {
beginAtZero: true,
max: 100,
ticks: {
callback: function(value) {
return value + '%';
}
}
}
},
plugins: {
legend: {
display: false
}
}
}
});
}
}
// CPU Usage 30d Chart
const cpuHistory30dCanvas = document.getElementById('cpuHistory30dChart');
if (cpuHistory30dCanvas) {
const history = JSON.parse(cpuHistory30dCanvas.dataset.history || '[]');
if (history.length > 0) {
new Chart(cpuHistory30dCanvas, {
type: 'line',
data: {
labels: history.map(item => item.time),
datasets: [{
label: 'CPU Usage %',
data: history.map(item => item.value),
backgroundColor: 'rgba(249, 115, 22, 0.2)',
borderColor: 'rgba(249, 115, 22, 1)',
borderWidth: 2,
fill: true,
tension: 0.4
}]
},
options: {
responsive: true,
maintainAspectRatio: true,
scales: {
y: {
beginAtZero: true,
max: 100,
ticks: {
callback: function(value) {
return value + '%';
}
}
}
},
plugins: {
legend: {
display: false
}
}
}
});
}
}
// RAM Usage 30d Chart
const ramHistory30dCanvas = document.getElementById('ramHistory30dChart');
if (ramHistory30dCanvas) {
const history = JSON.parse(ramHistory30dCanvas.dataset.history || '[]');
if (history.length > 0) {
new Chart(ramHistory30dCanvas, {
type: 'line',
data: {
labels: history.map(item => item.time),
datasets: [{
label: 'RAM Usage %',
data: history.map(item => item.value),
backgroundColor: 'rgba(6, 182, 212, 0.2)',
borderColor: 'rgba(6, 182, 212, 1)',
borderWidth: 2,
fill: true,
tension: 0.4
}]
},
options: {
responsive: true,
maintainAspectRatio: true,
scales: {
y: {
beginAtZero: true,
max: 100,
ticks: {
callback: function(value) {
return value + '%';
}
}
}
},
plugins: {
legend: {
display: false
}
}
}
});
}
}
}
function initAdminGroups() {
// Get AJAX URL and CSRF token from page
const container = document.querySelector('[data-ajax-url]');
const ajaxUrl = container ? container.dataset.ajaxUrl : '/admin/ajax';
const csrfToken = container ? container.dataset.csrfToken : document.querySelector('meta[name="csrf-token"]')?.content;
// Toggle group active/inactive status
window.ajax_group_status = function(id, status) {
fetch(ajaxUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-CSRF-TOKEN': csrfToken,
'X-Requested-With': 'XMLHttpRequest'
},
body: new URLSearchParams({
action: 'toggle_group_active_status',
group_id: id,
group_status: status
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
const groupCell = document.getElementById('group-' + id);
if (groupCell && data.newStatus !== undefined) {
const isActive = data.newStatus == 1;
groupCell.innerHTML = 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>`;
}
if (typeof showToast === 'function') {
showToast(data.message || 'Group status updated', 'success');
}
} else {
if (typeof showToast === 'function') {
showToast(data.message || 'Error updating group status', 'error');
}
}
})
.catch(error => {
console.error('Error:', error);
if (typeof showToast === 'function') {
showToast('Error updating group status', 'error');
}
});
};
// Toggle backfill status
window.ajax_backfill_status = function(id, status) {
fetch(ajaxUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-CSRF-TOKEN': csrfToken,
'X-Requested-With': 'XMLHttpRequest'
},
body: new URLSearchParams({
action: 'toggle_group_backfill',
group_id: id,
backfill: status
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
const backfillCell = document.getElementById('backfill-' + id);
if (backfillCell && data.newStatus !== undefined) {
const isEnabled = data.newStatus == 1;
backfillCell.innerHTML = 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>`;
}
if (typeof showToast === 'function') {
showToast(data.message || 'Backfill status updated', 'success');
}
} else {
if (typeof showToast === 'function') {
showToast(data.message || 'Error updating backfill status', 'error');
}
}
})
.catch(error => {
console.error('Error:', error);
if (typeof showToast === 'function') {
showToast('Error updating backfill status', 'error');
}
});
};
// Reset group
window.ajax_group_reset = function(id) {
showConfirm({
title: 'Reset Group',
message: 'Are you sure you want to reset this group?',
details: 'This will reset the article pointers back to the current state.',
type: 'warning',
confirmText: 'Reset',
cancelText: 'Cancel',
onConfirm: function() {
fetch(ajaxUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-CSRF-TOKEN': csrfToken,
'X-Requested-With': 'XMLHttpRequest'
},
body: new URLSearchParams({
action: 'reset_group',
group_id: id
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
if (typeof showToast === 'function') {
showToast(data.message || 'Group reset successfully', 'success');
}
} else {
if (typeof showToast === 'function') {
showToast(data.message || 'Error resetting group', 'error');
}
}
})
.catch(error => {
console.error('Error:', error);
if (typeof showToast === 'function') {
showToast('Error resetting group', 'error');
}
});
}
});
};
// Delete group
window.confirmGroupDelete = function(id) {
showConfirm({
title: 'Delete Group',
message: 'Are you sure you want to delete this group?',
details: 'This action cannot be undone.',
type: 'danger',
confirmText: 'Delete',
cancelText: 'Cancel',
onConfirm: function() {
fetch(ajaxUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-CSRF-TOKEN': csrfToken,
'X-Requested-With': 'XMLHttpRequest'
},
body: new URLSearchParams({
action: 'delete_group',
group_id: id
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
const row = document.getElementById('grouprow-' + id);
if (row) {
row.style.transition = 'opacity 0.3s';
row.style.opacity = '0';
setTimeout(() => row.remove(), 300);
}
if (typeof showToast === 'function') {
showToast(data.message || 'Group deleted successfully', 'success');
}
} else {
if (typeof showToast === 'function') {
showToast(data.message || 'Error deleting group', 'error');
}
}
})
.catch(error => {
console.error('Error:', error);
if (typeof showToast === 'function') {
showToast('Error deleting group', 'error');
}
});
}
});
};
// Purge group
window.confirmGroupPurge = function(id) {
showConfirm({
title: 'Purge Group',
message: 'Are you sure you want to purge this group?',
details: 'This will delete all releases and binaries for this group. This action cannot be undone!',
type: 'danger',
confirmText: 'Purge',
cancelText: 'Cancel',
onConfirm: function() {
fetch(ajaxUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-CSRF-TOKEN': csrfToken,
'X-Requested-With': 'XMLHttpRequest'
},
body: new URLSearchParams({
action: 'purge_group',
group_id: id
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
if (typeof showToast === 'function') {
showToast(data.message || 'Group purged successfully', 'success');
}
} else {
if (typeof showToast === 'function') {
showToast(data.message || 'Error purging group', 'error');
}
}
})
.catch(error => {
console.error('Error:', error);
if (typeof showToast === 'function') {
showToast('Error purging group', 'error');
}
});
}
});
};
// Reset all groups
window.ajax_group_reset_all = function() {
hideResetAllModal();
fetch(ajaxUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-CSRF-TOKEN': csrfToken,
'X-Requested-With': 'XMLHttpRequest'
},
body: new URLSearchParams({
action: 'reset_all_groups'
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
if (typeof showToast === 'function') {
showToast(data.message || 'All groups reset successfully', 'success');
}
} else {
if (typeof showToast === 'function') {
showToast(data.message || 'Error resetting all groups', 'error');
}
}
})
.catch(error => {
console.error('Error:', error);
if (typeof showToast === 'function') {
showToast('Error resetting all groups', 'error');
}
});
};
// Purge all groups
window.ajax_group_purge_all = function() {
hidePurgeAllModal();
fetch(ajaxUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-CSRF-TOKEN': csrfToken,
'X-Requested-With': 'XMLHttpRequest'
},
body: new URLSearchParams({
action: 'purge_all_groups'
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
if (typeof showToast === 'function') {
showToast(data.message || 'All groups purged successfully', 'success');
}
} else {
if (typeof showToast === 'function') {
showToast(data.message || 'Error purging all groups', 'error');
}
}
})
.catch(error => {
console.error('Error:', error);
if (typeof showToast === 'function') {
showToast('Error purging all groups', 'error');
}
});
};
// Modal helpers
window.showResetAllModal = function() {
const modal = document.getElementById('resetAllModal');
if (modal) {
modal.classList.remove('hidden');
}
};
window.hideResetAllModal = function() {
const modal = document.getElementById('resetAllModal');
if (modal) {
modal.classList.add('hidden');
}
};
window.showPurgeAllModal = function() {
const modal = document.getElementById('purgeAllModal');
if (modal) {
modal.classList.remove('hidden');
}
};
window.hidePurgeAllModal = function() {
const modal = document.getElementById('purgeAllModal');
if (modal) {
modal.classList.add('hidden');
}
};
// Reset Selected Modal helpers
window.showResetSelectedModal = function() {
const modal = document.getElementById('resetSelectedModal');
const countSpan = document.getElementById('reset-selected-count');
const listDiv = document.getElementById('reset-selected-list');
if (modal) {
const selectedGroups = getSelectedGroups();
if (selectedGroups.length === 0) {
if (typeof showToast === 'function') {
showToast('No groups selected', 'warning');
}
return;
}
if (countSpan) {
countSpan.textContent = selectedGroups.length;
}
if (listDiv) {
listDiv.innerHTML = selectedGroups.map(g =>
`<div class="py-1 border-b border-gray-200 dark:border-gray-700 last:border-0">${escapeHtml(g.name)}</div>`
).join('');
}
modal.classList.remove('hidden');
}
};
window.hideResetSelectedModal = function() {
const modal = document.getElementById('resetSelectedModal');
if (modal) {
modal.classList.add('hidden');
}
};
// Get selected groups
window.getSelectedGroups = function() {
const checkboxes = document.querySelectorAll('.group-checkbox:checked');
const groups = [];
checkboxes.forEach(cb => {
groups.push({
id: cb.dataset.groupId,
name: cb.dataset.groupName
});
});
return groups;
};
// Toggle select all groups
window.toggleSelectAllGroups = function(checkbox) {
const isChecked = checkbox.checked;
const groupCheckboxes = document.querySelectorAll('.group-checkbox');
groupCheckboxes.forEach(cb => {
cb.checked = isChecked;
});
updateSelectionUI();
};
// Update selection UI (counter, button visibility)
window.updateSelectionUI = function() {
const selectedGroups = getSelectedGroups();
const count = selectedGroups.length;
const counter = document.getElementById('selection-counter');
const countSpan = document.getElementById('selected-count');
const resetSelectedBtn = document.getElementById('reset-selected-btn');
const selectAllCheckbox = document.getElementById('select-all-groups');
const allCheckboxes = document.querySelectorAll('.group-checkbox');
if (counter && countSpan) {
if (count > 0) {
counter.classList.remove('hidden');
countSpan.textContent = count;
} else {
counter.classList.add('hidden');
}
}
if (resetSelectedBtn) {
if (count > 0) {
resetSelectedBtn.classList.remove('hidden');
} else {
resetSelectedBtn.classList.add('hidden');
}
}
// Update select-all checkbox state
if (selectAllCheckbox && allCheckboxes.length > 0) {
const allChecked = Array.from(allCheckboxes).every(cb => cb.checked);
const someChecked = Array.from(allCheckboxes).some(cb => cb.checked);
selectAllCheckbox.checked = allChecked;
selectAllCheckbox.indeterminate = someChecked && !allChecked;
}
};
// Reset selected groups
window.ajax_group_reset_selected = function() {
hideResetSelectedModal();
const selectedGroups = getSelectedGroups();
if (selectedGroups.length === 0) {
if (typeof showToast === 'function') {
showToast('No groups selected', 'warning');
}
return;
}
const groupIds = selectedGroups.map(g => g.id);
fetch(ajaxUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-CSRF-TOKEN': csrfToken,
'X-Requested-With': 'XMLHttpRequest'
},
body: new URLSearchParams({
action: 'reset_selected_groups',
group_ids: JSON.stringify(groupIds)
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
if (typeof showToast === 'function') {
showToast(data.message || `${selectedGroups.length} group(s) reset successfully`, 'success');
}
// Clear selection
document.querySelectorAll('.group-checkbox:checked').forEach(cb => {
cb.checked = false;
});
document.getElementById('select-all-groups').checked = false;
updateSelectionUI();
} else {
if (typeof showToast === 'function') {
showToast(data.message || 'Error resetting selected groups', 'error');
}
}
})
.catch(error => {
console.error('Error:', error);
if (typeof showToast === 'function') {
showToast('Error resetting selected groups', 'error');
}
});
};
// Initialize checkbox change listeners
document.addEventListener('change', function(e) {
if (e.target.classList.contains('group-checkbox')) {
updateSelectionUI();
}
});
}
// 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);
}
});
});
}
// Admin Image Preview functionality
function initAdminImagePreview() {
// Handle admin page image previews (for cover images, etc.)
// This uses the existing image modal functionality
document.addEventListener('click', function(e) {
const imageTrigger = e.target.closest('[data-admin-image-preview]');
if (imageTrigger) {
e.preventDefault();
const imageUrl = imageTrigger.getAttribute('data-admin-image-preview');
if (imageUrl && typeof openImageModal === 'function') {
openImageModal(imageUrl);
}
}
});
// Preview image on file input change (for admin edit pages)
document.querySelectorAll('input[type="file"][accept*="image"]').forEach(function(fileInput) {
fileInput.addEventListener('change', function(e) {
const file = e.target.files[0];
if (file && file.type.startsWith('image/')) {
const reader = new FileReader();
reader.onload = function(event) {
// Find preview container
const previewContainer = fileInput.closest('div').querySelector('.image-preview');
if (previewContainer) {
let previewImg = previewContainer.querySelector('img');
if (!previewImg) {
previewImg = document.createElement('img');
previewImg.className = 'w-full h-auto rounded-lg';
previewContainer.innerHTML = '';
previewContainer.appendChild(previewImg);
}
previewImg.src = event.target.result;
}
};
reader.readAsDataURL(file);
}
});
});
}
// Admin Invitations Select All functionality
function initAdminInvitationsSelectAll() {
const selectAllCheckbox = document.getElementById('select_all');
if (!selectAllCheckbox) {
return; // Not on invitations page
}
const invitationCheckboxes = document.querySelectorAll('.invitation-checkbox');
// Handle select all checkbox change
selectAllCheckbox.addEventListener('change', function() {
invitationCheckboxes.forEach(checkbox => {
checkbox.checked = this.checked;
});
});
// Handle individual checkbox changes to update select all state
invitationCheckboxes.forEach(checkbox => {
checkbox.addEventListener('change', function() {
const allChecked = Array.from(invitationCheckboxes).every(cb => cb.checked);
const someChecked = Array.from(invitationCheckboxes).some(cb => cb.checked);
selectAllCheckbox.checked = allChecked;
selectAllCheckbox.indeterminate = someChecked && !allChecked;
});
});
// Initialize the select all state on page load
const allChecked = invitationCheckboxes.length > 0 && Array.from(invitationCheckboxes).every(cb => cb.checked);
selectAllCheckbox.checked = allChecked;
}
// Admin Regex Form Validation
function initAdminRegexFormValidation() {
const regexForm = document.getElementById('regexForm');
if (!regexForm) {
return; // Not on a regex edit page
}
// Validate regex pattern on form submit
regexForm.addEventListener('submit', function(e) {
const regexInputs = regexForm.querySelectorAll('input[name*="regex"], textarea[name*="regex"]');
let isValid = true;
const errors = [];
regexInputs.forEach(input => {
const value = input.value.trim();
if (value && input.hasAttribute('required')) {
// Basic regex validation - check if it looks like a valid regex pattern
// Regex should typically start with a delimiter (/, #, ~, etc.)
// and have matching delimiters
if (value.length > 0) {
const firstChar = value[0];
const delimiters = ['/', '#', '~', '%', '@', '!',];
if (delimiters.includes(firstChar)) {
// Check if regex has matching closing delimiter
let delimiterCount = 0;
let flags = '';
for (let i = 0; i < value.length; i++) {
if (value[i] === firstChar && i > 0) {
delimiterCount++;
// Check if there are flags after this delimiter
flags = value.substring(i + 1);
break;
}
}
if (delimiterCount === 0) {
isValid = false;
errors.push(`Regex pattern "${input.name}" is missing closing delimiter "${firstChar}"`);
input.classList.add('border-red-500');
} else {
input.classList.remove('border-red-500');
// Validate flags if present
if (flags && !/^[gimsux]*$/.test(flags)) {
isValid = false;
errors.push(`Invalid regex flags in "${input.name}". Allowed: g, i, m, s, u, x`);
input.classList.add('border-red-500');
}
}
} else if (value.length > 0) {
// Regex doesn't start with a delimiter, might still be valid but warn
input.classList.add('border-yellow-500');
}
}
}
});
if (!isValid && errors.length > 0) {
e.preventDefault();
const errorMsg = errors.join('\n');
if (typeof showToast === 'function') {
showToast('Please fix regex validation errors', 'error');
} else {
alert(errorMsg);
}
}
});
// Real-time validation feedback for regex inputs
const regexInputs = regexForm.querySelectorAll('input[name*="regex"], textarea[name*="regex"]');
regexInputs.forEach(input => {
input.addEventListener('blur', function() {
const value = this.value.trim();
if (value) {
const firstChar = value[0];
const delimiters = ['/', '#', '~', '%', '@', '!',];
if (delimiters.includes(firstChar)) {
let delimiterCount = 0;
for (let i = 1; i < value.length; i++) {
if (value[i] === firstChar) {
delimiterCount++;
break;
}
}
if (delimiterCount === 0) {
this.classList.add('border-red-500');
this.classList.remove('border-green-500');
} else {
this.classList.remove('border-red-500');
this.classList.add('border-green-500');
}
} else {
this.classList.remove('border-red-500', 'border-green-500');
}
} else {
this.classList.remove('border-red-500', 'border-green-500');
}
});
});
}
// Admin Tmux Select Colors functionality
function initAdminTmuxSelectColors() {
// This function is for future tmux color selection functionality
// Currently, no color selection is needed in tmux settings
// If color pickers are added to tmux-edit page in the future, add them here
// Check if we're on a page that might have color inputs
const colorInputs = document.querySelectorAll('input[type="color"], select[data-color-select]');
if (colorInputs.length > 0) {
colorInputs.forEach(function(input) {
// Add color preview functionality if needed
if (input.type === 'color') {
input.addEventListener('change', function() {
// Update preview or related elements if needed
const preview = input.closest('.form-group')?.querySelector('.color-preview');
if (preview) {
preview.style.backgroundColor = input.value;
}
});
}
});
}
}
// 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;
});
});
}
// Update selectAll state when individual checkboxes change
const userCheckboxes = document.querySelectorAll('.user-checkbox');
userCheckboxes.forEach(checkbox => {
checkbox.addEventListener('change', function() {
const allChecked = Array.from(userCheckboxes).every(cb => cb.checked);
const someChecked = Array.from(userCheckboxes).some(cb => cb.checked);
if (selectAllCheckbox) {
selectAllCheckbox.checked = allChecked;
selectAllCheckbox.indeterminate = someChecked && !allChecked;
}
});
});
// Bulk action form submit handler
const bulkActionForm = document.getElementById('bulkActionForm');
if (bulkActionForm) {
bulkActionForm.addEventListener('submit', function(e) {
e.preventDefault();
const action = document.getElementById('bulkAction')?.value;
const checkedBoxes = document.querySelectorAll('.user-checkbox:checked');
const validationError = document.getElementById('validationError');
const validationErrorMessage = document.getElementById('validationErrorMessage');
// Hide any previous validation errors
if (validationError) {
validationError.classList.add('hidden');
}
// Validate action selected
if (!action) {
if (validationError && validationErrorMessage) {
validationErrorMessage.textContent = 'Please select an action from the dropdown.';
validationError.classList.remove('hidden');
}
return false;
}
// Validate at least one user selected
if (checkedBoxes.length === 0) {
if (validationError && validationErrorMessage) {
validationErrorMessage.textContent = 'Please select at least one user.';
validationError.classList.remove('hidden');
}
return false;
}
const count = checkedBoxes.length;
const actionText = action === 'restore' ? 'restore' : 'permanently delete';
const type = action === 'restore' ? 'success' : 'danger';
const title = action === 'restore' ? 'Restore Users' : 'Delete Users';
showConfirm({
title: title,
message: `Are you sure you want to ${actionText} ${count} user${count > 1 ? 's' : ''}?`,
type: type,
confirmText: action === 'restore' ? 'Restore' : 'Delete',
onConfirm: function() {
bulkActionForm.submit();
}
});
});
}
// Event delegation for restore buttons
document.addEventListener('click', function(e) {
const restoreBtn = e.target.closest('.restore-user-btn');
if (restoreBtn) {
e.preventDefault();
const userId = restoreBtn.dataset.userId;
const username = restoreBtn.dataset.username;
if (userId && username) {
restoreUser(userId, username);
}
}
const deleteBtn = e.target.closest('.delete-user-btn');
if (deleteBtn) {
e.preventDefault();
const userId = deleteBtn.dataset.userId;
const username = deleteBtn.dataset.username;
if (userId && username) {
permanentDeleteUser(userId, username);
}
}
});
// Bulk action confirmation (kept for backward compatibility)
window.confirmBulkAction = function(event) {
event?.preventDefault();
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';
const type = action === 'restore' ? 'success' : 'danger';
const title = action === 'restore' ? 'Restore Users' : 'Delete Users';
showConfirm({
title: title,
message: `Are you sure you want to ${actionText} ${count} user${count > 1 ? 's' : ''}?`,
type: type,
confirmText: action === 'restore' ? 'Restore' : 'Delete',
onConfirm: function() {
document.getElementById('bulkActionForm')?.submit();
}
});
return false;
};
}
// Individual user restore/delete actions for deleted users page
window.restoreUser = function(userId, username) {
showConfirm({
title: 'Restore User',
message: `Are you sure you want to restore user '${username}'?`,
type: 'success',
confirmText: 'Restore',
onConfirm: function() {
const form = document.getElementById('individualActionForm');
if (form) {
const baseUrl = window.location.origin;
form.action = `${baseUrl}/admin/deleted-users/restore/${userId}`;
form.submit();
}
}
});
};
window.permanentDeleteUser = function(userId, username) {
showConfirm({
title: 'Permanently Delete User',
message: `Are you sure you want to PERMANENTLY delete user '${username}'?`,
details: 'This action cannot be undone!',
type: 'danger',
confirmText: 'Delete Permanently',
onConfirm: function() {
const form = document.getElementById('individualActionForm');
if (form) {
const baseUrl = window.location.origin;
// Use the correct route: permanent-delete
form.action = `${baseUrl}/admin/deleted-users/permanent-delete/${userId}`;
form.submit();
}
}
});
};
// Recent Activity Auto-Refresh (20 minutes)
function initRecentActivityRefresh() {
const activityContainer = document.getElementById('recent-activity-container');
if (!activityContainer) {
return; // Not on admin dashboard page
}
const refreshUrl = activityContainer.getAttribute('data-refresh-url');
if (!refreshUrl) {
return;
}
// Function to refresh activity data
function refreshRecentActivity() {
fetch(refreshUrl, {
method: 'GET',
headers: {
'X-Requested-With': 'XMLHttpRequest',
'Accept': 'application/json'
},
credentials: 'same-origin'
})
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
if (data.success && data.activities) {
updateActivityDisplay(data.activities);
updateLastRefreshTime();
}
})
.catch(error => {
console.error('Error refreshing recent activity:', error);
});
}
// Function to update the activity display
function updateActivityDisplay(activities) {
const container = document.getElementById('recent-activity-container');
if (!container) return;
// Clear existing content
container.innerHTML = '';
if (activities.length === 0) {
container.innerHTML = '<p class="text-gray-500 dark:text-gray-400 text-center py-4" id="no-activity-message">No recent activity</p>';
return;
}
// Add each activity item
activities.forEach(activity => {
const activityItem = document.createElement('div');
activityItem.className = 'flex items-start activity-item rounded-lg p-2 hover:bg-gray-50 dark:hover:bg-gray-900 transition-colors';
activityItem.innerHTML = `
<div class="w-8 h-8 ${activity.icon_bg} rounded-full flex items-center justify-center mr-3 flex-shrink-0">
<i class="fas fa-${activity.icon} ${activity.icon_color} text-sm"></i>
</div>
<div class="flex-1">
<p class="text-sm text-gray-800 dark:text-gray-200">${escapeHtml(activity.message)}</p>
<p class="text-xs text-gray-500 dark:text-gray-400">${escapeHtml(activity.created_at)}</p>
</div>
`;
container.appendChild(activityItem);
});
}
// Function to update last refresh time
function updateLastRefreshTime() {
const lastUpdatedElement = document.getElementById('activity-last-updated');
if (lastUpdatedElement) {
const now = new Date();
const timeString = now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
lastUpdatedElement.innerHTML = `<i class="fas fa-sync-alt"></i> Last updated: ${timeString}`;
}
}
// Helper function to escape HTML to prevent XSS
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Set up auto-refresh every 20 minutes (1200000 milliseconds)
const refreshInterval = 20 * 60 * 1000; // 20 minutes in milliseconds
setInterval(refreshRecentActivity, refreshInterval);
// Also refresh on page visibility change (when user comes back to tab)
document.addEventListener('visibilitychange', function() {
if (!document.hidden) {
refreshRecentActivity();
}
});
}
// Profile page functionality
let downloadsChart = null;
let apiRequestsChart = null;
function initProfilePage() {
// Animate progress bars
animateProgressBars();
}
function animateProgressBars() {
document.querySelectorAll('.progress-bar').forEach(bar => {
const width = bar.dataset.width;
if (width) {
setTimeout(() => {
bar.style.width = width + '%';
}, 100);
}
});
}
// Theme Management (moved from main.blade.php)
function initThemeManagement() {
function updateThemeButton(theme) {
const themeIcon = document.getElementById('theme-icon');
const themeLabel = document.getElementById('theme-label');
const themeToggle = document.getElementById('theme-toggle');
const icons = {
'light': 'fa-sun',
'dark': 'fa-moon',
'system': 'fa-desktop'
};
const labels = {
'light': 'Light',
'dark': 'Dark',
'system': 'System'
};
const titles = {
'light': 'Light Mode',
'dark': 'Dark Mode',
'system': 'System Mode'
};
if (themeIcon) {
themeIcon.classList.remove('fa-sun', 'fa-moon', 'fa-desktop');
themeIcon.classList.add(icons[theme]);
}
if (themeLabel) {
themeLabel.textContent = labels[theme];
}
if (themeToggle) {
themeToggle.setAttribute('title', titles[theme]);
}
}
// Get current theme from data attribute or localStorage
const currentThemeElement = document.getElementById('current-theme-data');
let currentTheme = currentThemeElement ? currentThemeElement.dataset.theme : 'light';
const isAuthenticated = currentThemeElement ? currentThemeElement.dataset.authenticated === 'true' : false;
if (!isAuthenticated) {
currentTheme = localStorage.getItem('theme') || 'light';
}
// Listen for OS theme changes
themeMediaQuery.addEventListener('change', () => {
if (currentTheme === 'system') {
applyTheme('system');
}
});
// Listen for custom theme change events
document.addEventListener('themeChanged', function(e) {
if (e.detail && e.detail.theme) {
updateThemeButton(e.detail.theme);
}
});
// Dark mode toggle (user area - only if not already handled by admin)
const themeToggle = document.getElementById('theme-toggle');
// Check if this is admin area by looking for admin-specific elements
const isAdminArea = document.querySelector('aside.bg-gray-900.dark\\:bg-gray-950') &&
document.querySelector('a[href*="admin"]');
if (themeToggle && !isAdminArea) {
// Only handle if not in admin area (admin has its own handler in initAdminMenu)
themeToggle.addEventListener('click', function() {
const metaTheme = document.querySelector('meta[name="theme-preference"]');
const currentTheme = metaTheme ? metaTheme.content : localStorage.getItem('theme') || 'light';
let nextTheme;
if (currentTheme === 'light') {
nextTheme = 'dark';
} else if (currentTheme === 'dark') {
nextTheme = 'system';
} else {
nextTheme = 'light';
}
applyTheme(nextTheme);
saveThemePreference(nextTheme);
});
}
// Mobile sidebar toggle
document.getElementById('mobile-sidebar-toggle')?.addEventListener('click', function() {
document.getElementById('sidebar')?.classList.toggle('hidden');
});
}
// Copy to Clipboard functionality - consolidated and optimized
function initCopyToClipboard() {
// Unified copy function with visual feedback
function copyToClipboard(text, button) {
// Try modern Clipboard API first
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(function() {
showCopyFeedback(button);
}).catch(function() {
// Fallback for older browsers
fallbackCopy(text, button);
});
} else {
fallbackCopy(text, button);
}
}
function fallbackCopy(text, button) {
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.style.position = 'fixed';
textarea.style.opacity = '0';
document.body.appendChild(textarea);
textarea.select();
textarea.setSelectionRange(0, 99999);
try {
document.execCommand('copy');
showCopyFeedback(button);
} catch (err) {
console.error('Failed to copy:', err);
}
document.body.removeChild(textarea);
}
function showCopyFeedback(button) {
if (!button) return;
const icon = button.querySelector('i');
if (icon) {
icon.classList.remove('fa-copy');
icon.classList.add('fa-check');
}
if (button.classList) {
button.classList.add('text-green-600');
}
setTimeout(function() {
if (!button) return;
if (icon) {
icon.classList.remove('fa-check');
icon.classList.add('fa-copy');
}
if (button.classList) {
button.classList.remove('text-green-600');
}
}, 2000);
}
// Event delegation for copy buttons
document.addEventListener('click', function(e) {
const copyBtn = e.target.closest('.copy-btn');
if (copyBtn) {
const targetId = copyBtn.getAttribute('data-copy-target');
const input = document.getElementById(targetId);
if (input) {
e.preventDefault();
input.select();
input.setSelectionRange(0, 99999);
copyToClipboard(input.value, copyBtn);
}
}
// API Token copy button (specific ID)
const apiTokenBtn = e.target.id === 'copyApiToken' ? e.target : e.target.closest('#copyApiToken');
if (apiTokenBtn) {
const input = document.getElementById('apiTokenInput');
if (input) {
e.preventDefault();
input.select();
input.setSelectionRange(0, 99999);
copyToClipboard(input.value, apiTokenBtn);
}
}
});
}
// Verify User Modal functionality
function initVerifyUserModal() {
let verifyForm = null;
// Show verify modal
window.showVerifyModal = function(event, form) {
event.preventDefault();
verifyForm = form;
const modal = document.getElementById('verifyUserModal');
if (modal) {
modal.classList.remove('hidden');
modal.classList.add('flex');
}
};
// Hide verify modal
window.hideVerifyModal = function() {
const modal = document.getElementById('verifyUserModal');
if (modal) {
modal.classList.add('hidden');
modal.classList.remove('flex');
}
verifyForm = null;
};
// Submit verify form
window.submitVerifyForm = function() {
if (verifyForm) {
verifyForm.submit();
} else {
// Fallback: find form by data attribute
const formId = document.querySelector('[data-show-verify-modal]')?.getAttribute('data-form-id');
if (formId) {
const form = document.querySelector(`form[action*="admin.verify"] input[value="${formId}"]`)?.closest('form');
if (form) {
form.submit();
}
}
}
hideVerifyModal();
};
// Event delegation for verify modal
document.addEventListener('click', function(e) {
if (e.target.hasAttribute('data-show-verify-modal') || e.target.closest('[data-show-verify-modal]')) {
const element = e.target.hasAttribute('data-show-verify-modal') ? e.target : e.target.closest('[data-show-verify-modal]');
const form = element.closest('form');
if (form) {
showVerifyModal(e, form);
}
}
if (e.target.hasAttribute('data-close-verify-modal') || e.target.closest('[data-close-verify-modal]')) {
e.preventDefault();
hideVerifyModal();
}
if (e.target.hasAttribute('data-submit-verify-form') || e.target.closest('[data-submit-verify-form]')) {
e.preventDefault();
submitVerifyForm();
}
});
// Close on Escape key
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
const modal = document.getElementById('verifyUserModal');
if (modal && !modal.classList.contains('hidden')) {
hideVerifyModal();
}
}
});
}
// Flash Messages as Toast Notifications
function initFlashMessages() {
const flashMessagesElement = document.getElementById('flash-messages-data');
if (!flashMessagesElement) {
return;
}
const flashMessages = JSON.parse(flashMessagesElement.dataset.messages || '{}');
if (flashMessages.success && typeof showToast === 'function') {
showToast(flashMessages.success, 'success');
}
if (flashMessages.error) {
if (Array.isArray(flashMessages.error)) {
flashMessages.error.forEach(error => {
if (typeof showToast === 'function') {
showToast(error, 'error');
}
});
} else if (typeof showToast === 'function') {
showToast(flashMessages.error, 'error');
}
}
if (flashMessages.warning && typeof showToast === 'function') {
showToast(flashMessages.warning, 'warning');
}
if (flashMessages.info && typeof showToast === 'function') {
showToast(flashMessages.info, 'info');
}
}
// Initialize on DOMContentLoaded
(function() {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function() {
initThemeManagement();
initFlashMessages();
});
} else {
initThemeManagement();
initFlashMessages();
}
})();
// Tmux Edit - Remove Crap Releases Toggle
function toggleCrapTypes(value) {
const container = document.getElementById('crap_types_container');
if (container) {
if (value === 'Custom') {
container.style.display = 'block';
// Add a slight animation effect
setTimeout(() => {
container.style.opacity = '1';
}, 10);
} else {
container.style.opacity = '0.5';
setTimeout(() => {
container.style.display = 'none';
container.style.opacity = '1';
}, 200);
}
}
}
// Initialize tmux edit page functionality
function initTmuxEdit() {
// Initialize crap types toggle on page load
const checkedRadio = document.querySelector('input[name="fix_crap_opt"]:checked');
if (checkedRadio) {
toggleCrapTypes(checkedRadio.value);
}
// Add event listeners to all radio buttons
document.querySelectorAll('input[name="fix_crap_opt"]').forEach(function(radio) {
radio.addEventListener('change', function() {
toggleCrapTypes(this.value);
});
});
}
// Initialize on DOMContentLoaded if on tmux-edit page
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function() {
if (document.getElementById('crap_types_container')) {
initTmuxEdit();
}
});
} else {
if (document.getElementById('crap_types_container')) {
initTmuxEdit();
}
}
// Initialize quality filter for movie releases (resolution + source)
function initQualityFilter() {
const resolutionButtons = document.querySelectorAll('.resolution-filter-btn');
const sourceButtons = document.querySelectorAll('.source-filter-btn');
const releaseItems = document.querySelectorAll('.release-item');
const releaseCount = document.getElementById('release-count');
if (!releaseItems.length || !releaseCount) {
return; // Exit if elements don't exist on the page
}
const totalReleases = releaseItems.length;
let activeResolution = 'all';
let activeSource = 'all';
// Function to apply filters
function applyFilters() {
let visibleCount = 0;
releaseItems.forEach(item => {
const releaseName = item.getAttribute('data-release-name');
if (!releaseName) return;
let matchesResolution = true;
let matchesSource = true;
// Check resolution filter
if (activeResolution !== 'all') {
matchesResolution = releaseName.includes(activeResolution.toLowerCase());
}
// Check source filter
if (activeSource !== 'all') {
const sourceLower = activeSource.toLowerCase();
// Handle different naming variations
if (sourceLower === 'bluray') {
matchesSource = releaseName.includes('bluray') ||
releaseName.includes('blu-ray') ||
releaseName.includes('bdrip') ||
releaseName.includes('brrip');
} else if (sourceLower === 'web-dl') {
matchesSource = releaseName.includes('web-dl') ||
releaseName.includes('webdl') ||
releaseName.includes('web.dl');
} else if (sourceLower === 'webrip') {
matchesSource = releaseName.includes('webrip') ||
releaseName.includes('web-rip') ||
releaseName.includes('web.rip');
} else {
matchesSource = releaseName.includes(sourceLower);
}
}
// Show/hide based on both filters
if (matchesResolution && matchesSource) {
item.style.display = '';
visibleCount++;
} else {
item.style.display = 'none';
}
});
// Update count display
if (activeResolution === 'all' && activeSource === 'all') {
releaseCount.textContent = `(${totalReleases} total)`;
} else {
releaseCount.textContent = `(${visibleCount} of ${totalReleases})`;
}
}
// Resolution filter buttons
resolutionButtons.forEach(button => {
button.addEventListener('click', function() {
const filter = this.getAttribute('data-filter');
activeResolution = filter;
// Update active button styling
resolutionButtons.forEach(btn => {
btn.classList.remove('active', 'bg-blue-600', 'text-white');
btn.classList.add('bg-gray-200', 'dark:bg-gray-700', 'text-gray-700', 'dark:text-gray-300');
});
this.classList.add('active', 'bg-blue-600', 'text-white');
this.classList.remove('bg-gray-200', 'dark:bg-gray-700', 'text-gray-700', 'dark:text-gray-300');
applyFilters();
});
});
// Source filter buttons
sourceButtons.forEach(button => {
button.addEventListener('click', function() {
const filter = this.getAttribute('data-filter');
activeSource = filter;
// Update active button styling
sourceButtons.forEach(btn => {
btn.classList.remove('active', 'bg-purple-600', 'text-white');
btn.classList.add('bg-gray-200', 'dark:bg-gray-700', 'text-gray-700', 'dark:text-gray-300');
});
this.classList.add('active', 'bg-purple-600', 'text-white');
this.classList.remove('bg-gray-200', 'dark:bg-gray-700', 'text-gray-700', 'dark:text-gray-300');
applyFilters();
});
});
}
// Admin Content List - Toggle Enable/Disable
function initContentToggle() {
document.addEventListener('click', function(e) {
const toggleBtn = e.target.closest('.content-toggle-status');
if (!toggleBtn) return;
e.preventDefault();
const contentId = toggleBtn.getAttribute('data-content-id');
const currentStatus = parseInt(toggleBtn.getAttribute('data-current-status'));
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content;
if (!contentId || !csrfToken) {
if (typeof showToast === 'function') {
showToast('Error: Missing required data', 'error');
}
return;
}
// Disable button during request
toggleBtn.disabled = true;
toggleBtn.style.opacity = '0.6';
fetch('/admin/content-toggle-status', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': csrfToken,
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify({ id: contentId })
})
.then(response => response.json())
.then(data => {
if (data.success) {
const newStatus = data.status;
const row = toggleBtn.closest('tr');
// Find the status cell (6th td in the row - 0-indexed = 5)
const statusCell = row.cells[5];
// Update the status badge
if (newStatus === 1) {
statusCell.innerHTML = `
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-100">
<i class="fa fa-check mr-1"></i>Enabled
</span>
`;
} else {
statusCell.innerHTML = `
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-100">
<i class="fa fa-times mr-1"></i>Disabled
</span>
`;
}
// Update the toggle button itself
toggleBtn.setAttribute('data-current-status', newStatus);
toggleBtn.title = newStatus === 1 ? 'Disable' : 'Enable';
// Update button color classes
if (newStatus === 1) {
toggleBtn.className = 'content-toggle-status text-green-600 dark:text-green-400 hover:text-green-900 dark:hover:text-green-300';
toggleBtn.innerHTML = '<i class="fa fa-toggle-on"></i>';
} else {
toggleBtn.className = 'content-toggle-status text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-300';
toggleBtn.innerHTML = '<i class="fa fa-toggle-off"></i>';
}
// Re-enable button
toggleBtn.disabled = false;
toggleBtn.style.opacity = '1';
if (typeof showToast === 'function') {
showToast(data.message, 'success');
}
} else {
if (typeof showToast === 'function') {
showToast(data.message || 'Failed to toggle content status', 'error');
}
toggleBtn.disabled = false;
toggleBtn.style.opacity = '1';
}
})
.catch(error => {
console.error('Error toggling content status:', error);
if (typeof showToast === 'function') {
showToast('An error occurred while toggling content status', 'error');
}
toggleBtn.disabled = false;
toggleBtn.style.opacity = '1';
});
});
}
// Admin Content List - Delete Content
function initContentDelete() {
document.addEventListener('click', function(e) {
const deleteBtn = e.target.closest('.content-delete');
if (!deleteBtn) return;
e.preventDefault();
const contentId = deleteBtn.getAttribute('data-content-id');
const contentTitle = deleteBtn.getAttribute('data-content-title');
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content;
if (!contentId || !csrfToken) {
if (typeof showToast === 'function') {
showToast('Error: Missing required data', 'error');
}
return;
}
// Show styled confirmation modal
showConfirm({
title: 'Delete Content',
message: `Are you sure you want to delete "${contentTitle}"?`,
details: 'This action cannot be undone.',
type: 'danger',
confirmText: 'Delete',
cancelText: 'Cancel',
onConfirm: function() {
// Disable button during request
deleteBtn.disabled = true;
deleteBtn.style.opacity = '0.6';
fetch('/admin/content-delete', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': csrfToken,
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify({ id: contentId })
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Remove the row from the table
const row = deleteBtn.closest('tr');
if (row) {
row.style.transition = 'opacity 0.3s';
row.style.opacity = '0';
setTimeout(() => {
row.remove();
// Check if table is empty now
const tbody = document.querySelector('tbody');
if (tbody && tbody.children.length === 0) {
location.reload(); // Reload to show "no content found" message
}
}, 300);
}
if (typeof showToast === 'function') {
showToast(data.message, 'success');
}
} else {
if (typeof showToast === 'function') {
showToast(data.message || 'Failed to delete content', 'error');
}
deleteBtn.disabled = false;
deleteBtn.style.opacity = '1';
}
})
.catch(error => {
console.error('Error deleting content:', error);
if (typeof showToast === 'function') {
showToast('An error occurred while deleting content', 'error');
}
deleteBtn.disabled = false;
deleteBtn.style.opacity = '1';
});
}
});
});
}
// Initialize synchronized horizontal scrollbars for user list table
function initUserListScrollSync() {
const topScroll = document.getElementById('topScroll');
const bottomScroll = document.getElementById('bottomScroll');
const topScrollContent = document.getElementById('topScrollContent');
const table = bottomScroll ? bottomScroll.querySelector('table') : null;
if (!topScroll || !bottomScroll || !topScrollContent || !table) {
return; // Elements not found, likely not on user list page
}
// Set the width of the top scroll content to match the table width
function updateTopScrollWidth() {
topScrollContent.style.width = table.scrollWidth + 'px';
}
// Initial width setup
updateTopScrollWidth();
// Update on window resize
window.addEventListener('resize', updateTopScrollWidth);
// Synchronize scrolling from top to bottom
topScroll.addEventListener('scroll', function() {
if (!topScroll.isSyncing) {
bottomScroll.isSyncing = true;
bottomScroll.scrollLeft = topScroll.scrollLeft;
bottomScroll.isSyncing = false;
}
});
// Synchronize scrolling from bottom to top
bottomScroll.addEventListener('scroll', function() {
if (!bottomScroll.isSyncing) {
topScroll.isSyncing = true;
topScroll.scrollLeft = bottomScroll.scrollLeft;
topScroll.isSyncing = false;
}
});
}
/**
* Initialize search autocomplete functionality
* Handles both the main search page and header search inputs
*/
function initSearchAutocomplete() {
// Initialize header autocomplete (desktop and mobile)
initAutocompleteInput('header-search-input', 'header-autocomplete-dropdown', 'header-search-form', 'header-autocomplete-item');
initAutocompleteInput('mobile-search-input', 'mobile-autocomplete-dropdown', 'mobile-search-form-el', 'header-autocomplete-item');
// Initialize main search page autocomplete
initAutocompleteInput('search', 'autocomplete-dropdown', 'searchForm', 'autocomplete-item');
// Initialize any dynamic autocomplete inputs (from Blade components)
document.querySelectorAll('[data-autocomplete-input]').forEach(function(input) {
const dropdownId = input.getAttribute('data-autocomplete-input');
const formId = input.getAttribute('data-autocomplete-form');
const inputId = input.id;
if (inputId && dropdownId) {
initAutocompleteInput(inputId, dropdownId, formId, 'autocomplete-suggestion');
}
});
// Apply CSP-safe styles to modals (replaces inline styles)
initModalStyles();
}
/**
* Apply CSP-safe styles to modals that previously had inline styles
*/
function initModalStyles() {
// Preview modal
const previewModal = document.getElementById('previewModal');
if (previewModal) {
previewModal.style.display = 'none';
previewModal.style.zIndex = '9999';
}
// NFO modal
const nfoModal = document.getElementById('nfoModal');
if (nfoModal) {
nfoModal.style.display = 'none';
}
// Image modal
const imageModal = document.getElementById('imageModal');
if (imageModal) {
imageModal.style.display = 'none';
}
// MediaInfo modal
const mediainfoModal = document.getElementById('mediainfoModal');
if (mediainfoModal) {
mediainfoModal.style.display = 'none';
mediainfoModal.style.zIndex = '9999';
}
// Filelist modal
const filelistModal = document.getElementById('filelistModal');
if (filelistModal) {
filelistModal.style.display = 'none';
filelistModal.style.zIndex = '9999';
}
// MediaInfo content scroll
const mediainfoContent = document.getElementById('mediainfoContent');
if (mediainfoContent) {
mediainfoContent.style.maxHeight = 'calc(90vh - 80px)';
}
// Filelist content scroll
const filelistContent = document.getElementById('filelistContent');
if (filelistContent) {
filelistContent.style.maxHeight = 'calc(90vh - 80px)';
}
}
/**
* Initialize autocomplete for a specific input element
* @param {string} inputId - ID of the search input
* @param {string} dropdownId - ID of the dropdown container
* @param {string} formId - ID of the form to submit
* @param {string} itemClass - CSS class for dropdown items
*/
function initAutocompleteInput(inputId, dropdownId, formId, itemClass) {
const searchInput = document.getElementById(inputId);
const dropdown = document.getElementById(dropdownId);
const form = document.getElementById(formId);
if (!searchInput || !dropdown) return;
let debounceTimer;
let currentIndex = -1;
let suggestions = [];
// Input handler with debounce
searchInput.addEventListener('input', function() {
clearTimeout(debounceTimer);
const query = this.value.trim();
if (query.length < 2) {
hideDropdown();
return;
}
debounceTimer = setTimeout(async () => {
try {
const response = await fetch(`/api/search/autocomplete?q=${encodeURIComponent(query)}`);
const data = await response.json();
if (data.success && data.suggestions && data.suggestions.length > 0) {
suggestions = data.suggestions;
renderDropdown(suggestions, query);
} else {
hideDropdown();
}
} catch (error) {
console.error('Autocomplete error:', error);
hideDropdown();
}
}, 200);
});
// Keyboard navigation
searchInput.addEventListener('keydown', function(e) {
if (!dropdown.classList.contains('hidden')) {
const items = dropdown.querySelectorAll('.' + itemClass);
switch(e.key) {
case 'ArrowDown':
e.preventDefault();
currentIndex = Math.min(currentIndex + 1, items.length - 1);
updateSelection(items);
break;
case 'ArrowUp':
e.preventDefault();
currentIndex = Math.max(currentIndex - 1, 0);
updateSelection(items);
break;
case 'Enter':
if (currentIndex >= 0 && items[currentIndex]) {
e.preventDefault();
searchInput.value = suggestions[currentIndex];
hideDropdown();
if (form) form.submit();
}
break;
case 'Escape':
hideDropdown();
break;
}
}
});
// Click outside to close
document.addEventListener('click', function(e) {
if (!searchInput.contains(e.target) && !dropdown.contains(e.target)) {
hideDropdown();
}
});
function renderDropdown(items, query) {
currentIndex = -1;
const escapeRegex = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const regex = new RegExp(`(${escapeRegex(query)})`, 'gi');
const maxItems = itemClass === 'header-autocomplete-item' ? 8 : 10;
const sizeClass = itemClass === 'header-autocomplete-item' ? 'px-3 py-2 text-sm' : 'px-4 py-2';
const iconSizeClass = itemClass === 'header-autocomplete-item' ? 'text-xs' : '';
dropdown.innerHTML = items.slice(0, maxItems).map((item, index) => {
const highlighted = item.replace(regex, '<mark class="bg-yellow-200 dark:bg-yellow-700 px-0.5 rounded">$1</mark>');
return `
<div class="${itemClass} ${sizeClass} cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-900 dark:text-gray-100"
data-index="${index}">
<i class="fas fa-search text-gray-400 mr-2 ${iconSizeClass}"></i>
${highlighted}
</div>
`;
}).join('');
// Add click handlers
dropdown.querySelectorAll('.' + itemClass).forEach((item, index) => {
item.addEventListener('click', function() {
searchInput.value = suggestions[index];
hideDropdown();
if (form) form.submit();
});
item.addEventListener('mouseenter', function() {
currentIndex = index;
updateSelection(dropdown.querySelectorAll('.' + itemClass));
});
});
dropdown.classList.remove('hidden');
}
function hideDropdown() {
dropdown.classList.add('hidden');
dropdown.innerHTML = '';
suggestions = [];
currentIndex = -1;
}
function updateSelection(items) {
items.forEach((item, index) => {
if (index === currentIndex) {
item.classList.add('bg-blue-100', 'dark:bg-blue-900');
} else {
item.classList.remove('bg-blue-100', 'dark:bg-blue-900');
}
});
}
}
// ============================================================================
// Sort Dropdown Component
// ============================================================================
/**
* Initialize sort dropdown functionality
* Handles the custom dropdown for sorting releases on browse/search pages
*/
function initSortDropdowns() {
document.querySelectorAll('.sort-dropdown').forEach(function(dropdown) {
var toggle = dropdown.querySelector('.sort-dropdown-toggle');
var menu = dropdown.querySelector('.sort-dropdown-menu');
var chevron = dropdown.querySelector('.sort-dropdown-chevron');
if (!toggle || !menu) return;
// Skip if already initialized
if (toggle.hasAttribute('data-sort-initialized')) return;
toggle.setAttribute('data-sort-initialized', 'true');
toggle.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
var isOpen = !menu.classList.contains('hidden');
// Close all other dropdowns first
document.querySelectorAll('.sort-dropdown-menu').forEach(function(m) {
m.classList.add('hidden');
});
document.querySelectorAll('.sort-dropdown-chevron').forEach(function(c) {
c.classList.remove('rotate-180');
});
if (!isOpen) {
menu.classList.remove('hidden');
if (chevron) chevron.classList.add('rotate-180');
}
});
});
// Close dropdowns when clicking outside (single global listener)
if (!window._sortDropdownOutsideListenerAdded) {
window._sortDropdownOutsideListenerAdded = true;
document.addEventListener('click', function(e) {
var isInsideDropdown = e.target.closest('.sort-dropdown');
if (!isInsideDropdown) {
document.querySelectorAll('.sort-dropdown-menu').forEach(function(m) {
m.classList.add('hidden');
});
document.querySelectorAll('.sort-dropdown-chevron').forEach(function(c) {
c.classList.remove('rotate-180');
});
}
});
}
}
// Initialize sort dropdowns on DOMContentLoaded
document.addEventListener('DOMContentLoaded', initSortDropdowns);
// =============================================================================
// RELEASE REPORT FUNCTIONALITY
// =============================================================================
/**
* Initialize release report buttons and modals
* CSP-safe implementation for reporting bad releases
*/
function initReleaseReportButtons() {
// Find all report button containers
var containers = document.querySelectorAll('.report-button-container');
containers.forEach(function(container) {
var trigger = container.querySelector('.report-trigger');
var modal = container.querySelector('.report-modal');
if (!trigger || !modal) return;
// Skip if already initialized
if (trigger.hasAttribute('data-report-initialized')) return;
trigger.setAttribute('data-report-initialized', 'true');
var releaseId = trigger.getAttribute('data-report-release-id');
var form = modal.querySelector('.report-form');
var closeButtons = modal.querySelectorAll('.report-modal-close');
var backdrop = modal.querySelector('.report-modal-backdrop');
var reasonSelect = modal.querySelector('.report-reason');
var descriptionTextarea = modal.querySelector('.report-description');
var charCount = modal.querySelector('.report-char-count');
var submitButton = modal.querySelector('.report-submit');
var errorDiv = modal.querySelector('.report-error');
var successDiv = modal.querySelector('.report-success');
var reportLabel = trigger.querySelector('.report-label');
var flagIcon = trigger.querySelector('i');
var hasReported = false;
var isSubmitting = false;
// Open modal
trigger.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
if (hasReported) return;
modal.classList.remove('hidden');
// Reset form state
if (errorDiv) errorDiv.classList.add('hidden');
if (successDiv) successDiv.classList.add('hidden');
if (reasonSelect) reasonSelect.value = '';
if (descriptionTextarea) descriptionTextarea.value = '';
if (charCount) charCount.textContent = '0/1000 characters';
if (submitButton) submitButton.disabled = true;
});
// Close modal function
function closeModal() {
modal.classList.add('hidden');
if (hasReported) {
trigger.disabled = true;
trigger.classList.add('opacity-50', 'cursor-not-allowed');
if (flagIcon) flagIcon.classList.add('text-red-500');
if (reportLabel) reportLabel.textContent = 'Reported';
}
}
// Close button handlers
closeButtons.forEach(function(closeBtn) {
closeBtn.addEventListener('click', function(e) {
e.preventDefault();
closeModal();
});
});
// Backdrop click to close
if (backdrop) {
backdrop.addEventListener('click', function() {
closeModal();
});
}
// Character count for description
if (descriptionTextarea && charCount) {
descriptionTextarea.addEventListener('input', function() {
charCount.textContent = this.value.length + '/1000 characters';
});
}
// Enable/disable submit based on reason selection
if (reasonSelect && submitButton) {
reasonSelect.addEventListener('change', function() {
submitButton.disabled = !this.value;
});
}
// Submit button click handler
if (submitButton) {
submitButton.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
if (isSubmitting || !reasonSelect || !reasonSelect.value) return;
isSubmitting = true;
submitButton.disabled = true;
submitButton.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i> Submitting...';
if (errorDiv) errorDiv.classList.add('hidden');
if (successDiv) successDiv.classList.add('hidden');
var csrfToken = document.querySelector('meta[name="csrf-token"]');
var csrfValue = csrfToken ? csrfToken.getAttribute('content') : '';
var requestBody = {
release_id: releaseId,
reason: reasonSelect.value,
description: descriptionTextarea ? descriptionTextarea.value : ''
};
fetch('/release-report', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': csrfValue,
'Accept': 'application/json'
},
body: JSON.stringify(requestBody)
})
.then(function(response) {
return response.json().then(function(data) {
return { ok: response.ok, data: data };
});
})
.then(function(result) {
if (result.ok && result.data.success) {
if (successDiv) {
successDiv.querySelector('p').textContent = result.data.message;
successDiv.classList.remove('hidden');
}
hasReported = true;
setTimeout(closeModal, 2000);
} else {
if (errorDiv) {
errorDiv.querySelector('p').textContent = result.data.message || 'An error occurred. Please try again.';
errorDiv.classList.remove('hidden');
}
}
})
.catch(function(error) {
if (errorDiv) {
errorDiv.querySelector('p').textContent = 'Network error. Please try again.';
errorDiv.classList.remove('hidden');
}
})
.finally(function() {
isSubmitting = false;
if (submitButton) {
submitButton.disabled = !reasonSelect.value;
submitButton.innerHTML = 'Submit Report';
}
});
});
}
});
}
// Initialize report buttons on DOMContentLoaded
document.addEventListener('DOMContentLoaded', initReleaseReportButtons);
// Re-initialize on dynamic content load (for AJAX-loaded content)
document.addEventListener('contentLoaded', initReleaseReportButtons);
// =============================================================================
// ADMIN RELEASE REPORTS PAGE FUNCTIONALITY
// =============================================================================
/**
* Initialize admin release reports page functionality
* Handles description modal, select all, and bulk actions
*/
function initAdminReleaseReports() {
// Report Description Modal
var descModal = document.getElementById('reportDescriptionModal');
var descButtons = document.querySelectorAll('.report-description-btn');
var descContent = document.getElementById('reportDescContent');
var descReason = document.getElementById('reportDescReason');
var descReporter = document.getElementById('reportDescReporter');
var descCloseButtons = document.querySelectorAll('.report-desc-modal-close');
var descBackdrop = document.querySelector('.report-desc-modal-backdrop');
// Open description modal
descButtons.forEach(function(btn) {
if (btn.hasAttribute('data-desc-initialized')) return;
btn.setAttribute('data-desc-initialized', 'true');
btn.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
var description = this.getAttribute('data-description');
var reason = this.getAttribute('data-reason');
var reporter = this.getAttribute('data-reporter');
if (descContent) descContent.textContent = description || 'No additional details provided.';
if (descReason) descReason.textContent = reason || 'Unknown';
if (descReporter) descReporter.textContent = reporter || 'Unknown';
if (descModal) descModal.classList.remove('hidden');
});
});
// Close description modal
function closeDescModal() {
if (descModal) descModal.classList.add('hidden');
}
descCloseButtons.forEach(function(btn) {
btn.addEventListener('click', function(e) {
e.preventDefault();
closeDescModal();
});
});
if (descBackdrop) {
descBackdrop.addEventListener('click', closeDescModal);
}
// Select All functionality
var selectAll = document.getElementById('select-all');
var checkboxes = document.querySelectorAll('.report-checkbox');
if (selectAll && !selectAll.hasAttribute('data-selectall-initialized')) {
selectAll.setAttribute('data-selectall-initialized', 'true');
selectAll.addEventListener('change', function() {
checkboxes.forEach(function(checkbox) {
checkbox.checked = selectAll.checked;
});
});
}
// Update select all checkbox when individual checkboxes change
checkboxes.forEach(function(checkbox) {
if (checkbox.hasAttribute('data-checkbox-initialized')) return;
checkbox.setAttribute('data-checkbox-initialized', 'true');
checkbox.addEventListener('change', function() {
var allChecked = Array.from(checkboxes).every(function(cb) { return cb.checked; });
var someChecked = Array.from(checkboxes).some(function(cb) { return cb.checked; });
if (selectAll) {
selectAll.checked = allChecked;
selectAll.indeterminate = someChecked && !allChecked;
}
});
});
// Bulk action form validation
var bulkForm = document.getElementById('bulk-action-form');
if (bulkForm && !bulkForm.hasAttribute('data-bulk-initialized')) {
bulkForm.setAttribute('data-bulk-initialized', 'true');
bulkForm.addEventListener('submit', function(e) {
var actionSelect = this.querySelector('select[name="action"]');
var action = actionSelect ? actionSelect.value : '';
var checkedCount = document.querySelectorAll('.report-checkbox:checked').length;
if (!action) {
e.preventDefault();
alert('Please select an action.');
return;
}
if (checkedCount === 0) {
e.preventDefault();
alert('Please select at least one report.');
return;
}
if (action === 'delete') {
if (!confirm('Are you sure you want to DELETE ' + checkedCount + ' release(s)? This action cannot be undone.')) {
e.preventDefault();
}
}
});
}
}
// Initialize admin release reports on DOMContentLoaded
document.addEventListener('DOMContentLoaded', initAdminReleaseReports);