Replace collapsible stream groups with sub-tabs navigation
- Replace expandable/collapsible groups with tab bar (Screen Capture, Static Image, Processed) - Persist active stream tab in localStorage - Shorten tab labels by removing "Streams" suffix - Remove type badge from cards (redundant with tab separation) - Add count badge on each tab with active highlight Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3087,58 +3087,23 @@ async function loadPictureStreams() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleStreamGroup(groupKey) {
|
function switchStreamTab(tabKey) {
|
||||||
const stored = JSON.parse(localStorage.getItem('streamGroupState') || '{}');
|
document.querySelectorAll('.stream-tab-btn').forEach(btn =>
|
||||||
stored[groupKey] = !stored[groupKey];
|
btn.classList.toggle('active', btn.dataset.streamTab === tabKey)
|
||||||
localStorage.setItem('streamGroupState', JSON.stringify(stored));
|
);
|
||||||
renderPictureStreamsList(_cachedStreams);
|
document.querySelectorAll('.stream-tab-panel').forEach(panel =>
|
||||||
}
|
panel.classList.toggle('active', panel.id === `stream-tab-${tabKey}`)
|
||||||
|
);
|
||||||
function isStreamGroupCollapsed(groupKey) {
|
localStorage.setItem('activeStreamTab', tabKey);
|
||||||
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 `<div class="stream-group${collapsed ? ' collapsed' : ''}">
|
|
||||||
<div class="stream-group-header" onclick="toggleStreamGroup('${groupKey}')">
|
|
||||||
<span class="stream-group-chevron">${chevron}</span>
|
|
||||||
<span class="stream-group-icon">${icon}</span>
|
|
||||||
<span class="stream-group-title">${t(titleKey)}</span>
|
|
||||||
<span class="stream-group-count">${count}</span>
|
|
||||||
</div>
|
|
||||||
<div class="templates-grid">
|
|
||||||
${cardsHtml}
|
|
||||||
<div class="template-card add-template-card" onclick="showAddStreamModal('${addType}')">
|
|
||||||
<div class="add-template-icon">+</div>
|
|
||||||
<div class="add-template-label">${t(addLabelKey)}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderPictureStreamsList(streams) {
|
function renderPictureStreamsList(streams) {
|
||||||
const container = document.getElementById('streams-list');
|
const container = document.getElementById('streams-list');
|
||||||
|
const activeTab = localStorage.getItem('activeStreamTab') || 'raw';
|
||||||
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 renderCard = (stream) => {
|
const renderCard = (stream) => {
|
||||||
const typeIcons = { raw: '🖥️', processed: '🎨', static_image: '🖼️' };
|
const typeIcons = { raw: '🖥️', processed: '🎨', static_image: '🖼️' };
|
||||||
const typeIcon = typeIcons[stream.stream_type] || '📺';
|
const typeIcon = typeIcons[stream.stream_type] || '📺';
|
||||||
const typeBadges = {
|
|
||||||
raw: `<span class="badge badge-raw">${t('streams.type.raw')}</span>`,
|
|
||||||
processed: `<span class="badge badge-processed">${t('streams.type.processed')}</span>`,
|
|
||||||
static_image: `<span class="badge badge-processed">${t('streams.type.static_image')}</span>`,
|
|
||||||
};
|
|
||||||
const typeBadge = typeBadges[stream.stream_type] || '';
|
|
||||||
|
|
||||||
let detailsHtml = '';
|
let detailsHtml = '';
|
||||||
if (stream.stream_type === 'raw') {
|
if (stream.stream_type === 'raw') {
|
||||||
@@ -3178,7 +3143,6 @@ function renderPictureStreamsList(streams) {
|
|||||||
<div class="template-name">
|
<div class="template-name">
|
||||||
${typeIcon} ${escapeHtml(stream.name)}
|
${typeIcon} ${escapeHtml(stream.name)}
|
||||||
</div>
|
</div>
|
||||||
${typeBadge}
|
|
||||||
</div>
|
</div>
|
||||||
${detailsHtml}
|
${detailsHtml}
|
||||||
${stream.description ? `<div class="template-config" style="opacity:0.7;">${escapeHtml(stream.description)}</div>` : ''}
|
${stream.description ? `<div class="template-config" style="opacity:0.7;">${escapeHtml(stream.description)}</div>` : ''}
|
||||||
@@ -3198,10 +3162,32 @@ function renderPictureStreamsList(streams) {
|
|||||||
const processedStreams = streams.filter(s => s.stream_type === 'processed');
|
const processedStreams = streams.filter(s => s.stream_type === 'processed');
|
||||||
const staticImageStreams = streams.filter(s => s.stream_type === 'static_image');
|
const staticImageStreams = streams.filter(s => s.stream_type === 'static_image');
|
||||||
|
|
||||||
container.innerHTML =
|
const addCard = (type, labelKey) => `
|
||||||
renderStreamGroup('raw', '🖥️', 'streams.group.raw', rawStreams.length, 'raw', 'streams.add.raw', rawStreams.map(renderCard).join('')) +
|
<div class="template-card add-template-card" onclick="showAddStreamModal('${type}')">
|
||||||
renderStreamGroup('static_image', '🖼️', 'streams.group.static_image', staticImageStreams.length, 'static_image', 'streams.add.static_image', staticImageStreams.map(renderCard).join('')) +
|
<div class="add-template-icon">+</div>
|
||||||
renderStreamGroup('processed', '🎨', 'streams.group.processed', processedStreams.length, 'processed', 'streams.add.processed', processedStreams.map(renderCard).join(''));
|
<div class="add-template-label">${t(labelKey)}</div>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
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 = `<div class="stream-tab-bar">${tabs.map(tab =>
|
||||||
|
`<button class="stream-tab-btn${tab.key === activeTab ? ' active' : ''}" data-stream-tab="${tab.key}" onclick="switchStreamTab('${tab.key}')">${tab.icon} ${t(tab.titleKey)} <span class="stream-tab-count">${tab.streams.length}</span></button>`
|
||||||
|
).join('')}</div>`;
|
||||||
|
|
||||||
|
const panels = tabs.map(tab =>
|
||||||
|
`<div class="stream-tab-panel${tab.key === activeTab ? ' active' : ''}" id="stream-tab-${tab.key}">
|
||||||
|
<div class="templates-grid">
|
||||||
|
${tab.streams.map(renderCard).join('')}
|
||||||
|
${addCard(tab.key, tab.addLabelKey)}
|
||||||
|
</div>
|
||||||
|
</div>`
|
||||||
|
).join('');
|
||||||
|
|
||||||
|
container.innerHTML = tabBar + panels;
|
||||||
}
|
}
|
||||||
|
|
||||||
function onStreamTypeChange() {
|
function onStreamTypeChange() {
|
||||||
|
|||||||
@@ -200,8 +200,8 @@
|
|||||||
"common.edit": "Edit",
|
"common.edit": "Edit",
|
||||||
"streams.title": "\uD83D\uDCFA Picture Streams",
|
"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.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.raw": "Screen Capture",
|
||||||
"streams.group.processed": "Processed Streams",
|
"streams.group.processed": "Processed",
|
||||||
"streams.add": "Add Picture Stream",
|
"streams.add": "Add Picture Stream",
|
||||||
"streams.add.raw": "Add Screen Capture",
|
"streams.add.raw": "Add Screen Capture",
|
||||||
"streams.add.processed": "Add Processed Stream",
|
"streams.add.processed": "Add Processed Stream",
|
||||||
@@ -281,7 +281,7 @@
|
|||||||
"device.stream_settings.smoothing": "Smoothing:",
|
"device.stream_settings.smoothing": "Smoothing:",
|
||||||
"device.stream_settings.smoothing_hint": "Temporal blending between frames (0=none, 1=full). Reduces flicker.",
|
"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",
|
"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.add.static_image": "Add Static Image",
|
||||||
"streams.edit.static_image": "Edit Static Image",
|
"streams.edit.static_image": "Edit Static Image",
|
||||||
"streams.type.static_image": "Static Image",
|
"streams.type.static_image": "Static Image",
|
||||||
|
|||||||
@@ -200,8 +200,8 @@
|
|||||||
"common.edit": "Редактировать",
|
"common.edit": "Редактировать",
|
||||||
"streams.title": "\uD83D\uDCFA Видеопотоки",
|
"streams.title": "\uD83D\uDCFA Видеопотоки",
|
||||||
"streams.description": "Видеопотоки определяют конвейер захвата. Сырой поток захватывает экран с помощью шаблона захвата. Обработанный поток применяет постобработку к другому потоку. Назначайте потоки устройствам.",
|
"streams.description": "Видеопотоки определяют конвейер захвата. Сырой поток захватывает экран с помощью шаблона захвата. Обработанный поток применяет постобработку к другому потоку. Назначайте потоки устройствам.",
|
||||||
"streams.group.raw": "Потоки Захвата Экрана",
|
"streams.group.raw": "Захват Экрана",
|
||||||
"streams.group.processed": "Обработанные Потоки",
|
"streams.group.processed": "Обработанные",
|
||||||
"streams.add": "Добавить Видеопоток",
|
"streams.add": "Добавить Видеопоток",
|
||||||
"streams.add.raw": "Добавить Захват Экрана",
|
"streams.add.raw": "Добавить Захват Экрана",
|
||||||
"streams.add.processed": "Добавить Обработанный",
|
"streams.add.processed": "Добавить Обработанный",
|
||||||
@@ -281,7 +281,7 @@
|
|||||||
"device.stream_settings.smoothing": "Сглаживание:",
|
"device.stream_settings.smoothing": "Сглаживание:",
|
||||||
"device.stream_settings.smoothing_hint": "Временное смешивание между кадрами (0=нет, 1=полное). Уменьшает мерцание.",
|
"device.stream_settings.smoothing_hint": "Временное смешивание между кадрами (0=нет, 1=полное). Уменьшает мерцание.",
|
||||||
"device.tip.stream_selector": "Настройки видеопотока и проекции LED для этого устройства",
|
"device.tip.stream_selector": "Настройки видеопотока и проекции LED для этого устройства",
|
||||||
"streams.group.static_image": "Статические изображения",
|
"streams.group.static_image": "Статические",
|
||||||
"streams.add.static_image": "Добавить статическое изображение",
|
"streams.add.static_image": "Добавить статическое изображение",
|
||||||
"streams.edit.static_image": "Редактировать статическое изображение",
|
"streams.edit.static_image": "Редактировать статическое изображение",
|
||||||
"streams.type.static_image": "Статическое изображение",
|
"streams.type.static_image": "Статическое изображение",
|
||||||
|
|||||||
@@ -2176,67 +2176,58 @@ input:-webkit-autofill:focus {
|
|||||||
margin-right: 6px;
|
margin-right: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Stream group sections */
|
/* Stream sub-tabs */
|
||||||
.stream-group {
|
.stream-tab-bar {
|
||||||
margin-bottom: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stream-group:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stream-group-header {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 4px;
|
||||||
margin-bottom: 12px;
|
|
||||||
padding-bottom: 8px;
|
|
||||||
border-bottom: 2px solid var(--border-color);
|
border-bottom: 2px solid var(--border-color);
|
||||||
cursor: pointer;
|
margin-bottom: 16px;
|
||||||
user-select: none;
|
|
||||||
transition: opacity 0.2s;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.stream-group-header:hover {
|
.stream-tab-btn {
|
||||||
opacity: 0.8;
|
background: none;
|
||||||
}
|
border: none;
|
||||||
|
padding: 8px 14px;
|
||||||
.stream-group-chevron {
|
font-size: 0.9rem;
|
||||||
font-size: 10px;
|
font-weight: 500;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
flex-shrink: 0;
|
cursor: pointer;
|
||||||
width: 12px;
|
border-bottom: 2px solid transparent;
|
||||||
transition: transform 0.2s;
|
margin-bottom: -2px;
|
||||||
|
transition: color 0.2s, border-color 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stream-group.collapsed .stream-group-header {
|
.stream-tab-btn:hover {
|
||||||
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;
|
|
||||||
color: var(--text-color);
|
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);
|
background: var(--border-color);
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
font-size: 0.75rem;
|
font-size: 0.7rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
padding: 2px 8px;
|
padding: 1px 6px;
|
||||||
border-radius: 10px;
|
border-radius: 8px;
|
||||||
min-width: 20px;
|
margin-left: 4px;
|
||||||
text-align: center;
|
}
|
||||||
|
|
||||||
|
.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 */
|
/* Image Lightbox */
|
||||||
|
|||||||
Reference in New Issue
Block a user