fix: filter homepage recent sections by hidden status
This commit is contained in:
+75
-9
@@ -980,7 +980,8 @@ video::cue {
|
||||
|
||||
.epg-grid {
|
||||
height: calc(100vh - var(--navbar-height) - 60px);
|
||||
overflow: auto;
|
||||
overflow: hidden;
|
||||
overflow-y: auto;
|
||||
padding: var(--space-md);
|
||||
-webkit-overflow-scrolling: touch;
|
||||
overscroll-behavior: contain;
|
||||
@@ -1887,6 +1888,7 @@ kbd {
|
||||
--sidebar-width: 320px;
|
||||
--space-lg: 16px;
|
||||
--space-xl: 24px;
|
||||
--epg-sidebar-width: 180px;
|
||||
}
|
||||
|
||||
/* Navigation - Hide text labels, show only icons */
|
||||
@@ -1900,7 +1902,8 @@ kbd {
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.navbar-menu span:not(.nav-icon) {
|
||||
/* Hide nav link text on mobile, but keep Now Playing visible */
|
||||
.navbar-menu .nav-link span:not(.nav-icon) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -2039,7 +2042,11 @@ kbd {
|
||||
}
|
||||
|
||||
.epg-channel-info {
|
||||
width: 180px;
|
||||
width: var(--epg-sidebar-width);
|
||||
}
|
||||
|
||||
.epg-time-header {
|
||||
margin-left: var(--epg-sidebar-width);
|
||||
}
|
||||
|
||||
.epg-time-slot {
|
||||
@@ -4314,7 +4321,7 @@ kbd {
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 14px;
|
||||
margin-right: 16px;
|
||||
margin-right: var(--space-md);
|
||||
background: linear-gradient(135deg, var(--color-accent) 0%, #8b5cf6 100%);
|
||||
border-radius: var(--radius-full);
|
||||
color: white;
|
||||
@@ -4360,13 +4367,16 @@ kbd {
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile responsive */
|
||||
/* Mobile responsive - show in dropdown menu */
|
||||
@media (max-width: 768px) {
|
||||
.now-playing-indicator {
|
||||
max-width: 140px;
|
||||
padding: 5px 10px;
|
||||
font-size: 12px;
|
||||
margin-right: 8px;
|
||||
width: calc(100% - 32px);
|
||||
max-width: none;
|
||||
margin: 0 16px var(--space-sm) 16px;
|
||||
padding: 10px 16px;
|
||||
font-size: 13px;
|
||||
border-radius: var(--radius-md);
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4406,7 +4416,63 @@ kbd {
|
||||
color: var(--color-text-bright);
|
||||
}
|
||||
|
||||
/* Scroll Wrapper with Navigation Arrows */
|
||||
.scroll-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.scroll-arrow {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
z-index: 10;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all var(--transition-fast);
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
.scroll-arrow:hover {
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
border-color: var(--color-accent);
|
||||
transform: translateY(-50%) scale(1.1);
|
||||
}
|
||||
|
||||
.scroll-arrow svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.scroll-arrow.scroll-left {
|
||||
left: -8px;
|
||||
}
|
||||
|
||||
.scroll-arrow.scroll-right {
|
||||
right: -8px;
|
||||
}
|
||||
|
||||
.scroll-arrow.hidden {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Hide arrows on touch devices (they can swipe) */
|
||||
@media (hover: none) {
|
||||
.scroll-arrow {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.horizontal-scroll {
|
||||
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
overflow-x: auto;
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 259 B After Width: | Height: | Size: 23 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 47 KiB |
+1
-1
@@ -38,12 +38,12 @@
|
||||
<span></span>
|
||||
<span></span>
|
||||
</button>
|
||||
<div class="navbar-menu" id="navbar-menu">
|
||||
<!-- Now Playing indicator for movies/series -->
|
||||
<a href="#" class="now-playing-indicator hidden" id="now-playing-indicator" data-page="watch">
|
||||
<span class="now-playing-icon">▶</span>
|
||||
<span class="now-playing-text" id="now-playing-text">Now Playing</span>
|
||||
</a>
|
||||
<div class="navbar-menu" id="navbar-menu">
|
||||
<a href="#" class="nav-link active" data-page="home">
|
||||
<span class="nav-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"
|
||||
class="icon">
|
||||
|
||||
@@ -439,7 +439,7 @@ class EpgGuide {
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
<div class="epg-scroll-container" style="overflow-y: auto; max-height: calc(100vh - 200px);">
|
||||
<div class="epg-scroll-container" style="overflow: auto; max-height: calc(100vh - 200px);">
|
||||
<div class="epg-spacer" style="height: ${totalHeight}px; position: relative;">
|
||||
<div class="epg-channel-rows" style="position: absolute; top: 0; left: 0; right: 0;"></div>
|
||||
</div>
|
||||
@@ -461,11 +461,25 @@ class EpgGuide {
|
||||
if (this._scrollHandler) {
|
||||
this.scrollContainer?.removeEventListener('scroll', this._scrollHandler);
|
||||
}
|
||||
if (this._hScrollHandler) {
|
||||
this.scrollContainer?.removeEventListener('scroll', this._hScrollHandler);
|
||||
}
|
||||
|
||||
// Set up scroll handler for virtual scrolling
|
||||
// Get reference to time header for horizontal scroll sync
|
||||
this.timeHeader = this.container.querySelector('.epg-time-header');
|
||||
|
||||
// Set up scroll handler for virtual scrolling (debounced for performance)
|
||||
this._scrollHandler = this.debounce(() => this.updateVisibleRows(), 16); // ~60fps
|
||||
this.scrollContainer.addEventListener('scroll', this._scrollHandler);
|
||||
|
||||
// Set up horizontal scroll sync (instant, not debounced)
|
||||
this._hScrollHandler = () => {
|
||||
if (this.timeHeader && this.scrollContainer) {
|
||||
this.timeHeader.style.transform = `translateX(-${this.scrollContainer.scrollLeft}px)`;
|
||||
}
|
||||
};
|
||||
this.scrollContainer.addEventListener('scroll', this._hScrollHandler);
|
||||
|
||||
// Initial render of visible rows
|
||||
this.updateVisibleRows();
|
||||
|
||||
|
||||
@@ -1251,6 +1251,9 @@ class VideoPlayer {
|
||||
* Stop playback
|
||||
*/
|
||||
stop() {
|
||||
// Stop any running transcode session first
|
||||
this.stopTranscodeSession();
|
||||
|
||||
if (this.hls) {
|
||||
this.hls.destroy();
|
||||
this.hls = null;
|
||||
|
||||
@@ -35,42 +35,124 @@ class HomePage {
|
||||
<div class="section-header">
|
||||
<h2>Favorite Channels</h2>
|
||||
</div>
|
||||
<div class="scroll-wrapper">
|
||||
<button class="scroll-arrow scroll-left" aria-label="Scroll left">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"/></svg>
|
||||
</button>
|
||||
<div class="horizontal-scroll channel-tiles" id="favorite-channels-list">
|
||||
<div class="loading-state">Loading favorites...</div>
|
||||
</div>
|
||||
<button class="scroll-arrow scroll-right" aria-label="Scroll right">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="dashboard-section" id="continue-watching-section">
|
||||
<div class="section-header">
|
||||
<h2>Continue Watching</h2>
|
||||
</div>
|
||||
<div class="scroll-wrapper">
|
||||
<button class="scroll-arrow scroll-left" aria-label="Scroll left">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"/></svg>
|
||||
</button>
|
||||
<div class="horizontal-scroll" id="continue-watching-list">
|
||||
<div class="loading-state">Loading history...</div>
|
||||
</div>
|
||||
<button class="scroll-arrow scroll-right" aria-label="Scroll right">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="dashboard-section">
|
||||
<div class="section-header">
|
||||
<h2>Recently Added Movies</h2>
|
||||
</div>
|
||||
<div class="scroll-wrapper">
|
||||
<button class="scroll-arrow scroll-left" aria-label="Scroll left">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"/></svg>
|
||||
</button>
|
||||
<div class="horizontal-scroll" id="recent-movies-list">
|
||||
<div class="empty-state">Coming soon</div>
|
||||
</div>
|
||||
<button class="scroll-arrow scroll-right" aria-label="Scroll right">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="dashboard-section">
|
||||
<div class="section-header">
|
||||
<h2>Recently Added Series</h2>
|
||||
</div>
|
||||
<div class="scroll-wrapper">
|
||||
<button class="scroll-arrow scroll-left" aria-label="Scroll left">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"/></svg>
|
||||
</button>
|
||||
<div class="horizontal-scroll" id="recent-series-list">
|
||||
<div class="empty-state">Coming soon</div>
|
||||
</div>
|
||||
<button class="scroll-arrow scroll-right" aria-label="Scroll right">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
`;
|
||||
this.container = document.getElementById('home-content');
|
||||
|
||||
// Attach scroll arrow handlers
|
||||
this.initScrollArrows();
|
||||
}
|
||||
|
||||
initScrollArrows() {
|
||||
this.container.querySelectorAll('.scroll-wrapper').forEach(wrapper => {
|
||||
const scrollContainer = wrapper.querySelector('.horizontal-scroll');
|
||||
const leftBtn = wrapper.querySelector('.scroll-left');
|
||||
const rightBtn = wrapper.querySelector('.scroll-right');
|
||||
|
||||
if (!scrollContainer || !leftBtn || !rightBtn) return;
|
||||
|
||||
const scrollAmount = 300; // pixels to scroll per click
|
||||
|
||||
leftBtn.addEventListener('click', () => {
|
||||
scrollContainer.scrollBy({ left: -scrollAmount, behavior: 'smooth' });
|
||||
});
|
||||
|
||||
rightBtn.addEventListener('click', () => {
|
||||
scrollContainer.scrollBy({ left: scrollAmount, behavior: 'smooth' });
|
||||
});
|
||||
|
||||
// Update arrow visibility based on scroll position
|
||||
const updateArrows = () => {
|
||||
const { scrollLeft, scrollWidth, clientWidth } = scrollContainer;
|
||||
leftBtn.classList.toggle('hidden', scrollLeft <= 0);
|
||||
rightBtn.classList.toggle('hidden', scrollLeft + clientWidth >= scrollWidth - 5);
|
||||
};
|
||||
|
||||
// Store reference for later updates
|
||||
wrapper._updateArrows = updateArrows;
|
||||
|
||||
scrollContainer.addEventListener('scroll', updateArrows);
|
||||
// Initial check after content loads
|
||||
setTimeout(updateArrows, 100);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-check scroll arrow visibility for all sections
|
||||
* Call this after dynamically loading content
|
||||
*/
|
||||
updateScrollArrows() {
|
||||
this.container?.querySelectorAll('.scroll-wrapper').forEach(wrapper => {
|
||||
if (wrapper._updateArrows) {
|
||||
wrapper._updateArrows();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
async loadDashboardData() {
|
||||
if (this.isLoading) return;
|
||||
this.isLoading = true;
|
||||
@@ -147,6 +229,9 @@ class HomePage {
|
||||
});
|
||||
});
|
||||
|
||||
// Update scroll arrows after content renders
|
||||
this.updateScrollArrows();
|
||||
|
||||
} catch (err) {
|
||||
console.error('[Dashboard] Error loading favorite channels:', err);
|
||||
list.innerHTML = '<div class="empty-state hint">Error loading favorites</div>';
|
||||
@@ -161,7 +246,7 @@ class HomePage {
|
||||
return `
|
||||
<div class="channel-tile" data-channel-id="${channel.id}" data-source-id="${channel.sourceId}">
|
||||
<div class="tile-logo">
|
||||
<img src="${logoUrl}" alt="${name}" loading="lazy" onerror="this.src='/img/placeholder.png'">
|
||||
<img src="${logoUrl}" alt="${name}" loading="lazy" onerror="this.onerror=null;this.src='/img/placeholder.png'">
|
||||
</div>
|
||||
<div class="tile-name" title="${name}">${name}</div>
|
||||
</div>
|
||||
@@ -223,6 +308,9 @@ class HomePage {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Update scroll arrows after content renders
|
||||
this.updateScrollArrows();
|
||||
}
|
||||
|
||||
navigateToSeries(item) {
|
||||
@@ -268,6 +356,9 @@ class HomePage {
|
||||
if (item) this.playItem(item);
|
||||
});
|
||||
});
|
||||
|
||||
// Update scroll arrows after content renders
|
||||
this.updateScrollArrows();
|
||||
} catch (err) {
|
||||
console.error('[Dashboard] Error loading recent movies:', err);
|
||||
}
|
||||
@@ -294,6 +385,9 @@ class HomePage {
|
||||
if (item) this.navigateToSeries(item);
|
||||
});
|
||||
});
|
||||
|
||||
// Update scroll arrows after content renders
|
||||
this.updateScrollArrows();
|
||||
} catch (err) {
|
||||
console.error('[Dashboard] Error loading recent series:', err);
|
||||
}
|
||||
@@ -311,7 +405,7 @@ class HomePage {
|
||||
return `
|
||||
<div class="dashboard-card" data-id="${item_id}" data-type="${type}">
|
||||
<div class="card-image">
|
||||
<img src="${posterUrl}" alt="${data.title || item.name}" loading="lazy" onerror="this.src='/img/poster-placeholder.jpg'">
|
||||
<img src="${posterUrl}" alt="${data.title || item.name}" loading="lazy" onerror="this.onerror=null;this.src='/img/poster-placeholder.jpg'">
|
||||
<div class="progress-bar-container">
|
||||
<div class="progress-bar" style="width: ${percent}%"></div>
|
||||
</div>
|
||||
@@ -336,7 +430,7 @@ class HomePage {
|
||||
return `
|
||||
<div class="dashboard-card" data-id="${item_id}" data-type="${type}">
|
||||
<div class="card-image">
|
||||
<img src="${posterUrl}" alt="${item.name}" loading="lazy" onerror="this.src='/img/poster-placeholder.jpg'">
|
||||
<img src="${posterUrl}" alt="${item.name}" loading="lazy" onerror="this.onerror=null;this.src='/img/poster-placeholder.jpg'">
|
||||
<div class="play-icon-overlay">
|
||||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>
|
||||
</div>
|
||||
|
||||
@@ -888,7 +888,14 @@ class WatchPage {
|
||||
renderDetails() {
|
||||
if (!this.content) return;
|
||||
|
||||
this.posterEl.src = this.content.poster || '/img/placeholder.png';
|
||||
const isChannel = this.content.type === 'channel' || !this.content.type; // Default to channel if unknown
|
||||
const fallback = isChannel ? '/img/placeholder.png' : '/img/poster-placeholder.jpg';
|
||||
|
||||
this.posterEl.onerror = () => {
|
||||
this.posterEl.onerror = null;
|
||||
this.posterEl.src = fallback;
|
||||
};
|
||||
this.posterEl.src = this.content.poster || fallback;
|
||||
this.posterEl.alt = this.content.title || '';
|
||||
this.contentTitleEl.textContent = this.content.title || '';
|
||||
this.yearEl.textContent = this.content.year || '';
|
||||
@@ -984,7 +991,7 @@ class WatchPage {
|
||||
<div class="watch-recommended-card" data-id="${movie.stream_id}" data-source="${sourceId}">
|
||||
<img src="${movie.stream_icon || movie.cover || '/img/placeholder.png'}"
|
||||
alt="${movie.name}"
|
||||
onerror="this.src='/img/placeholder.png'" loading="lazy">
|
||||
onerror="this.onerror=null;this.src='/img/placeholder.png'" loading="lazy">
|
||||
<p>${movie.name}</p>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
@@ -298,9 +298,17 @@ router.get('/recent', async (req, res) => {
|
||||
|
||||
const db = getDb();
|
||||
const recentItems = db.prepare(`
|
||||
SELECT * FROM playlist_items
|
||||
WHERE type = ? AND is_hidden = 0
|
||||
ORDER BY added_at DESC
|
||||
SELECT * FROM playlist_items p
|
||||
WHERE p.type = ?
|
||||
AND p.is_hidden = 0
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM categories c
|
||||
WHERE c.source_id = p.source_id
|
||||
AND c.category_id = p.category_id
|
||||
AND c.type = p.type
|
||||
AND c.is_hidden = 1
|
||||
)
|
||||
ORDER BY p.added_at DESC
|
||||
LIMIT ?
|
||||
`).all(type, parseInt(limit));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user