dragonflight/services/web-ui/public/player.html
Zac 3ea896c368 fix(web-ui): bust JS cache so api.js fix actually reaches the browser
The api.js library-list fix from the previous commit never reached the browser because nginx served all .js with `Cache-Control: public, immutable; max-age=31536000`. The HTML referenced api.js with no version query, so the browser kept its year-cached buggy copy.

* nginx.conf: drop .js from the immutable long-cache block, add a no-cache must-revalidate block so future redeploys are picked up immediately.
* All HTML files: tag api.js refs with ?v=4 so already-running browsers fetch the new version on next page load.
2026-05-17 08:31:00 -04:00

488 lines
18 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Wild Dragon - Asset Player</title>
<link rel="stylesheet" href="/css/common.css">
<style>
.player-container {
display: grid;
grid-template-columns: 1fr 300px;
gap: var(--spacing-lg);
height: calc(100vh - 110px);
padding: var(--spacing-lg);
overflow: hidden;
}
.player-main {
display: flex;
flex-direction: column;
gap: var(--spacing-lg);
}
.video-container {
flex: 1;
background-color: var(--color-bg-tertiary);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
.video-player {
width: 100%;
height: 100%;
background-color: var(--color-bg-primary);
}
.metadata-panel {
display: flex;
flex-direction: column;
gap: var(--spacing-md);
}
.metadata-section {
padding: var(--spacing-md);
background-color: var(--color-bg-tertiary);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
}
.metadata-section-title {
font-size: 0.85rem;
font-weight: 700;
color: var(--color-text-secondary);
text-transform: uppercase;
margin-bottom: var(--spacing-md);
letter-spacing: 0.3px;
}
.metadata-row {
display: flex;
flex-direction: column;
gap: var(--spacing-xs);
margin-bottom: var(--spacing-sm);
padding-bottom: var(--spacing-sm);
border-bottom: 1px solid var(--color-border);
}
.metadata-row:last-child {
margin-bottom: 0;
padding-bottom: 0;
border-bottom: none;
}
.metadata-label {
font-size: 0.75rem;
color: var(--color-text-tertiary);
text-transform: uppercase;
letter-spacing: 0.2px;
}
.metadata-value {
font-size: 0.9rem;
color: var(--color-text-primary);
font-weight: 500;
}
.metadata-editable {
display: flex;
gap: var(--spacing-sm);
flex-direction: column;
}
.metadata-textarea {
width: 100%;
padding: var(--spacing-sm);
background-color: var(--color-bg-primary);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
color: var(--color-text-primary);
font-family: 'Courier New', monospace;
font-size: 0.85rem;
resize: vertical;
min-height: 80px;
}
.metadata-textarea:focus {
outline: none;
border-color: var(--color-accent-primary);
box-shadow: 0 0 0 3px rgba(233, 69, 96, 0.1);
}
.sidebar {
overflow-y: auto;
}
.tags-list {
display: flex;
flex-wrap: wrap;
gap: var(--spacing-sm);
margin-bottom: var(--spacing-md);
}
.tag-badge {
display: inline-flex;
align-items: center;
gap: var(--spacing-xs);
padding: 4px 10px;
background-color: var(--color-bg-primary);
border: 1px solid var(--color-border);
border-radius: 20px;
font-size: 0.8rem;
color: var(--color-text-secondary);
cursor: pointer;
transition: all var(--transition-fast);
}
.tag-badge:hover {
border-color: var(--color-accent-primary);
color: var(--color-accent-primary);
}
.tag-remove {
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-weight: bold;
}
.edit-controls {
display: flex;
gap: var(--spacing-sm);
margin-top: var(--spacing-md);
}
.back-button {
display: flex;
align-items: center;
gap: var(--spacing-sm);
padding: var(--spacing-sm) var(--spacing-md);
background-color: var(--color-bg-tertiary);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
color: var(--color-text-secondary);
cursor: pointer;
transition: all var(--transition-fast);
}
.back-button:hover {
border-color: var(--color-accent-primary);
color: var(--color-accent-primary);
}
@media (max-width: 768px) {
.player-container {
grid-template-columns: 1fr;
height: auto;
}
.metadata-panel {
display: grid;
grid-template-columns: 1fr 1fr;
}
.video-container {
height: 400px;
}
}
</style>
</head>
<body>
<div class="app-container">
<!-- Header -->
<header class="header">
<div class="header-logo">
<div class="header-logo-icon">D</div>
<span>WILD DRAGON</span>
</div>
<nav class="header-nav">
<div class="nav-item" data-page="assets">Assets</div>
<div class="nav-item" data-page="capture">Capture</div>
<div class="nav-item" data-page="projects">Projects</div>
</nav>
</header>
<!-- Main Content -->
<div class="main-content">
<div class="content-area">
<div class="content-main">
<button class="back-button" onclick="goBack()">
<span></span> Back to Assets
</button>
<div class="player-container">
<!-- Main Player -->
<div class="player-main">
<div class="video-container">
<video class="video-player" id="videoPlayer" controls></video>
</div>
</div>
<!-- Sidebar -->
<div class="sidebar">
<!-- File Info -->
<div class="metadata-section">
<div class="metadata-section-title">File Information</div>
<div class="metadata-row">
<div class="metadata-label">Filename</div>
<div class="metadata-value" id="metaFilename"></div>
</div>
<div class="metadata-row">
<div class="metadata-label">Format</div>
<div class="metadata-value" id="metaFormat"></div>
</div>
<div class="metadata-row">
<div class="metadata-label">Codec</div>
<div class="metadata-value" id="metaCodec"></div>
</div>
<div class="metadata-row">
<div class="metadata-label">Resolution</div>
<div class="metadata-value" id="metaResolution"></div>
</div>
<div class="metadata-row">
<div class="metadata-label">Framerate</div>
<div class="metadata-value" id="metaFps"></div>
</div>
<div class="metadata-row">
<div class="metadata-label">Duration</div>
<div class="metadata-value" id="metaDuration"></div>
</div>
<div class="metadata-row">
<div class="metadata-label">File Size</div>
<div class="metadata-value" id="metaFileSize"></div>
</div>
<div class="metadata-row">
<div class="metadata-label">Status</div>
<div class="metadata-value" id="metaStatus"></div>
</div>
<div class="metadata-row">
<div class="metadata-label">Captured</div>
<div class="metadata-value" id="metaCreated"></div>
</div>
</div>
<!-- Tags -->
<div class="metadata-section">
<div class="metadata-section-title">Tags</div>
<div class="tags-list" id="tagsList"></div>
<input
type="text"
id="newTagInput"
class="form-input"
placeholder="Add tag..."
style="font-size: 0.85rem;"
>
<button class="btn btn-secondary btn-sm" style="width: 100%; margin-top: var(--spacing-sm);" onclick="addTag()">Add Tag</button>
</div>
<!-- Notes -->
<div class="metadata-section">
<div class="metadata-section-title">Notes</div>
<textarea id="notesInput" class="metadata-textarea" placeholder="Add notes about this asset..."></textarea>
<div class="edit-controls">
<button class="btn btn-primary btn-sm" style="flex: 1;" onclick="saveMetadata()">Save</button>
<button class="btn btn-secondary btn-sm" style="flex: 1;" onclick="resetMetadata()">Reset</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Status Bar -->
<footer class="status-bar">
<div class="status-item">
<span class="status-indicator" id="statusIndicator"></span>
<span id="statusText">Connected</span>
</div>
</footer>
</div>
<script src="/js/api.js?v=4"></script>
<script>
// ============================================================
// STATE MANAGEMENT
// ============================================================
let playerState = {
assetId: null,
asset: null,
tags: [],
notes: '',
originalTags: [],
originalNotes: '',
};
// ============================================================
// INITIALIZATION
// ============================================================
document.addEventListener('DOMContentLoaded', () => {
setupEventListeners();
const params = new URLSearchParams(window.location.search);
playerState.assetId = params.get('id');
if (playerState.assetId) {
loadAsset();
} else {
document.getElementById('statusText').textContent = 'No asset selected';
}
});
function setupEventListeners() {
document.getElementById('newTagInput').addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
addTag();
e.preventDefault();
}
});
document.querySelectorAll('[data-page]').forEach(el => {
el.addEventListener('click', (e) => navigateTo(e.target.dataset.page));
});
}
async function loadAsset() {
try {
const result = await getAsset(playerState.assetId);
if (result.success) {
playerState.asset = result.data;
playerState.tags = result.data.tags || [];
playerState.notes = result.data.notes || '';
playerState.originalTags = [...playerState.tags];
playerState.originalNotes = playerState.notes;
renderAsset();
// Load video stream
const streamResult = await getAssetStreamUrl(playerState.assetId);
if (streamResult.success) {
document.getElementById('videoPlayer').src = streamResult.data.url;
}
} else {
document.getElementById('statusText').textContent = 'Failed to load asset';
}
} catch (error) {
console.error('Error loading asset:', error);
document.getElementById('statusText').textContent = 'Error loading asset';
}
}
// ============================================================
// RENDERING
// ============================================================
function renderAsset() {
const asset = playerState.asset;
document.getElementById('metaFilename').textContent = asset.filename;
document.getElementById('metaFormat').textContent = asset.format || '—';
document.getElementById('metaCodec').textContent = asset.codec || '—';
document.getElementById('metaResolution').textContent = asset.resolution || '—';
document.getElementById('metaFps').textContent = asset.fps ? `${asset.fps} fps` : '—';
document.getElementById('metaDuration').textContent = formatDuration(asset.duration);
document.getElementById('metaFileSize').textContent = formatFileSize(asset.file_size || 0);
document.getElementById('metaStatus').innerHTML =
`<span class="badge ${getStatusBadgeClass(asset.status)}">${getStatusLabel(asset.status)}</span>`;
document.getElementById('metaCreated').textContent = new Date(asset.created_at).toLocaleDateString();
renderTags();
document.getElementById('notesInput').value = playerState.notes;
document.title = `${asset.filename} - Wild Dragon Player`;
}
function renderTags() {
const container = document.getElementById('tagsList');
container.innerHTML = '';
playerState.tags.forEach((tag, index) => {
const badge = document.createElement('div');
badge.className = 'tag-badge';
badge.innerHTML = `
<span>${tag}</span>
<span class="tag-remove" onclick="removeTag(${index})">×</span>
`;
container.appendChild(badge);
});
}
// ============================================================
// METADATA EDITING
// ============================================================
function addTag() {
const input = document.getElementById('newTagInput');
const tag = input.value.trim().toLowerCase();
if (tag && !playerState.tags.includes(tag)) {
playerState.tags.push(tag);
renderTags();
input.value = '';
}
}
function removeTag(index) {
playerState.tags.splice(index, 1);
renderTags();
}
async function saveMetadata() {
if (!playerState.assetId) return;
playerState.notes = document.getElementById('notesInput').value;
try {
const result = await updateAsset(playerState.assetId, {
tags: playerState.tags,
notes: playerState.notes,
});
if (result.success) {
playerState.originalTags = [...playerState.tags];
playerState.originalNotes = playerState.notes;
document.getElementById('statusText').textContent = 'Changes saved';
setTimeout(() => {
document.getElementById('statusText').textContent = 'Connected';
}, 2000);
} else {
document.getElementById('statusText').textContent = 'Failed to save changes';
}
} catch (error) {
console.error('Error saving metadata:', error);
document.getElementById('statusText').textContent = 'Error saving changes';
}
}
function resetMetadata() {
playerState.tags = [...playerState.originalTags];
playerState.notes = playerState.originalNotes;
renderTags();
document.getElementById('notesInput').value = playerState.notes;
}
// ============================================================
// NAVIGATION
// ============================================================
function navigateTo(page) {
if (page === 'assets') {
window.location.href = '/index.html';
} else if (page === 'capture') {
window.location.href = '/capture.html';
}
}
function goBack() {
window.location.href = '/index.html';
}
</script>
</body>
</html>