Changed content manager group behavior and added save button

This commit is contained in:
Trevor Mears
2025-12-28 16:36:58 -08:00
parent dc15e1f396
commit 2ebe52685f
4 changed files with 113 additions and 128 deletions
+3 -1
View File
@@ -305,7 +305,8 @@
</label> </label>
<div class="setting-info"> <div class="setting-info">
<span class="setting-label">Force Backend Proxy</span> <span class="setting-label">Force Backend Proxy</span>
<span class="setting-hint">Route all streams through the proxy server. Enable if streams don't play directly.</span> <span class="setting-hint">Route all streams through the proxy server. Enable if streams don't play
directly.</span>
</div> </div>
</div> </div>
</div> </div>
@@ -339,6 +340,7 @@
<div class="content-actions"> <div class="content-actions">
<button class="btn btn-sm btn-secondary" id="content-show-all">Show All</button> <button class="btn btn-sm btn-secondary" id="content-show-all">Show All</button>
<button class="btn btn-sm btn-secondary" id="content-hide-all">Hide All</button> <button class="btn btn-sm btn-secondary" id="content-hide-all">Hide All</button>
<button class="btn btn-sm btn-primary" id="content-save">💾 Save Changes</button>
</div> </div>
</div> </div>
<p class="hint" style="margin-bottom: var(--space-sm);">✓ Checked = Visible, ☐ Unchecked = Hidden</p> <p class="hint" style="margin-bottom: var(--space-sm);">✓ Checked = Visible, ☐ Unchecked = Hidden</p>
+15 -7
View File
@@ -283,25 +283,33 @@ class ChannelList {
if (channels.length === 0) continue; if (channels.length === 0) continue;
const isFavoritesGroup = groupName === 'Favorites'; const isFavoritesGroup = groupName === 'Favorites';
const groupHidden = !isFavoritesGroup && channels.length > 0 && this.isHidden('group', channels[0].sourceId, groupName);
if (groupHidden && !this.showHidden) continue; // Pre-filter visible channels for this group
const visibleChannels = channels.filter(channel => {
if (isFavoritesGroup) return true;
const rawChannelId = channel.streamId || channel.id;
const channelHidden = this.isHidden('channel', channel.sourceId, rawChannelId);
return !channelHidden || this.showHidden;
});
// Skip group if no visible channels (derived visibility)
if (visibleChannels.length === 0) continue;
html += ` html += `
<div class="channel-group"> <div class="channel-group">
<div class="group-header ${groupHidden ? 'hidden' : ''} ${this.collapsedGroups.has(groupName) ? 'collapsed' : ''} ${isFavoritesGroup ? 'favorites-group' : ''}" data-group="${groupName}"> <div class="group-header ${this.collapsedGroups.has(groupName) ? 'collapsed' : ''} ${isFavoritesGroup ? 'favorites-group' : ''}" data-group="${groupName}">
<span class="group-toggle">▼</span> <span class="group-toggle">▼</span>
<span class="group-name">${groupName}</span> <span class="group-name">${groupName}</span>
<span class="group-count">${channels.length}</span> <span class="group-count">${visibleChannels.length}</span>
</div> </div>
<div class="group-channels"> <div class="group-channels">
`; `;
for (const channel of channels) {
// Use streamId (raw ID) for hidden check since that's what SourceManager stores for (const channel of visibleChannels) {
// Check hidden again for styling (showHidden mode)
const rawChannelId = channel.streamId || channel.id; const rawChannelId = channel.streamId || channel.id;
const channelHidden = !isFavoritesGroup && this.isHidden('channel', channel.sourceId, rawChannelId); const channelHidden = !isFavoritesGroup && this.isHidden('channel', channel.sourceId, rawChannelId);
if (channelHidden && !this.showHidden) continue;
const isActive = this.currentChannel?.id === channel.id; const isActive = this.currentChannel?.id === channel.id;
const isFavorite = this.isFavorite(channel.sourceId, channel.id); const isFavorite = this.isFavorite(channel.sourceId, channel.id);
+3 -2
View File
@@ -283,12 +283,13 @@ class EpgGuide {
if (!channelList) return; if (!channelList) return;
// Get channels and filter out hidden ones (always enforce hidden in EPG) // Get channels and filter out hidden ones (always enforce hidden in EPG)
// Note: We only check individual channel visibility, not group visibility
// A group is implicitly visible if it has any visible children
const playableChannels = (channelList.channels || []).filter(ch => { const playableChannels = (channelList.channels || []).filter(ch => {
// Use streamId (raw ID) for hidden check since that's what SourceManager stores // Use streamId (raw ID) for hidden check since that's what SourceManager stores
const rawChannelId = ch.streamId || ch.id; const rawChannelId = ch.streamId || ch.id;
const isChannelHidden = channelList.isHidden('channel', ch.sourceId, rawChannelId); const isChannelHidden = channelList.isHidden('channel', ch.sourceId, rawChannelId);
const isGroupHidden = channelList.isHidden('group', ch.sourceId, ch.groupTitle); return !isChannelHidden;
return !isChannelHidden && !isGroupHidden;
}); });
+92 -118
View File
@@ -371,6 +371,9 @@ class SourceManager {
// Show All / Hide All buttons // Show All / Hide All buttons
document.getElementById('content-show-all')?.addEventListener('click', () => this.setAllVisibility(true)); document.getElementById('content-show-all')?.addEventListener('click', () => this.setAllVisibility(true));
document.getElementById('content-hide-all')?.addEventListener('click', () => this.setAllVisibility(false)); document.getElementById('content-hide-all')?.addEventListener('click', () => this.setAllVisibility(false));
// Save Changes button
document.getElementById('content-save')?.addEventListener('click', () => this.saveContentChanges());
} }
/** /**
@@ -516,11 +519,11 @@ class SourceManager {
* Get HTML for a group (and its items if expanded) * Get HTML for a group (and its items if expanded)
*/ */
getGroupHtml(group) { getGroupHtml(group) {
const isHidden = this.hiddenSet.has(`group:${group.name}`); // We use name as ID for groups usually
const isExpanded = this.expandedGroups.has(group.id); const isExpanded = this.expandedGroups.has(group.id);
// Calculate partial state if needed, but for now simple checked/unchecked // Group checkbox is checked if ANY child is visible (derived state)
const checked = !isHidden; const hasVisibleChild = group.items.some(item => !this.hiddenSet.has(`${item.type}:${item.id}`));
const checked = hasVisibleChild;
let itemsHtml = ''; let itemsHtml = '';
if (isExpanded) { if (isExpanded) {
@@ -718,16 +721,15 @@ class SourceManager {
} }
/** /**
* Toggle visibility of a single item * Toggle visibility of a single item (LOCAL STATE ONLY - use Save to persist)
* Checked = show (remove from hidden), Unchecked = hide (add to hidden) * Checked = show (remove from hidden), Unchecked = hide (add to hidden)
*/ */
async toggleVisibility(checkbox) { toggleVisibility(checkbox) {
const sourceId = parseInt(checkbox.dataset.sourceId);
const itemType = checkbox.dataset.type; const itemType = checkbox.dataset.type;
const itemId = checkbox.dataset.id; const itemId = checkbox.dataset.id;
const isVisible = checkbox.checked; // This is the NEW state taken from UI const isVisible = checkbox.checked;
// Update local state directly // Update local state only (will be persisted when Save is clicked)
const key = `${itemType}:${itemId}`; const key = `${itemType}:${itemId}`;
if (isVisible) { if (isVisible) {
this.hiddenSet.delete(key); this.hiddenSet.delete(key);
@@ -735,166 +737,138 @@ class SourceManager {
this.hiddenSet.add(key); this.hiddenSet.add(key);
} }
try { // Update parent group checkbox to reflect derived state
// Fire API call const groupEl = checkbox.closest('.content-group');
const apiCall = isVisible if (groupEl) {
? API.channels.show(sourceId, itemType, itemId) const groupCheckbox = groupEl.querySelector('.group-checkbox');
: API.channels.hide(sourceId, itemType, itemId); if (groupCheckbox) {
const groupId = groupEl.dataset.groupId;
if (itemType === 'vod_category' || itemType === 'series_category') { const group = this.treeData.groups.find(g => g.id === groupId);
apiCall.catch(err => { if (group) {
console.error(`Error toggling ${itemType} visibility:`, err); const hasVisibleChild = group.items.some(item => !this.hiddenSet.has(`${item.type}:${item.id}`));
checkbox.checked = !isVisible; groupCheckbox.checked = hasVisibleChild;
}); }
return;
} }
await apiCall;
// No need to "refresh" since we updated state locally and UI is already correct
// Sync Channel List
if (window.app?.channelList) {
// We could just pass the change? But safely reloading hidden is better
await window.app.channelList.loadHiddenItems();
window.app.channelList.render();
}
} catch (err) {
console.error('Error toggling visibility:', err);
// Revert state and UI
if (isVisible) this.hiddenSet.add(key);
else this.hiddenSet.delete(key);
checkbox.checked = !isVisible;
} }
} }
/** /**
* Toggle all children of a group efficiently * Toggle all children of a group (LOCAL STATE ONLY - use Save to persist)
*/ */
async toggleGroupChildren(groupCb) { toggleGroupChildren(groupCb) {
const groupName = groupCb.dataset.id; // Helper uses name as ID const groupName = groupCb.dataset.id;
const group = this.treeData.groups.find(g => g.name === groupName); const group = this.treeData.groups.find(g => g.name === groupName);
if (!group) return; if (!group) return;
const isChecked = groupCb.checked; const isChecked = groupCb.checked;
const itemsToUpdate = [];
// 1. Update State for GROUP itself
if (isChecked) {
this.hiddenSet.delete(`group:${group.name}`);
} else {
this.hiddenSet.add(`group:${group.name}`);
}
// Fire API for the group visibility change
const groupApiCall = isChecked
? API.channels.show(this.treeData.sourceId, 'group', group.name)
: API.channels.hide(this.treeData.sourceId, 'group', group.name);
groupApiCall.catch(err => console.error('Error updating group visibility:', err));
// Update state for all children
group.items.forEach(item => { group.items.forEach(item => {
const key = `${item.type}:${item.id}`; const key = `${item.type}:${item.id}`;
const currentlyHidden = this.hiddenSet.has(key); if (isChecked) {
// Only update if state changes
if (isChecked && currentlyHidden) {
this.hiddenSet.delete(key); this.hiddenSet.delete(key);
itemsToUpdate.push({ sourceId: this.treeData.sourceId, itemType: item.type, itemId: item.id }); } else {
} else if (!isChecked && !currentlyHidden) {
this.hiddenSet.add(key); this.hiddenSet.add(key);
itemsToUpdate.push({ sourceId: this.treeData.sourceId, itemType: item.type, itemId: item.id });
} }
}); });
// 2. Update UI (re-render group without collapse) // Re-render group to update all checkboxes
// Find group element and update its checkboxes manually OR re-render
// Re-rendering is safest and fast for single group
const groupEl = this.contentTree.querySelector(`.content-group[data-group-id="${CSS.escape(group.id)}"]`); const groupEl = this.contentTree.querySelector(`.content-group[data-group-id="${CSS.escape(group.id)}"]`);
if (groupEl) { if (groupEl) {
groupEl.outerHTML = this.getGroupHtml(group); groupEl.outerHTML = this.getGroupHtml(group);
const newEl = this.contentTree.querySelector(`.content-group[data-group-id="${CSS.escape(group.id)}"]`); const newEl = this.contentTree.querySelector(`.content-group[data-group-id="${CSS.escape(group.id)}"]`);
if (newEl) this.attachTreeListeners(newEl); if (newEl) this.attachTreeListeners(newEl);
} }
if (itemsToUpdate.length === 0) return;
try {
if (isChecked) {
await API.channels.bulkShow(itemsToUpdate);
} else {
await API.channels.bulkHide(itemsToUpdate);
}
if (window.app?.channelList) {
await window.app.channelList.loadHiddenItems();
window.app.channelList.render();
}
} catch (err) {
console.error('Error toggling group children:', err);
// Revert? Complex.
}
} }
/** /**
* Set visibility for all items using Virtual State * Set visibility for all items (LOCAL STATE ONLY - use Save to persist)
*/ */
async setAllVisibility(visible) { setAllVisibility(visible) {
if (!this.treeData || !this.treeData.groups) return; if (!this.treeData || !this.treeData.groups) return;
// 1. Update State // Update state for all items
const items = [];
this.treeData.groups.forEach(group => { this.treeData.groups.forEach(group => {
// Group visibility (if it's a channel group)
if (group.type === 'group') {
const groupKey = `group:${group.name}`;
const isGroupHidden = this.hiddenSet.has(groupKey);
// Only update if state changes
if (visible && isGroupHidden) {
this.hiddenSet.delete(groupKey);
items.push({ sourceId: this.treeData.sourceId, itemType: 'group', itemId: group.name });
} else if (!visible && !isGroupHidden) {
this.hiddenSet.add(groupKey);
items.push({ sourceId: this.treeData.sourceId, itemType: 'group', itemId: group.name });
}
}
// Items visibility
group.items.forEach(item => { group.items.forEach(item => {
const key = `${item.type}:${item.id}`; const key = `${item.type}:${item.id}`;
const isHidden = this.hiddenSet.has(key); if (visible) {
if (visible && isHidden) {
this.hiddenSet.delete(key); this.hiddenSet.delete(key);
items.push({ sourceId: this.treeData.sourceId, itemType: item.type, itemId: item.id }); } else {
} else if (!visible && !isHidden) {
this.hiddenSet.add(key); this.hiddenSet.add(key);
items.push({ sourceId: this.treeData.sourceId, itemType: item.type, itemId: item.id });
} }
}); });
}); });
// 2. Re-render View (Fast because we only render headers + expanded groups) // Re-render to reflect changes
this.renderTree(); this.renderTree();
}
if (items.length === 0) return; /**
* Save all content visibility changes to the server
*/
async saveContentChanges() {
if (!this.treeData) {
alert('No content loaded to save');
return;
}
const saveBtn = document.getElementById('content-save');
if (saveBtn) {
saveBtn.disabled = true;
saveBtn.textContent = '⏳ Saving...';
}
try { try {
if (visible) { const sourceId = this.treeData.sourceId;
await API.channels.bulkShow(items); const itemsToShow = [];
} else { const itemsToHide = [];
await API.channels.bulkHide(items);
// Collect all items and determine their visibility
this.treeData.groups.forEach(group => {
group.items.forEach(item => {
const key = `${item.type}:${item.id}`;
const isHidden = this.hiddenSet.has(key);
if (isHidden) {
itemsToHide.push({ sourceId, itemType: item.type, itemId: item.id });
} else {
itemsToShow.push({ sourceId, itemType: item.type, itemId: item.id });
}
});
});
// Execute bulk operations
const promises = [];
if (itemsToShow.length > 0) {
promises.push(API.channels.bulkShow(itemsToShow));
}
if (itemsToHide.length > 0) {
promises.push(API.channels.bulkHide(itemsToHide));
} }
await Promise.all(promises);
// Sync Channel List
if (window.app?.channelList) { if (window.app?.channelList) {
await window.app.channelList.loadHiddenItems(); await window.app.channelList.loadHiddenItems();
window.app.channelList.render(); window.app.channelList.render();
} }
if (saveBtn) {
saveBtn.textContent = '✓ Saved!';
setTimeout(() => {
saveBtn.textContent = '💾 Save Changes';
saveBtn.disabled = false;
}, 1500);
}
} catch (err) { } catch (err) {
console.error('Error setting all visibility:', err); console.error('Error saving content changes:', err);
alert('Failed to update visibility'); alert('Failed to save changes: ' + err.message);
if (saveBtn) {
saveBtn.textContent = '💾 Save Changes';
saveBtn.disabled = false;
}
} }
} }