mirror of
https://github.com/NNTmux/newznab-tmux.git
synced 2026-08-29 01:08:56 +00:00
6020 lines
227 KiB
JavaScript
6020 lines
227 KiB
JavaScript
// 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 = {
|
||
'&': '&',
|
||
'<': '<',
|
||
'>': '>',
|
||
'"': '"',
|
||
"'": '''
|
||
};
|
||
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);
|