add services/web-ui/public/player.html
This commit is contained in:
parent
28be46403a
commit
e444162800
1 changed files with 488 additions and 0 deletions
488
services/web-ui/public/player.html
Normal file
488
services/web-ui/public/player.html
Normal file
|
|
@ -0,0 +1,488 @@
|
|||
<!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"></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>
|
||||
Loading…
Reference in a new issue