diff --git a/server/src/wled_controller/static/app.js b/server/src/wled_controller/static/app.js
index 79e1f3d..1d7b535 100644
--- a/server/src/wled_controller/static/app.js
+++ b/server/src/wled_controller/static/app.js
@@ -3087,58 +3087,23 @@ async function loadPictureStreams() {
}
}
-function toggleStreamGroup(groupKey) {
- const stored = JSON.parse(localStorage.getItem('streamGroupState') || '{}');
- stored[groupKey] = !stored[groupKey];
- localStorage.setItem('streamGroupState', JSON.stringify(stored));
- renderPictureStreamsList(_cachedStreams);
-}
-
-function isStreamGroupCollapsed(groupKey) {
- const stored = JSON.parse(localStorage.getItem('streamGroupState') || '{}');
- return !!stored[groupKey];
-}
-
-function renderStreamGroup(groupKey, icon, titleKey, count, addType, addLabelKey, cardsHtml) {
- const collapsed = isStreamGroupCollapsed(groupKey);
- const chevron = collapsed ? '▶' : '▼';
- return `
-
-
- ${cardsHtml}
-
-
+
-
${t(addLabelKey)}
-
-
-
`;
+function switchStreamTab(tabKey) {
+ document.querySelectorAll('.stream-tab-btn').forEach(btn =>
+ btn.classList.toggle('active', btn.dataset.streamTab === tabKey)
+ );
+ document.querySelectorAll('.stream-tab-panel').forEach(panel =>
+ panel.classList.toggle('active', panel.id === `stream-tab-${tabKey}`)
+ );
+ localStorage.setItem('activeStreamTab', tabKey);
}
function renderPictureStreamsList(streams) {
const container = document.getElementById('streams-list');
-
- if (streams.length === 0) {
- container.innerHTML =
- renderStreamGroup('raw', '🖥️', 'streams.group.raw', 0, 'raw', 'streams.add.raw', '') +
- renderStreamGroup('static_image', '🖼️', 'streams.group.static_image', 0, 'static_image', 'streams.add.static_image', '') +
- renderStreamGroup('processed', '🎨', 'streams.group.processed', 0, 'processed', 'streams.add.processed', '');
- return;
- }
+ const activeTab = localStorage.getItem('activeStreamTab') || 'raw';
const renderCard = (stream) => {
const typeIcons = { raw: '🖥️', processed: '🎨', static_image: '🖼️' };
const typeIcon = typeIcons[stream.stream_type] || '📺';
- const typeBadges = {
- raw: `${t('streams.type.raw')}`,
- processed: `${t('streams.type.processed')}`,
- static_image: `${t('streams.type.static_image')}`,
- };
- const typeBadge = typeBadges[stream.stream_type] || '';
let detailsHtml = '';
if (stream.stream_type === 'raw') {
@@ -3178,7 +3143,6 @@ function renderPictureStreamsList(streams) {
${typeIcon} ${escapeHtml(stream.name)}
- ${typeBadge}
${detailsHtml}
${stream.description ? `${escapeHtml(stream.description)}
` : ''}
@@ -3198,10 +3162,32 @@ function renderPictureStreamsList(streams) {
const processedStreams = streams.filter(s => s.stream_type === 'processed');
const staticImageStreams = streams.filter(s => s.stream_type === 'static_image');
- container.innerHTML =
- renderStreamGroup('raw', '🖥️', 'streams.group.raw', rawStreams.length, 'raw', 'streams.add.raw', rawStreams.map(renderCard).join('')) +
- renderStreamGroup('static_image', '🖼️', 'streams.group.static_image', staticImageStreams.length, 'static_image', 'streams.add.static_image', staticImageStreams.map(renderCard).join('')) +
- renderStreamGroup('processed', '🎨', 'streams.group.processed', processedStreams.length, 'processed', 'streams.add.processed', processedStreams.map(renderCard).join(''));
+ const addCard = (type, labelKey) => `
+ `;
+
+ const tabs = [
+ { key: 'raw', icon: '🖥️', titleKey: 'streams.group.raw', streams: rawStreams, addLabelKey: 'streams.add.raw' },
+ { key: 'static_image', icon: '🖼️', titleKey: 'streams.group.static_image', streams: staticImageStreams, addLabelKey: 'streams.add.static_image' },
+ { key: 'processed', icon: '🎨', titleKey: 'streams.group.processed', streams: processedStreams, addLabelKey: 'streams.add.processed' },
+ ];
+
+ const tabBar = `${tabs.map(tab =>
+ ``
+ ).join('')}
`;
+
+ const panels = tabs.map(tab =>
+ `
+
+ ${tab.streams.map(renderCard).join('')}
+ ${addCard(tab.key, tab.addLabelKey)}
+
+
`
+ ).join('');
+
+ container.innerHTML = tabBar + panels;
}
function onStreamTypeChange() {
diff --git a/server/src/wled_controller/static/locales/en.json b/server/src/wled_controller/static/locales/en.json
index ca53714..1a30ef0 100644
--- a/server/src/wled_controller/static/locales/en.json
+++ b/server/src/wled_controller/static/locales/en.json
@@ -200,8 +200,8 @@
"common.edit": "Edit",
"streams.title": "\uD83D\uDCFA Picture Streams",
"streams.description": "Picture streams define the capture pipeline. A raw stream captures from a display using a capture template. A processed stream applies postprocessing to another stream. Assign streams to devices.",
- "streams.group.raw": "Screen Capture Streams",
- "streams.group.processed": "Processed Streams",
+ "streams.group.raw": "Screen Capture",
+ "streams.group.processed": "Processed",
"streams.add": "Add Picture Stream",
"streams.add.raw": "Add Screen Capture",
"streams.add.processed": "Add Processed Stream",
@@ -281,7 +281,7 @@
"device.stream_settings.smoothing": "Smoothing:",
"device.stream_settings.smoothing_hint": "Temporal blending between frames (0=none, 1=full). Reduces flicker.",
"device.tip.stream_selector": "Configure picture stream and LED projection settings for this device",
- "streams.group.static_image": "Static Image Streams",
+ "streams.group.static_image": "Static Image",
"streams.add.static_image": "Add Static Image",
"streams.edit.static_image": "Edit Static Image",
"streams.type.static_image": "Static Image",
diff --git a/server/src/wled_controller/static/locales/ru.json b/server/src/wled_controller/static/locales/ru.json
index c20e559..1cdd16a 100644
--- a/server/src/wled_controller/static/locales/ru.json
+++ b/server/src/wled_controller/static/locales/ru.json
@@ -200,8 +200,8 @@
"common.edit": "Редактировать",
"streams.title": "\uD83D\uDCFA Видеопотоки",
"streams.description": "Видеопотоки определяют конвейер захвата. Сырой поток захватывает экран с помощью шаблона захвата. Обработанный поток применяет постобработку к другому потоку. Назначайте потоки устройствам.",
- "streams.group.raw": "Потоки Захвата Экрана",
- "streams.group.processed": "Обработанные Потоки",
+ "streams.group.raw": "Захват Экрана",
+ "streams.group.processed": "Обработанные",
"streams.add": "Добавить Видеопоток",
"streams.add.raw": "Добавить Захват Экрана",
"streams.add.processed": "Добавить Обработанный",
@@ -281,7 +281,7 @@
"device.stream_settings.smoothing": "Сглаживание:",
"device.stream_settings.smoothing_hint": "Временное смешивание между кадрами (0=нет, 1=полное). Уменьшает мерцание.",
"device.tip.stream_selector": "Настройки видеопотока и проекции LED для этого устройства",
- "streams.group.static_image": "Статические изображения",
+ "streams.group.static_image": "Статические",
"streams.add.static_image": "Добавить статическое изображение",
"streams.edit.static_image": "Редактировать статическое изображение",
"streams.type.static_image": "Статическое изображение",
diff --git a/server/src/wled_controller/static/style.css b/server/src/wled_controller/static/style.css
index 2625b18..02c40a5 100644
--- a/server/src/wled_controller/static/style.css
+++ b/server/src/wled_controller/static/style.css
@@ -2176,67 +2176,58 @@ input:-webkit-autofill:focus {
margin-right: 6px;
}
-/* Stream group sections */
-.stream-group {
- margin-bottom: 24px;
-}
-
-.stream-group:last-child {
- margin-bottom: 0;
-}
-
-.stream-group-header {
+/* Stream sub-tabs */
+.stream-tab-bar {
display: flex;
align-items: center;
- gap: 8px;
- margin-bottom: 12px;
- padding-bottom: 8px;
+ gap: 4px;
border-bottom: 2px solid var(--border-color);
- cursor: pointer;
- user-select: none;
- transition: opacity 0.2s;
+ margin-bottom: 16px;
}
-.stream-group-header:hover {
- opacity: 0.8;
-}
-
-.stream-group-chevron {
- font-size: 10px;
+.stream-tab-btn {
+ background: none;
+ border: none;
+ padding: 8px 14px;
+ font-size: 0.9rem;
+ font-weight: 500;
color: var(--text-secondary);
- flex-shrink: 0;
- width: 12px;
- transition: transform 0.2s;
+ cursor: pointer;
+ border-bottom: 2px solid transparent;
+ margin-bottom: -2px;
+ transition: color 0.2s, border-color 0.2s;
}
-.stream-group.collapsed .stream-group-header {
- margin-bottom: 0;
- border-bottom-color: transparent;
-}
-
-.stream-group.collapsed .templates-grid {
- display: none;
-}
-
-.stream-group-icon {
- font-size: 1.2rem;
-}
-
-.stream-group-title {
- font-size: 1rem;
- font-weight: 600;
+.stream-tab-btn:hover {
color: var(--text-color);
}
-.stream-group-count {
+.stream-tab-btn.active {
+ color: var(--primary-color);
+ border-bottom-color: var(--primary-color);
+}
+
+.stream-tab-count {
background: var(--border-color);
color: var(--text-secondary);
- font-size: 0.75rem;
+ font-size: 0.7rem;
font-weight: 600;
- padding: 2px 8px;
- border-radius: 10px;
- min-width: 20px;
- text-align: center;
+ padding: 1px 6px;
+ border-radius: 8px;
+ margin-left: 4px;
+}
+
+.stream-tab-btn.active .stream-tab-count {
+ background: var(--primary-color);
+ color: #fff;
+}
+
+.stream-tab-panel {
+ display: none;
+}
+
+.stream-tab-panel.active {
+ display: block;
}
/* Image Lightbox */