Changed content manager group behavior and added save button
This commit is contained in:
+3
-1
@@ -305,7 +305,8 @@
|
||||
</label>
|
||||
<div class="setting-info">
|
||||
<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>
|
||||
@@ -339,6 +340,7 @@
|
||||
<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-hide-all">Hide All</button>
|
||||
<button class="btn btn-sm btn-primary" id="content-save">💾 Save Changes</button>
|
||||
</div>
|
||||
</div>
|
||||
<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;
|
||||
|
||||
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 += `
|
||||
<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-name">${groupName}</span>
|
||||
<span class="group-count">${channels.length}</span>
|
||||
<span class="group-count">${visibleChannels.length}</span>
|
||||
</div>
|
||||
<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 channelHidden = !isFavoritesGroup && this.isHidden('channel', channel.sourceId, rawChannelId);
|
||||
if (channelHidden && !this.showHidden) continue;
|
||||
|
||||
const isActive = this.currentChannel?.id === channel.id;
|
||||
const isFavorite = this.isFavorite(channel.sourceId, channel.id);
|
||||
|
||||
@@ -283,12 +283,13 @@ class EpgGuide {
|
||||
if (!channelList) return;
|
||||
|
||||
// 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 => {
|
||||
// Use streamId (raw ID) for hidden check since that's what SourceManager stores
|
||||
const rawChannelId = ch.streamId || ch.id;
|
||||
const isChannelHidden = channelList.isHidden('channel', ch.sourceId, rawChannelId);
|
||||
const isGroupHidden = channelList.isHidden('group', ch.sourceId, ch.groupTitle);
|
||||
return !isChannelHidden && !isGroupHidden;
|
||||
return !isChannelHidden;
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -371,6 +371,9 @@ class SourceManager {
|
||||
// Show All / Hide All buttons
|
||||
document.getElementById('content-show-all')?.addEventListener('click', () => this.setAllVisibility(true));
|
||||
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)
|
||||
*/
|
||||
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);
|
||||
|
||||
// Calculate partial state if needed, but for now simple checked/unchecked
|
||||
const checked = !isHidden;
|
||||
// Group checkbox is checked if ANY child is visible (derived state)
|
||||
const hasVisibleChild = group.items.some(item => !this.hiddenSet.has(`${item.type}:${item.id}`));
|
||||
const checked = hasVisibleChild;
|
||||
|
||||
let itemsHtml = '';
|
||||
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)
|
||||
*/
|
||||
async toggleVisibility(checkbox) {
|
||||
const sourceId = parseInt(checkbox.dataset.sourceId);
|
||||
toggleVisibility(checkbox) {
|
||||
const itemType = checkbox.dataset.type;
|
||||
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}`;
|
||||
if (isVisible) {
|
||||
this.hiddenSet.delete(key);
|
||||
@@ -735,166 +737,138 @@ class SourceManager {
|
||||
this.hiddenSet.add(key);
|
||||
}
|
||||
|
||||
try {
|
||||
// Fire API call
|
||||
const apiCall = isVisible
|
||||
? API.channels.show(sourceId, itemType, itemId)
|
||||
: API.channels.hide(sourceId, itemType, itemId);
|
||||
|
||||
if (itemType === 'vod_category' || itemType === 'series_category') {
|
||||
apiCall.catch(err => {
|
||||
console.error(`Error toggling ${itemType} visibility:`, err);
|
||||
checkbox.checked = !isVisible;
|
||||
});
|
||||
return;
|
||||
// Update parent group checkbox to reflect derived state
|
||||
const groupEl = checkbox.closest('.content-group');
|
||||
if (groupEl) {
|
||||
const groupCheckbox = groupEl.querySelector('.group-checkbox');
|
||||
if (groupCheckbox) {
|
||||
const groupId = groupEl.dataset.groupId;
|
||||
const group = this.treeData.groups.find(g => g.id === groupId);
|
||||
if (group) {
|
||||
const hasVisibleChild = group.items.some(item => !this.hiddenSet.has(`${item.type}:${item.id}`));
|
||||
groupCheckbox.checked = hasVisibleChild;
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
const groupName = groupCb.dataset.id; // Helper uses name as ID
|
||||
toggleGroupChildren(groupCb) {
|
||||
const groupName = groupCb.dataset.id;
|
||||
const group = this.treeData.groups.find(g => g.name === groupName);
|
||||
if (!group) return;
|
||||
|
||||
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 => {
|
||||
const key = `${item.type}:${item.id}`;
|
||||
const currentlyHidden = this.hiddenSet.has(key);
|
||||
|
||||
// Only update if state changes
|
||||
if (isChecked && currentlyHidden) {
|
||||
if (isChecked) {
|
||||
this.hiddenSet.delete(key);
|
||||
itemsToUpdate.push({ sourceId: this.treeData.sourceId, itemType: item.type, itemId: item.id });
|
||||
} else if (!isChecked && !currentlyHidden) {
|
||||
} else {
|
||||
this.hiddenSet.add(key);
|
||||
itemsToUpdate.push({ sourceId: this.treeData.sourceId, itemType: item.type, itemId: item.id });
|
||||
}
|
||||
});
|
||||
|
||||
// 2. Update UI (re-render group without collapse)
|
||||
// Find group element and update its checkboxes manually OR re-render
|
||||
// Re-rendering is safest and fast for single group
|
||||
// Re-render group to update all checkboxes
|
||||
const groupEl = this.contentTree.querySelector(`.content-group[data-group-id="${CSS.escape(group.id)}"]`);
|
||||
if (groupEl) {
|
||||
groupEl.outerHTML = this.getGroupHtml(group);
|
||||
const newEl = this.contentTree.querySelector(`.content-group[data-group-id="${CSS.escape(group.id)}"]`);
|
||||
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;
|
||||
|
||||
// 1. Update State
|
||||
const items = [];
|
||||
|
||||
// Update state for all items
|
||||
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 => {
|
||||
const key = `${item.type}:${item.id}`;
|
||||
const isHidden = this.hiddenSet.has(key);
|
||||
|
||||
if (visible && isHidden) {
|
||||
if (visible) {
|
||||
this.hiddenSet.delete(key);
|
||||
items.push({ sourceId: this.treeData.sourceId, itemType: item.type, itemId: item.id });
|
||||
} else if (!visible && !isHidden) {
|
||||
} else {
|
||||
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();
|
||||
}
|
||||
|
||||
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 {
|
||||
if (visible) {
|
||||
await API.channels.bulkShow(items);
|
||||
} else {
|
||||
await API.channels.bulkHide(items);
|
||||
const sourceId = this.treeData.sourceId;
|
||||
const itemsToShow = [];
|
||||
const itemsToHide = [];
|
||||
|
||||
// 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) {
|
||||
await window.app.channelList.loadHiddenItems();
|
||||
window.app.channelList.render();
|
||||
}
|
||||
|
||||
if (saveBtn) {
|
||||
saveBtn.textContent = '✓ Saved!';
|
||||
setTimeout(() => {
|
||||
saveBtn.textContent = '💾 Save Changes';
|
||||
saveBtn.disabled = false;
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.error('Error setting all visibility:', err);
|
||||
alert('Failed to update visibility');
|
||||
console.error('Error saving content changes:', err);
|
||||
alert('Failed to save changes: ' + err.message);
|
||||
if (saveBtn) {
|
||||
saveBtn.textContent = '💾 Save Changes';
|
||||
saveBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user