diff --git a/public/index.html b/public/index.html index d07b82f..e2942b4 100644 --- a/public/index.html +++ b/public/index.html @@ -189,6 +189,12 @@ + + + + + @@ -517,6 +523,10 @@ Select a source... + + + × + Show All Hide All @@ -674,6 +684,12 @@ + + + + + diff --git a/public/js/components/SourceManager.js b/public/js/components/SourceManager.js index 98113d2..e9c900c 100644 --- a/public/js/components/SourceManager.js +++ b/public/js/components/SourceManager.js @@ -15,6 +15,7 @@ class SourceManager { this.hiddenSet = new Set(); // Set of hidden item keys (current state) this.originalHiddenSet = new Set(); // Set of hidden item keys (state when loaded) this.expandedGroups = new Set(); // Set of expanded group IDs + this.searchQuery = ''; // Search filter for content browser this.init(); } @@ -426,6 +427,23 @@ class SourceManager { // Save Changes button document.getElementById('content-save')?.addEventListener('click', () => this.saveContentChanges()); + + // Search input + const searchInput = document.getElementById('content-search'); + const searchClear = searchInput?.parentElement?.querySelector('.search-clear'); + + searchInput?.addEventListener('input', (e) => { + this.searchQuery = e.target.value.toLowerCase().trim(); + this.renderTree(); + }); + + searchClear?.addEventListener('click', () => { + if (searchInput) { + searchInput.value = ''; + this.searchQuery = ''; + this.renderTree(); + } + }); } /** @@ -565,16 +583,45 @@ class SourceManager { } } + /** + * Get groups filtered by search query + */ + getFilteredGroups() { + if (!this.treeData?.groups) return []; + if (!this.searchQuery) return this.treeData.groups; + + return this.treeData.groups + .map(group => { + // Check if group name matches + const groupMatches = group.name.toLowerCase().includes(this.searchQuery); + + // Filter items that match + const matchingItems = group.items.filter(item => + item.name.toLowerCase().includes(this.searchQuery) + ); + + // Include group if name matches OR has matching items + if (groupMatches || matchingItems.length > 0) { + return { ...group, items: groupMatches ? group.items : matchingItems }; + } + return null; + }) + .filter(Boolean); + } + /** * Render the full tree based on current state */ renderTree() { - if (!this.treeData || !this.treeData.groups.length) { - this.contentTree.innerHTML = 'No content found'; + const groups = this.getFilteredGroups(); + + if (!groups.length) { + const msg = this.searchQuery ? 'No matches found' : 'No content found'; + this.contentTree.innerHTML = `${msg}`; return; } - const html = this.treeData.groups.map(group => this.getGroupHtml(group)).join(''); + const html = groups.map(group => this.getGroupHtml(group)).join(''); this.contentTree.innerHTML = html; // Attach event listeners @@ -669,10 +716,11 @@ class SourceManager { this.expandedGroups.add(groupId); } - // Re-render only this group + // Re-render only this group - use filtered groups to respect search const groupEl = this.contentTree.querySelector(`.content-group[data-group-id="${CSS.escape(groupId)}"]`); if (groupEl) { - const group = this.treeData.groups.find(g => g.id === groupId); + const filteredGroups = this.getFilteredGroups(); + const group = filteredGroups.find(g => g.id === groupId); if (group) { const newHtml = this.getGroupHtml(group); groupEl.outerHTML = newHtml; diff --git a/public/js/components/VideoPlayer.js b/public/js/components/VideoPlayer.js index 5c33b75..fe558e3 100644 --- a/public/js/components/VideoPlayer.js +++ b/public/js/components/VideoPlayer.js @@ -267,6 +267,13 @@ class VideoPlayer { this.toggleFullscreen(); }); + // Picture-in-Picture + const btnPip = document.getElementById('btn-pip'); + btnPip?.addEventListener('click', (e) => { + e.stopPropagation(); + this.togglePictureInPicture(); + }); + this.container.addEventListener('dblclick', () => this.toggleFullscreen()); // Overlay Auto-hide Logic @@ -325,6 +332,24 @@ class VideoPlayer { } } + /** + * Toggle Picture-in-Picture mode + */ + async togglePictureInPicture() { + try { + if (document.pictureInPictureElement) { + await document.exitPictureInPicture(); + } else if (document.pictureInPictureEnabled && this.video.readyState >= 2) { + await this.video.requestPictureInPicture(); + } + } catch (err) { + // Silently fail - Firefox users can use native PiP button + if (err.name !== 'NotAllowedError') { + console.error('Picture-in-Picture error:', err); + } + } + } + /** diff --git a/public/js/pages/WatchPage.js b/public/js/pages/WatchPage.js index ff1a1dc..b6b64d6 100644 --- a/public/js/pages/WatchPage.js +++ b/public/js/pages/WatchPage.js @@ -102,6 +102,10 @@ class WatchPage { // Fullscreen this.fullscreenBtn?.addEventListener('click', () => this.toggleFullscreen()); + // Picture-in-Picture + const pipBtn = document.getElementById('watch-pip'); + pipBtn?.addEventListener('click', () => this.togglePictureInPicture()); + // Progress bar this.progressSlider?.addEventListener('input', (e) => this.seek(e.target.value)); @@ -374,6 +378,21 @@ class WatchPage { } } + async togglePictureInPicture() { + try { + if (document.pictureInPictureElement) { + await document.exitPictureInPicture(); + } else if (document.pictureInPictureEnabled && this.video.readyState >= 2) { + await this.video.requestPictureInPicture(); + } + } catch (err) { + // Silently fail - Firefox users can use native PiP button + if (err.name !== 'NotAllowedError') { + console.error('Picture-in-Picture error:', err); + } + } + } + // === UI Updates === updateProgress() {
No content found
${msg}