Added auth support and a helper script, targeting frankenphp
This commit is contained in:
parent
8f545c3f67
commit
0783e08fe9
8 changed files with 655 additions and 4 deletions
374
index.php
374
index.php
|
|
@ -342,6 +342,49 @@
|
|||
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;
|
||||
|
|
@ -352,6 +395,119 @@
|
|||
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, var(--accent-primary) 0%, var(--accent-secondary) 100%);
|
||||
border-color: var(--accent-primary);
|
||||
color: var(--bg-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);
|
||||
|
|
@ -392,12 +548,22 @@
|
|||
</aside>
|
||||
|
||||
<main class="player-section">
|
||||
<div class="topbar">
|
||||
<button class="btn" id="logoutBtn" style="display:none;">Logout</button>
|
||||
</div>
|
||||
|
||||
<div class="player-container" id="playerContainer">
|
||||
<div class="player-placeholder">
|
||||
<div class="player-placeholder-icon">🎵</div>
|
||||
<p>Select a file to start playing</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="player-controls-row" id="audioControls" style="display:none;">
|
||||
<div class="audio-track-label">Audio Track</div>
|
||||
<select id="audioSelect" class="audio-track-select"></select>
|
||||
</div>
|
||||
<div class="audio-track-hint" id="audioHint" style="display:none;"></div>
|
||||
|
||||
<div class="now-playing" id="nowPlaying" style="display: none;">
|
||||
<div class="now-playing-label">Now Playing</div>
|
||||
<div class="now-playing-title" id="nowPlayingTitle"></div>
|
||||
|
|
@ -406,7 +572,70 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="overlay" id="loginOverlay" style="display:none;">
|
||||
<div class="login-card">
|
||||
<div class="login-title">Login</div>
|
||||
<div class="login-subtitle">Sign in to access your media library.</div>
|
||||
|
||||
<form id="loginForm">
|
||||
<div class="login-row">
|
||||
<label for="username">Username</label>
|
||||
<input id="username" name="username" autocomplete="username" required />
|
||||
</div>
|
||||
|
||||
<div class="login-row">
|
||||
<label for="password">Password</label>
|
||||
<input id="password" name="password" type="password" autocomplete="current-password" required />
|
||||
</div>
|
||||
|
||||
<div class="login-actions">
|
||||
<button class="btn btn-primary" type="submit" id="loginBtn">Login</button>
|
||||
<span id="loginSpinner" class="loading" style="display:none;"></span>
|
||||
</div>
|
||||
|
||||
<div class="login-error" id="loginError"></div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<script>
|
||||
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,
|
||||
|
|
@ -415,10 +644,77 @@
|
|||
|
||||
// Initialize
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
loadFiles('videos');
|
||||
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 = `
|
||||
<div class="player-placeholder">
|
||||
<div class="player-placeholder-icon">🎵</div>
|
||||
<p>Select a file to start playing</p>
|
||||
</div>
|
||||
`;
|
||||
document.getElementById('nowPlaying').style.display = 'none';
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function setupTabListeners() {
|
||||
document.querySelectorAll('.tab-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
|
|
@ -436,7 +732,7 @@
|
|||
browser.innerHTML = '<div class="loading"></div>';
|
||||
|
||||
try {
|
||||
const response = await fetch(`get_files.php?type=${type}`);
|
||||
const response = await authedFetch(`get_files.php?type=${type}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
|
|
@ -516,18 +812,92 @@
|
|||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
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;
|
||||
|
||||
if (isVideo) {
|
||||
// Some browsers only populate audioTracks after metadata is available
|
||||
mediaElement.addEventListener('loadedmetadata', () => {
|
||||
setupAudioTrackSelector(mediaElement);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Add error handling
|
||||
mediaElement.addEventListener('error', (e) => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue