Initial commit: DashCaddy v1.0
Full codebase including API server (32 modules + routes), dashboard frontend, DashCA certificate distribution, installer script, and deployment skills.
This commit is contained in:
431
status/js/smart-arr-connect.js
Normal file
431
status/js/smart-arr-connect.js
Normal file
@@ -0,0 +1,431 @@
|
||||
// ========== SMART ARR CONNECT ==========
|
||||
(function() {
|
||||
injectModal('arr-setup-modal', `<div id="arr-setup-modal" class="weather-modal">
|
||||
<div class="weather-modal-content" style="min-width: 600px; max-width: 720px;">
|
||||
<h3>🎬 Smart Arr Connect</h3>
|
||||
<p style="color: var(--muted); margin: 0 0 16px; font-size: 0.9rem;">
|
||||
Auto-discover and connect your entire media stack.
|
||||
</p>
|
||||
|
||||
<!-- Phase 1: Detection -->
|
||||
<div id="smart-phase-detect">
|
||||
<div style="text-align: center; padding: 20px;">
|
||||
<span class="brand-spinner" style="margin-bottom: 12px; display: inline-block;"></span>
|
||||
<div style="color: var(--muted); font-size: 0.9rem;">Scanning for services...</div>
|
||||
</div>
|
||||
<div id="smart-detect-results" style="display: none;"></div>
|
||||
</div>
|
||||
|
||||
<!-- Phase 2: Credential Input (only for services needing keys) -->
|
||||
<div id="smart-phase-credentials" style="display: none;">
|
||||
<h4 class="heading-accent-section">Enter Missing API Keys</h4>
|
||||
<div id="smart-credential-inputs"></div>
|
||||
|
||||
<!-- Connection Options -->
|
||||
<div style="margin-top: 16px; padding: 12px; background: var(--card-base); border-radius: 8px; border: 1px solid var(--border);">
|
||||
<h4 style="margin: 0 0 10px; font-size: 0.85rem; color: var(--accent);">Connection Options</h4>
|
||||
<label class="option-label">
|
||||
<input type="checkbox" id="smart-opt-seerr" checked class="checkbox-sm" />
|
||||
Configure Seerr with Radarr + Sonarr
|
||||
</label>
|
||||
<label class="option-label">
|
||||
<input type="checkbox" id="smart-opt-plex" checked class="checkbox-sm" />
|
||||
Connect Plex to Seerr
|
||||
</label>
|
||||
<label class="option-label">
|
||||
<input type="checkbox" id="smart-opt-prowlarr" checked class="checkbox-sm" />
|
||||
Connect Prowlarr to Radarr + Sonarr
|
||||
</label>
|
||||
<label style="display: flex; align-items: center; gap: 8px; cursor: pointer; font-size: 0.85rem;">
|
||||
<input type="checkbox" id="smart-opt-save" checked class="checkbox-sm" />
|
||||
Save API keys for one-click reconnect
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button id="smart-connect-btn" style="width: 100%; margin-top: 16px; padding: 12px; background: linear-gradient(135deg, #e74c3c 0%, #9b59b6 100%); border: none; color: white; font-weight: 600; font-size: 1rem; border-radius: 8px; cursor: pointer;">
|
||||
Smart Connect
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Phase 3: Connection Progress -->
|
||||
<div id="smart-phase-progress" style="display: none;">
|
||||
<h4 class="heading-accent-section">Connecting Everything...</h4>
|
||||
<div id="smart-progress-steps" style="display: flex; flex-direction: column; gap: 6px;"></div>
|
||||
</div>
|
||||
|
||||
<!-- Phase 4: Results -->
|
||||
<div id="smart-phase-results" style="display: none;">
|
||||
<div id="smart-results-content"></div>
|
||||
<div id="smart-plex-libraries" style="display: none; margin-top: 12px;"></div>
|
||||
<div style="display: flex; gap: 8px; margin-top: 16px;">
|
||||
<button id="smart-retry-btn" style="display: none; flex: 1; padding: 10px; background: #f39c12; border: none; color: white; font-weight: 500; border-radius: 8px; cursor: pointer;">
|
||||
Retry Failed Steps
|
||||
</button>
|
||||
<a id="smart-open-seerr" href="#" target="_blank" rel="noopener noreferrer"
|
||||
style="flex: 1; padding: 10px; background: var(--accent); color: white; border: none; border-radius: 8px; cursor: pointer; font-weight: 500; text-decoration: none; text-align: center;">
|
||||
Open Seerr
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Help Text -->
|
||||
<div style="font-size: 0.75rem; color: var(--muted); margin-top: 12px; padding: 10px; background: color-mix(in srgb, var(--accent) 5%, transparent); border-radius: 6px;">
|
||||
<strong>Where to find API keys:</strong><br>
|
||||
Radarr/Sonarr/Prowlarr: Settings -> General -> Security -> API Key
|
||||
</div>
|
||||
|
||||
<!-- Close Button -->
|
||||
<div class="weather-modal-buttons modal-footer-bar">
|
||||
<button id="arr-setup-cancel">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>`);
|
||||
|
||||
const modal = document.getElementById('arr-setup-modal');
|
||||
const openBtn = document.getElementById('arr-setup-btn');
|
||||
const cancelBtn = document.getElementById('arr-setup-cancel');
|
||||
const connectBtn = document.getElementById('smart-connect-btn');
|
||||
|
||||
// Phase elements
|
||||
const phaseDetect = document.getElementById('smart-phase-detect');
|
||||
const phaseCredentials = document.getElementById('smart-phase-credentials');
|
||||
const phaseProgress = document.getElementById('smart-phase-progress');
|
||||
const phaseResults = document.getElementById('smart-phase-results');
|
||||
|
||||
const detectResults = document.getElementById('smart-detect-results');
|
||||
const credentialInputs = document.getElementById('smart-credential-inputs');
|
||||
const progressSteps = document.getElementById('smart-progress-steps');
|
||||
const resultsContent = document.getElementById('smart-results-content');
|
||||
const plexLibraries = document.getElementById('smart-plex-libraries');
|
||||
const retryBtn = document.getElementById('smart-retry-btn');
|
||||
|
||||
let detectedData = null; // Store detection results for smart-connect
|
||||
|
||||
const serviceIcons = { plex: '🎬', radarr: '🎬', sonarr: '📺', prowlarr: '🔍', seerr: '📋' };
|
||||
const serviceLabels = { plex: 'Plex', radarr: 'Radarr (Movies)', sonarr: 'Sonarr (TV)', prowlarr: 'Prowlarr (Indexers)', seerr: 'Seerr' };
|
||||
|
||||
function showPhase(phase) {
|
||||
phaseDetect.style.display = phase === 'detect' ? 'block' : 'none';
|
||||
phaseCredentials.style.display = phase === 'credentials' ? 'block' : 'none';
|
||||
phaseProgress.style.display = phase === 'progress' ? 'block' : 'none';
|
||||
phaseResults.style.display = phase === 'results' ? 'block' : 'none';
|
||||
}
|
||||
|
||||
function statusBadge(status) {
|
||||
const colors = {
|
||||
connected: { bg: 'var(--ok-fg)', icon: '✓', text: 'Connected' },
|
||||
needs_key: { bg: '#f39c12', icon: '🔑', text: 'Needs API Key' },
|
||||
not_found: { bg: 'var(--muted)', icon: '—', text: 'Not Found' },
|
||||
error: { bg: 'var(--bad-fg)', icon: '✗', text: 'Error' }
|
||||
};
|
||||
const c = colors[status] || colors.not_found;
|
||||
return `<span style="display: inline-flex; align-items: center; gap: 4px; padding: 2px 8px; border-radius: 4px; font-size: 0.75rem; background: color-mix(in srgb, ${c.bg} 20%, transparent); color: ${c.bg};">${c.icon} ${c.text}</span>`;
|
||||
}
|
||||
|
||||
// Phase 1: Smart Detection
|
||||
async function smartDetect() {
|
||||
showPhase('detect');
|
||||
detectResults.style.display = 'none';
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/v1/arr/smart-detect');
|
||||
detectedData = await response.json();
|
||||
|
||||
if (!detectedData.success) {
|
||||
detectResults.innerHTML = `<div style="padding: 12px; color: var(--bad-fg);">Detection failed: ${escapeHtml(detectedData.error)}</div>`;
|
||||
detectResults.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
|
||||
// Render detection results
|
||||
let html = '<div style="display: flex; flex-direction: column; gap: 8px;">';
|
||||
|
||||
for (const [svc, info] of Object.entries(detectedData.services)) {
|
||||
const icon = serviceIcons[svc] || '📦';
|
||||
const label = serviceLabels[svc] || svc;
|
||||
const source = info.source ? `<span style="font-size: 0.7rem; color: var(--muted); padding: 1px 6px; background: var(--card-bg); border-radius: 3px;">${escapeHtml(info.source)}</span>` : '';
|
||||
const version = info.version ? `<span style="font-size: 0.7rem; color: var(--muted);">v${escapeHtml(info.version)}</span>` : '';
|
||||
const keySaved = (info.hasApiKey || info.hasToken) && info.status === 'connected'
|
||||
? '<span style="font-size: 0.7rem; color: var(--ok-fg);">Key saved</span>' : '';
|
||||
|
||||
html += `<div style="display: flex; align-items: center; gap: 10px; padding: 10px 12px; background: var(--card-base); border-radius: 8px; border: 1px solid var(--border);">
|
||||
<span style="font-size: 1.1rem;">${icon}</span>
|
||||
<div style="flex: 1;">
|
||||
<div style="font-weight: 500; font-size: 0.9rem;">${label}</div>
|
||||
<div style="display: flex; gap: 6px; align-items: center; flex-wrap: wrap;">
|
||||
${source} ${version} ${keySaved}
|
||||
</div>
|
||||
</div>
|
||||
${statusBadge(info.status)}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
html += '</div>';
|
||||
|
||||
// Summary
|
||||
const s = detectedData.summary;
|
||||
html += `<div style="margin-top: 12px; padding: 10px; background: color-mix(in srgb, var(--accent) 8%, transparent); border-radius: 8px; font-size: 0.85rem;">
|
||||
${escapeHtml(String(s.fullyConnected))}/${escapeHtml(String(s.totalDetected + (5 - s.totalDetected)))} services detected ·
|
||||
${escapeHtml(String(s.fullyConnected))} connected${s.needsApiKey > 0 ? ` · <strong>${escapeHtml(String(s.needsApiKey))} needs API key</strong>` : ''}
|
||||
</div>`;
|
||||
|
||||
detectResults.innerHTML = html;
|
||||
detectResults.style.display = 'block';
|
||||
|
||||
// Build credential inputs for services that need keys
|
||||
buildCredentialInputs(detectedData);
|
||||
|
||||
// Auto-advance after a short delay
|
||||
setTimeout(() => {
|
||||
showPhase('credentials');
|
||||
}, 800);
|
||||
|
||||
} catch (e) {
|
||||
detectResults.innerHTML = `<div style="padding: 12px; color: var(--bad-fg);">Error: ${escapeHtml(e.message)}</div>`;
|
||||
detectResults.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
// Build credential input fields
|
||||
function buildCredentialInputs(data) {
|
||||
let html = '';
|
||||
const services = data.services;
|
||||
const arrServices = ['radarr', 'sonarr', 'prowlarr'];
|
||||
|
||||
for (const svc of arrServices) {
|
||||
const info = services[svc];
|
||||
if (!info || info.status === 'not_found' && !info.url) continue;
|
||||
|
||||
const icon = serviceIcons[svc];
|
||||
const label = serviceLabels[svc];
|
||||
const isConnected = info.status === 'connected';
|
||||
const borderColor = isConnected ? 'var(--ok-fg)' : 'var(--border)';
|
||||
|
||||
html += `<div style="margin-bottom: 10px; padding: 12px; background: var(--card-base); border-radius: 8px; border: 1px solid ${borderColor};">
|
||||
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px;">
|
||||
<span style="font-size: 1.1rem;">${icon}</span>
|
||||
<span style="font-weight: 500;">${label}</span>
|
||||
<span id="smart-${svc}-status" style="margin-left: auto; font-size: 0.75rem;">
|
||||
${isConnected ? '<span style="color: var(--ok-fg);">✓ Connected</span>' : ''}
|
||||
</span>
|
||||
</div>
|
||||
<div style="display: grid; grid-template-columns: 2fr 1fr; gap: 8px;">
|
||||
<div>
|
||||
<label style="font-size: 0.75rem; color: var(--muted);">URL:</label>
|
||||
<input type="text" id="smart-${svc}-url" value="${escapeHtml(info.url || '')}" placeholder="https://seedbox.com/${svc}/"
|
||||
style="width: 100%; font-size: 0.85rem; padding: 6px 8px; background: var(--card-bg); color: var(--fg); border: 1px solid var(--border); border-radius: 4px;" />
|
||||
</div>
|
||||
<div>
|
||||
<label style="font-size: 0.75rem; color: var(--muted);">API Key:</label>
|
||||
<input type="password" id="smart-${svc}-key" placeholder="${isConnected ? '(saved)' : 'Paste API key'}"
|
||||
style="width: 100%; font-size: 0.85rem; padding: 6px 8px; background: var(--card-bg); color: var(--fg); border: 1px solid var(--border); border-radius: 4px;" />
|
||||
</div>
|
||||
</div>
|
||||
<button onclick="smartTestConnection('${svc}')" style="margin-top: 8px; padding: 4px 12px; font-size: 0.75rem; cursor: pointer;">Test</button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// Plex status (non-editable, just shows status)
|
||||
const plex = services.plex;
|
||||
if (plex) {
|
||||
const plexConnected = plex.status === 'connected';
|
||||
html += `<div style="padding: 10px 12px; background: var(--card-base); border-radius: 8px; border: 1px solid ${plexConnected ? 'var(--ok-fg)' : 'var(--border)'}; margin-bottom: 10px;">
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<span style="font-size: 1.1rem;">🎬</span>
|
||||
<span style="font-weight: 500;">Plex</span>
|
||||
${statusBadge(plex.status)}
|
||||
<span style="margin-left: auto; font-size: 0.75rem; color: var(--muted);">${escapeHtml(plex.source || '')}</span>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// Seerr status
|
||||
const seerr = services.seerr;
|
||||
if (seerr) {
|
||||
const seerrOk = seerr.status === 'connected';
|
||||
let configuredHtml = '';
|
||||
if (seerr.configuredServices) {
|
||||
const cs = seerr.configuredServices;
|
||||
configuredHtml = `<div style="font-size: 0.75rem; color: var(--muted); margin-top: 4px;">
|
||||
Configured: ${cs.radarr ? '✓ Radarr' : '✗ Radarr'} ·
|
||||
${cs.sonarr ? '✓ Sonarr' : '✗ Sonarr'} ·
|
||||
${cs.plex ? '✓ Plex' : '✗ Plex'}
|
||||
</div>`;
|
||||
}
|
||||
html += `<div style="padding: 10px 12px; background: var(--card-base); border-radius: 8px; border: 1px solid ${seerrOk ? 'var(--ok-fg)' : 'var(--border)'};">
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<span style="font-size: 1.1rem;">📋</span>
|
||||
<span style="font-weight: 500;">Seerr</span>
|
||||
${statusBadge(seerr.status)}
|
||||
</div>
|
||||
${configuredHtml}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
credentialInputs.innerHTML = html;
|
||||
}
|
||||
|
||||
// Test connection (global for onclick)
|
||||
window.smartTestConnection = async function(service) {
|
||||
const urlInput = document.getElementById(`smart-${service}-url`);
|
||||
const keyInput = document.getElementById(`smart-${service}-key`);
|
||||
const statusSpan = document.getElementById(`smart-${service}-status`);
|
||||
|
||||
const url = urlInput?.value.trim();
|
||||
const apiKey = keyInput?.value.trim();
|
||||
|
||||
if (!url || !apiKey) {
|
||||
statusSpan.innerHTML = '<span style="color: var(--bad-fg);">Enter URL and API key</span>';
|
||||
return;
|
||||
}
|
||||
|
||||
statusSpan.innerHTML = '<span class="brand-spinner"></span>';
|
||||
|
||||
try {
|
||||
const response = await secureFetch('/api/v1/arr/test-connection', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ service, url, apiKey })
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
statusSpan.innerHTML = `<span style="color: var(--ok-fg);">✓ ${escapeHtml(data.appName || 'Connected')} v${escapeHtml(data.version || '')}</span>`;
|
||||
} else {
|
||||
statusSpan.innerHTML = `<span style="color: var(--bad-fg);">✗ ${escapeHtml(data.error)}</span>`;
|
||||
}
|
||||
} catch (e) {
|
||||
statusSpan.innerHTML = `<span style="color: var(--bad-fg);">✗ ${escapeHtml(e.message)}</span>`;
|
||||
}
|
||||
};
|
||||
|
||||
// Phase 3: Smart Connect
|
||||
async function smartConnect() {
|
||||
showPhase('progress');
|
||||
progressSteps.innerHTML = '<div style="text-align: center; padding: 20px;"><span class="brand-spinner"></span><div style="color: var(--muted); margin-top: 8px;">Connecting services...</div></div>';
|
||||
|
||||
// Gather input
|
||||
const services = {};
|
||||
for (const svc of ['radarr', 'sonarr', 'prowlarr']) {
|
||||
const url = document.getElementById(`smart-${svc}-url`)?.value.trim();
|
||||
const apiKey = document.getElementById(`smart-${svc}-key`)?.value.trim();
|
||||
if (apiKey && url) {
|
||||
services[svc] = { apiKey, url };
|
||||
} else if (apiKey) {
|
||||
// Key provided without URL - let backend resolve
|
||||
services[svc] = { apiKey };
|
||||
}
|
||||
// If no key entered but service was already connected, backend uses stored credentials
|
||||
}
|
||||
|
||||
const payload = {
|
||||
services: Object.keys(services).length > 0 ? services : undefined,
|
||||
configurePlex: document.getElementById('smart-opt-plex')?.checked,
|
||||
configureProwlarr: document.getElementById('smart-opt-prowlarr')?.checked,
|
||||
configureSeerr: document.getElementById('smart-opt-seerr')?.checked,
|
||||
saveCredentials: document.getElementById('smart-opt-save')?.checked
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await secureFetch('/api/v1/arr/smart-connect', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Render progress steps
|
||||
let stepsHtml = '';
|
||||
for (const step of (data.steps || [])) {
|
||||
const icon = step.status === 'success' ? '<span style="color: var(--ok-fg);">✓</span>'
|
||||
: '<span style="color: var(--bad-fg);">✗</span>';
|
||||
const detailColor = step.status === 'success' ? 'var(--muted)' : 'var(--bad-fg)';
|
||||
stepsHtml += `<div style="display: flex; align-items: center; gap: 8px; padding: 6px 10px; background: var(--card-base); border-radius: 6px; font-size: 0.85rem;">
|
||||
${icon}
|
||||
<span>${escapeHtml(step.step)}</span>
|
||||
<span style="margin-left: auto; font-size: 0.75rem; color: ${detailColor};">${escapeHtml(step.details || '')}</span>
|
||||
</div>`;
|
||||
}
|
||||
progressSteps.innerHTML = stepsHtml;
|
||||
|
||||
// Show results after brief delay
|
||||
setTimeout(() => showResults(data), 500);
|
||||
|
||||
} catch (e) {
|
||||
progressSteps.innerHTML = `<div style="padding: 12px; color: var(--bad-fg);">Connection error: ${escapeHtml(e.message)}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 4: Results
|
||||
function showResults(data) {
|
||||
showPhase('results');
|
||||
|
||||
const s = data.summary || {};
|
||||
const allGood = s.failed === 0 && s.succeeded > 0;
|
||||
const headerColor = allGood ? 'var(--ok-fg)' : '#f39c12';
|
||||
const headerIcon = allGood ? '✓' : '⚠';
|
||||
const headerText = allGood ? 'All Connected!' : `${escapeHtml(String(s.succeeded))}/${escapeHtml(String(s.totalSteps))} Steps Succeeded`;
|
||||
|
||||
let html = `<div style="text-align: center; padding: 16px; background: color-mix(in srgb, ${headerColor} 12%, transparent); border-radius: 10px; border: 1px solid ${headerColor}; margin-bottom: 12px;">
|
||||
<div style="font-size: 1.5rem; color: ${headerColor};">${headerIcon}</div>
|
||||
<div style="font-size: 1.1rem; font-weight: 600; color: ${headerColor};">${headerText}</div>
|
||||
<div style="font-size: 0.85rem; color: var(--muted); margin-top: 4px;">${escapeHtml(String(s.succeeded))} succeeded, ${escapeHtml(String(s.failed))} failed</div>
|
||||
</div>`;
|
||||
|
||||
// Steps detail
|
||||
html += '<div style="display: flex; flex-direction: column; gap: 4px;">';
|
||||
for (const step of (data.steps || [])) {
|
||||
const icon = step.status === 'success' ? '<span style="color: var(--ok-fg);">✓</span>'
|
||||
: '<span style="color: var(--bad-fg);">✗</span>';
|
||||
html += `<div style="display: flex; align-items: center; gap: 8px; padding: 4px 8px; font-size: 0.8rem;">
|
||||
${icon} ${escapeHtml(step.step)} <span style="margin-left: auto; color: var(--muted); font-size: 0.75rem;">${escapeHtml(step.details || '')}</span>
|
||||
</div>`;
|
||||
}
|
||||
html += '</div>';
|
||||
|
||||
resultsContent.innerHTML = html;
|
||||
|
||||
// Show retry button if any failures
|
||||
retryBtn.style.display = s.failed > 0 ? 'block' : 'none';
|
||||
|
||||
// Fetch Plex libraries if Plex was connected
|
||||
if (data.steps?.some(st => st.step.includes('Plex') && st.status === 'success')) {
|
||||
fetchPlexLibraries();
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchPlexLibraries() {
|
||||
try {
|
||||
const res = await fetch('/api/v1/plex/libraries');
|
||||
const data = await res.json();
|
||||
if (data.success && data.libraries?.length > 0) {
|
||||
let html = `<div style="padding: 12px; background: var(--card-base); border-radius: 8px; border: 1px solid var(--border);">
|
||||
<h4 style="margin: 0 0 8px; font-size: 0.85rem; color: var(--accent);">🎬 ${escapeHtml(data.serverName)} Libraries</h4>
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 8px;">`;
|
||||
for (const lib of data.libraries) {
|
||||
const typeIcon = lib.type === 'movie' ? '🎬' : lib.type === 'show' ? '📺' : '🎵';
|
||||
html += `<div style="padding: 8px 12px; background: var(--card-bg); border-radius: 6px; font-size: 0.85rem;">
|
||||
${typeIcon} <strong>${escapeHtml(lib.title)}</strong>
|
||||
<span style="color: var(--muted); font-size: 0.75rem;">${escapeHtml(String(lib.count))} items</span>
|
||||
</div>`;
|
||||
}
|
||||
html += '</div></div>';
|
||||
plexLibraries.innerHTML = html;
|
||||
plexLibraries.style.display = 'block';
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore Plex library fetch errors
|
||||
}
|
||||
}
|
||||
|
||||
// Event listeners
|
||||
openBtn?.addEventListener('click', () => {
|
||||
modal.classList.add('show');
|
||||
plexLibraries.style.display = 'none';
|
||||
smartDetect();
|
||||
});
|
||||
|
||||
wireModal(modal, cancelBtn);
|
||||
connectBtn?.addEventListener('click', smartConnect);
|
||||
retryBtn?.addEventListener('click', smartConnect);
|
||||
})();
|
||||
Reference in New Issue
Block a user