Added picture-in-picture button to video player and search filter for manage content page
This commit is contained in:
@@ -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">×</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" />
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
Reference in New Issue
Block a user