From 3ec3a930d367bb8ea350d79d74307599909e32ef Mon Sep 17 00:00:00 2001 From: teorock100 Date: Sun, 19 Oct 2025 08:10:53 -0500 Subject: [PATCH 01/17] Design Swap --- lib/util/landingTemplate.js | 3862 ++++++++++++++++++++++++++--------- 1 file changed, 2870 insertions(+), 992 deletions(-) diff --git a/lib/util/landingTemplate.js b/lib/util/landingTemplate.js index 278d074..4762dd5 100644 --- a/lib/util/landingTemplate.js +++ b/lib/util/landingTemplate.js @@ -1,1015 +1,2893 @@ -// --- Mobile-Friendly Rebrand --- -// - Added the viewport meta tag for proper mobile scaling. -// - Switched to 'rem' units for scalable typography. -// - Updated #addon styles to be responsive by default. -// - Added a @media query to fine-tune styles for screens under 768px. - -const STYLESHEET = ` -* { - box-sizing: border-box; -} - -body, -html { - margin: 0; - padding: 0; - width: 100%; - min-height: 100%; -} - -html { - background-color: #0a192f; - background-size: cover; - background-position: center center; - background-repeat: no-repeat; - font-size: 16px; /* Set a base font size */ -} - -body { - display: flex; - align-items: center; - justify-content: center; - font-family: 'Open Sans', Arial, sans-serif; - color: #ccd6f6; - line-height: 1.5; - padding: 1em; /* Add some padding for small screens */ -} - -#addon { - width: 90%; /* Use percentage for responsive width */ - max-width: 700px; /* Max width for larger screens */ - margin: auto; - padding: 2em 3em; - background: rgba(10, 25, 47, 0.85); - border: 1px solid rgba(255, 255, 255, 0.1); - border-radius: 15px; - backdrop-filter: blur(10px); - -webkit-backdrop-filter: blur(10px); - box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37); -} - -.logo { - height: 100px; - width: 100px; - margin: 0 auto 1.5em; -} - -.logo img { - width: 100%; -} - -h1 { - font-size: 2.5rem; - font-weight: 700; - text-align: center; - color: #fff; -} - -h2 { - font-size: 1.1rem; - font-weight: normal; - font-style: italic; - opacity: 0.8; - text-align: center; - margin-top: 0.5em; -} - -h3 { - font-size: 1.2rem; - font-weight: 600; - color: #64ffda; - border-bottom: 1px solid #233554; - padding-bottom: 0.5em; - margin-top: 1.5em; -} -h1, h2, h3, p, label { - margin: 0; - text-shadow: 0 0 10px rgba(0, 0, 0, 0.2); -} - -a { - color: #64ffda; - text-decoration: none; - transition: color 0.2s ease-in-out; -} - -a:hover { - color: #fff; -} - -ul { - margin: 1em 0; - padding-left: 20px; - list-style: none; -} - -li { - margin-top: 0.5em; - position: relative; -} - -li::before { - content: '▹'; - position: absolute; - left: -20px; - color: #64ffda; -} - -.separator { - margin: 2em 0; - border: 0; - height: 1px; - background-color: #233554; -} - -.form-element { - margin-bottom: 1.5em; -} - -.label-to-top { - display: block; - margin-bottom: 0.5em; - font-weight: 600; - color: #ccd6f6; -} - -.full-width { - width: 100%; -} -select, input[type="text"] { - background-color: #112240; - border: 1px solid #233554; - color: #ccd6f6; - padding: 0.8em; - border-radius: 5px; - font-size: 1rem; - transition: border-color 0.2s ease-in-out; -} - -select:focus, input[type="text"]:focus { - outline: none; - border-color: #64ffda; -} - -.checkbox-container { - display: flex; - align-items: center; - margin-top: 1em; -} - -input[type="checkbox"] { - margin-right: 10px; - accent-color: #64ffda; - width: 1.2em; - height: 1.2em; -} - -button { - border: 1px solid #64ffda; - outline: 0; - color: #64ffda; - background: transparent; - padding: 0.8em 1.5em; - margin: 1.5em auto 0; - text-align: center; - font-family: 'Open Sans', Arial, sans-serif; - font-size: 1.1rem; - font-weight: 600; - cursor: pointer; - display: block; - border-radius: 5px; - transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out; -} - -button:hover { - background-color: rgba(100, 255, 218, 0.1); -} - -button:active { - background-color: rgba(100, 255, 218, 0.2); -} - -.copy-link-btn { - font-size: 0.9rem; - padding: 0.6em 1.2em; - margin: 0.5em auto 0; - background: transparent; +function renderAdvancedConfigPage(manifest, config, logo) { + + return ` + + + + + ${manifest.name} - Advanced Configuration + + + + + + + + +
+
+ +
+ Sootio + Sootio +
+ +
+
+ + + + +
+ + + + +
+
+ + +
+
+

Dashboard

+

v${manifest.version || '0.0.0'} | Overview of your Sootio configuration

+
+ + +
+
+
+ Services Configured + +
+
${config.DebridServices ? config.DebridServices.length : 0}
+
+
+
+ Configuration Status + +
+
Active
+
+
+ + +
+

Quick Actions

+
+ + +
+
+
+ + +
+
+

Debrid Services

+

Manage your debrid and usenet services

+
+ +
+

Services are queried simultaneously for best results

+ +
+ + +
+ + + + +
+ + +
+
+

Language Filter

+

Filter content by audio language

+
+ +
+

Select one or more languages. If none selected, no language filter is applied.

+
+
+ + + + +
+ + +
+
+

File Size Filter

+

Set minimum and maximum file sizes

+
+ +
+

Set to 0-200 for no filtering

+ +
+
+
+ + 0 GB +
+ +
+
+
+ + 200 GB +
+ +
+
+
+ + + + +
+ + +
+ Report issues on Github +
+
+
+
+ + + + + +
+
+
+ Sootio + Menu +
+ +
+ +
+ + + + + + + `; } -.toast { - position: fixed; - bottom: 2em; - left: 50%; - transform: translateX(-50%); - background: rgba(100, 255, 218, 0.9); - color: #0a192f; - padding: 1em 2em; - border-radius: 5px; - font-weight: 600; - opacity: 0; - transition: opacity 0.3s ease-in-out; - z-index: 1000; - pointer-events: none; +function renderStandardConfigPage(manifest, config, logo) { + return ` + + + + + + ${manifest.name} - Configuration + + + + + + +
+
+ +
+ Sootio Logo +

${manifest.name}

+

v${manifest.version || '0.0.0'}

+
+ + +
+

Configuration

+ + +
+ +

Add one or more services. All will be queried simultaneously.

+
+ +
+ + +
+ +

Select preferred languages. If none selected, no language filter is applied.

+
+
+
+
+ + +
+ +

Set minimum and maximum file size in GB. Set to 0-200 for no filtering.

+
+
+
+ Minimum: + 0 GB +
+ +
+
+
+ Maximum: + 200 GB +
+ +
+
+
+ + + + + +
+ +
+ Report issues on Github + • + +
+
+
+ + +
+ Configuration updated! +
+ + + + + + + `; } -.toast.show { - opacity: 1; -} +function landingTemplateNew(manifest, config) { + const logo = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Cdefs%3E%3ClinearGradient id='grad' x1='0%25' y1='0%25' x2='100%25' y2='100%25'%3E%3Cstop offset='0%25' style='stop-color:%2364ffda;stop-opacity:1' /%3E%3Cstop offset='100%25' style='stop-color:%2300A7B5;stop-opacity:1' /%3E%3C/linearGradient%3E%3C/defs%3E%3Cpath fill='url(%23grad)' d='M50,5 C74.85,5 95,25.15 95,50 C95,74.85 74.85,95 50,95 C35,95 22.33,87.6 15,76 C25,85 40,85 50,80 C60,75 65,65 65,50 C65,35 55,25 40,25 C25,25 15,40 15,50 C15,55 16,60 18,64 C8.5,58 5,45 5,50 C5,25.15 25.15,5 50,5 Z'/%3E%3C/svg%3E"; + + // Check if user already has a configuration + const hasExistingConfig = config && ( + (config.DebridServices && Array.isArray(config.DebridServices) && config.DebridServices.length > 0) || + (config.DebridProvider && config.DebridApiKey) + ); + + // If user has existing config, show the appropriate configuration page + if (hasExistingConfig) { + // Check if they want advanced mode + if (config.configStyle === 'advanced') { + return renderAdvancedConfigPage(manifest, config, logo); + } + return renderStandardConfigPage(manifest, config, logo); + } -.contact { - text-align: center; - margin-top: 2em; - opacity: 0.7; + return ` + + + + + + ${manifest.name} - Configuration + + + + + + + +
+ +
+ +
+ + + +
+ Manifest link copied! +
+ + + + + + + `; } -/* --- DRAG AND DROP STYLES --- */ -.service-row { - transition: all 0.3s ease; - position: relative; -} +export default landingTemplateNew; -.service-row.dragging { - opacity: 0.4; - transform: scale(1.02) rotate(2deg); - box-shadow: 0 10px 30px rgba(100, 255, 218, 0.3); - z-index: 1000; - cursor: grabbing !important; -} -.service-row.drag-over-top { - border-top: 3px solid #64ffda !important; - margin-top: 1em; -} -.service-row.drag-over-bottom { - border-bottom: 3px solid #64ffda !important; - margin-bottom: 1em; -} -.drag-handle { - display: inline-flex; - align-items: center; - justify-content: center; - cursor: grab; - color: #64ffda; - margin-right: 0.5em; - font-size: 1.5rem; - user-select: none; - opacity: 0.6; - transition: opacity 0.2s ease, transform 0.2s ease; - padding: 0.2em; -} -.drag-handle:hover { - opacity: 1; - transform: scale(1.1); -} -.drag-handle:active { - cursor: grabbing; -} -.service-row:hover .drag-handle { - opacity: 0.9; -} -/* --- NEW: MEDIA QUERY FOR MOBILE DEVICES --- */ -@media (max-width: 768px) { - body { - font-size: 14px; /* Slightly smaller base font on mobile */ - display: block; /* Let content flow from top */ - } - #addon { - width: 100%; - max-width: none; - padding: 2em 1.5em; - margin: 0; - border-radius: 0; - border: none; - } - h1 { - font-size: 2rem; - } -} -` -function landingTemplate(manifest, config = {}) { - const background = 'https://images.unsplash.com/photo-1534796636912-3b95b3ab5986?q=80&w=2071&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D'; - const logo = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Cdefs%3E%3ClinearGradient id='grad' x1='0%25' y1='0%25' x2='100%25' y2='100%25'%3E%3Cstop offset='0%25' style='stop-color:%2364ffda;stop-opacity:1' /%3E%3Cstop offset='100%25' style='stop-color:%2300A7B5;stop-opacity:1' /%3E%3C/linearGradient%3E%3C/defs%3E%3Cpath fill='url(%23grad)' d='M50,5 C74.85,5 95,25.15 95,50 C95,74.85 74.85,95 50,95 C35,95 22.33,87.6 15,76 C25,85 40,85 50,80 C60,75 65,65 65,50 C65,35 55,25 40,25 C25,25 15,40 15,50 C15,55 16,60 18,64 C8.5,58 5,45 5,50 C5,25.15 25.15,5 50,5 Z'/%3E%3C/svg%3E"; - const contactHTML = manifest.contactEmail ? - `
-

Contact ${manifest.name} creator:

- ${manifest.contactEmail} -
` : '' - - let formHTML = '' - let script = '' - - formHTML = ` -
-
- -

Add one or more services. All services will be queried simultaneously. Drag the ⋮⋮ handle to reorder services.

-
- -
- -
- -
- - -

Select one or more languages. If none are selected, no language filter is applied. Selecting English keeps English or unlabeled.

-
- -
- -
-
- Min: 0 GB - Max: 200 GB -
-
- - -
-
-

Filter streams by file size. Drag sliders to set min/max size in GB. Set to 0-200 for no filtering.

-
- -
- - -
-
- ` - - script += ` - const mainForm = document.getElementById('mainForm'); - const installLink = document.getElementById('installLink'); - const container = document.getElementById('debridServicesContainer'); - const addServiceBtn = document.getElementById('addServiceBtn'); - const usenetEnabled = document.getElementById('UsenetEnabled'); - const usenetConfig = document.getElementById('usenetConfig'); - - let serviceIndex = 0; - - // Initialize with existing config or one empty service - const existingServices = ${JSON.stringify(config.DebridServices || (config.DebridProvider ? [{ provider: config.DebridProvider, apiKey: config.DebridApiKey }] : [{ provider: 'RealDebrid', apiKey: '' }]))}; - - const getDebridServices = () => { - const services = []; - const rows = container.querySelectorAll('[data-index]'); - rows.forEach(row => { - const provider = row.querySelector('.debrid-provider').value; - const apiKey = row.querySelector('.debrid-apikey').value; - - if (provider === 'Usenet') { - const newznabUrl = row.querySelector('.newznab-url')?.value; - const sabnzbdUrl = row.querySelector('.sabnzbd-url')?.value; - const sabnzbdApiKey = row.querySelector('.sabnzbd-apikey')?.value; - const fileServerUrl = row.querySelector('.file-server-url')?.value || ''; - const fileServerPassword = row.querySelector('.file-server-password')?.value || ''; - const deleteOnStreamStop = row.querySelector('.usenet-delete-on-stop')?.checked || false; - const autoCleanOldFiles = row.querySelector('.usenet-auto-clean')?.checked || false; - const autoCleanAgeDays = parseInt(row.querySelector('.usenet-clean-age')?.value) || 7; - - if (newznabUrl && apiKey && sabnzbdUrl && sabnzbdApiKey && fileServerUrl) { - services.push({ - provider: 'Usenet', - apiKey, - newznabUrl, - sabnzbdUrl, - sabnzbdApiKey, - fileServerUrl, - fileServerPassword, - deleteOnStreamStop, - autoCleanOldFiles, - autoCleanAgeDays - }); - } - } else if (provider === 'HomeMedia') { - const homeMediaUrl = row.querySelector('.homemedia-url')?.value; - - if (homeMediaUrl) { - services.push({ - provider: 'HomeMedia', - apiKey: apiKey || '', // API key is optional, use empty string if not provided - homeMediaUrl - }); - } - } else if (provider === 'httpstreaming') { - const http4khdhub = row.querySelector('.http-4khdhub')?.checked ?? true; - const httpUHDMovies = row.querySelector('.http-uhdmovies')?.checked ?? true; - services.push({ - provider, - http4khdhub, - httpUHDMovies - }); - } else if (provider && apiKey) { - services.push({ provider, apiKey }); - } - }); - return services; - }; - - const updateLink = () => { - const formData = new FormData(mainForm); - const services = getDebridServices(); - - const minSize = parseInt(document.getElementById('minSize').value); - const maxSize = parseInt(document.getElementById('maxSize').value); - const showCatalog = document.getElementById('ShowCatalog').checked; - - const config = { - DebridServices: services, - Languages: formData.getAll('Languages'), - minSize: minSize, - maxSize: maxSize, - ShowCatalog: showCatalog - }; - - // Backward compatibility: if only one non-Usenet service, also set old fields - const nonUsenetServices = services.filter(s => s.provider !== 'Usenet'); - if (nonUsenetServices.length === 1) { - config.DebridProvider = nonUsenetServices[0].provider; - config.DebridApiKey = nonUsenetServices[0].apiKey; - } else if (nonUsenetServices.length > 1) { - // Use first non-Usenet service as primary for backwards compatibility - config.DebridProvider = nonUsenetServices[0].provider; - config.DebridApiKey = nonUsenetServices[0].apiKey; - } - - const allValid = services.every(s => { - if (s.provider === 'Usenet') { - return s.provider && s.apiKey && s.newznabUrl && s.sabnzbdUrl && s.sabnzbdApiKey && s.fileServerUrl; - } else if (s.provider === 'HomeMedia') { - return s.provider && s.homeMediaUrl; // API key is optional for Home Media - } else if (s.provider === 'httpstreaming') { - return true; - } - return s.provider && s.apiKey; - }); - - if (services.length > 0 && allValid) { - installLink.href = 'stremio://' + window.location.host + '/' + encodeURIComponent(JSON.stringify(config)) + '/manifest.json'; - } else { - installLink.href = '#'; - } - }; - - const createServiceRow = (provider = 'RealDebrid', apiKey = '', extraConfig = {}) => { - const index = serviceIndex++; - const row = document.createElement('div'); - row.className = 'form-element service-row'; - row.style.cssText = 'display: flex; gap: 1em; align-items: flex-start; margin-bottom: 1em; padding: 1em; background: rgba(35, 53, 84, 0.3); border-radius: 5px;'; - row.dataset.index = index; - row.draggable = true; - - row.innerHTML = \` - ⋮⋮ -
- -
- -
-
- - \`; - - const select = row.querySelector('.debrid-provider'); - const input = row.querySelector('.debrid-apikey'); - const configDiv = row.querySelector('.service-config'); - const removeBtn = row.querySelector('.remove-service'); - - select.value = provider; - input.value = apiKey; - - // Handle provider-specific fields - const updateUsenetFields = () => { - if (select.value === 'HomeMedia') { - input.placeholder = 'Home Media API Key (Optional)'; - - // Add Home Media URL field if it doesn't exist - if (!configDiv.querySelector('.homemedia-url')) { - const homeMediaUrlInput = document.createElement('input'); - homeMediaUrlInput.type = 'text'; - homeMediaUrlInput.className = 'homemedia-url full-width'; - homeMediaUrlInput.placeholder = 'Home Media Server URL (e.g., http://localhost:3003)'; - homeMediaUrlInput.style.marginTop = '0.5em'; - homeMediaUrlInput.value = extraConfig.homeMediaUrl || ''; - configDiv.insertBefore(homeMediaUrlInput, input); - homeMediaUrlInput.addEventListener('input', updateLink); - - // Add help text with setup link - const helpText = document.createElement('small'); - helpText.style.color = '#888'; - helpText.style.marginTop = '0.3em'; - helpText.style.display = 'block'; - helpText.innerHTML = 'URL to your personal media file server - Setup Guide'; - configDiv.appendChild(helpText); - } - } else if (select.value === 'Usenet') { - input.placeholder = 'Newznab API Key'; - - // Add additional Usenet fields if they don't exist - if (!configDiv.querySelector('.newznab-url')) { - const newznabUrlInput = document.createElement('input'); - newznabUrlInput.type = 'text'; - newznabUrlInput.className = 'newznab-url full-width'; - newznabUrlInput.placeholder = 'Newznab URL (e.g., https://api.nzbgeek.info)'; - newznabUrlInput.style.marginTop = '0.5em'; - newznabUrlInput.value = extraConfig.newznabUrl || ''; - configDiv.insertBefore(newznabUrlInput, input); - newznabUrlInput.addEventListener('input', updateLink); - } - - if (!configDiv.querySelector('.sabnzbd-url')) { - const sabnzbdUrlInput = document.createElement('input'); - sabnzbdUrlInput.type = 'text'; - sabnzbdUrlInput.className = 'sabnzbd-url full-width'; - sabnzbdUrlInput.placeholder = 'SABnzbd URL (e.g., localhost:8080 or http://ip:port)'; - sabnzbdUrlInput.style.marginTop = '0.5em'; - sabnzbdUrlInput.value = extraConfig.sabnzbdUrl || ''; - configDiv.appendChild(sabnzbdUrlInput); - sabnzbdUrlInput.addEventListener('input', updateLink); - - const sabnzbdApiInput = document.createElement('input'); - sabnzbdApiInput.type = 'text'; - sabnzbdApiInput.className = 'sabnzbd-apikey full-width'; - sabnzbdApiInput.placeholder = 'SABnzbd API Key'; - sabnzbdApiInput.style.marginTop = '0.5em'; - sabnzbdApiInput.value = extraConfig.sabnzbdApiKey || ''; - configDiv.appendChild(sabnzbdApiInput); - sabnzbdApiInput.addEventListener('input', updateLink); - - const fileServerInput = document.createElement('input'); - fileServerInput.type = 'text'; - fileServerInput.className = 'file-server-url full-width'; - fileServerInput.placeholder = 'File Server URL (Required - e.g., http://localhost:8081)'; - fileServerInput.style.marginTop = '0.5em'; - fileServerInput.value = extraConfig.fileServerUrl || ''; - configDiv.appendChild(fileServerInput); - fileServerInput.addEventListener('input', updateLink); - - // Add file server password field - const fileServerPasswordInput = document.createElement('input'); - fileServerPasswordInput.type = 'text'; - fileServerPasswordInput.className = 'file-server-password full-width'; - fileServerPasswordInput.placeholder = 'File Server Password (Optional - leave empty if not set)'; - fileServerPasswordInput.style.marginTop = '0.5em'; - fileServerPasswordInput.value = extraConfig.fileServerPassword || ''; - configDiv.appendChild(fileServerPasswordInput); - fileServerPasswordInput.addEventListener('input', updateLink); - - // Add help text with setup link - const helpText = document.createElement('small'); - helpText.style.color = '#888'; - helpText.style.marginTop = '0.3em'; - helpText.style.display = 'block'; - helpText.innerHTML = 'Required: File server for direct streaming - Setup Guide'; - configDiv.appendChild(helpText); - - // Add cleanup options - const cleanupOptionsDiv = document.createElement('div'); - cleanupOptionsDiv.style.cssText = 'margin-top: 1em; padding: 0.8em; background: rgba(100, 255, 218, 0.05); border-radius: 5px; border: 1px solid rgba(100, 255, 218, 0.2);'; - cleanupOptionsDiv.innerHTML = \` -
Cleanup Options
-
- - -
-
- - -
-
- - -
- \`; - configDiv.appendChild(cleanupOptionsDiv); - - // Set values from extraConfig - const deleteOnStopCheckbox = cleanupOptionsDiv.querySelector('.usenet-delete-on-stop'); - const autoCleanCheckbox = cleanupOptionsDiv.querySelector('.usenet-auto-clean'); - const cleanAgeInput = cleanupOptionsDiv.querySelector('.usenet-clean-age'); - - if (extraConfig.deleteOnStreamStop) deleteOnStopCheckbox.checked = true; - if (extraConfig.autoCleanOldFiles) autoCleanCheckbox.checked = true; - if (extraConfig.autoCleanAgeDays) cleanAgeInput.value = extraConfig.autoCleanAgeDays; - - // Add change listeners - deleteOnStopCheckbox.addEventListener('change', updateLink); - autoCleanCheckbox.addEventListener('change', updateLink); - cleanAgeInput.addEventListener('input', updateLink); - } - } else if (select.value === 'httpstreaming') { - input.style.display = 'none'; - // Add HTTP Streaming configuration if it doesn't exist - if (!configDiv.querySelector('.http-streaming-config')) { - const httpConfigDiv = document.createElement('div'); - httpConfigDiv.className = 'http-streaming-config'; - httpConfigDiv.style.cssText = 'margin-top: 1em; padding: 0.8em; background: rgba(100, 255, 218, 0.05); border-radius: 5px; border: 1px solid rgba(100, 255, 218, 0.2);'; - httpConfigDiv.innerHTML = \`
HTTP Streaming Sources
\`; - configDiv.appendChild(httpConfigDiv); - // Add event listeners to update link when checkboxes change - httpConfigDiv.querySelectorAll('input[type="checkbox"]').forEach(checkbox => { - checkbox.addEventListener('change', updateLink); - }); - } - } else { - input.placeholder = 'Enter API key'; - // Remove provider-specific fields - const homeMediaUrl = configDiv.querySelector('.homemedia-url'); - const newznabUrl = configDiv.querySelector('.newznab-url'); - const sabnzbdUrl = configDiv.querySelector('.sabnzbd-url'); - const sabnzbdApiKey = configDiv.querySelector('.sabnzbd-apikey'); - const fileServerUrl = configDiv.querySelector('.file-server-url'); - const fileServerPassword = configDiv.querySelector('.file-server-password'); - const helpText = configDiv.querySelector('small'); - if (homeMediaUrl) homeMediaUrl.remove(); - if (newznabUrl) newznabUrl.remove(); - if (sabnzbdUrl) sabnzbdUrl.remove(); - if (sabnzbdApiKey) sabnzbdApiKey.remove(); - if (fileServerUrl) fileServerUrl.remove(); - if (fileServerPassword) fileServerPassword.remove(); - if (helpText) helpText.remove(); - } - }; - - updateUsenetFields(); - - select.addEventListener('change', () => { - updateUsenetFields(); - updateLink(); - }); - input.addEventListener('input', updateLink); - removeBtn.addEventListener('click', () => { - row.remove(); - updateLink(); - }); - - // Drag and drop event handlers - let dragCounter = 0; - - row.addEventListener('dragstart', (e) => { - setTimeout(() => { - row.classList.add('dragging'); - }, 0); - e.dataTransfer.effectAllowed = 'move'; - e.dataTransfer.setData('text/plain', row.dataset.index); - }); - - row.addEventListener('dragend', () => { - row.classList.remove('dragging'); - // Remove all drag-over classes - document.querySelectorAll('.drag-over-top, .drag-over-bottom').forEach(el => { - el.classList.remove('drag-over-top', 'drag-over-bottom'); - }); - updateLink(); - }); - - row.addEventListener('dragover', (e) => { - e.preventDefault(); - e.dataTransfer.dropEffect = 'move'; - - const draggingElement = document.querySelector('.dragging'); - if (!draggingElement || draggingElement === row) { - return; - } - - // Get the bounding rectangle of the target row - const rect = row.getBoundingClientRect(); - const midpoint = rect.top + rect.height / 2; - - // Clear previous indicators from all rows - document.querySelectorAll('.drag-over-top, .drag-over-bottom').forEach(el => { - if (el !== row) { - el.classList.remove('drag-over-top', 'drag-over-bottom'); - } - }); - - // Determine insert position and actually move the element - if (e.clientY < midpoint) { - // Insert before current row - row.classList.add('drag-over-top'); - row.classList.remove('drag-over-bottom'); - if (draggingElement.nextSibling !== row) { - container.insertBefore(draggingElement, row); - } - } else { - // Insert after current row - row.classList.add('drag-over-bottom'); - row.classList.remove('drag-over-top'); - if (draggingElement.previousSibling !== row) { - container.insertBefore(draggingElement, row.nextSibling); - } - } - }); - - row.addEventListener('drop', (e) => { - e.preventDefault(); - const draggingElement = document.querySelector('.dragging'); - - if (!draggingElement || draggingElement === row) { - return; - } - - // Determine if we should insert before or after based on mouse position - const rect = row.getBoundingClientRect(); - const midpoint = rect.top + rect.height / 2; - - if (e.clientY < midpoint) { - container.insertBefore(draggingElement, row); - } else { - container.insertBefore(draggingElement, row.nextSibling); - } - - // Clean up classes - row.classList.remove('drag-over-top', 'drag-over-bottom'); - }); - - row.addEventListener('dragenter', (e) => { - e.preventDefault(); - dragCounter++; - }); - - row.addEventListener('dragleave', () => { - dragCounter--; - if (dragCounter === 0) { - row.classList.remove('drag-over-top', 'drag-over-bottom'); - } - }); - - container.appendChild(row); - return row; - }; - - // Initialize services - existingServices.forEach(service => { - let extraConfig = {}; - if (service.provider === 'Usenet') { - extraConfig = { - newznabUrl: service.newznabUrl || '', - sabnzbdUrl: service.sabnzbdUrl || '', - sabnzbdApiKey: service.sabnzbdApiKey || '', - fileServerUrl: service.fileServerUrl || '', - fileServerPassword: service.fileServerPassword || '', - deleteOnStreamStop: service.deleteOnStreamStop || false, - autoCleanOldFiles: service.autoCleanOldFiles || false, - autoCleanAgeDays: service.autoCleanAgeDays || 7 - }; - } else if (service.provider === 'HomeMedia') { - extraConfig = { - homeMediaUrl: service.homeMediaUrl || '' - }; - } - createServiceRow(service.provider, service.apiKey || '', extraConfig); - }); - - addServiceBtn.addEventListener('click', () => { - createServiceRow(); - updateLink(); - }); - - // Add drag-over handling to the container itself - container.addEventListener('dragover', (e) => { - e.preventDefault(); - e.dataTransfer.dropEffect = 'move'; - }); - - container.addEventListener('drop', (e) => { - e.preventDefault(); - }); - - const languages = ${JSON.stringify(config.Languages) || '[]'}; - const languagesSelect = document.getElementById('Languages'); - for (const option of languagesSelect.options) { - if (languages.includes(option.value)) { - option.selected = true; - } - } - - // Initialize size sliders - const minSizeSlider = document.getElementById('minSize'); - const maxSizeSlider = document.getElementById('maxSize'); - const minSizeLabel = document.getElementById('minSizeLabel'); - const maxSizeLabel = document.getElementById('maxSizeLabel'); - - // Set initial values from config - const initialMinSize = ${config.minSize || 0}; - const initialMaxSize = ${config.maxSize || 200}; - minSizeSlider.value = initialMinSize; - maxSizeSlider.value = initialMaxSize; - minSizeLabel.textContent = initialMinSize + ' GB'; - maxSizeLabel.textContent = initialMaxSize + ' GB'; - - // Initialize ShowCatalog checkbox from config (default to true) - const showCatalogCheckbox = document.getElementById('ShowCatalog'); - showCatalogCheckbox.checked = ${config.ShowCatalog !== false}; // Default to true unless explicitly false - showCatalogCheckbox.addEventListener('change', updateLink); - - // Update labels when sliders change - minSizeSlider.addEventListener('input', () => { - let minVal = parseInt(minSizeSlider.value); - let maxVal = parseInt(maxSizeSlider.value); - if (minVal > maxVal) { - minSizeSlider.value = maxVal; - minVal = maxVal; - } - minSizeLabel.textContent = minVal + ' GB'; - updateLink(); - }); - - maxSizeSlider.addEventListener('input', () => { - let minVal = parseInt(minSizeSlider.value); - let maxVal = parseInt(maxSizeSlider.value); - if (maxVal < minVal) { - maxSizeSlider.value = minVal; - maxVal = minVal; - } - maxSizeLabel.textContent = maxVal + ' GB'; - updateLink(); - }); - - installLink.onclick = (event) => { - const services = getDebridServices(); - const allValid = services.every(s => { - if (s.provider === 'Usenet') { - return s.provider && s.apiKey && s.newznabUrl && s.sabnzbdUrl && s.sabnzbdApiKey && s.fileServerUrl; - } else if (s.provider === 'HomeMedia') { - return s.provider && s.homeMediaUrl; // API key is optional for Home Media - } else if (s.provider === 'httpstreaming') { - return true; - } - return s.provider && s.apiKey; - }); - - if (services.length === 0 || !allValid) { - event.preventDefault(); - alert('Please complete all required fields for your services.'); - } - } - - const copyLinkBtn = document.getElementById('copyLinkBtn'); - const toast = document.getElementById('toast'); - - const showToast = () => { - toast.classList.add('show'); - setTimeout(() => { - toast.classList.remove('show'); - }, 2000); - }; - - copyLinkBtn.onclick = () => { - const services = getDebridServices(); - const allValid = services.every(s => { - if (s.provider === 'Usenet') { - return s.provider && s.apiKey && s.newznabUrl && s.sabnzbdUrl && s.sabnzbdApiKey && s.fileServerUrl; - } else if (s.provider === 'HomeMedia') { - return s.provider && s.homeMediaUrl; // API key is optional for Home Media - } else if (s.provider === 'httpstreaming') { - return true; - } - return s.provider && s.apiKey; - }); - - if (services.length === 0 || !allValid) { - alert('Please complete all required fields for your services.'); - return; - } - - const manifestUrl = installLink.href.replace('stremio://', 'https://'); - - navigator.clipboard.writeText(manifestUrl).then(() => { - showToast(); - }).catch(err => { - // Fallback for older browsers - const textArea = document.createElement('textarea'); - textArea.value = manifestUrl; - textArea.style.position = 'fixed'; - textArea.style.left = '-999999px'; - document.body.appendChild(textArea); - textArea.select(); - try { - document.execCommand('copy'); - showToast(); - } catch (err) { - alert('Failed to copy to clipboard'); - } - document.body.removeChild(textArea); - }); - }; - - mainForm.oninput = updateLink; - updateLink(); - ` - return ` - - - - - - - ${manifest.name} | Stremio Addon - - - - - - -
- -

${manifest.name}

-

v${manifest.version || '0.0.0'} | ${manifest.description || ''}

- -
- - ${formHTML} - -
- -

API Keys

-

Click the links below to get your API key from your debrid service provider:

- -

Report any issues on Github

- - - - - - ${contactHTML} -
Manifest link copied to clipboard!
-
- - - - ` -} -export default landingTemplate From a5fe7e17034ca0f7347ca79801d674803527be45 Mon Sep 17 00:00:00 2001 From: spookyhost1 Date: Sun, 19 Oct 2025 08:14:44 -0500 Subject: [PATCH 02/17] Design commit Design Commit push --- lib/util/landingTemplate.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/util/landingTemplate.js b/lib/util/landingTemplate.js index 4762dd5..cfb4b88 100644 --- a/lib/util/landingTemplate.js +++ b/lib/util/landingTemplate.js @@ -2889,5 +2889,3 @@ export default landingTemplateNew; - - From 714c9ca2a6c2f07201321351c1b97a87154655e8 Mon Sep 17 00:00:00 2001 From: spookyhost1 Date: Sun, 19 Oct 2025 09:53:35 -0500 Subject: [PATCH 03/17] Last fix - UX, Accessibility Install button semantic fix from nested button to button element Added visible focus styles Added ARIA labels for api key inputs that are dynamically genned Added API key password toggle hide/show --- lib/util/landingTemplate.js | 107 +++++++++++++++++++++++++----------- 1 file changed, 75 insertions(+), 32 deletions(-) diff --git a/lib/util/landingTemplate.js b/lib/util/landingTemplate.js index cfb4b88..7cdaf18 100644 --- a/lib/util/landingTemplate.js +++ b/lib/util/landingTemplate.js @@ -1,5 +1,3 @@ - - function renderAdvancedConfigPage(manifest, config, logo) { return ` @@ -2071,6 +2069,37 @@ function landingTemplateNew(manifest, config) { scrollbar-width: thin; scrollbar-color: #374151 #111827; } + + /* Accessibility */ + button:focus, + a:focus, + input:focus, + select:focus, + textarea:focus { + outline: 3px solid rgba(100, 255, 218, 0.5); + outline-offset: 2px; + } + + button:focus:not(:focus-visible), + a:focus:not(:focus-visible), + input:focus:not(:focus-visible), + select:focus:not(:focus-visible), + textarea:focus:not(:focus-visible) { + outline: none; + } + + /* Screen reader only */ + .sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border-width: 0; + } @@ -2094,7 +2123,7 @@ function landingTemplateNew(manifest, config) {
-

If you have already configured, your settings will autopopulate.

+

If you have already configured, your settings will autopopulate.

@@ -2232,9 +2261,9 @@ function landingTemplateNew(manifest, config) {

Your Sootio addon is ready to install.

- - INSTALL ADDON - + +
Get key
@@ -2463,6 +2505,15 @@ function landingTemplateNew(manifest, config) { select.value = provider; input.value = apiKey; + const passwordToggle = serviceRow.querySelector('.toggle-password'); + passwordToggle.addEventListener('click', (e) => { + e.stopPropagation(); + const type = input.type === 'password' ? 'text' : 'password'; + input.type = type; + serviceRow.querySelector('.eye-open').classList.toggle('hidden'); + serviceRow.querySelector('.eye-closed').classList.toggle('hidden'); + }); + toggleBtn.addEventListener('click', (e) => { e.stopPropagation(); configWrapper.classList.toggle('hidden'); @@ -2716,21 +2767,17 @@ function landingTemplateNew(manifest, config) { const configStr = JSON.stringify(config); const encodedConfig = encodeURIComponent(configStr); - const installLink = document.getElementById('installLink'); - if (installLink) { - const host = window.location.host; - installLink.href = 'stremio://' + host + '/' + encodedConfig + '/manifest.json'; - } + const host = window.location.host; + window.manifestUrl = 'stremio://' + host + '/' + encodedConfig + '/manifest.json'; } function copyManifestLink() { - const installLink = document.getElementById('installLink'); - if (!installLink || !installLink.href || installLink.href === '#' || installLink.href === '' || installLink.href.indexOf('stremio://') === -1) { + if (!window.manifestUrl || window.manifestUrl === '#' || window.manifestUrl === '' || window.manifestUrl.indexOf('stremio://') === -1) { alert('Please complete the configuration first'); return; } - const manifestUrl = installLink.href.replace('stremio://', 'https://'); + const manifestUrl = window.manifestUrl.replace('stremio://', 'https://'); if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(manifestUrl).then(() => { @@ -2775,20 +2822,27 @@ function landingTemplateNew(manifest, config) { document.getElementById('validationModal').classList.add('hidden'); } - const installLink = document.getElementById('installLink'); - if (installLink) { - installLink.addEventListener('click', function(e) { + const installBtn = document.getElementById('installBtn'); + if (installBtn) { + installBtn.addEventListener('click', function(e) { + if (!window.manifestUrl || window.manifestUrl === '#' || window.manifestUrl.indexOf('stremio://') === -1) { + alert('Please complete the configuration first'); + return; + } + const btn = e.currentTarget; const orig = btn.textContent; - const baseClass = 'inline-block transition-colors text-lg font-bold rounded-lg px-16 py-4'; + const baseClass = 'transition-colors text-lg font-bold rounded-lg px-16 py-4'; btn.textContent = 'Sending to Stremio...'; btn.className = baseClass + ' bg-gray-600 text-white pointer-events-none'; + window.location.href = window.manifestUrl; + setTimeout(() => btn.textContent = 'Sent!', 800); setTimeout(() => { btn.textContent = orig; - btn.className = baseClass + ' bg-[#4ac4b1] hover:bg-[#4fd4c1] text-[#0a0a0a]'; + btn.className = baseClass + ' bg-[#4ac4b1] hover:bg-[#4fd4c1] text-[#0a0a0a] cursor-pointer'; }, 2200); }); } @@ -2878,14 +2932,3 @@ function landingTemplateNew(manifest, config) { export default landingTemplateNew; - - - - - - - - - - - From 329270dbdd75eaf287f829c5a8589d1b9b9d8054 Mon Sep 17 00:00:00 2001 From: spookyhost1 Date: Sun, 19 Oct 2025 10:08:47 -0500 Subject: [PATCH 04/17] configstyle selector quick fix Made it explicit and more reliable just a minor fix --- lib/util/landingTemplate.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/util/landingTemplate.js b/lib/util/landingTemplate.js index 7cdaf18..50cc518 100644 --- a/lib/util/landingTemplate.js +++ b/lib/util/landingTemplate.js @@ -2143,7 +2143,7 @@ function landingTemplateNew(manifest, config) {
-
+
Standard Configuration Preview
@@ -2154,7 +2154,7 @@ function landingTemplateNew(manifest, config) {
-
+
Advanced Configuration Preview
@@ -2426,15 +2426,15 @@ function landingTemplateNew(manifest, config) { }); } - function selectConfigStyle(style) { + function selectConfigStyle(el, style) { configStyle = style; const cards = document.querySelectorAll('.config-style-card'); cards.forEach(card => { card.classList.remove('border-[#4ac4b1]'); card.classList.add('border-gray-700'); }); - event.currentTarget.classList.remove('border-gray-700'); - event.currentTarget.classList.add('border-[#4ac4b1]'); + el.classList.remove('border-gray-700'); + el.classList.add('border-[#4ac4b1]'); } function addService(provider = 'RealDebrid', apiKey = '', extraConfig = {}) { From 33a732f02e73f28701882c23a89039fa48a3d3c1 Mon Sep 17 00:00:00 2001 From: spookyhost1 Date: Sun, 19 Oct 2025 17:50:46 -0500 Subject: [PATCH 05/17] Drag and drop for debrid/usenets services Didn't have it in wireframe; added it to conform with current sootio ui --- lib/util/landingTemplate.js | 91 ++++++++++++++++++++++++++++++++++++- 1 file changed, 89 insertions(+), 2 deletions(-) diff --git a/lib/util/landingTemplate.js b/lib/util/landingTemplate.js index 50cc518..7cdccd2 100644 --- a/lib/util/landingTemplate.js +++ b/lib/util/landingTemplate.js @@ -2100,6 +2100,22 @@ function landingTemplateNew(manifest, config) { white-space: nowrap; border-width: 0; } + + .service-row { + transition: opacity 0.2s ease, border-color 0.2s ease; + } + + .service-row[draggable="true"] { + cursor: move; + } + + .drag-handle { + touch-action: none; + } + + .drag-handle:active { + transform: scale(1.1); + } @@ -2425,7 +2441,7 @@ function landingTemplateNew(manifest, config) { } }); } - + // Select Config Style function selectConfigStyle(el, style) { configStyle = style; const cards = document.querySelectorAll('.config-style-card'); @@ -2436,7 +2452,7 @@ function landingTemplateNew(manifest, config) { el.classList.remove('border-gray-700'); el.classList.add('border-[#4ac4b1]'); } - + // Add Service function addService(provider = 'RealDebrid', apiKey = '', extraConfig = {}) { const container = document.getElementById('servicesContainer'); const index = serviceIndex++; @@ -2447,6 +2463,11 @@ function landingTemplateNew(manifest, config) { serviceRow.innerHTML = \`
+ @@ -2523,6 +2567,8 @@ function landingTemplateNew(manifest, config) { const toggleBtn = serviceRow.querySelector('.toggle-config'); const chevronIcon = serviceRow.querySelector('.chevron-icon'); const getKeyLink = serviceRow.querySelector('.get-key-link'); + const moveUpBtn = serviceRow.querySelector('.move-up-btn'); + const moveDownBtn = serviceRow.querySelector('.move-down-btn'); select.value = provider; input.value = apiKey; @@ -2682,32 +2728,83 @@ function landingTemplateNew(manifest, config) { input.addEventListener('input', updateLink); removeBtn.addEventListener('click', () => { serviceRow.remove(); + updateButtonVisibility(); updateLink(); }); - // Drag and drop functionality - const dragHandle = serviceRow.querySelector('.drag-handle'); + // Mobile up/down buttons + moveUpBtn.addEventListener('click', (e) => { + e.stopPropagation(); + const prev = serviceRow.previousElementSibling; + if (prev && prev.classList.contains('service-row')) { + container.insertBefore(serviceRow, prev); + updateButtonVisibility(); + updateLink(); + } + }); - dragHandle.addEventListener('mousedown', () => { - serviceRow.setAttribute('draggable', 'true'); + moveDownBtn.addEventListener('click', (e) => { + e.stopPropagation(); + const next = serviceRow.nextElementSibling; + if (next && next.classList.contains('service-row')) { + container.insertBefore(next, serviceRow); + updateButtonVisibility(); + updateLink(); + } }); - dragHandle.addEventListener('touchstart', () => { + // Update button visibility based on position + function updateButtonVisibility() { + const rows = container.querySelectorAll('.service-row'); + rows.forEach((row, idx) => { + const upBtn = row.querySelector('.move-up-btn'); + const downBtn = row.querySelector('.move-down-btn'); + + // Show buttons on mobile + upBtn.classList.remove('hidden'); + downBtn.classList.remove('hidden'); + + // Disable first item's up button + if (idx === 0) { + upBtn.disabled = true; + upBtn.classList.add('opacity-30', 'cursor-not-allowed'); + } else { + upBtn.disabled = false; + upBtn.classList.remove('opacity-30', 'cursor-not-allowed'); + } + + // Disable last item's down button + if (idx === rows.length - 1) { + downBtn.disabled = true; + downBtn.classList.add('opacity-30', 'cursor-not-allowed'); + } else { + downBtn.disabled = false; + downBtn.classList.remove('opacity-30', 'cursor-not-allowed'); + } + }); + } + + // Drag and drop functionality (desktop only) + const dragHandle = serviceRow.querySelector('.drag-handle'); + + // Desktop drag-and-drop + dragHandle.addEventListener('mousedown', () => { serviceRow.setAttribute('draggable', 'true'); }); serviceRow.addEventListener('dragstart', (e) => { draggedElement = serviceRow; - serviceRow.classList.add('opacity-50'); + serviceRow.classList.add('dragging'); e.dataTransfer.effectAllowed = 'move'; }); serviceRow.addEventListener('dragend', () => { - serviceRow.classList.remove('opacity-50'); + serviceRow.classList.remove('dragging'); serviceRow.setAttribute('draggable', 'false'); document.querySelectorAll('.service-row').forEach(row => { - row.classList.remove('border-[#4ac4b1]', 'border-t-2'); + row.classList.remove('drag-over-top', 'drag-over-bottom'); }); + updateButtonVisibility(); updateLink(); }); @@ -2720,17 +2817,23 @@ function landingTemplateNew(manifest, config) { const midpoint = rect.top + rect.height / 2; document.querySelectorAll('.service-row').forEach(row => { - row.classList.remove('border-[#4ac4b1]', 'border-t-2', 'border-b-2'); + row.classList.remove('drag-over-top', 'drag-over-bottom'); }); if (e.clientY < midpoint) { - serviceRow.classList.add('border-t-2', 'border-[#4ac4b1]'); + serviceRow.classList.add('drag-over-top'); } else { - serviceRow.classList.add('border-b-2', 'border-[#4ac4b1]'); + serviceRow.classList.add('drag-over-bottom'); } } }); + serviceRow.addEventListener('dragleave', (e) => { + if (e.target === serviceRow) { + serviceRow.classList.remove('drag-over-top', 'drag-over-bottom'); + } + }); + serviceRow.addEventListener('drop', (e) => { e.preventDefault(); @@ -2746,7 +2849,7 @@ function landingTemplateNew(manifest, config) { } document.querySelectorAll('.service-row').forEach(row => { - row.classList.remove('border-[#4ac4b1]', 'border-t-2', 'border-b-2'); + row.classList.remove('drag-over-top', 'drag-over-bottom'); }); }); @@ -2756,6 +2859,9 @@ function landingTemplateNew(manifest, config) { configWrapper.classList.remove('hidden'); chevronIcon.classList.add('rotate-180'); } + + // Update button visibility after adding service + updateButtonVisibility(); } function initLanguages() { From 45cb97fdfb2afa6139e90567e16c9d651df0d801 Mon Sep 17 00:00:00 2001 From: spookyhost1 Date: Sun, 19 Oct 2025 19:00:31 -0500 Subject: [PATCH 08/17] rename main function for ease renamed main function from demo landingtemplatenew to og landing template since its been replaced --- lib/util/landingTemplate.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/util/landingTemplate.js b/lib/util/landingTemplate.js index aeda455..9865059 100644 --- a/lib/util/landingTemplate.js +++ b/lib/util/landingTemplate.js @@ -1658,7 +1658,7 @@ function renderStandardConfigPage(manifest, config, logo) { `; } -function landingTemplateNew(manifest, config) { +function landingTemplate(manifest, config) { const logo = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Cdefs%3E%3ClinearGradient id='grad' x1='0%25' y1='0%25' x2='100%25' y2='100%25'%3E%3Cstop offset='0%25' style='stop-color:%2364ffda;stop-opacity:1' /%3E%3Cstop offset='100%25' style='stop-color:%2300A7B5;stop-opacity:1' /%3E%3C/linearGradient%3E%3C/defs%3E%3Cpath fill='url(%23grad)' d='M50,5 C74.85,5 95,25.15 95,50 C95,74.85 74.85,95 50,95 C35,95 22.33,87.6 15,76 C25,85 40,85 50,80 C60,75 65,65 65,50 C65,35 55,25 40,25 C25,25 15,40 15,50 C15,55 16,60 18,64 C8.5,58 5,45 5,50 C5,25.15 25.15,5 50,5 Z'/%3E%3C/svg%3E"; // Check if user already has a configuration @@ -3123,5 +3123,5 @@ function landingTemplateNew(manifest, config) { `; } -export default landingTemplateNew; +export default landingTemplate; From e9c5e6553ad1a7fc415c6b237c64d7432422cd75 Mon Sep 17 00:00:00 2001 From: spookyhost1 Date: Sun, 19 Oct 2025 19:34:07 -0500 Subject: [PATCH 09/17] Standard config / advanced services fixes Added drag/drop & arrow modes, added api key hide/show, fixed services clipping over container removed restriction --- lib/util/landingTemplate.js | 491 ++++++++++++++++++++++++++++++++---- 1 file changed, 446 insertions(+), 45 deletions(-) diff --git a/lib/util/landingTemplate.js b/lib/util/landingTemplate.js index 9865059..41004f6 100644 --- a/lib/util/landingTemplate.js +++ b/lib/util/landingTemplate.js @@ -37,6 +37,49 @@ function renderAdvancedConfigPage(manifest, config, logo) { .custom-border { border-color: #2a3547; } .custom-cyan { color: #4ac4b1; } + .service-item { + transition: opacity 0.2s ease, border-color 0.2s ease, transform 0.2s ease; + position: relative; + } + .service-item[draggable="true"] { + cursor: move; + } + .drag-handle { + touch-action: none; + cursor: move; + } + .drag-handle:active { + transform: scale(1.1); + } + .service-item.drag-over-top::before { + content: ''; + position: absolute; + top: -2px; + left: 0; + right: 0; + height: 4px; + background: linear-gradient(90deg, #4ac4b1 0%, #64ffda 100%); + border-radius: 2px; + box-shadow: 0 0 8px rgba(100, 255, 218, 0.6); + z-index: 10; + } + .service-item.drag-over-bottom::after { + content: ''; + position: absolute; + bottom: -2px; + left: 0; + right: 0; + height: 4px; + background: linear-gradient(90deg, #4ac4b1 0%, #64ffda 100%); + border-radius: 2px; + box-shadow: 0 0 8px rgba(100, 255, 218, 0.6); + z-index: 10; + } + .service-item.dragging { + opacity: 0.4; + transform: scale(0.98); + } + .custom-btn { background-color: #4ac4b1; color: #0a0f1c; @@ -454,6 +497,7 @@ function renderAdvancedConfigPage(manifest, config, logo) { From 07308551440f9158b61af9c9cec8d85ead533e4f Mon Sep 17 00:00:00 2001 From: spookyhost1 Date: Wed, 29 Oct 2025 21:51:38 -0500 Subject: [PATCH 15/17] design fix nitpick --- lib/util/landingTemplate.js | 111 +++++++++++++++++++++++++++--------- 1 file changed, 85 insertions(+), 26 deletions(-) diff --git a/lib/util/landingTemplate.js b/lib/util/landingTemplate.js index 22e1d41..d9fdfad 100644 --- a/lib/util/landingTemplate.js +++ b/lib/util/landingTemplate.js @@ -2958,7 +2958,8 @@ function landingTemplate(manifest, config) {