diff --git a/assets/css/style.css b/assets/css/style.css new file mode 100644 index 0000000..259337f --- /dev/null +++ b/assets/css/style.css @@ -0,0 +1,521 @@ +:root { + --bg-primary: #0a0e14; + --bg-secondary: #151b24; + --bg-tertiary: #1f2937; + --accent-primary: #00ffcc; + --accent-secondary: #ff00ff; + --text-primary: #e8edf3; + --text-secondary: #8b95a8; + --border-color: #2d3748; + --shadow-glow: 0 0 20px rgba(0, 255, 204, 0.3); +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Archivo', sans-serif; + background: linear-gradient(135deg, var(--bg-primary) 0%, #0d1117 50%, var(--bg-secondary) 100%); + color: var(--text-primary); + min-height: 100vh; + overflow-x: hidden; +} + +body::before { + content: ''; + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: + radial-gradient(circle at 20% 50%, rgba(0, 255, 204, 0.05) 0%, transparent 50%), + radial-gradient(circle at 80% 20%, rgba(255, 0, 255, 0.05) 0%, transparent 50%); + pointer-events: none; + z-index: 0; +} + +.container { + position: relative; + max-width: 1600px; + margin: 0 auto; + padding: 2rem; + z-index: 1; +} + +header { + text-align: center; + margin-bottom: 3rem; + animation: slideDown 0.8s ease-out; +} + +@keyframes slideDown { + from { + opacity: 0; + transform: translateY(-30px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +h1 { + font-family: 'Orbitron', sans-serif; + font-size: 3.5rem; + font-weight: 900; + background: linear-gradient(135deg, var(--accent-primary) 0%, var(--accent-secondary) 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + letter-spacing: 0.05em; + text-transform: uppercase; + margin-bottom: 0.5rem; + filter: drop-shadow(0 0 30px rgba(0, 255, 204, 0.5)); +} + +.subtitle { + color: var(--text-secondary); + font-size: 0.9rem; + letter-spacing: 0.2em; + text-transform: uppercase; +} + +.main-layout { + display: grid; + grid-template-columns: 350px 1fr; + gap: 2rem; + animation: fadeIn 1s ease-out 0.3s both; +} + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +.sidebar { + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: 12px; + padding: 1.5rem; + height: fit-content; + position: sticky; + top: 2rem; + backdrop-filter: blur(10px); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4); +} + +.media-type-tabs { + display: flex; + gap: 0.5rem; + margin-bottom: 1.5rem; +} + +.tab-btn { + flex: 1; + padding: 0.75rem; + background: var(--bg-tertiary); + border: 1px solid var(--border-color); + color: var(--text-secondary); + font-family: 'Orbitron', sans-serif; + font-size: 0.85rem; + text-transform: uppercase; + letter-spacing: 0.05em; + cursor: pointer; + border-radius: 6px; + transition: all 0.3s ease; +} + +.tab-btn:hover { + background: var(--bg-primary); + border-color: var(--accent-primary); + color: var(--accent-primary); +} + +.tab-btn.active { + background: linear-gradient(135deg, var(--accent-primary) 0%, var(--accent-secondary) 100%); + border-color: var(--accent-primary); + color: var(--text-primary); + font-weight: 700; + box-shadow: var(--shadow-glow); +} + +.file-browser { + max-height: 600px; + overflow-y: auto; + padding-right: 0.5rem; +} + +.file-browser::-webkit-scrollbar { + width: 6px; +} + +.file-browser::-webkit-scrollbar-track { + background: var(--bg-tertiary); + border-radius: 3px; +} + +.file-browser::-webkit-scrollbar-thumb { + background: var(--accent-primary); + border-radius: 3px; +} + +.folder { + margin-bottom: 0.5rem; +} + +.folder-header { + display: flex; + align-items: center; + padding: 0.6rem 0.8rem; + background: var(--bg-tertiary); + border: 1px solid var(--border-color); + border-radius: 6px; + cursor: pointer; + transition: all 0.2s ease; + font-size: 0.9rem; +} + +.folder-header:hover { + background: var(--bg-primary); + border-color: var(--accent-primary); + transform: translateX(3px); +} + +.folder-icon { + margin-right: 0.5rem; + font-size: 1.1rem; + transition: transform 0.2s ease; +} + +.folder.open .folder-icon { + transform: rotate(90deg); +} + +.folder-content { + display: none; + padding-left: 1.5rem; + margin-top: 0.5rem; +} + +.folder.open .folder-content { + display: block; +} + +.file-item { + display: flex; + align-items: center; + padding: 0.6rem 0.8rem; + margin-bottom: 0.3rem; + background: var(--bg-primary); + border: 1px solid transparent; + border-radius: 6px; + cursor: pointer; + transition: all 0.2s ease; + font-size: 0.85rem; +} + +.file-item:hover { + border-color: var(--accent-primary); + background: var(--bg-tertiary); + transform: translateX(5px); + box-shadow: -3px 0 0 0 var(--accent-primary); +} + +.file-item.active { + background: linear-gradient(90deg, rgba(0, 255, 204, 0.15) 0%, transparent 100%); + border-color: var(--accent-primary); + color: var(--accent-primary); + box-shadow: -3px 0 0 0 var(--accent-primary); +} + +.file-icon { + margin-right: 0.5rem; + font-size: 1rem; +} + +.player-section { + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: 12px; + padding: 2rem; + backdrop-filter: blur(10px); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4); +} + +.player-container { + background: var(--bg-primary); + border: 2px solid var(--border-color); + border-radius: 12px; + padding: 2rem; + min-height: 400px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + position: relative; + overflow: hidden; +} + +.player-container::before { + content: ''; + position: absolute; + top: -50%; + left: -50%; + width: 200%; + height: 200%; + background: radial-gradient(circle, rgba(0, 255, 204, 0.03) 0%, transparent 70%); + animation: pulse 4s ease-in-out infinite; +} + +@keyframes pulse { + 0%, 100% { + transform: scale(1); + opacity: 1; + } + 50% { + transform: scale(1.1); + opacity: 0.8; + } +} + +.player-placeholder { + text-align: center; + color: var(--text-secondary); + z-index: 1; +} + +.player-placeholder-icon { + font-size: 4rem; + margin-bottom: 1rem; + opacity: 0.3; +} + +video, audio { + max-width: 100%; + max-height: 500px; + border-radius: 8px; + box-shadow: 0 10px 40px rgba(201, 137, 225, 0.63); + z-index: 1; +} + +audio { + width: 100%; +} + +.now-playing { + margin-top: 1.5rem; + padding: 1.5rem; + background: var(--bg-tertiary); + border: 1px solid var(--border-color); + border-radius: 8px; + text-align: center; +} + +.now-playing-label { + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.1em; + color: var(--text-secondary); + margin-bottom: 0.5rem; +} + +.now-playing-title { + font-family: 'Orbitron', sans-serif; + font-size: 1.3rem; + color: var(--accent-primary); + font-weight: 600; +} + +.player-controls-row { + width: 100%; + display: flex; + align-items: center; + justify-content: center; + gap: 0.75rem; + margin-top: 1rem; + z-index: 1; +} + +.audio-track-label { + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.1em; + color: var(--text-secondary); + font-family: 'Orbitron', sans-serif; +} + +.audio-track-select { + background: var(--bg-tertiary); + border: 1px solid var(--border-color); + color: var(--text-primary); + padding: 0.55rem 0.75rem; + border-radius: 8px; + outline: none; + min-width: 220px; + font-family: 'Archivo', sans-serif; +} + +.audio-track-select:focus { + border-color: var(--accent-primary); + box-shadow: var(--shadow-glow); +} + +.audio-track-hint { + color: var(--text-secondary); + font-size: 0.8rem; + margin-top: 0.5rem; + z-index: 1; + text-align: center; +} + + +.loading { + display: inline-block; + width: 20px; + height: 20px; + border: 3px solid var(--border-color); + border-top-color: var(--accent-primary); + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +.overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.72); + backdrop-filter: blur(6px); + z-index: 9999; + display: flex; + align-items: center; + justify-content: center; + padding: 1.5rem; +} + +.login-card { + width: 100%; + max-width: 420px; + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: 14px; + box-shadow: 0 10px 40px rgba(0,0,0,0.6); + padding: 1.75rem; +} + +.login-title { + font-family: 'Orbitron', sans-serif; + font-size: 1.4rem; + color: var(--accent-primary); + margin-bottom: 0.35rem; +} + +.login-subtitle { + color: var(--text-secondary); + font-size: 0.9rem; + margin-bottom: 1.25rem; +} + +.login-row { + display: flex; + flex-direction: column; + gap: 0.4rem; + margin-bottom: 0.9rem; +} + +.login-row label { + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.1em; + color: var(--text-secondary); + font-family: 'Orbitron', sans-serif; +} + +.login-row input { + background: var(--bg-tertiary); + border: 1px solid var(--border-color); + color: var(--text-primary); + padding: 0.75rem 0.85rem; + border-radius: 10px; + outline: none; + font-family: 'Archivo', sans-serif; +} + +.login-row input:focus { + border-color: var(--accent-primary); + box-shadow: var(--shadow-glow); +} + +.login-actions { + display: flex; + gap: 0.75rem; + margin-top: 1rem; + align-items: center; + justify-content: space-between; +} + +.btn { + cursor: pointer; + border: 1px solid var(--border-color); + border-radius: 10px; + padding: 0.75rem 1rem; + font-family: 'Orbitron', sans-serif; + letter-spacing: 0.05em; + text-transform: uppercase; + font-size: 0.85rem; + transition: all 0.2s ease; + background: var(--bg-tertiary); + color: var(--text-primary); +} + +.btn-primary { + background: linear-gradient(135deg, #093c31 0%, var(--accent-secondary) 100%); + border-color: var(--accent-primary); + color: var(--text-primary); + font-weight: 700; + box-shadow: var(--shadow-glow); +} + +.btn:hover { + transform: translateY(-1px); +} + +.login-error { + color: #ff6b6b; + margin-top: 0.75rem; + font-size: 0.9rem; + display: none; +} + +.topbar { + display: flex; + justify-content: flex-end; + margin-bottom: 1rem; +} + + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +@media (max-width: 1024px) { + .main-layout { + grid-template-columns: 1fr; + } + + .sidebar { + position: static; + } + + h1 { + font-size: 2.5rem; + } +} diff --git a/assets/js/app.js b/assets/js/app.js new file mode 100644 index 0000000..ab84b4a --- /dev/null +++ b/assets/js/app.js @@ -0,0 +1,428 @@ +function showLogin(message = '') { + document.getElementById('loginOverlay').style.display = 'flex'; + document.getElementById('logoutBtn').style.display = 'none'; + + const err = document.getElementById('loginError'); + if (message) { + err.textContent = message; + err.style.display = 'block'; + } else { + err.textContent = ''; + err.style.display = 'none'; + } +} + +function hideLogin() { + document.getElementById('loginOverlay').style.display = 'none'; + document.getElementById('loginError').style.display = 'none'; + document.getElementById('logoutBtn').style.display = 'inline-block'; +} + +async function authedFetch(url, options = {}) { + // Ensure cookies (session) are sent + const resp = await fetch(url, { + ...options, + credentials: 'same-origin', + }); + + if (resp.status === 401) { + // Session missing/expired + showLogin('Please log in to continue.'); + throw new Error('Unauthorized'); + } + + return resp; +} + +const state = { + currentType: 'videos', + currentFile: null, + filesData: null, + currentItemEl: null +}; + +// Initialize +document.addEventListener('DOMContentLoaded', () => { + setupTabListeners(); + setupLoginHandlers(); + + // Try loading files; if 401, overlay will appear. + loadFiles('videos'); +}); + +function setupLoginHandlers() { + const form = document.getElementById('loginForm'); + const spinner = document.getElementById('loginSpinner'); + const err = document.getElementById('loginError'); + const logoutBtn = document.getElementById('logoutBtn'); + + // Show overlay until proven authed + showLogin(''); + + form.addEventListener('submit', async (e) => { + e.preventDefault(); + + err.style.display = 'none'; + spinner.style.display = 'inline-block'; + + const username = document.getElementById('username').value.trim(); + const password = document.getElementById('password').value; + + try { + const resp = await fetch('login.php', { + method: 'POST', + credentials: 'same-origin', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ username, password }) + }); + + const data = await resp.json().catch(() => ({})); + + if (!resp.ok) { + throw new Error(data.error || `Login failed (${resp.status})`); + } + + hideLogin(); + + // Reload file browser after login + loadFiles(state.currentType); + } catch (ex) { + err.textContent = ex.message || 'Login failed'; + err.style.display = 'block'; + } finally { + spinner.style.display = 'none'; + } + }); + + logoutBtn.addEventListener('click', async () => { + try { + await fetch('logout.php', { method: 'POST', credentials: 'same-origin' }); + } catch (_) { + // ignore + } + showLogin('Logged out.'); + // Clear UI + document.getElementById('fileBrowser').innerHTML = ''; + document.getElementById('playerContainer').innerHTML = ` +
+
🎵
+

Select a file to start playing

+
+ `; + document.getElementById('nowPlaying').style.display = 'none'; + }); +} + + +function setupTabListeners() { + document.querySelectorAll('.tab-btn').forEach(btn => { + btn.addEventListener('click', () => { + const type = btn.dataset.type; + document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active')); + btn.classList.add('active'); + state.currentType = type; + loadFiles(type); + }); + }); +} + +async function loadFiles(type) { + const browser = document.getElementById('fileBrowser'); + browser.innerHTML = '
'; + + try { + const response = await authedFetch(`get_files.php?type=${type}`); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const contentType = response.headers.get('content-type'); + if (!contentType || !contentType.includes('application/json')) { + const text = await response.text(); + console.error('Non-JSON response:', text); + throw new Error('Server returned non-JSON response'); + } + + const data = await response.json(); + console.log('Loaded files:', data); + + if (data.error) { + browser.innerHTML = `

${data.error}

`; + return; + } + + if (data.message) { + browser.innerHTML = `

${data.message}

`; + return; + } + + state.filesData = data; + renderFileTree(data); + } catch (error) { + console.error('Error loading files:', error); + browser.innerHTML = `

Error loading files: ${error.message}

`; + } +} + +function setActiveFileItem(fileItemEl) { + // Clear existing "active" state + document.querySelectorAll('.file-item').forEach(f => f.classList.remove('active')); + + // Set new "active" + if (fileItemEl) { + fileItemEl.classList.add('active'); + fileItemEl.scrollIntoView({ block: 'nearest', behavior: 'smooth' }); + } + + state.currentItemEl = fileItemEl || null; +} + +/** + * Finds the "next" file-item in UI order, preferring the same folder/div + * the current item is in. If there's no next sibling, it walks up the DOM + * and continues searching. + */ +function findNextFileItem(currentEl) { + if (!currentEl) return null; + + // 1) Prefer "next siblings" within the same container + let n = currentEl.nextElementSibling; + while (n) { + if (n.classList && n.classList.contains('file-item')) return n; + n = n.nextElementSibling; + } + + // 2) If none, walk upward and look for the next file-item after the + // parent container (e.g., next folder section) + let parent = currentEl.parentElement; + while (parent) { + // Stop at the file browser root + if (parent.id === 'fileBrowser') break; + + let pNext = parent.nextElementSibling; + while (pNext) { + // If the next sibling is a file-item, play it + if (pNext.classList && pNext.classList.contains('file-item')) return pNext; + + // If it's a folder, search inside it for the first file-item + const inside = pNext.querySelector && pNext.querySelector('.file-item'); + if (inside) return inside; + + pNext = pNext.nextElementSibling; + } + + parent = parent.parentElement; + } + + // No next item found + return null; +} + +function playNextItem() { + const next = findNextFileItem(state.currentItemEl); + if (!next) { + console.log('Reached end of list; no next item.'); + return; + } + + // Trigger the same behavior as clicking it + next.click(); +} + + +function renderFileTree(files, container = document.getElementById('fileBrowser'), depth = 0) { + if (depth === 0) { + container.innerHTML = ''; + } + + for (const [name, content] of Object.entries(files)) { + if (typeof content === 'object' && !Array.isArray(content)) { + // It's a folder + const folder = document.createElement('div'); + folder.className = 'folder'; + + const header = document.createElement('div'); + header.className = 'folder-header'; + header.innerHTML = `${name}`; + + const folderContent = document.createElement('div'); + folderContent.className = 'folder-content'; + + header.addEventListener('click', () => { + folder.classList.toggle('open'); + }); + + folder.appendChild(header); + folder.appendChild(folderContent); + container.appendChild(folder); + + renderFileTree(content, folderContent, depth + 1); + } else if (typeof content === 'string') { + // It's a file + const fileItem = document.createElement('div'); + fileItem.className = 'file-item'; + + const icon = state.currentType === 'videos' ? '🎬' : '🎵'; + fileItem.innerHTML = `${icon}${name}`; + + fileItem.dataset.path = content; + fileItem.dataset.name = name; + + fileItem.addEventListener('click', () => { + // Play and mark active + playFile(content, name, fileItem); + setActiveFileItem(fileItem); + }); + + + container.appendChild(fileItem); + } + } +} + +function resetAudioTrackUI() { + const audioControls = document.getElementById('audioControls'); + const audioSelect = document.getElementById('audioSelect'); + const audioHint = document.getElementById('audioHint'); + + audioControls.style.display = 'none'; + audioHint.style.display = 'none'; + audioHint.textContent = ''; + audioSelect.innerHTML = ''; +} + +function setupAudioTrackSelector(videoEl) { + const audioControls = document.getElementById('audioControls'); + const audioSelect = document.getElementById('audioSelect'); + const audioHint = document.getElementById('audioHint'); + + // Not all browsers expose audioTracks (and MKV often won't) + const tracks = videoEl.audioTracks; + + if (!tracks) { + audioControls.style.display = 'none'; + audioHint.style.display = 'block'; + audioHint.textContent = 'Audio track switching not supported for this file/browser, open in VLC to change audio track if needed.'; + return; + } + + if (tracks.length <= 1) { + audioControls.style.display = 'none'; + audioHint.style.display = 'none'; + return; + } + + // Build dropdown + audioSelect.innerHTML = ''; + for (let i = 0; i < tracks.length; i++) { + const t = tracks[i]; + const opt = document.createElement('option'); + opt.value = String(i); + + // label is often empty; language may exist depending on browser/container + const label = (t.label && t.label.trim()) ? t.label.trim() : null; + const lang = (t.language && t.language.trim()) ? t.language.trim() : null; + + opt.textContent = label || (lang ? `Track ${i + 1} (${lang})` : `Track ${i + 1}`); + audioSelect.appendChild(opt); + + // Set currently enabled track as selected + if (t.enabled) { + audioSelect.value = String(i); + } + } + + audioSelect.onchange = () => { + const idx = parseInt(audioSelect.value, 10); + for (let i = 0; i < tracks.length; i++) { + tracks[i].enabled = (i === idx); + } + }; + + audioHint.style.display = 'none'; + audioControls.style.display = 'flex'; +} + + +function playFile(path, name, sourceItemEl = null) { + const playerContainer = document.getElementById('playerContainer'); + const nowPlaying = document.getElementById('nowPlaying'); + const nowPlayingTitle = document.getElementById('nowPlayingTitle'); + + state.currentFile = { path, name }; + resetAudioTrackUI(); + + + const isVideo = state.currentType === 'videos'; + const mediaElement = document.createElement(isVideo ? 'video' : 'audio'); + + mediaElement.controls = true; + mediaElement.autoplay = true; + + // Keep track of which UI element launched playback (used for "next") + if (sourceItemEl) { + state.currentItemEl = sourceItemEl; + } + + if (isVideo) { + // Some browsers only populate audioTracks after metadata is available + mediaElement.addEventListener('loadedmetadata', () => { + setupAudioTrackSelector(mediaElement); + }); + } + + + // Add error handling + mediaElement.addEventListener('error', (e) => { + console.error('Media playback error:', { + error: mediaElement.error, + code: mediaElement.error?.code, + message: mediaElement.error?.message, + path: path, + name: name + }); + + playerContainer.innerHTML = ` +
+
⚠️
+

Error playing: ${name}

+

+ ${mediaElement.error?.message || 'Unknown error'} +

+
+ `; + }); + + // Add loading state + mediaElement.addEventListener('loadstart', () => { + console.log('Loading media:', name); + }); + + mediaElement.addEventListener('canplay', () => { + console.log('Media ready to play:', name); + }); + + mediaElement.addEventListener('ended', () => { + // Auto-advance to next item when current finishes + playNextItem(); + }); + + + // URL encode the path properly + const encodedPath = encodeURIComponent(path); + mediaElement.src = `serve_media.php?file=${encodedPath}`; + + console.log('Playing file:', { + name: name, + path: path, + encodedPath: encodedPath, + fullUrl: `serve_media.php?file=${encodedPath}` + }); + + playerContainer.innerHTML = ''; + playerContainer.appendChild(mediaElement); + + nowPlayingTitle.textContent = name; + nowPlaying.style.display = 'block'; +} \ No newline at end of file diff --git a/index.php b/index.php index 509f254..1c6abdf 100644 --- a/index.php +++ b/index.php @@ -5,529 +5,7 @@ Freax Web Media Player - +
@@ -600,436 +78,7 @@
- +