From 779ae33bacbc6c9588e0bfbf13f74f6f424b862d Mon Sep 17 00:00:00 2001 From: eduard256 Date: Fri, 21 Nov 2025 23:25:30 +0300 Subject: [PATCH] Redesign stream discovery UI with vertical list layout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace carousel navigation with scrollable vertical list - Remove statistics counters (Tested/Found/Remaining) - Add collapsible stream details with expand/collapse toggle - Show stream URL preview in header, full URL in details - Position URL below stream type badge for better readability - Add new StreamList component replacing StreamCarousel - Update CSS with improved layout and hover effects 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- webui/web/css/main.css | 231 ++++++++++++++++++--------------- webui/web/index.html | 37 +----- webui/web/js/main.js | 32 +---- webui/web/js/ui/stream-list.js | 111 ++++++++++++++++ 4 files changed, 244 insertions(+), 167 deletions(-) create mode 100644 webui/web/js/ui/stream-list.js diff --git a/webui/web/css/main.css b/webui/web/css/main.css index 02a617b..d8eb3e3 100644 --- a/webui/web/css/main.css +++ b/webui/web/css/main.css @@ -586,148 +586,173 @@ body { margin-bottom: var(--space-6); } -/* ===== CAROUSEL ===== */ -.carousel-wrapper { - position: relative; +/* ===== STREAMS LIST ===== */ +.streams-list { display: flex; - align-items: center; - gap: var(--space-4); - margin-bottom: var(--space-4); + flex-direction: column; + gap: var(--space-3); + padding: var(--space-2); } -.carousel { - flex: 1; +/* Custom scrollbar */ +.streams-list::-webkit-scrollbar { + width: 8px; +} + +.streams-list::-webkit-scrollbar-track { + background: var(--bg-secondary); + border-radius: 4px; +} + +.streams-list::-webkit-scrollbar-thumb { + background: var(--purple-primary); + border-radius: 4px; +} + +.streams-list::-webkit-scrollbar-thumb:hover { + background: var(--purple-light); +} + +/* Stream item */ +.stream-item { + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: 8px; + transition: all var(--transition-base); overflow: hidden; } -.carousel-track { - display: flex; - transition: transform var(--transition-slow); -} - -.stream-card { - flex: 0 0 100%; - width: 100%; - padding: var(--space-6); - background: var(--bg-secondary); - border: 1px solid var(--border-color); - border-radius: 12px; - transition: all var(--transition-base); -} - -.stream-card:hover { +.stream-item:hover { border-color: var(--purple-primary); - box-shadow: 0 8px 24px var(--purple-glow); + box-shadow: 0 4px 12px var(--purple-glow); } -.stream-type { +.stream-item.expanded { + border-color: var(--purple-primary); +} + +/* Stream item header */ +.stream-item-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--space-4); + padding: var(--space-4); + cursor: pointer; +} + +.stream-item-main { + display: flex; + align-items: center; + gap: var(--space-3); + flex: 1; + min-width: 0; +} + +.stream-info-left { + display: flex; + flex-direction: column; + gap: var(--space-2); + flex: 1; + min-width: 0; +} + +.stream-type-badge { display: flex; align-items: center; gap: var(--space-2); font-size: var(--text-sm); font-weight: 600; color: var(--purple-primary); - margin-bottom: var(--space-4); text-transform: uppercase; letter-spacing: 0.05em; + white-space: nowrap; } -.stream-type svg { +.stream-type-badge svg { width: 20px; height: 20px; + flex-shrink: 0; } -.stream-url { +.stream-url-preview { + font-family: var(--font-mono); + font-size: var(--text-xs); + color: var(--text-secondary); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.stream-toggle { + background: none; + border: none; + padding: var(--space-2); + cursor: pointer; + color: var(--text-secondary); + transition: all var(--transition-fast); + display: flex; + align-items: center; + justify-content: center; +} + +.stream-toggle:hover { + color: var(--purple-primary); +} + +.stream-toggle .chevron { + transition: transform var(--transition-fast); +} + +.stream-item.expanded .stream-toggle .chevron { + transform: rotate(180deg); +} + +.btn-use-stream { + flex-shrink: 0; + white-space: nowrap; + padding: var(--space-3) var(--space-4); + font-size: var(--text-sm); +} + +/* Stream item details */ +.stream-item-details { + max-height: 0; + overflow: hidden; + transition: max-height var(--transition-base); + padding: 0 var(--space-4); +} + +.stream-item-details.visible { + max-height: 500px; + padding: 0 var(--space-4) var(--space-4) var(--space-4); +} + +.stream-url-full { font-family: var(--font-mono); font-size: var(--text-sm); color: var(--text-primary); word-break: break-all; - margin-bottom: var(--space-4); + margin-bottom: var(--space-3); padding: var(--space-3); background: var(--bg-tertiary); border-radius: 6px; + border: 1px solid var(--border-color); } -.stream-meta { +.stream-meta-item { font-size: var(--text-sm); color: var(--text-secondary); margin-bottom: var(--space-2); } -.stream-actions { - margin-top: var(--space-6); +.stream-meta-item:last-child { + margin-bottom: 0; } -.carousel-arrow { - flex-shrink: 0; - width: 48px; - height: 48px; - background: var(--bg-elevated); - border: 1px solid var(--border-color); - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - transition: all var(--transition-fast); - color: var(--text-secondary); -} - -.carousel-arrow:hover:not(:disabled) { - background: var(--purple-primary); - border-color: var(--purple-primary); - color: white; - box-shadow: 0 4px 12px var(--purple-glow); -} - -.carousel-arrow:disabled { - opacity: 0.3; - cursor: not-allowed; -} - -@media (max-width: 767px) { - .carousel-wrapper { - flex-direction: column; - gap: var(--space-3); - } - - .carousel-arrow { - display: none; - } -} - -.carousel-info { - text-align: center; -} - -.carousel-counter { - font-size: var(--text-sm); - color: var(--text-secondary); - margin-bottom: var(--space-3); -} - -.carousel-dots { - display: flex; - justify-content: center; - gap: var(--space-2); -} - -.carousel-dot { - width: 8px; - height: 8px; - border-radius: 50%; - background: rgba(139, 92, 246, 0.3); - border: none; - cursor: pointer; - transition: all var(--transition-base); - padding: 0; -} - -.carousel-dot.active { - width: 24px; - border-radius: 4px; - background: var(--purple-primary); - box-shadow: 0 0 8px var(--purple-glow); +.meta-label { + font-weight: 600; + color: var(--text-primary); } /* ===== SELECTED STREAM INFO ===== */ diff --git a/webui/web/index.html b/webui/web/index.html index 67434d6..90783a8 100644 --- a/webui/web/index.html +++ b/webui/web/index.html @@ -216,46 +216,11 @@

Starting scan...

-
-
- 0 - Tested -
-
- 0 - Found -
-
- 0 - Remaining -
-
diff --git a/webui/web/js/main.js b/webui/web/js/main.js index 30f531b..6a9181b 100644 --- a/webui/web/js/main.js +++ b/webui/web/js/main.js @@ -3,7 +3,7 @@ import { StreamDiscoveryAPI } from './api/stream-discovery.js'; import { MockCameraAPI } from './mock/mock-camera-api.js'; import { MockStreamAPI } from './mock/mock-stream-api.js'; import { SearchForm } from './ui/search-form.js'; -import { StreamCarousel } from './ui/stream-carousel.js'; +import { StreamList } from './ui/stream-list.js'; import { ConfigPanel } from './ui/config-panel.js'; import { FrigateGenerator } from './config-generators/frigate/index.js'; import { showToast } from './utils/toast.js'; @@ -30,7 +30,7 @@ class StrixApp { } this.searchForm = new SearchForm(); - this.carousel = new StreamCarousel(); + this.streamList = new StreamList(); this.configPanel = new ConfigPanel(); this.currentAddress = ''; @@ -95,24 +95,6 @@ class StrixApp { this.showScreen('config'); }); - // Carousel navigation - document.getElementById('carousel-prev').addEventListener('click', () => { - this.carousel.prev(); - }); - - document.getElementById('carousel-next').addEventListener('click', () => { - this.carousel.next(); - }); - - // Keyboard navigation - document.addEventListener('keydown', (e) => { - const currentScreen = document.querySelector('.screen.active').id; - if (currentScreen === 'screen-discovery') { - if (e.key === 'ArrowLeft') this.carousel.prev(); - if (e.key === 'ArrowRight') this.carousel.next(); - } - }); - // Screen 4: Configuration output document.getElementById('btn-back-to-streams').addEventListener('click', () => { this.isSelectingSubStream = false; @@ -303,9 +285,6 @@ class StrixApp { resetDiscoveryUI() { document.getElementById('progress-fill').style.width = '0%'; document.getElementById('progress-text').textContent = 'Starting scan...'; - document.getElementById('stat-tested').textContent = '0'; - document.getElementById('stat-found').textContent = '0'; - document.getElementById('stat-remaining').textContent = '0'; document.getElementById('streams-section').classList.add('hidden'); this.currentStreams = []; } @@ -316,9 +295,6 @@ class StrixApp { document.getElementById('progress-fill').style.width = `${percentage}%`; document.getElementById('progress-text').textContent = `Testing streams... ${Math.round(percentage)}%`; - document.getElementById('stat-tested').textContent = data.tested; - document.getElementById('stat-found').textContent = data.found; - document.getElementById('stat-remaining').textContent = data.remaining; } handleStreamFound(data) { @@ -330,8 +306,8 @@ class StrixApp { streamsSection.classList.remove('hidden'); } - // Update carousel - this.carousel.render(this.currentStreams, (stream, index) => { + // Update stream list + this.streamList.render(this.currentStreams, (stream, index) => { this.selectStream(stream, index); }); } diff --git a/webui/web/js/ui/stream-list.js b/webui/web/js/ui/stream-list.js new file mode 100644 index 0000000..bddc934 --- /dev/null +++ b/webui/web/js/ui/stream-list.js @@ -0,0 +1,111 @@ +export class StreamList { + constructor() { + this.listContainer = document.getElementById('streams-list'); + this.streams = []; + this.onUseCallback = null; + this.expandedIndex = null; + } + + render(streams, onUseCallback) { + this.streams = streams; + this.onUseCallback = onUseCallback; + + // Render stream items + this.listContainer.innerHTML = streams.map((stream, index) => this.renderItem(stream, index)).join(''); + + // Attach event listeners + this.attachEventListeners(); + } + + renderItem(stream, index) { + const icon = this.getStreamIcon(stream.type); + const isExpanded = this.expandedIndex === index; + const truncatedUrl = this.truncateURL(stream.url, 60); + + return ` +
+
+
+
+
+ ${icon} + ${stream.type} +
+
${truncatedUrl}
+
+ +
+ +
+
+
${stream.url}
+ ${stream.resolution ? `
Resolution: ${stream.resolution}
` : ''} + ${stream.codec ? `
Codec: ${stream.codec}${stream.fps ? ` • ${stream.fps} fps` : ''}${stream.bitrate ? ` • ${Math.round(stream.bitrate / 1000)} Kbps` : ''}
` : ''} + ${stream.has_audio ? '
Audio: Yes
' : ''} +
+
+ `; + } + + truncateURL(url, maxLength = 60) { + if (url.length <= maxLength) { + return url; + } + return url.substring(0, maxLength) + '...'; + } + + getStreamIcon(type) { + const icons = { + 'FFMPEG': '', + 'ONVIF': '', + 'JPEG': '', + 'MJPEG': '', + 'HLS': '', + 'HTTP_VIDEO': '' + }; + return icons[type] || icons['FFMPEG']; + } + + attachEventListeners() { + // Click on header to toggle + this.listContainer.querySelectorAll('.stream-item-header').forEach(header => { + header.addEventListener('click', (e) => { + // Don't toggle if clicking "Use Stream" button + if (e.target.closest('.btn-use-stream')) { + return; + } + + const index = parseInt(header.dataset.index); + this.toggleExpand(index); + }); + }); + + // Use Stream buttons + this.listContainer.querySelectorAll('.btn-use-stream').forEach(btn => { + btn.addEventListener('click', (e) => { + e.stopPropagation(); // Prevent toggle + const index = parseInt(e.target.dataset.index); + if (this.onUseCallback) { + this.onUseCallback(this.streams[index], index); + } + }); + }); + } + + toggleExpand(index) { + if (this.expandedIndex === index) { + // Collapse if already expanded + this.expandedIndex = null; + } else { + // Expand new item + this.expandedIndex = index; + } + + // Re-render to update state + this.render(this.streams, this.onUseCallback); + } +}