From 0c492eb26d86bfe5005414b5c111263337df407d Mon Sep 17 00:00:00 2001 From: Nirvana Date: Tue, 23 Dec 2025 10:41:50 +0100 Subject: [PATCH] Enable provider --- resources/web/provider_enable.css | 193 ++++++++++++++++++++++ resources/web/provider_enable.js | 261 ++++++++++++++++++++++++++++++ 2 files changed, 454 insertions(+) create mode 100644 resources/web/provider_enable.css create mode 100644 resources/web/provider_enable.js diff --git a/resources/web/provider_enable.css b/resources/web/provider_enable.css new file mode 100644 index 0000000..1afdf9b --- /dev/null +++ b/resources/web/provider_enable.css @@ -0,0 +1,193 @@ +/* Provider Enable/Disable Styles */ + +/* Toggle Switch */ +.provider-toggle { + position: relative; + display: inline-block; + width: 50px; + height: 26px; + margin-left: auto; +} + +.provider-toggle input { + opacity: 0; + width: 0; + height: 0; +} + +.toggle-slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #ccc; + transition: 0.3s; + border-radius: 26px; +} + +.toggle-slider:before { + position: absolute; + content: ""; + height: 20px; + width: 20px; + left: 3px; + bottom: 3px; + background-color: white; + transition: 0.3s; + border-radius: 50%; +} + +.provider-toggle input:checked + .toggle-slider { + background-color: var(--success-color); +} + +.provider-toggle input:checked + .toggle-slider:before { + transform: translateX(24px); +} + +.provider-toggle input:disabled + .toggle-slider { + opacity: 0.5; + cursor: not-allowed; +} + +.toggle-slider.loading { + opacity: 0.6; + pointer-events: none; +} + +.toggle-slider.loading:after { + content: ""; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 16px; + height: 16px; + border: 2px solid white; + border-top-color: transparent; + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +/* Disabled Provider Card */ +.provider-card.disabled { + opacity: 0.5; + background: #f5f5f5; + border-color: #ddd; + position: relative; +} + +.provider-card.disabled::after { + content: "DISABLED"; + position: absolute; + top: 10px; + right: 10px; + background: var(--warning-color); + color: white; + padding: 4px 12px; + border-radius: 12px; + font-size: 0.75rem; + font-weight: 700; + letter-spacing: 0.5px; +} + +.provider-card.disabled:hover { + transform: none; + border-color: #ddd; +} + +.provider-card.disabled .provider-logo { + filter: grayscale(100%); + opacity: 0.6; +} + +.provider-card.disabled .provider-info h3, +.provider-card.disabled .provider-info { + color: var(--gray-color); +} + +/* Hide forms and buttons when disabled */ +.provider-card.disabled .form-group, +.provider-card.disabled .btn-group, +.provider-card.disabled .no-credentials-required, +.provider-card.disabled .status-indicator, +.provider-card.disabled .token-expiration, +.provider-card.disabled .auth-info { + display: none; +} + +/* Filter Section */ +.filter-section { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; + padding: 15px; + background: white; + border-radius: var(--border-radius); + box-shadow: var(--box-shadow); +} + +.filter-section label { + font-weight: 600; + margin-right: 10px; + color: var(--dark-color); +} + +.filter-select { + padding: 8px 35px 8px 12px; + border: 2px solid #dee2e6; + border-radius: var(--border-radius); + font-size: 0.95rem; + background: white; + cursor: pointer; + transition: var(--transition); + appearance: none; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23333' d='M6 9L1 4h10z'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 10px center; +} + +.filter-select:focus { + outline: none; + border-color: var(--primary-color); + box-shadow: 0 0 0 3px rgba(67, 97, 238, 0.2); +} + +.filter-info { + color: var(--gray-color); + font-size: 0.9rem; +} + +.filter-info strong { + color: var(--primary-color); +} + +/* Responsive adjustments */ +@media (max-width: 768px) { + .filter-section { + flex-direction: column; + gap: 10px; + align-items: flex-start; + } + + .filter-select { + width: 100%; + } + + .provider-toggle { + width: 44px; + height: 24px; + } + + .toggle-slider:before { + height: 18px; + width: 18px; + } + + .provider-toggle input:checked + .toggle-slider:before { + transform: translateX(20px); + } +} \ No newline at end of file diff --git a/resources/web/provider_enable.js b/resources/web/provider_enable.js new file mode 100644 index 0000000..af0a9b7 --- /dev/null +++ b/resources/web/provider_enable.js @@ -0,0 +1,261 @@ +// Provider Enable/Disable Manager +class ProviderEnableManager { + constructor() { + this.enabledStatus = {}; + this.currentFilter = 'all'; // 'all', 'enabled', 'disabled' + } + + async init(providers) { + this.providers = providers; + await this.loadEnabledStatus(); + } + + async loadEnabledStatus() { + try { + const response = await fetch(`${API_BASE}/api/providers/enabled`); + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } + const data = await response.json(); + this.enabledStatus = data.providers || {}; + console.log('Loaded enabled status:', this.enabledStatus); + } catch (error) { + console.error('Error loading enabled status:', error); + // Initialize with defaults (all enabled) + this.providers.forEach(p => { + this.enabledStatus[p.name] = { + enabled: true, + source: 'default', + can_modify: true + }; + }); + } + } + + isEnabled(providerName) { + return this.enabledStatus[providerName]?.enabled !== false; + } + + canModify(providerName) { + return this.enabledStatus[providerName]?.can_modify !== false; + } + + getSource(providerName) { + return this.enabledStatus[providerName]?.source || 'default'; + } + + async toggleProvider(providerName, newState) { + const statusInfo = this.enabledStatus[providerName]; + + if (!statusInfo?.can_modify) { + showAlert('error', `Cannot modify ${providerName} - controlled by ${statusInfo?.source || 'external source'}`); + return false; + } + + try { + const response = await fetch(`${API_BASE}/api/providers/${providerName}/enabled`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ enabled: newState }) + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || 'Failed to update status'); + } + + const result = await response.json(); + + // Update local status + this.enabledStatus[providerName] = { + enabled: newState, + source: result.source || 'file', + can_modify: true + }; + + return true; + } catch (error) { + console.error('Error toggling provider:', error); + showAlert('error', `Failed to ${newState ? 'enable' : 'disable'} ${providerName}: ${error.message}`); + return false; + } + } + + setFilter(filter) { + this.currentFilter = filter; + this.applyFilter(); + } + + applyFilter() { + const cards = document.querySelectorAll('.provider-card'); + let visibleCount = 0; + + cards.forEach(card => { + const providerName = card.dataset.provider; + const isEnabled = this.isEnabled(providerName); + + let shouldShow = false; + switch (this.currentFilter) { + case 'all': + shouldShow = true; + break; + case 'enabled': + shouldShow = isEnabled; + break; + case 'disabled': + shouldShow = !isEnabled; + break; + } + + if (shouldShow) { + card.style.display = ''; + visibleCount++; + } else { + card.style.display = 'none'; + } + }); + + // Update filter info + this.updateFilterInfo(visibleCount); + } + + updateFilterInfo(visibleCount) { + const filterInfo = document.querySelector('.filter-info'); + if (filterInfo) { + const totalEnabled = Object.values(this.enabledStatus).filter(s => s.enabled).length; + const totalDisabled = Object.values(this.enabledStatus).filter(s => !s.enabled).length; + + let infoText = ''; + switch (this.currentFilter) { + case 'all': + infoText = `Showing all ${visibleCount} providers (${totalEnabled} enabled, ${totalDisabled} disabled)`; + break; + case 'enabled': + infoText = `Showing ${visibleCount} enabled providers`; + break; + case 'disabled': + infoText = `Showing ${visibleCount} disabled providers`; + break; + } + filterInfo.innerHTML = infoText; + } + } + + shouldShowInTab(providerName, tabId) { + // In credentials tab, show all (enabled and disabled) + if (tabId === 'credentials') { + return true; + } + + // In other tabs (proxy, epg-mapping), only show enabled + return this.isEnabled(providerName); + } + + getFilteredProviders(tabId) { + if (!this.providers) return []; + + return this.providers.filter(provider => + this.shouldShowInTab(provider.name, tabId) + ); + } +} + +// Create global instance +window.providerEnableManager = new ProviderEnableManager(); + +// Add filter UI to credentials tab +function addFilterUI() { + const credentialsTab = document.getElementById('tab-credentials'); + const cardBody = credentialsTab.querySelector('.card-body'); + + // Check if filter already exists + if (document.querySelector('.filter-section')) { + return; + } + + const filterHTML = ` +
+
+ + +
+
+ Loading... +
+
+ `; + + // Insert after the alert + const alert = cardBody.querySelector('.alert'); + if (alert) { + alert.insertAdjacentHTML('afterend', filterHTML); + } else { + cardBody.insertAdjacentHTML('afterbegin', filterHTML); + } + + // Add event listener + const filterSelect = document.getElementById('provider-filter'); + filterSelect.addEventListener('change', (e) => { + window.providerEnableManager.setFilter(e.target.value); + }); +} + +// Create toggle switch HTML +function createToggleSwitch(providerName, isEnabled, canModify) { + const toggleId = `toggle-${providerName}`; + return ` + + `; +} + +// Handle toggle change +async function handleProviderToggle(providerName, checkbox) { + const slider = checkbox.nextElementSibling; + const card = checkbox.closest('.provider-card'); + + // Add loading state + slider.classList.add('loading'); + checkbox.disabled = true; + + const newState = checkbox.checked; + const success = await window.providerEnableManager.toggleProvider(providerName, newState); + + if (success) { + // Update card appearance + if (newState) { + card.classList.remove('disabled'); + showAlert('success', `${providerName} enabled`); + } else { + card.classList.add('disabled'); + showAlert('success', `${providerName} disabled`); + } + + // Update filter info + window.providerEnableManager.applyFilter(); + } else { + // Revert checkbox on failure + checkbox.checked = !newState; + } + + // Remove loading state + slider.classList.remove('loading'); + checkbox.disabled = !window.providerEnableManager.canModify(providerName); +} + +// Export for global use +window.handleProviderToggle = handleProviderToggle; +window.addFilterUI = addFilterUI; +window.createToggleSwitch = createToggleSwitch; \ No newline at end of file