Merge pull request #22 from yurividal/dev

Implement Mobile Responsiveness
This commit is contained in:
Technomancer702
2026-01-01 11:42:13 -08:00
committed by GitHub
4 changed files with 1210 additions and 10 deletions
+985 -6
View File
File diff suppressed because it is too large Load Diff
+48 -4
View File
@@ -3,7 +3,10 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="mobile-web-app-capable" content="yes">
<title>NodeCast TV | IPTV Player</title>
<meta name="description" content="Stream live TV, movies, and series with EPG support">
<link rel="stylesheet" href="css/main.css">
@@ -25,7 +28,12 @@
</svg></span>
<span class="brand-text">NodeCast <span class="brand-accent">TV</span></span>
</div>
<div class="navbar-menu">
<button class="mobile-menu-toggle" id="mobile-menu-toggle" aria-label="Toggle menu">
<span></span>
<span></span>
<span></span>
</button>
<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">
@@ -73,8 +81,19 @@
<!-- Home Page -->
<div id="page-home" class="page active">
<div class="home-layout">
<!-- Channel Toggle Button (Mobile) -->
<button class="channel-toggle-btn" id="channel-toggle-btn">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="icon">
<path d="M3 13h2v-2H3v2zm0 4h2v-2H3v2zm0-8h2V7H3v2zm4 4h14v-2H7v2zm0 4h14v-2H7v2zM7 7v2h14V7H7z" />
</svg>
<span>Channels</span>
</button>
<!-- Overlay for drawer -->
<div class="channel-sidebar-overlay" id="channel-sidebar-overlay"></div>
<!-- Channel Sidebar -->
<aside class="channel-sidebar">
<aside class="channel-sidebar" id="channel-sidebar">
<div class="sidebar-header">
<div class="search-wrapper">
<input type="text" id="channel-search" placeholder="Search channels..." class="search-input">
@@ -104,7 +123,32 @@
<!-- Video Player Area -->
<section class="player-section">
<div class="video-container">
<video id="video-player" controls></video>
<video id="video-player" controls playsinline></video>
<!-- Custom Mobile Controls -->
<div class="video-controls" id="video-controls">
<button class="video-control-btn" id="btn-play" title="Play/Pause">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="icon">
<path d="M8 5v14l11-7z"/>
</svg>
</button>
<button class="video-control-btn" id="btn-mute" title="Mute/Unmute">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="icon">
<path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"/>
</svg>
</button>
<button class="video-control-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 1.98 2 1.98h18c1.1 0 2-.88 2-1.98V5c0-1.1-.9-2-2-2zm0 16.01H3V4.98h18v14.03z"/>
</svg>
</button>
<button class="video-control-btn" id="btn-fullscreen" title="Fullscreen">
<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"/>
</svg>
</button>
</div>
<div class="player-overlay" id="player-overlay">
<div class="overlay-content">
<p>Select a channel to start watching</p>
+53
View File
@@ -24,6 +24,59 @@ class App {
}
async init() {
// Mobile menu toggle
const mobileMenuToggle = document.getElementById('mobile-menu-toggle');
const navbarMenu = document.getElementById('navbar-menu');
if (mobileMenuToggle && navbarMenu) {
mobileMenuToggle.addEventListener('click', () => {
mobileMenuToggle.classList.toggle('active');
navbarMenu.classList.toggle('active');
});
// Close menu when a nav link is clicked
document.querySelectorAll('.nav-link').forEach(link => {
link.addEventListener('click', () => {
mobileMenuToggle.classList.remove('active');
navbarMenu.classList.remove('active');
});
});
// Close menu when clicking outside
document.addEventListener('click', (e) => {
if (!e.target.closest('.navbar')) {
mobileMenuToggle.classList.remove('active');
navbarMenu.classList.remove('active');
}
});
}
// Channel drawer toggle (mobile)
const channelToggleBtn = document.getElementById('channel-toggle-btn');
const channelSidebar = document.getElementById('channel-sidebar');
const channelOverlay = document.getElementById('channel-sidebar-overlay');
if (channelToggleBtn && channelSidebar && channelOverlay) {
const toggleChannelDrawer = () => {
channelSidebar.classList.toggle('active');
channelOverlay.classList.toggle('active');
};
channelToggleBtn.addEventListener('click', toggleChannelDrawer);
channelOverlay.addEventListener('click', toggleChannelDrawer);
// Close drawer when a channel is selected
channelSidebar.addEventListener('click', (e) => {
if (e.target.closest('.channel-item')) {
// Small delay to let the channel selection happen
setTimeout(() => {
channelSidebar.classList.remove('active');
channelOverlay.classList.remove('active');
}, 300);
}
});
}
// Navigation handling
document.querySelectorAll('.nav-link').forEach(link => {
link.addEventListener('click', (e) => {
+124
View File
@@ -133,6 +133,127 @@ class VideoPlayer {
};
}
/**
* Initialize custom video controls for mobile
*/
initCustomControls() {
const btnPlay = document.getElementById('btn-play');
const btnMute = document.getElementById('btn-mute');
const btnPip = document.getElementById('btn-pip');
const btnFullscreen = document.getElementById('btn-fullscreen');
const controls = document.getElementById('video-controls');
if (!btnPlay || !btnMute || !btnPip || !btnFullscreen || !controls) return;
// Always use native controls
this.video.controls = true;
// Play/Pause button
btnPlay.addEventListener('click', () => {
if (this.video.paused) {
this.video.play();
} else {
this.video.pause();
}
});
// Update play/pause icon
const updatePlayIcon = () => {
const icon = btnPlay.querySelector('.icon');
if (this.video.paused) {
icon.innerHTML = '<path d="M8 5v14l11-7z"/>';
btnPlay.title = 'Play';
} else {
icon.innerHTML = '<path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/>';
btnPlay.title = 'Pause';
}
};
this.video.addEventListener('play', updatePlayIcon);
this.video.addEventListener('pause', updatePlayIcon);
// Mute/Unmute button
btnMute.addEventListener('click', () => {
this.video.muted = !this.video.muted;
btnMute.classList.toggle('muted', this.video.muted);
// Update icon
const icon = btnMute.querySelector('.icon');
if (this.video.muted) {
icon.innerHTML = '<path d="M16.5 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c.03-.2.05-.41.05-.63zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51C20.63 14.91 21 13.5 21 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM4.27 3L3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06c1.38-.31 2.63-.95 3.69-1.81L19.73 21 21 19.73l-9-9L4.27 3zM12 4L9.91 6.09 12 8.18V4z"/>';
} else {
icon.innerHTML = '<path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"/>';
}
});
// Picture-in-Picture button
if (document.pictureInPictureEnabled) {
btnPip.addEventListener('click', async () => {
try {
if (document.pictureInPictureElement) {
await document.exitPictureInPicture();
} else {
await this.video.requestPictureInPicture();
}
} catch (err) {
console.error('PiP error:', err);
}
});
} else {
btnPip.style.display = 'none';
}
// Fullscreen button
btnFullscreen.addEventListener('click', () => {
if (document.fullscreenElement) {
document.exitFullscreen();
} else {
this.container.requestFullscreen().catch(err => {
console.error('Fullscreen error:', err);
});
}
});
// Show controls on tap/touch for mobile (only hide after interaction)
if (isMobile) {
let controlsTimeout;
const hideControls = () => {
clearTimeout(controlsTimeout);
controlsTimeout = setTimeout(() => {
controls.classList.remove('show');
}, 4000);
};
const showControls = () => {
controls.classList.add('show');
hideControls();
};
this.video.addEventListener('click', showControls);
this.video.addEventListener('touchstart', showControls);
// Keep controls visible when video is paused
this.video.addEventListener('pause', () => {
controls.classList.add('show');
clearTimeout(controlsTimeout);
});
this.video.addEventListener('play', () => {
hideControls();
});
}
// Update fullscreen icon
document.addEventListener('fullscreenchange', () => {
const icon = btnFullscreen.querySelector('.icon');
if (document.fullscreenElement) {
icon.innerHTML = '<path d="M5 16h3v3h2v-5H5v2zm3-8H5v2h5V5H8v3zm6 11h2v-3h3v-2h-5v5zm2-11V5h-2v5h5V8h-3z"/>';
} else {
icon.innerHTML = '<path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"/>';
}
});
}
init() {
// Apply default/remembered volume
const volume = this.settings.rememberVolume ? this.settings.lastVolume : this.settings.defaultVolume;
@@ -146,6 +267,9 @@ class VideoPlayer {
}
});
// Setup custom video controls
this.initCustomControls();
// Initialize HLS.js if supported
if (Hls.isSupported()) {
this.hls = new Hls(this.getHlsConfig());