/** * Admin feature components: TinyMCE, user edit, verify modal, scroll sync, etc. */ import Alpine from '@alpinejs/csp'; function escapeHtml(text) { const map = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }; return String(text).replace(/[&<>"']/g, m => map[m]); } // TinyMCE editor (loads from CDN, remains imperative) Alpine.data('tinyMceEditor', () => ({ init() { if (!document.getElementById('body') && !document.querySelector('.tinymce-editor')) return; this._loadAndInit(); }, _loadAndInit() { if (typeof tinymce !== 'undefined') { this._doInit(); return; } const body = document.getElementById('body'); const editors = document.querySelectorAll('.tinymce-editor'); let apiKey = 'no-api-key'; const meta = document.querySelector('meta[name="tinymce-api-key"]'); if (meta) { apiKey = meta.content; } else if (window.NNTmuxConfig?.tinymceApiKey) apiKey = window.NNTmuxConfig.tinymceApiKey; else if (body?.dataset.tinymceApiKey) apiKey = body.dataset.tinymceApiKey; else { for (let e of editors) { if (e.dataset.tinymceApiKey) { apiKey = e.dataset.tinymceApiKey; break; } } } const script = document.createElement('script'); script.src = 'https://cdn.tiny.cloud/1/' + apiKey + '/tinymce/8/tinymce.min.js'; script.referrerPolicy = 'origin'; // Add CSP nonce for Content Security Policy compliance const nonceMeta = document.querySelector('meta[name="csp-nonce"]'); if (nonceMeta) { script.nonce = nonceMeta.content; } script.onload = () => this._doInit(); script.onerror = () => { document.querySelectorAll('#body, .tinymce-editor').forEach(ta => { if (ta.parentElement) { const err = document.createElement('div'); err.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'; err.innerHTML = 'TinyMCE editor failed to load.'; ta.parentElement.insertBefore(err, ta.nextSibling); } }); }; document.head.appendChild(script); }, _doInit() { const dark = document.documentElement.classList.contains('dark'); tinymce.init({ selector: '#body, .tinymce-editor', height: 500, menubar: true, skin: dark ? 'oxide-dark' : 'oxide', content_css: dark ? '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, valid_elements: '*[*]', extended_valid_elements: '*[*]', setup: function(editor) { editor.on('change blur keyup submit', function() { editor.save(); }); } }); // Watch dark mode changes to reinit TinyMCE new MutationObserver(() => { if (!tinymce.editors.length) return; const contents = {}; tinymce.editors.forEach(e => { contents[e.id] = e.getContent(); }); tinymce.remove(); const d = document.documentElement.classList.contains('dark'); tinymce.init({ selector: '#body, .tinymce-editor', height: 500, menubar: true, skin: d ? 'oxide-dark' : 'oxide', content_css: d ? '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', branding: false, promotion: false, valid_elements: '*[*]', extended_valid_elements: '*[*]', setup: function(editor) { editor.on('change blur keyup submit', function() { editor.save(); }); } }).then(eds => { eds.forEach(e => { if (contents[e.id]) e.setContent(contents[e.id]); }); }); }).observe(document.documentElement, { attributes: true, attributeFilter: ['class'] }); } })); // User Edit datetime picker Alpine.data('adminUserEdit', () => ({ init() { if (!document.getElementById('rolechangedate')) return; this._initDateTime(); this._setupExpiry(); }, _initDateTime() { const hidden = document.getElementById('rolechangedate'); if (!hidden?.value) return; const d = new Date(hidden.value); if (isNaN(d.getTime())) return; this._setSelects(d); this._updatePreview(); }, _setSelects(d) { const ids = ['expiry_year','expiry_month','expiry_day','expiry_hour','expiry_minute']; const vals = [d.getFullYear().toString(), String(d.getMonth()+1).padStart(2,'0'), String(d.getDate()).padStart(2,'0'), String(d.getHours()).padStart(2,'0'), String(d.getMinutes()).padStart(2,'0')]; ids.forEach((id, i) => { const el = document.getElementById(id); if (el) el.value = vals[i]; }); }, updateDateTime() { const y = document.getElementById('expiry_year')?.value; const mo = document.getElementById('expiry_month')?.value; const d = document.getElementById('expiry_day')?.value; const h = document.getElementById('expiry_hour')?.value; const mi = document.getElementById('expiry_minute')?.value; if (y && mo && d && h && mi) { document.getElementById('rolechangedate').value = y + '-' + mo + '-' + d + 'T' + h + ':' + mi + ':00'; this._updatePreview(); } }, updateValidDays() { const y = parseInt(document.getElementById('expiry_year')?.value); const m = parseInt(document.getElementById('expiry_month')?.value); const daySelect = document.getElementById('expiry_day'); if (!y || !m || !daySelect) return; const days = new Date(y, m, 0).getDate(); const cur = parseInt(daySelect.value); daySelect.innerHTML = ''; for (let i = 1; i <= days; i++) { const o = document.createElement('option'); o.value = String(i).padStart(2,'0'); o.textContent = i; daySelect.appendChild(o); } if (cur && cur <= days) daySelect.value = String(cur).padStart(2,'0'); }, setExpiry(days, hours) { let base; const y = document.getElementById('expiry_year')?.value; const mo = document.getElementById('expiry_month')?.value; const d = document.getElementById('expiry_day')?.value; const h = document.getElementById('expiry_hour')?.value; const mi = document.getElementById('expiry_minute')?.value; const orig = document.getElementById('original_user_expiry')?.value; if (y && mo && d && h && mi) base = new Date(y, parseInt(mo)-1, d, h, mi); else if (orig) base = new Date(orig); else base = new Date(); base.setDate(base.getDate() + days); base.setHours(base.getHours() + hours); this._setSelects(base); this.updateValidDays(); this.updateDateTime(); }, setEndOfDay() { const d = new Date(); d.setHours(23,59,0,0); this._setSelects(d); this.updateValidDays(); this.updateDateTime(); }, clearExpiry() { ['expiry_year','expiry_month','expiry_day','expiry_hour','expiry_minute'].forEach(id => { const el = document.getElementById(id); if (el) el.value = ''; }); document.getElementById('rolechangedate').value = ''; const p = document.getElementById('datetime_preview'); if (p) p.classList.add('hidden'); }, _setupExpiry() { ['expiry_year','expiry_month','expiry_day','expiry_hour','expiry_minute'].forEach(id => { const el = document.getElementById(id); if (el) el.addEventListener('change', () => this.updateDateTime()); }); const y = document.getElementById('expiry_year'), m = document.getElementById('expiry_month'); if (y) y.addEventListener('change', () => this.updateValidDays()); if (m) m.addEventListener('change', () => this.updateValidDays()); // Setup quick action buttons document.querySelectorAll('[data-expiry-action]').forEach(btn => { btn.addEventListener('click', () => { const action = btn.dataset.expiryAction; if (action === 'set') { const days = parseInt(btn.dataset.days) || 0; const hours = parseInt(btn.dataset.hours) || 0; this.setExpiry(days, hours); } else if (action === 'end-of-day') { this.setEndOfDay(); } else if (action === 'clear') { this.clearExpiry(); } }); }); }, _updatePreview() { const hidden = document.getElementById('rolechangedate'); const preview = document.getElementById('datetime_preview'); const display = document.getElementById('datetime_display'); if (!hidden?.value || !preview || !display) { if (preview) preview.classList.add('hidden'); return; } const d = new Date(hidden.value); display.textContent = d.toLocaleDateString('en-US', { weekday:'short', year:'numeric', month:'long', day:'numeric', hour:'2-digit', minute:'2-digit' }); preview.classList.remove('hidden'); const diff = d - new Date(); display.classList.remove('text-red-600','dark:text-red-400','text-yellow-600','dark:text-yellow-400','text-blue-600','dark:text-blue-400'); if (diff < 0) display.classList.add('text-red-600','dark:text-red-400'); else if (diff < 7*24*60*60*1000) display.classList.add('text-yellow-600','dark:text-yellow-400'); else display.classList.add('text-blue-600','dark:text-blue-400'); } })); // Admin deleted users Alpine.data('adminDeletedUsers', () => ({ allChecked: false, toggleAll() { this.$el.querySelectorAll('.user-checkbox').forEach(cb => { cb.checked = this.allChecked; }); }, onCheckboxChange() { const all = this.$el.querySelectorAll('.user-checkbox'); const checked = this.$el.querySelectorAll('.user-checkbox:checked'); this.allChecked = all.length > 0 && checked.length === all.length; }, submitBulkAction(e) { e.preventDefault(); const action = this.$el.querySelector('#bulkAction')?.value; const checked = this.$el.querySelectorAll('.user-checkbox:checked'); const errEl = document.getElementById('validationError'); const errMsg = document.getElementById('validationErrorMessage'); if (errEl) errEl.classList.add('hidden'); if (!action) { if (errEl && errMsg) { errMsg.textContent = 'Please select an action.'; errEl.classList.remove('hidden'); } return; } if (checked.length === 0) { if (errEl && errMsg) { errMsg.textContent = 'Please select at least one user.'; errEl.classList.remove('hidden'); } return; } const text = action === 'restore' ? 'restore' : 'permanently delete'; const type = action === 'restore' ? 'success' : 'danger'; const form = this.$el.querySelector('#bulkActionForm'); showConfirm({ title: action === 'restore' ? 'Restore Users' : 'Delete Users', message: 'Are you sure you want to ' + text + ' ' + checked.length + ' user(s)?', type: type, confirmText: action === 'restore' ? 'Restore' : 'Delete', onConfirm: function() { if (form) form.submit(); } }); }, restoreUser(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) { form.action = window.location.origin + '/admin/deleted-users/restore/' + userId; form.submit(); } }}); }, deleteUser(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) { form.action = window.location.origin + '/admin/deleted-users/permanent-delete/' + userId; form.submit(); } }}); } })); // Invitations select all Alpine.data('adminInvitations', () => ({ allChecked: false, toggleAll() { this.$el.querySelectorAll('.invitation-checkbox').forEach(cb => { cb.checked = this.allChecked; }); }, onCheckboxChange() { const all = this.$el.querySelectorAll('.invitation-checkbox'); const checked = this.$el.querySelectorAll('.invitation-checkbox:checked'); this.allChecked = all.length > 0 && checked.length === all.length; } })); // Regex form validation Alpine.data('adminRegexForm', () => ({ validate(e) { const inputs = this.$el.querySelectorAll('input[name*="regex"], textarea[name*="regex"]'); let valid = true; inputs.forEach(input => { const val = input.value.trim(); if (val && input.hasAttribute('required')) { const delims = ['/','/','#','~','%','@','!']; if (delims.includes(val[0])) { let found = false; for (let i = 1; i < val.length; i++) { if (val[i] === val[0]) { found = true; break; } } if (!found) { valid = false; input.classList.add('border-red-500'); } else input.classList.remove('border-red-500'); } } }); if (!valid) { e.preventDefault(); showToast('Please fix regex validation errors', 'error'); } } })); // Tmux crap types toggle Alpine.data('tmuxEdit', () => ({ customCrap: false, init() { const checked = document.querySelector('input[name="fix_crap_opt"]:checked'); if (checked) this.customCrap = checked.value === 'Custom'; }, setCrapOpt(value) { this.customCrap = value === 'Custom'; } })); // Image fallbacks (global) Alpine.data('imageFallback', () => ({ onError(e) { const fallback = e.target.getAttribute('data-fallback-src'); if (fallback && e.target.src !== fallback) e.target.src = fallback; } })); // Select redirect Alpine.data('selectRedirect', () => ({ onChange(e) { const url = e.target.value; if (url && url !== '#') window.location.href = url; } })); // Confirm delete (link or form) Alpine.data('confirmAction', () => ({ confirmDelete(e, message) { e.preventDefault(); e.stopPropagation(); const el = e.currentTarget; const form = el.closest('form'); showConfirm({ message: message || 'Are you sure you want to delete this item?', type: 'danger', confirmText: 'Delete', onConfirm: function() { if (form) form.submit(); else if (el.href) window.location.href = el.href; } }); } })); // Download NZB toast Alpine.data('downloadNzb', () => ({ onClick() { showToast('Downloading NZB...', 'success'); } })); // Logout Alpine.data('logout', () => ({ submit(e) { e.preventDefault(); const form = document.getElementById('logout-form') || document.getElementById('sidebar-logout-form'); if (form) form.submit(); } })); // Promotion toggle/delete Alpine.data('promotionAction', () => ({ togglePromotion(e, name, isActive, href) { e.preventDefault(); 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 "' + name + '"?', type: isActive ? 'warning' : 'success', confirmText: action.charAt(0).toUpperCase() + action.slice(1), onConfirm: function() { window.location.href = href; } }); }, deletePromotion(e, name) { e.preventDefault(); const form = e.currentTarget.closest('form'); showConfirm({ title: 'Delete Promotion', message: 'Are you sure you want to delete the promotion "' + name + '"?', details: 'This action cannot be undone.', type: 'danger', confirmText: 'Delete', onConfirm: function() { if (form) form.submit(); } }); } })); // Binary blacklist Alpine.data('binaryBlacklist', () => ({ deleteBlacklist(e, id) { e.preventDefault(); if (confirm('Are you sure? This will delete the blacklist from this list.')) { if (typeof ajax_binaryblacklist_delete === 'function') ajax_binaryblacklist_delete(id); } } })); // Regex delete Alpine.data('regexDelete', () => ({ deleteRegex(e, id, deleteUrl) { e.preventDefault(); if (confirm('Are you sure you want to delete this regex? This action cannot be undone.')) { const csrf = document.querySelector('meta[name="csrf-token"]')?.content; fetch(deleteUrl + '?id=' + id, { method: 'GET', headers: { 'X-Requested-With': 'XMLHttpRequest', 'X-CSRF-TOKEN': csrf } }) .then(r => r.json()) .then(d => { if (d.success) { const row = document.getElementById('row-' + id); if (row) { row.style.transition = 'opacity 0.3s'; row.style.opacity = '0'; setTimeout(() => row.remove(), 300); } showToast('Regex deleted successfully', 'success'); } else showToast('Error deleting regex', 'error'); }).catch(() => showToast('Error deleting regex', 'error')); } } })); // My Movies confirm Alpine.data('myMovies', () => ({ confirmRemove(e) { if (!confirm('Are you sure you want to remove this movie from your watchlist?')) e.preventDefault(); } })); /** * Document-level delegation for admin and global UI handlers. * Replaces event-bridge.js; keeps delegation for elements that may not have x-data. */ (function() { // --- Click delegation --- document.addEventListener('click', function(e) { // Admin menu submenu toggle var menuToggle = e.target.closest('[data-toggle-submenu]'); if (menuToggle) { var menuId = menuToggle.getAttribute('data-toggle-submenu'); if (menuId) { var submenu = document.getElementById(menuId); var icon = document.getElementById(menuId + '-icon'); if (submenu) submenu.classList.toggle('hidden'); if (icon) icon.classList.toggle('rotate-180'); } return; } // Admin groups data-action delegation var actionTarget = e.target.closest('[data-action]'); if (actionTarget) { var action = actionTarget.dataset.action; var groupId = actionTarget.dataset.groupId; var status = actionTarget.dataset.status; switch (action) { case 'show-reset-modal': if (typeof showResetAllModal === 'function') showResetAllModal(); break; case 'hide-reset-modal': if (typeof hideResetAllModal === 'function') hideResetAllModal(); break; case 'show-purge-modal': if (typeof showPurgeAllModal === 'function') showPurgeAllModal(); break; case 'hide-purge-modal': if (typeof hidePurgeAllModal === 'function') hidePurgeAllModal(); break; case 'show-reset-selected-modal': if (typeof showResetSelectedModal === 'function') showResetSelectedModal(); break; case 'hide-reset-selected-modal': if (typeof hideResetSelectedModal === 'function') hideResetSelectedModal(); break; case 'select-all-groups': if (typeof toggleSelectAllGroups === 'function') toggleSelectAllGroups(actionTarget); break; case 'toggle-group-status': if (typeof ajax_group_status === 'function') ajax_group_status(groupId, status); break; case 'toggle-backfill': if (typeof ajax_backfill_status === 'function') ajax_backfill_status(groupId, status); break; case 'reset-group': if (typeof ajax_group_reset === 'function') ajax_group_reset(groupId); break; case 'delete-group': if (typeof confirmGroupDelete === 'function') confirmGroupDelete(groupId); break; case 'purge-group': if (typeof confirmGroupPurge === 'function') confirmGroupPurge(groupId); break; case 'reset-all': if (typeof ajax_group_reset_all === 'function') ajax_group_reset_all(); break; case 'purge-all': if (typeof ajax_group_purge_all === 'function') ajax_group_purge_all(); break; case 'reset-selected': if (typeof ajax_group_reset_selected === 'function') ajax_group_reset_selected(); break; } return; } // Verify user modal var verifyBtn = e.target.closest('[data-show-verify-modal]'); if (verifyBtn) { var form = verifyBtn.closest('form'); if (form && typeof showVerifyModal === 'function') showVerifyModal(e, form); return; } if (e.target.closest('[data-close-verify-modal]')) { e.preventDefault(); if (typeof hideVerifyModal === 'function') hideVerifyModal(); return; } if (e.target.closest('[data-submit-verify-form]')) { e.preventDefault(); if (typeof submitVerifyForm === 'function') submitVerifyForm(); return; } // Restore/delete user buttons var restoreBtn = e.target.closest('.restore-user-btn'); if (restoreBtn) { e.preventDefault(); var userId = restoreBtn.getAttribute('data-user-id'); var username = restoreBtn.getAttribute('data-username') || 'this user'; showConfirm({ title: 'Restore User', message: 'Are you sure you want to restore user "' + username + '"?', type: 'success', confirmText: 'Restore', onConfirm: function() { var form = document.getElementById('individualActionForm'); if (form) { form.action = '/admin/deleted-users/restore/' + userId; form.method = 'POST'; form.submit(); } }}); return; } var deleteUserBtn = e.target.closest('.delete-user-btn'); if (deleteUserBtn) { e.preventDefault(); var userId = deleteUserBtn.getAttribute('data-user-id'); var 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.', type: 'danger', confirmText: 'Delete Permanently', onConfirm: function() { var form = document.getElementById('individualActionForm'); if (form) { form.action = '/admin/deleted-users/permanent-delete/' + userId; form.method = 'POST'; form.submit(); } }}); return; } // Promotion toggle/delete var promoToggle = e.target.closest('.promotion-toggle-btn'); if (promoToggle) { e.preventDefault(); var name = promoToggle.getAttribute('data-promotion-name'); var active = promoToggle.getAttribute('data-promotion-active') === '1'; var action = active ? 'deactivate' : 'activate'; showConfirm({ title: action.charAt(0).toUpperCase() + action.slice(1) + ' Promotion', message: 'Are you sure you want to ' + action + ' the promotion "' + name + '"?', type: active ? 'warning' : 'success', confirmText: action.charAt(0).toUpperCase() + action.slice(1), onConfirm: function() { window.location.href = promoToggle.href; } }); return; } var promoDelete = e.target.closest('.promotion-delete-btn'); if (promoDelete) { e.preventDefault(); var name = promoDelete.getAttribute('data-promotion-name'); var form = promoDelete.closest('form'); showConfirm({ title: 'Delete Promotion', message: 'Are you sure you want to delete the promotion "' + name + '"?', details: 'This action cannot be undone.', type: 'danger', confirmText: 'Delete', onConfirm: function() { if (form) form.submit(); } }); return; } // Content toggle/delete var contentToggle = e.target.closest('.content-toggle-status'); if (contentToggle) { e.preventDefault(); var id = contentToggle.getAttribute('data-content-id'); var csrf = document.querySelector('meta[name="csrf-token"]')?.content; if (!id || !csrf) return; contentToggle.disabled = true; contentToggle.style.opacity = '0.6'; fetch('/admin/content-toggle-status', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrf, 'X-Requested-With': 'XMLHttpRequest' }, body: JSON.stringify({ id: id }) }).then(function(r) { return r.json(); }).then(function(data) { if (data.success) { var row = contentToggle.closest('tr'), sc = row.cells[5], ns = data.status; if (ns === 1) { sc.innerHTML = 'Enabled'; contentToggle.className = 'content-toggle-status text-green-600 dark:text-green-400 hover:text-green-900 dark:hover:text-green-300'; contentToggle.innerHTML = ''; } else { sc.innerHTML = 'Disabled'; contentToggle.className = 'content-toggle-status text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-300'; contentToggle.innerHTML = ''; } contentToggle.setAttribute('data-current-status', ns); contentToggle.title = ns === 1 ? 'Disable' : 'Enable'; showToast(data.message, 'success'); } else showToast(data.message || 'Failed', 'error'); contentToggle.disabled = false; contentToggle.style.opacity = '1'; }).catch(function() { showToast('Error', 'error'); contentToggle.disabled = false; contentToggle.style.opacity = '1'; }); return; } var contentDelete = e.target.closest('.content-delete'); if (contentDelete) { e.preventDefault(); var id = contentDelete.getAttribute('data-content-id'); var title = contentDelete.getAttribute('data-content-title'); var csrf = document.querySelector('meta[name="csrf-token"]')?.content; if (!id || !csrf) return; showConfirm({ title: 'Delete Content', message: 'Are you sure you want to delete "' + title + '"?', details: 'This action cannot be undone.', type: 'danger', confirmText: 'Delete', onConfirm: function() { contentDelete.disabled = true; contentDelete.style.opacity = '0.6'; fetch('/admin/content-delete', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrf, 'X-Requested-With': 'XMLHttpRequest' }, body: JSON.stringify({ id: id }) }) .then(function(r) { return r.json(); }).then(function(data) { if (data.success) { var row = contentDelete.closest('tr'); if (row) { row.style.transition = 'opacity 0.3s'; row.style.opacity = '0'; setTimeout(function() { row.remove(); if (document.querySelector('tbody')?.children.length === 0) location.reload(); }, 300); } showToast(data.message, 'success'); } else { showToast(data.message || 'Failed', 'error'); contentDelete.disabled = false; contentDelete.style.opacity = '1'; } }).catch(function() { showToast('Error', 'error'); contentDelete.disabled = false; contentDelete.style.opacity = '1'; }); } }); return; } // Regex delete var regexDel = e.target.closest('[data-delete-regex]'); if (regexDel) { e.preventDefault(); var id = regexDel.getAttribute('data-delete-regex'); var url = regexDel.getAttribute('data-delete-url'); if (confirm('Are you sure you want to delete this regex?')) { var csrf = document.querySelector('meta[name="csrf-token"]')?.content; fetch(url + '?id=' + id, { method: 'GET', headers: { 'X-Requested-With': 'XMLHttpRequest', 'X-CSRF-TOKEN': csrf } }) .then(function(r) { return r.json(); }).then(function(d) { if (d.success) { var row = document.getElementById('row-' + id); if (row) { row.style.transition = 'opacity 0.3s'; row.style.opacity = '0'; setTimeout(function() { row.remove(); }, 300); } showToast('Regex deleted', 'success'); } else showToast('Error', 'error'); }).catch(function() { showToast('Error', 'error'); }); } return; } // Binary blacklist delete var blDel = e.target.closest('[data-delete-blacklist]'); if (blDel) { e.preventDefault(); var id = blDel.getAttribute('data-delete-blacklist'); if (confirm('Are you sure? This will delete the blacklist.')) { if (typeof ajax_binaryblacklist_delete === 'function') ajax_binaryblacklist_delete(id); } return; } // Season tab var seasonTab = e.target.closest('.season-tab'); if (seasonTab) { e.preventDefault(); var num = seasonTab.getAttribute('data-season'); if (num && typeof switchSeason === 'function') switchSeason(num); return; } // My Movies confirm var confirmAction = e.target.closest('.confirm_action'); if (confirmAction) { if (!confirm('Are you sure you want to remove this movie from your watchlist?')) e.preventDefault(); return; } }); // --- Change event delegation --- document.addEventListener('change', function(e) { if (e.target.hasAttribute('data-redirect-on-change')) { var url = e.target.value; if (url && url !== '#') window.location.href = url; } if (e.target.classList.contains('group-checkbox')) { if (typeof updateSelectionUI === 'function') updateSelectionUI(); } }); // --- Image fallbacks --- document.querySelectorAll('img[data-fallback-src], img[data-hide-on-error]').forEach(function(img) { img.addEventListener('error', function() { var fb = this.getAttribute('data-fallback-src'); if (fb && this.src !== fb) { this.src = fb; return; } if (this.hasAttribute('data-hide-on-error')) this.style.display = 'none'; }); }); // --- Admin select-alls and bulk forms --- // Invitations select-all var selectAllInv = document.getElementById('select_all'); if (selectAllInv) { var invBoxes = document.querySelectorAll('.invitation-checkbox'); selectAllInv.addEventListener('change', function() { invBoxes.forEach(function(cb) { cb.checked = selectAllInv.checked; }); }); invBoxes.forEach(function(cb) { cb.addEventListener('change', function() { var all = Array.from(invBoxes).every(function(c) { return c.checked; }); var some = Array.from(invBoxes).some(function(c) { return c.checked; }); selectAllInv.checked = all; selectAllInv.indeterminate = some && !all; }); }); } // Deleted users select-all var selectAllUsers = document.getElementById('selectAll'); if (selectAllUsers) { var userBoxes = document.querySelectorAll('.user-checkbox'); selectAllUsers.addEventListener('change', function() { userBoxes.forEach(function(cb) { cb.checked = selectAllUsers.checked; }); }); userBoxes.forEach(function(cb) { cb.addEventListener('change', function() { var all = Array.from(userBoxes).every(function(c) { return c.checked; }); var some = Array.from(userBoxes).some(function(c) { return c.checked; }); selectAllUsers.checked = all; selectAllUsers.indeterminate = some && !all; }); }); } // Bulk action form var bulkForm = document.getElementById('bulkActionForm'); if (bulkForm) { bulkForm.addEventListener('submit', function(ev) { ev.preventDefault(); var action = document.getElementById('bulkAction')?.value; var checked = document.querySelectorAll('.user-checkbox:checked'); var errEl = document.getElementById('validationError'), errMsg = document.getElementById('validationErrorMessage'); if (errEl) errEl.classList.add('hidden'); if (!action) { if (errEl && errMsg) { errMsg.textContent = 'Please select an action.'; errEl.classList.remove('hidden'); } return; } if (!checked.length) { if (errEl && errMsg) { errMsg.textContent = 'Please select at least one user.'; errEl.classList.remove('hidden'); } return; } var text = action === 'restore' ? 'restore' : 'permanently delete'; showConfirm({ title: action === 'restore' ? 'Restore Users' : 'Delete Users', message: 'Are you sure you want to ' + text + ' ' + checked.length + ' user(s)?', type: action === 'restore' ? 'success' : 'danger', confirmText: action === 'restore' ? 'Restore' : 'Delete', onConfirm: function() { bulkForm.submit(); } }); }); } // Tmux crap types toggle var checkedRadio = document.querySelector('input[name="fix_crap_opt"]:checked'); var crapContainer = document.getElementById('crap_types_container'); if (crapContainer) { if (checkedRadio) crapContainer.style.display = checkedRadio.value === 'Custom' ? 'block' : 'none'; document.querySelectorAll('input[name="fix_crap_opt"]').forEach(function(r) { r.addEventListener('change', function() { crapContainer.style.display = this.value === 'Custom' ? 'block' : 'none'; }); }); } })();