Changed content manager group behavior and added save button
This commit is contained in:
+3
-1
@@ -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>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user