Added picture-in-picture button to video player and search filter for manage content page

This commit is contained in:
Trevor Mears
2026-01-09 15:38:37 -08:00
parent e8e6ba27d7
commit 53a54c760c
4 changed files with 113 additions and 5 deletions
+16
View File
@@ -189,6 +189,12 @@
</div> </div>
</div> </div>
<button class="watch-btn" id="btn-pip" title="Picture-in-Picture">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="icon">
<path
d="M19 7h-8v6h8V7zm2-4H3c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H3V5h18v14z" />
</svg>
</button>
<button class="watch-btn" id="btn-fullscreen" title="Fullscreen"> <button class="watch-btn" id="btn-fullscreen" title="Fullscreen">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="icon"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="icon">
<path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z" /> <path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z" />
@@ -517,6 +523,10 @@
<select id="content-source-select" class="source-select"> <select id="content-source-select" class="source-select">
<option value="">Select a source...</option> <option value="">Select a source...</option>
</select> </select>
<div class="search-wrapper">
<input type="text" id="content-search" placeholder="Search groups/channels..." class="search-input">
<button type="button" class="search-clear" title="Clear search">&times;</button>
</div>
<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>
@@ -674,6 +684,12 @@
</div> </div>
</div> </div>
</div> </div>
<button class="watch-btn" id="watch-pip" title="Picture-in-Picture">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="icon">
<path
d="M19 7h-8v6h8V7zm2-4H3c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H3V5h18v14z" />
</svg>
</button>
<button class="watch-btn" id="watch-fullscreen" title="Fullscreen"> <button class="watch-btn" id="watch-fullscreen" title="Fullscreen">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="icon"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="icon">
<path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z" /> <path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z" />
+53 -5
View File
@@ -15,6 +15,7 @@ class SourceManager {
this.hiddenSet = new Set(); // Set of hidden item keys (current state) this.hiddenSet = new Set(); // Set of hidden item keys (current state)
this.originalHiddenSet = new Set(); // Set of hidden item keys (state when loaded) this.originalHiddenSet = new Set(); // Set of hidden item keys (state when loaded)
this.expandedGroups = new Set(); // Set of expanded group IDs this.expandedGroups = new Set(); // Set of expanded group IDs
this.searchQuery = ''; // Search filter for content browser
this.init(); this.init();
} }
@@ -426,6 +427,23 @@ class SourceManager {
// Save Changes button // Save Changes button
document.getElementById('content-save')?.addEventListener('click', () => this.saveContentChanges()); 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 * Render the full tree based on current state
*/ */
renderTree() { renderTree() {
if (!this.treeData || !this.treeData.groups.length) { const groups = this.getFilteredGroups();
this.contentTree.innerHTML = '<p class="hint">No content found</p>';
if (!groups.length) {
const msg = this.searchQuery ? 'No matches found' : 'No content found';
this.contentTree.innerHTML = `<p class="hint">${msg}</p>`;
return; 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; this.contentTree.innerHTML = html;
// Attach event listeners // Attach event listeners
@@ -669,10 +716,11 @@ class SourceManager {
this.expandedGroups.add(groupId); 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)}"]`); const groupEl = this.contentTree.querySelector(`.content-group[data-group-id="${CSS.escape(groupId)}"]`);
if (groupEl) { 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) { if (group) {
const newHtml = this.getGroupHtml(group); const newHtml = this.getGroupHtml(group);
groupEl.outerHTML = newHtml; groupEl.outerHTML = newHtml;
+25
View File
@@ -267,6 +267,13 @@ class VideoPlayer {
this.toggleFullscreen(); 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()); this.container.addEventListener('dblclick', () => this.toggleFullscreen());
// Overlay Auto-hide Logic // 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);
}
}
}
/** /**
+19
View File
@@ -102,6 +102,10 @@ class WatchPage {
// Fullscreen // Fullscreen
this.fullscreenBtn?.addEventListener('click', () => this.toggleFullscreen()); this.fullscreenBtn?.addEventListener('click', () => this.toggleFullscreen());
// Picture-in-Picture
const pipBtn = document.getElementById('watch-pip');
pipBtn?.addEventListener('click', () => this.togglePictureInPicture());
// Progress bar // Progress bar
this.progressSlider?.addEventListener('input', (e) => this.seek(e.target.value)); 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 === // === UI Updates ===
updateProgress() { updateProgress() {