Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 86 additions & 2 deletions static/css/base.css
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,90 @@ body { font-family: Arial, sans-serif; margin:0; }
.program.now { font-weight:bold; }
.now-line { position:absolute; top:0; bottom:0; width:2px; z-index:8; pointer-events:none; left: var(--now-offset); }

/* Search/Filter Bar */
.search-bar {
padding: 12px 16px;
background: var(--search-bg, rgba(0, 0, 0, 0.2));
border-bottom: 1px solid var(--search-border, rgba(255, 255, 255, 0.1));
position: sticky;
top: 0;
z-index: 100;
}

.search-container {
position: relative;
max-width: 600px;
margin: 0 auto;
}

.search-input {
width: 100%;
padding: 10px 40px 10px 16px;
font-size: 15px;
border: 2px solid var(--search-input-border, rgba(255, 255, 255, 0.2));
border-radius: 24px;
background: var(--search-input-bg, rgba(255, 255, 255, 0.1));
color: var(--search-input-color, inherit);
transition: all 0.2s ease;
box-sizing: border-box;
}

.search-input:focus {
outline: none;
border-color: var(--primary-color, #0af);
background: var(--search-input-bg-focus, rgba(255, 255, 255, 0.15));
box-shadow: 0 0 0 3px var(--search-focus-shadow, rgba(0, 170, 255, 0.1));
}

.search-input::placeholder {
color: var(--search-placeholder, rgba(255, 255, 255, 0.4));
}

.clear-search {
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
background: transparent;
border: none;
color: var(--search-clear-color, rgba(255, 255, 255, 0.6));
font-size: 20px;
cursor: pointer;
padding: 4px 8px;
line-height: 1;
transition: color 0.2s ease;
}

.clear-search:hover {
color: var(--search-clear-hover, #fff);
}

.search-results {
text-align: center;
margin-top: 8px;
font-size: 13px;
color: var(--search-results-color, rgba(255, 255, 255, 0.7));
}

.search-results #resultCount {
font-weight: 600;
color: var(--primary-color, #0af);
}

/* Hidden state for filtered rows */
.guide-row.hidden-by-search {
display: none !important;
}

/* Highlight matching text */
.search-highlight {
background: var(--search-highlight-bg, rgba(255, 255, 0, 0.3));
color: var(--search-highlight-color, inherit);
font-weight: 600;
border-radius: 2px;
padding: 0 2px;
}

/* Scrollbar reveal styles */
.grid-col.show-scroll,
.grid-col:focus-within {
Expand Down Expand Up @@ -231,7 +315,7 @@ body { font-family: Arial, sans-serif; margin:0; }
/* Theme variables & theme-specific rules preserved below (copied/preserved from original base.css) */

/* Dark theme */
body.dark { --timebar-bg:#222; --timebar-border:#444; --timebar-color:#ddd; --chan-col-bg:#1a1a1a; background:#111; color:#ddd; }
body.dark { --timebar-bg:#222; --timebar-border:#444; --timebar-color:#ddd; --chan-col-bg:#1a1a1a; background:#111; color:#ddd; --search-bg: rgba(0, 0, 0, 0.3); --search-border: rgba(255, 255, 255, 0.1); --search-input-bg: rgba(255, 255, 255, 0.05); --search-input-bg-focus: rgba(255, 255, 255, 0.1); --search-input-border: rgba(255, 255, 255, 0.2); --search-input-color: #fff; --search-placeholder: rgba(255, 255, 255, 0.4); --search-highlight-bg: rgba(0, 170, 255, 0.3); }
body.dark .time-header-fixed .now-line { background:#0f0; }
body.dark .header { background:#222; }
body.dark #video { background:#000; }
Expand All @@ -246,7 +330,7 @@ body.dark .program.now { background:#2d5030; border-color:#47a447; }
body.dark .now-line { background:#0f0; }

/* Light theme */
body.light { --timebar-bg:#fff; --timebar-border:#ddd; --timebar-color:#000; --chan-col-bg:#f9f9f9; background:#fff; color:#000; }
body.light { --timebar-bg:#fff; --timebar-border:#ddd; --timebar-color:#000; --chan-col-bg:#f9f9f9; background:#fff; color:#000; --search-bg: rgba(255, 255, 255, 0.9); --search-border: rgba(0, 0, 0, 0.1); --search-input-bg: rgba(0, 0, 0, 0.05); --search-input-bg-focus: rgba(0, 0, 0, 0.08); --search-input-border: rgba(0, 0, 0, 0.2); --search-input-color: #000; --search-placeholder: rgba(0, 0, 0, 0.4); --search-highlight-bg: rgba(255, 255, 0, 0.4); }
body.light .time-header-fixed .now-line { background:#090; }
body.light .header { background:#222; }
body.light #video { background:#fff; }
Expand Down
16 changes: 16 additions & 0 deletions static/css/mobile.css
Original file line number Diff line number Diff line change
Expand Up @@ -173,3 +173,19 @@ html, body {
list-style: none;
height: 0;
}

@media (max-width: 900px) {
.search-bar {
padding: 8px 12px;
}

.search-input {
font-size: 16px; /* Prevent zoom on iOS */
padding: 8px 36px 8px 12px;
}

.search-results {
font-size: 12px;
margin-top: 6px;
}
}
158 changes: 158 additions & 0 deletions templates/guide.html
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,26 @@ <h3>Program Info</h3>
<!-- content injected by JS -->
</div>

<!-- Search/Filter Bar -->
<div class="search-bar" id="searchBar">
<div class="search-container">
<input type="text"
id="searchInput"
class="search-input"
placeholder="Search channels or programs..."
autocomplete="off"
aria-label="Search channels or programs">
<button type="button"
id="clearSearch"
class="clear-search"
aria-label="Clear search"
style="display: none;">✕</button>
</div>
<div class="search-results" id="searchResults" style="display: none;">
<span id="resultCount">0</span> results found
</div>
</div>

<!-- Guide grid -->
<div class="guide-outer" id="guideOuter">
<!-- Keep original small time header inside grid for alignment, it will be hidden when fixed header is active -->
Expand Down Expand Up @@ -614,6 +634,144 @@ <h3>Program Info</h3>
});
</script>

<script>
// Search/Filter Functionality
document.addEventListener('DOMContentLoaded', () => {
const searchInput = document.getElementById('searchInput');
const clearBtn = document.getElementById('clearSearch');
const resultsDiv = document.getElementById('searchResults');
const resultCountSpan = document.getElementById('resultCount');
const guideRows = Array.from(document.querySelectorAll('.guide-row:not(.hide-in-grid)'));

let searchTimeout = null;

function clearHighlights() {
document.querySelectorAll('.search-highlight').forEach(el => {
const parent = el.parentNode;
if (parent) {
parent.replaceChild(document.createTextNode(el.textContent), el);
parent.normalize();
}
});
}

function highlightText(element, searchTerm) {
if (!searchTerm || searchTerm.length < 2) return;

const text = element.textContent;
const regex = new RegExp(`(${escapeRegex(searchTerm)})`, 'gi');

if (!regex.test(text)) return;

// Safely create highlighted HTML by using DOM manipulation
const parts = text.split(regex);
element.textContent = '';
parts.forEach((part, i) => {
if (i % 2 === 0) {
// Non-matching text
element.appendChild(document.createTextNode(part));
} else {
// Matching text - create highlight span
const span = document.createElement('span');
span.className = 'search-highlight';
span.textContent = part;
element.appendChild(span);
}
});
}

function escapeRegex(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

function performSearch(searchTerm) {
clearHighlights();

searchTerm = searchTerm.trim().toLowerCase();

// Show/hide clear button
clearBtn.style.display = searchTerm ? 'block' : 'none';

if (!searchTerm || searchTerm.length < 2) {
// Show all rows
guideRows.forEach(row => row.classList.remove('hidden-by-search'));
resultsDiv.style.display = 'none';
return;
}

let visibleCount = 0;

guideRows.forEach(row => {
const chanName = row.querySelector('.chan-name span');
const channelText = chanName ? chanName.textContent.toLowerCase() : '';

const programs = Array.from(row.querySelectorAll('.program'));
const programTexts = programs.map(p => {
const title = (p.dataset.title || p.textContent || '').toLowerCase();
const desc = (p.dataset.desc || '').toLowerCase();
return { title, desc, element: p };
});

// Check if channel name matches
const channelMatches = channelText.includes(searchTerm);

// Check if any program matches
const programMatches = programTexts.some(p =>
p.title.includes(searchTerm) || p.desc.includes(searchTerm)
);

if (channelMatches || programMatches) {
row.classList.remove('hidden-by-search');
visibleCount++;

// Highlight channel name if it matches
if (channelMatches && chanName) {
highlightText(chanName, searchTerm);
}

// Highlight program titles that match
programTexts.forEach(({ title, desc, element }) => {
if (title.includes(searchTerm)) {
highlightText(element, searchTerm);
}
});
} else {
row.classList.add('hidden-by-search');
}
});

// Update result count
resultCountSpan.textContent = visibleCount;
resultsDiv.style.display = 'block';
}

// Real-time search with debouncing
searchInput.addEventListener('input', (e) => {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
performSearch(e.target.value);
}, 250); // 250ms debounce
});

// Clear button
clearBtn.addEventListener('click', () => {
searchInput.value = '';
clearBtn.style.display = 'none';
performSearch('');
searchInput.focus();
});

// Clear on Escape key
searchInput.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
searchInput.value = '';
clearBtn.style.display = 'none';
performSearch('');
}
});
});
</script>

<!-- Mobile nav behavior (off-canvas toggle). Ensure this file contains the open/close and resize-dispatch code -->
<script src="{{ url_for('static', filename='js/mobile-nav.js') }}" defer></script>
<!-- Grid adapt script: computes scale on small screens so guide fits proportionally -->
Expand Down
Loading