diff --git a/.gitignore b/.gitignore
index a16533409..4019e5c59 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,6 +3,7 @@ config.php
docker/.zsh_history
docker/composer-auth.json
docker/php-overrides.local.ini
+docker-compose.yml
docker-compose.override.yml
.env
install.lock
@@ -20,7 +21,21 @@ test.php
/.idea/
/vendor/*
-/storage/covers/*
+# Ignore all files in covers directories but keep folder structure
+# This includes symlinks - Git treats symlinks as files, so they're automatically ignored
+/storage/covers/**/*
+!/storage/covers/**/
+!/storage/covers/**/.gitignore
+!/storage/covers/**/no-cover.jpg
+!/storage/covers/**/no-backdrop.jpg
+/public/covers/**/*
+!/public/covers/**/
+!/public/covers/**/.gitignore
+!/public/covers/**/no-cover.jpg
+!/public/covers/**/no-backdrop.jpg
+
+# Ignore symlinks in public/storage that point to storage/covers
+/public/storage/covers
/storage/nzb/*
/resources/tmp/*
!/resources/tmp/unrar/
@@ -28,8 +43,8 @@ test.php
!/resources/tmp/imdb_cache/
!/public/assets/*
-# Ignore Vite build manifest (regenerated locally)
-/public/build/manifest.json
+# Ignore Vite build output (regenerated locally)
+/public/build/
# Logs
npm-debug.log*
@@ -44,3 +59,14 @@ node_modules/
.vscode-test
/.phpunit.cache
+phpunit.xml.back
+
+# IDE helper files (generated locally)
+_ide_helper.php
+
+# Lock files (generated locally, should not be committed)
+*-lock
+composer.lock
+package-lock.json
+yarn.lock
+pnpm-lock.yaml
diff --git a/phpunit.xml.bak b/phpunit.xml.bak
new file mode 100644
index 000000000..b2b7cb350
--- /dev/null
+++ b/phpunit.xml.bak
@@ -0,0 +1,32 @@
+
+
+
+
+ Blacklight
+ app
+
+
+
+
+
+
+
+ tests/Install
+
+
+ tests/Unit
+
+
+ tests/Feature
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/resources/js/app.js b/resources/js/app.js
index 2c3d78a0d..b59bf9dae 100644
--- a/resources/js/app.js
+++ b/resources/js/app.js
@@ -4,17 +4,34 @@ import './bootstrap';
import '../css/csp-safe.css';
import './csp-safe.js';
-// Theme initialization - must run early to prevent flash
+// Theme initialization - optimized to prevent flash of unstyled content
(function() {
- const themePreference = document.querySelector('meta[name="theme-preference"]')?.content || 'light';
+ 'use strict';
+ // Use requestAnimationFrame for better performance
+ const applyTheme = function() {
+ const metaTheme = document.querySelector('meta[name="theme-preference"]');
+ const themePreference = metaTheme?.content || 'light';
- if (themePreference === 'system') {
- // Use OS preference
- if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
+ if (themePreference === 'system') {
+ // Use OS preference with proper media query listener
+ const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
+ if (mediaQuery.matches) {
+ document.documentElement.classList.add('dark');
+ } else {
+ document.documentElement.classList.remove('dark');
+ }
+ } else if (themePreference === 'dark') {
document.documentElement.classList.add('dark');
+ } else {
+ document.documentElement.classList.remove('dark');
}
- } else if (themePreference === 'dark') {
- document.documentElement.classList.add('dark');
+ };
+
+ // Apply theme immediately if DOM is ready, otherwise wait
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', applyTheme);
+ } else {
+ applyTheme();
}
})();
diff --git a/resources/js/csp-safe.js b/resources/js/csp-safe.js
index 457bd9d82..46549d1e8 100644
--- a/resources/js/csp-safe.js
+++ b/resources/js/csp-safe.js
@@ -48,50 +48,71 @@ document.addEventListener('DOMContentLoaded', function() {
initDetailsPageImageModal();
initMoviesLayoutToggle();
initProfilePage(); // Initialize profile page (progress bars and charts)
+ initCopyToClipboard(); // Copy functionality for RSS/Profile pages
+ initVerifyUserModal(); // Admin user verification modal
});
-// Event delegation for dynamically added elements
+// 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
- if (e.target.classList.contains('toast-close') || e.target.closest('.toast-close')) {
- const toast = e.target.closest('.toast-notification');
+ const toastClose = findElementWithClass(e, 'toast-close');
+ if (toastClose) {
+ const toast = toastClose.closest('.toast-notification');
if (toast) {
toast.remove();
}
+ return;
}
// Handle NFO modal close
- if (e.target.hasAttribute('data-close-nfo-modal') || e.target.closest('[data-close-nfo-modal]')) {
+ if (findElementWithAttr(e, 'data-close-nfo-modal')) {
e.preventDefault();
closeNfoModal();
+ return;
}
// Handle NFO modal open
- if (e.target.hasAttribute('data-open-nfo') || e.target.closest('[data-open-nfo]')) {
+ const nfoOpen = findElementWithAttr(e, 'data-open-nfo');
+ if (nfoOpen) {
e.preventDefault();
- const guid = e.target.getAttribute('data-open-nfo') || e.target.closest('[data-open-nfo]').getAttribute('data-open-nfo');
+ const guid = nfoOpen.getAttribute('data-open-nfo');
if (guid) {
openNfoModal(guid);
}
+ return;
}
// Handle logout
- if (e.target.hasAttribute('data-logout') || e.target.closest('[data-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
- if (e.target.hasAttribute('data-confirm-delete') || e.target.closest('[data-confirm-delete]')) {
+ const confirmDelete = findElementWithAttr(e, 'data-confirm-delete');
+ if (confirmDelete) {
e.preventDefault();
e.stopPropagation();
-
- const element = e.target.hasAttribute('data-confirm-delete') ? e.target : e.target.closest('[data-confirm-delete]');
- const form = element.closest('form');
+ const form = confirmDelete.closest('form');
showConfirm({
message: 'Are you sure you want to delete this item?',
@@ -100,21 +121,21 @@ function initEventDelegation() {
onConfirm: function() {
if (form) {
form.submit();
- } else if (element.href) {
- window.location.href = element.href;
+ } else if (confirmDelete.href) {
+ window.location.href = confirmDelete.href;
}
}
});
+ return;
}
// Handle confirm action - using styled modal
- if (e.target.hasAttribute('data-confirm') || e.target.closest('[data-confirm]')) {
+ const confirmAction = findElementWithAttr(e, 'data-confirm');
+ if (confirmAction) {
e.preventDefault();
e.stopPropagation();
-
- const element = e.target.hasAttribute('data-confirm') ? e.target : e.target.closest('[data-confirm]');
- const message = element.getAttribute('data-confirm');
- const form = element.closest('form');
+ const message = confirmAction.getAttribute('data-confirm');
+ const form = confirmAction.closest('form');
showConfirm({
message: message,
@@ -123,11 +144,12 @@ function initEventDelegation() {
onConfirm: function() {
if (form) {
form.submit();
- } else if (element.href) {
- window.location.href = element.href;
+ } else if (confirmAction.href) {
+ window.location.href = confirmAction.href;
}
}
});
+ return;
}
// Handle download NZB buttons
@@ -209,6 +231,70 @@ function initEventDelegation() {
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 select redirects
@@ -292,11 +378,16 @@ window.showConfirm = function(options) {
const config = { ...defaults, ...options };
- // Set modal content
- document.getElementById('confirmationModalTitleText').textContent = config.title;
- document.getElementById('confirmationModalMessage').textContent = config.message;
- document.getElementById('confirmationModalConfirmText').textContent = config.confirmText;
- document.getElementById('confirmationModalCancelText').textContent = config.cancelText;
+ // 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');
@@ -349,10 +440,10 @@ window.showConfirm = function(options) {
// Handle details
const detailsDiv = document.getElementById('confirmationModalDetails');
const detailsText = document.getElementById('confirmationModalDetailsText');
- if (config.details) {
+ if (config.details && detailsText && detailsDiv) {
detailsText.textContent = config.details;
detailsDiv.classList.remove('hidden');
- } else {
+ } else if (detailsDiv) {
detailsDiv.classList.add('hidden');
}
@@ -505,30 +596,45 @@ function initLogoutForms() {
// Already handled in event delegation
}
-// Season switcher for series
+// Season switcher for series - optimized
function initSeasonSwitcher() {
window.switchSeason = function(seasonNumber) {
- // Hide all season containers
- document.querySelectorAll('[data-season-container]').forEach(function(container) {
- container.style.display = 'none';
+ // Hide all season content
+ document.querySelectorAll('.season-content').forEach(function(content) {
+ content.classList.add('hidden');
});
- // Show selected season
- const selectedSeason = document.querySelector('[data-season-container="' + seasonNumber + '"]');
- if (selectedSeason) {
- selectedSeason.style.display = 'block';
+ // 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');
}
- // Update button states
- document.querySelectorAll('[data-season]').forEach(function(btn) {
- btn.classList.remove('active', 'bg-blue-600', 'text-white');
- btn.classList.add('bg-gray-200', 'text-gray-700');
- });
+ // 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');
- const activeBtn = document.querySelector('[data-season="' + seasonNumber + '"]');
- if (activeBtn) {
- activeBtn.classList.remove('bg-gray-200', 'text-gray-700');
- activeBtn.classList.add('active', 'bg-blue-600', 'text-white');
+ // 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');
+ }
}
};
}
@@ -1149,6 +1255,28 @@ function initMediainfoAndFilelist() {
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
@@ -1165,7 +1293,7 @@ function initMediainfoAndFilelist() {
if (mediainfoModal) {
mediainfoModal.addEventListener('click', function(e) {
// Close if clicking the backdrop OR the close button
- if (e.target === this || e.target.closest('[onclick*="closeMediainfoModal"]')) {
+ if (e.target === this || e.target.closest('[data-close-mediainfo-modal]')) {
closeMediainfoModal();
}
});
@@ -1175,7 +1303,7 @@ function initMediainfoAndFilelist() {
if (filelistModal) {
filelistModal.addEventListener('click', function(e) {
// Close if clicking the backdrop OR the close button
- if (e.target === this || e.target.closest('[onclick*="closeFilelistModal"]')) {
+ if (e.target === this || e.target.closest('[data-close-filelist-modal]')) {
closeFilelistModal();
}
});
@@ -2515,11 +2643,18 @@ function initMoviesLayoutToggle() {
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': document.querySelector('meta[name="csrf-token"]').content
+ 'X-CSRF-TOKEN': csrfToken.content
},
body: JSON.stringify({ layout: currentLayout })
})
@@ -2544,8 +2679,9 @@ function initMoviesLayoutToggle() {
// 1 Column Layout
moviesGrid.classList.remove('lg:grid-cols-2');
moviesGrid.classList.add('grid-cols-1');
- toggleText.textContent = '1 Column';
- toggleButton.querySelector('i').className = 'fas fa-th-list mr-2';
+ 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 => {
@@ -2573,8 +2709,9 @@ function initMoviesLayoutToggle() {
// 2 Column Layout
moviesGrid.classList.remove('grid-cols-1');
moviesGrid.classList.add('lg:grid-cols-2');
- toggleText.textContent = '2 Columns';
- toggleButton.querySelector('i').className = 'fas fa-th-large mr-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 => {
@@ -2602,11 +2739,6 @@ function initMoviesLayoutToggle() {
}
}
-function initProfilePage() {
- // Placeholder for profile page specific functionality
- // Progress bars and charts are handled elsewhere
-}
-
function initMobileEnhancements() {
// Handle mobile-specific enhancements
const mobileSidebarToggle = document.getElementById('mobile-sidebar-toggle');
@@ -2878,14 +3010,538 @@ function initTinyMCE() {
});
}
-function initAdminSpecificFeatures() {
- // Placeholder for admin-specific features
- // Most admin features are handled in dedicated functions
+// 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() {
- // Placeholder for recent activity auto-refresh
- // Activity refresh is handled on the admin dashboard
+ 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 = '
No recent activity
';
+ 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 = `
+
+
+
+
+
${escapeHtml(activity.message)}
+
${escapeHtml(activity.created_at)}
+
+ `;
+
+ 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 = ` 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)
@@ -3014,6 +3670,164 @@ function initThemeManagement() {
});
}
+// 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');
diff --git a/resources/views/admin/movies/edit.blade.php b/resources/views/admin/movies/edit.blade.php
index ccf08192c..43657494e 100644
--- a/resources/views/admin/movies/edit.blade.php
+++ b/resources/views/admin/movies/edit.blade.php
@@ -247,7 +247,7 @@
+ data-confirm="This will fetch and update movie data from TMDB. Continue?">
Update from TMDB
diff --git a/resources/views/admin/releases/failed.blade.php b/resources/views/admin/releases/failed.blade.php
index 660e0a9c3..22adeacf8 100644
--- a/resources/views/admin/releases/failed.blade.php
+++ b/resources/views/admin/releases/failed.blade.php
@@ -146,7 +146,7 @@
+ data-confirm="Are you sure you want to delete this release?">
diff --git a/resources/views/admin/releases/index.blade.php b/resources/views/admin/releases/index.blade.php
index e710464fa..e9fac5e57 100644
--- a/resources/views/admin/releases/index.blade.php
+++ b/resources/views/admin/releases/index.blade.php
@@ -94,7 +94,7 @@
+ data-confirm="Are you sure you want to delete this release?">
diff --git a/resources/views/admin/users/index.blade.php b/resources/views/admin/users/index.blade.php
index 21ff73efb..d1a900d6a 100644
--- a/resources/views/admin/users/index.blade.php
+++ b/resources/views/admin/users/index.blade.php
@@ -278,7 +278,8 @@
@@ -330,7 +331,7 @@
Verify User
-