fix: assets response shape, thumbnail lazy-load, bin sidebar wired up
This commit is contained in:
parent
7ef8476bd3
commit
b42199e597
1 changed files with 262 additions and 116 deletions
|
|
@ -40,12 +40,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes slideInRight {
|
@keyframes slideInRight {
|
||||||
from {
|
from { transform: translateX(100%); }
|
||||||
transform: translateX(100%);
|
to { transform: translateX(0); }
|
||||||
}
|
|
||||||
to {
|
|
||||||
transform: translateX(0);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-header {
|
.detail-header {
|
||||||
|
|
@ -71,9 +67,7 @@
|
||||||
height: 32px;
|
height: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-close:hover {
|
.detail-close:hover { color: var(--color-text-primary); }
|
||||||
color: var(--color-text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-body {
|
.detail-body {
|
||||||
padding: var(--spacing-lg);
|
padding: var(--spacing-lg);
|
||||||
|
|
@ -81,9 +75,7 @@
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-section {
|
.detail-section { margin-bottom: var(--spacing-lg); }
|
||||||
margin-bottom: var(--spacing-lg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-section-title {
|
.detail-section-title {
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
|
|
@ -154,6 +146,7 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--spacing-sm);
|
gap: var(--spacing-sm);
|
||||||
margin-bottom: var(--spacing-lg);
|
margin-bottom: var(--spacing-lg);
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-tag {
|
.filter-tag {
|
||||||
|
|
@ -187,8 +180,54 @@
|
||||||
transition: margin-right var(--transition-normal);
|
transition: margin-right var(--transition-normal);
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-main.detail-open {
|
.content-main.detail-open { margin-right: 400px; }
|
||||||
margin-right: 400px;
|
|
||||||
|
/* Thumbnail inside asset card */
|
||||||
|
.card-thumbnail {
|
||||||
|
position: relative;
|
||||||
|
aspect-ratio: 16/9;
|
||||||
|
background-color: var(--color-bg-tertiary);
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: var(--radius-md) var(--radius-md) 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-thumbnail img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
display: block;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-thumbnail img.loading { opacity: 0.3; }
|
||||||
|
.card-thumbnail img.loaded { opacity: 1; }
|
||||||
|
|
||||||
|
.card-thumbnail .thumb-placeholder {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 2rem;
|
||||||
|
opacity: 0.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Status pulse for recording/processing */
|
||||||
|
.card-status-dot {
|
||||||
|
display: inline-block;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
.card-status-dot.processing { background: #f59e0b; animation: pulse 1.5s infinite; }
|
||||||
|
.card-status-dot.ingesting { background: #3b82f6; animation: pulse 1.5s infinite; }
|
||||||
|
.card-status-dot.ready { background: #22c55e; }
|
||||||
|
.card-status-dot.error { background: #ef4444; }
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% { opacity: 1; }
|
||||||
|
50% { opacity: 0.3; }
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
@ -231,7 +270,7 @@
|
||||||
<div class="assets-controls">
|
<div class="assets-controls">
|
||||||
<button class="btn btn-secondary" id="refreshBtn">↻ Refresh</button>
|
<button class="btn btn-secondary" id="refreshBtn">↻ Refresh</button>
|
||||||
<button class="btn btn-primary" onclick="navigateTo('upload')">↑ Upload</button>
|
<button class="btn btn-primary" onclick="navigateTo('upload')">↑ Upload</button>
|
||||||
<button class="btn btn-primary" onclick="navigateTo('capture')">+ New Capture</button>
|
<button class="btn btn-primary" onclick="navigateTo('capture')">⏺ New Capture</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -251,7 +290,9 @@
|
||||||
<div class="empty-state" id="emptyState" style="display: none;">
|
<div class="empty-state" id="emptyState" style="display: none;">
|
||||||
<div class="empty-state-icon">📁</div>
|
<div class="empty-state-icon">📁</div>
|
||||||
<div class="empty-state-text">No assets found</div>
|
<div class="empty-state-text">No assets found</div>
|
||||||
<p style="color: var(--color-text-tertiary); font-size: 0.9rem;">Create a new capture to get started</p>
|
<p style="color: var(--color-text-tertiary); font-size: 0.9rem;">
|
||||||
|
Create a new capture or upload a file to get started
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -264,7 +305,7 @@
|
||||||
<button class="detail-close" onclick="closeDetailPanel()">✕</button>
|
<button class="detail-close" onclick="closeDetailPanel()">✕</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="detail-body">
|
<div class="detail-body">
|
||||||
<video class="video-player" id="videoPlayer" controls></video>
|
<video class="video-player" id="videoPlayer" controls preload="metadata"></video>
|
||||||
|
|
||||||
<div class="detail-section">
|
<div class="detail-section">
|
||||||
<div class="detail-section-title">File Information</div>
|
<div class="detail-section-title">File Information</div>
|
||||||
|
|
@ -272,10 +313,6 @@
|
||||||
<div class="detail-label">Filename</div>
|
<div class="detail-label">Filename</div>
|
||||||
<div class="detail-value" id="detailFilename">—</div>
|
<div class="detail-value" id="detailFilename">—</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="detail-row">
|
|
||||||
<div class="detail-label">Format</div>
|
|
||||||
<div class="detail-value" id="detailFormat">—</div>
|
|
||||||
</div>
|
|
||||||
<div class="detail-row">
|
<div class="detail-row">
|
||||||
<div class="detail-label">Codec</div>
|
<div class="detail-label">Codec</div>
|
||||||
<div class="detail-value" id="detailCodec">—</div>
|
<div class="detail-value" id="detailCodec">—</div>
|
||||||
|
|
@ -339,11 +376,10 @@
|
||||||
<script src="/js/api.js"></script>
|
<script src="/js/api.js"></script>
|
||||||
<script>
|
<script>
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// STATE MANAGEMENT
|
// STATE
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
let state = {
|
const state = {
|
||||||
currentPage: 'assets',
|
|
||||||
assets: [],
|
assets: [],
|
||||||
projects: [],
|
projects: [],
|
||||||
bins: [],
|
bins: [],
|
||||||
|
|
@ -352,35 +388,46 @@
|
||||||
selectedBin: null,
|
selectedBin: null,
|
||||||
searchQuery: '',
|
searchQuery: '',
|
||||||
filterStatus: null,
|
filterStatus: null,
|
||||||
|
// Cache of assetId -> thumbnail URL to avoid re-fetching
|
||||||
|
thumbCache: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// IntersectionObserver for lazy thumbnail loading
|
||||||
|
const thumbObserver = new IntersectionObserver((entries) => {
|
||||||
|
entries.forEach(entry => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
const img = entry.target;
|
||||||
|
const assetId = img.dataset.assetId;
|
||||||
|
if (assetId && !img.dataset.loaded) {
|
||||||
|
loadThumbnail(img, assetId);
|
||||||
|
}
|
||||||
|
thumbObserver.unobserve(img);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, { rootMargin: '100px' });
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// INITIALIZATION
|
// INIT
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', async () => {
|
document.addEventListener('DOMContentLoaded', async () => {
|
||||||
setupEventListeners();
|
setupEventListeners();
|
||||||
await loadInitialData();
|
await loadInitialData();
|
||||||
updateStatusBar();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function setupEventListeners() {
|
function setupEventListeners() {
|
||||||
// Navigation
|
|
||||||
document.querySelectorAll('[data-page]').forEach(el => {
|
document.querySelectorAll('[data-page]').forEach(el => {
|
||||||
el.addEventListener('click', (e) => navigateTo(e.target.dataset.page));
|
el.addEventListener('click', (e) => navigateTo(e.target.dataset.page));
|
||||||
});
|
});
|
||||||
|
|
||||||
// Search
|
|
||||||
const searchInput = document.getElementById('searchInput');
|
const searchInput = document.getElementById('searchInput');
|
||||||
searchInput.addEventListener('input', debounce((e) => {
|
searchInput.addEventListener('input', debounce((e) => {
|
||||||
state.searchQuery = e.target.value.toLowerCase();
|
state.searchQuery = e.target.value.toLowerCase();
|
||||||
renderAssets();
|
renderAssets();
|
||||||
}, 300));
|
}, 300));
|
||||||
|
|
||||||
// Refresh button
|
|
||||||
document.getElementById('refreshBtn').addEventListener('click', loadInitialData);
|
document.getElementById('refreshBtn').addEventListener('click', loadInitialData);
|
||||||
|
|
||||||
// Close detail panel on overlay click
|
|
||||||
document.addEventListener('keydown', (e) => {
|
document.addEventListener('keydown', (e) => {
|
||||||
if (e.key === 'Escape') closeDetailPanel();
|
if (e.key === 'Escape') closeDetailPanel();
|
||||||
});
|
});
|
||||||
|
|
@ -388,27 +435,27 @@
|
||||||
|
|
||||||
async function loadInitialData() {
|
async function loadInitialData() {
|
||||||
try {
|
try {
|
||||||
updateStatusBar('Loading data...');
|
updateStatusBar('Loading...');
|
||||||
|
|
||||||
// Load projects and assets in parallel
|
|
||||||
const [projectsRes, assetsRes] = await Promise.all([
|
const [projectsRes, assetsRes] = await Promise.all([
|
||||||
getProjects(),
|
getProjects(),
|
||||||
getAssets(),
|
getAssets({ limit: 100 }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (projectsRes.success) {
|
if (projectsRes.success) {
|
||||||
state.projects = projectsRes.data;
|
state.projects = projectsRes.data || [];
|
||||||
renderProjects();
|
renderProjects();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (assetsRes.success) {
|
if (assetsRes.success) {
|
||||||
state.assets = assetsRes.data;
|
// API returns { assets: [...], total: n }
|
||||||
|
state.assets = assetsRes.data?.assets ?? assetsRes.data ?? [];
|
||||||
renderAssets();
|
renderAssets();
|
||||||
}
|
}
|
||||||
|
|
||||||
updateStatusBar();
|
updateStatusBar();
|
||||||
} catch (error) {
|
} catch (err) {
|
||||||
console.error('Failed to load data:', error);
|
console.error('Failed to load data:', err);
|
||||||
updateStatusBar('Error loading data', true);
|
updateStatusBar('Error loading data', true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -421,6 +468,11 @@
|
||||||
const container = document.getElementById('projectsList');
|
const container = document.getElementById('projectsList');
|
||||||
container.innerHTML = '';
|
container.innerHTML = '';
|
||||||
|
|
||||||
|
if (state.projects.length === 0) {
|
||||||
|
container.innerHTML = '<div style="padding:8px;color:var(--color-text-tertiary);font-size:0.85rem;">No projects</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
state.projects.forEach(project => {
|
state.projects.forEach(project => {
|
||||||
const item = document.createElement('div');
|
const item = document.createElement('div');
|
||||||
item.className = `sidebar-item ${state.selectedProject === project.id ? 'active' : ''}`;
|
item.className = `sidebar-item ${state.selectedProject === project.id ? 'active' : ''}`;
|
||||||
|
|
@ -434,28 +486,36 @@
|
||||||
const grid = document.getElementById('assetsGrid');
|
const grid = document.getElementById('assetsGrid');
|
||||||
const emptyState = document.getElementById('emptyState');
|
const emptyState = document.getElementById('emptyState');
|
||||||
|
|
||||||
// Filter assets
|
let filtered = [...state.assets];
|
||||||
let filtered = state.assets;
|
|
||||||
|
|
||||||
if (state.searchQuery) {
|
if (state.searchQuery) {
|
||||||
filtered = filtered.filter(asset => {
|
filtered = filtered.filter(asset => {
|
||||||
const searchStr = `${asset.filename} ${asset.tags?.join(' ') || ''}`.toLowerCase();
|
const haystack = [
|
||||||
return searchStr.includes(state.searchQuery);
|
asset.filename,
|
||||||
|
asset.display_name,
|
||||||
|
...(asset.tags || []),
|
||||||
|
asset.notes || '',
|
||||||
|
].join(' ').toLowerCase();
|
||||||
|
return haystack.includes(state.searchQuery);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.filterStatus) {
|
if (state.filterStatus) {
|
||||||
filtered = filtered.filter(asset => asset.status === state.filterStatus);
|
filtered = filtered.filter(a => a.status === state.filterStatus);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.selectedProject) {
|
if (state.selectedProject) {
|
||||||
filtered = filtered.filter(asset => asset.project_id === state.selectedProject);
|
filtered = filtered.filter(a => a.project_id === state.selectedProject);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.selectedBin) {
|
||||||
|
filtered = filtered.filter(a => a.bin_id === state.selectedBin);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show/hide empty state
|
|
||||||
if (filtered.length === 0) {
|
if (filtered.length === 0) {
|
||||||
grid.innerHTML = '';
|
grid.innerHTML = '';
|
||||||
emptyState.style.display = 'flex';
|
emptyState.style.display = 'flex';
|
||||||
|
document.getElementById('assetCountItem').style.display = 'none';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -467,22 +527,35 @@
|
||||||
grid.appendChild(card);
|
grid.appendChild(card);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update filter group
|
|
||||||
renderFilterGroup();
|
renderFilterGroup();
|
||||||
|
updateStatusBar();
|
||||||
}
|
}
|
||||||
|
|
||||||
function createAssetCard(asset) {
|
function createAssetCard(asset) {
|
||||||
const card = document.createElement('div');
|
const card = document.createElement('div');
|
||||||
card.className = 'card';
|
card.className = 'card';
|
||||||
|
card.dataset.assetId = asset.id;
|
||||||
|
|
||||||
|
const durationStr = formatDuration(
|
||||||
|
asset.duration_ms ? Math.round(asset.duration_ms / 1000) : 0
|
||||||
|
);
|
||||||
|
|
||||||
card.innerHTML = `
|
card.innerHTML = `
|
||||||
<div class="card-thumbnail">
|
<div class="card-thumbnail">
|
||||||
<img src="${asset.thumbnail_url || '/placeholder.png'}" alt="${asset.filename}">
|
<span class="thumb-placeholder">🎬</span>
|
||||||
|
<img
|
||||||
|
data-asset-id="${asset.id}"
|
||||||
|
alt="${escapeHtml(asset.display_name || asset.filename)}"
|
||||||
|
class="loading"
|
||||||
|
style="position:absolute;inset:0;"
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<div class="card-title">${asset.filename}</div>
|
<div class="card-title">${escapeHtml(asset.display_name || asset.filename)}</div>
|
||||||
<div class="card-meta">
|
<div class="card-meta">
|
||||||
<div class="card-duration">${formatDuration(asset.duration || 0)}</div>
|
<div class="card-duration">${durationStr}</div>
|
||||||
<div class="card-status">
|
<div class="card-status">
|
||||||
|
<span class="card-status-dot ${asset.status}"></span>
|
||||||
<span class="badge ${getStatusBadgeClass(asset.status)}">
|
<span class="badge ${getStatusBadgeClass(asset.status)}">
|
||||||
${getStatusLabel(asset.status)}
|
${getStatusLabel(asset.status)}
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -490,32 +563,64 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
// Observe the img element for lazy thumbnail loading
|
||||||
|
const img = card.querySelector('img[data-asset-id]');
|
||||||
|
if (asset.thumbnail_s3_key) {
|
||||||
|
thumbObserver.observe(img);
|
||||||
|
} else {
|
||||||
|
// No thumbnail yet — hide the img entirely, show placeholder
|
||||||
|
img.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
card.addEventListener('click', () => openAssetDetail(asset));
|
card.addEventListener('click', () => openAssetDetail(asset));
|
||||||
return card;
|
return card;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadThumbnail(img, assetId) {
|
||||||
|
// Use cached URL if available
|
||||||
|
if (state.thumbCache[assetId]) {
|
||||||
|
img.src = state.thumbCache[assetId];
|
||||||
|
img.classList.remove('loading');
|
||||||
|
img.classList.add('loaded');
|
||||||
|
img.dataset.loaded = '1';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await api(`/assets/${assetId}/thumbnail`);
|
||||||
|
if (result.success) {
|
||||||
|
state.thumbCache[assetId] = result.data.url;
|
||||||
|
img.src = result.data.url;
|
||||||
|
img.onload = () => {
|
||||||
|
img.classList.remove('loading');
|
||||||
|
img.classList.add('loaded');
|
||||||
|
};
|
||||||
|
img.dataset.loaded = '1';
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// Silently ignore — placeholder stays
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function renderFilterGroup() {
|
function renderFilterGroup() {
|
||||||
const container = document.getElementById('filterGroup');
|
const container = document.getElementById('filterGroup');
|
||||||
container.innerHTML = '';
|
container.innerHTML = '';
|
||||||
|
|
||||||
const statuses = ['ingesting', 'processing', 'ready', 'error', 'archived'];
|
|
||||||
const counts = {};
|
const counts = {};
|
||||||
|
|
||||||
state.assets.forEach(asset => {
|
state.assets.forEach(asset => {
|
||||||
counts[asset.status] = (counts[asset.status] || 0) + 1;
|
counts[asset.status] = (counts[asset.status] || 0) + 1;
|
||||||
});
|
});
|
||||||
|
|
||||||
statuses.forEach(status => {
|
Object.entries(counts).forEach(([status, count]) => {
|
||||||
if (counts[status] > 0) {
|
const tag = document.createElement('div');
|
||||||
const tag = document.createElement('div');
|
tag.className = `filter-tag ${state.filterStatus === status ? 'active' : ''}`;
|
||||||
tag.className = `filter-tag ${state.filterStatus === status ? 'active' : ''}`;
|
tag.textContent = `${getStatusLabel(status)} (${count})`;
|
||||||
tag.textContent = `${status.charAt(0).toUpperCase() + status.slice(1)} (${counts[status]})`;
|
tag.addEventListener('click', () => {
|
||||||
tag.addEventListener('click', () => {
|
state.filterStatus = state.filterStatus === status ? null : status;
|
||||||
state.filterStatus = state.filterStatus === status ? null : status;
|
renderAssets();
|
||||||
renderAssets();
|
});
|
||||||
});
|
container.appendChild(tag);
|
||||||
container.appendChild(tag);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -526,41 +631,46 @@
|
||||||
async function openAssetDetail(asset) {
|
async function openAssetDetail(asset) {
|
||||||
state.selectedAsset = asset;
|
state.selectedAsset = asset;
|
||||||
const panel = document.getElementById('detailPanel');
|
const panel = document.getElementById('detailPanel');
|
||||||
const main = document.getElementById('contentMain');
|
const main = document.getElementById('contentMain');
|
||||||
|
const video = document.getElementById('videoPlayer');
|
||||||
|
|
||||||
// Fetch full asset details
|
// Clear previous video
|
||||||
const result = await getAsset(asset.id);
|
video.src = '';
|
||||||
if (result.success) {
|
|
||||||
const fullAsset = result.data;
|
|
||||||
|
|
||||||
// Populate detail panel
|
// Populate with what we already have
|
||||||
document.getElementById('detailFilename').textContent = fullAsset.filename;
|
document.getElementById('detailFilename').textContent = asset.display_name || asset.filename;
|
||||||
document.getElementById('detailFormat').textContent = fullAsset.format || '—';
|
document.getElementById('detailCodec').textContent = asset.codec || '—';
|
||||||
document.getElementById('detailCodec').textContent = fullAsset.codec || '—';
|
document.getElementById('detailResolution').textContent = asset.resolution || '—';
|
||||||
document.getElementById('detailResolution').textContent = fullAsset.resolution || '—';
|
document.getElementById('detailFps').textContent = asset.fps ? `${asset.fps} fps` : '—';
|
||||||
document.getElementById('detailFps').textContent = fullAsset.fps ? `${fullAsset.fps} fps` : '—';
|
document.getElementById('detailDuration').textContent = formatDuration(
|
||||||
document.getElementById('detailDuration').textContent = formatDuration(fullAsset.duration);
|
asset.duration_ms ? Math.round(asset.duration_ms / 1000) : 0
|
||||||
document.getElementById('detailFileSize').textContent = formatFileSize(fullAsset.file_size || 0);
|
);
|
||||||
document.getElementById('detailStatus').innerHTML = `<span class="badge ${getStatusBadgeClass(fullAsset.status)}">${getStatusLabel(fullAsset.status)}</span>`;
|
document.getElementById('detailFileSize').textContent = formatFileSize(asset.file_size || 0);
|
||||||
document.getElementById('detailCreated').textContent = new Date(fullAsset.created_at).toLocaleDateString();
|
document.getElementById('detailStatus').innerHTML =
|
||||||
document.getElementById('detailTags').value = fullAsset.tags?.join(', ') || '';
|
`<span class="badge ${getStatusBadgeClass(asset.status)}">${getStatusLabel(asset.status)}</span>`;
|
||||||
document.getElementById('detailNotes').value = fullAsset.notes || '';
|
document.getElementById('detailCreated').textContent = new Date(asset.created_at).toLocaleString();
|
||||||
|
document.getElementById('detailTags').value = (asset.tags || []).join(', ');
|
||||||
// Load video player
|
document.getElementById('detailNotes').value = asset.notes || '';
|
||||||
const streamResult = await getAssetStreamUrl(asset.id);
|
|
||||||
if (streamResult.success) {
|
|
||||||
document.getElementById('videoPlayer').src = streamResult.data.url;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
panel.classList.add('active');
|
panel.classList.add('active');
|
||||||
main.classList.add('detail-open');
|
main.classList.add('detail-open');
|
||||||
|
|
||||||
|
// Load proxy stream URL asynchronously
|
||||||
|
if (asset.proxy_s3_key) {
|
||||||
|
const streamResult = await getAssetStreamUrl(asset.id);
|
||||||
|
if (streamResult.success) {
|
||||||
|
video.src = streamResult.data.url;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeDetailPanel() {
|
function closeDetailPanel() {
|
||||||
const panel = document.getElementById('detailPanel');
|
const panel = document.getElementById('detailPanel');
|
||||||
const main = document.getElementById('contentMain');
|
const main = document.getElementById('contentMain');
|
||||||
|
const video = document.getElementById('videoPlayer');
|
||||||
|
|
||||||
|
video.pause();
|
||||||
|
video.src = '';
|
||||||
panel.classList.remove('active');
|
panel.classList.remove('active');
|
||||||
main.classList.remove('detail-open');
|
main.classList.remove('detail-open');
|
||||||
state.selectedAsset = null;
|
state.selectedAsset = null;
|
||||||
|
|
@ -570,53 +680,79 @@
|
||||||
if (!state.selectedAsset) return;
|
if (!state.selectedAsset) return;
|
||||||
|
|
||||||
const tags = document.getElementById('detailTags').value
|
const tags = document.getElementById('detailTags').value
|
||||||
.split(',')
|
.split(',').map(t => t.trim()).filter(Boolean);
|
||||||
.map(t => t.trim())
|
|
||||||
.filter(t => t);
|
|
||||||
const notes = document.getElementById('detailNotes').value;
|
const notes = document.getElementById('detailNotes').value;
|
||||||
|
|
||||||
const result = await updateAsset(state.selectedAsset.id, {
|
const result = await updateAsset(state.selectedAsset.id, { tags, notes });
|
||||||
tags,
|
|
||||||
notes,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
updateStatusBar('Asset saved');
|
updateStatusBar('Saved');
|
||||||
// Update asset in local state
|
|
||||||
const idx = state.assets.findIndex(a => a.id === state.selectedAsset.id);
|
const idx = state.assets.findIndex(a => a.id === state.selectedAsset.id);
|
||||||
if (idx >= 0) {
|
if (idx >= 0) {
|
||||||
state.assets[idx].tags = tags;
|
state.assets[idx] = { ...state.assets[idx], tags, notes };
|
||||||
state.assets[idx].notes = notes;
|
|
||||||
}
|
}
|
||||||
|
setTimeout(() => updateStatusBar(), 2000);
|
||||||
} else {
|
} else {
|
||||||
updateStatusBar('Failed to save asset', true);
|
updateStatusBar('Save failed', true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// PROJECT SELECTION
|
// PROJECT / BIN SELECTION
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
function selectProject(projectId) {
|
async function selectProject(projectId) {
|
||||||
state.selectedProject = state.selectedProject === projectId ? null : projectId;
|
state.selectedProject = state.selectedProject === projectId ? null : projectId;
|
||||||
|
state.selectedBin = null;
|
||||||
renderProjects();
|
renderProjects();
|
||||||
|
|
||||||
|
// Load bins for this project
|
||||||
|
const binsContainer = document.getElementById('binsList');
|
||||||
|
binsContainer.innerHTML = '';
|
||||||
|
|
||||||
|
if (state.selectedProject) {
|
||||||
|
const result = await getBins(state.selectedProject);
|
||||||
|
if (result.success) {
|
||||||
|
state.bins = result.data || [];
|
||||||
|
renderBins(state.bins, binsContainer, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
renderAssets();
|
renderAssets();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderBins(bins, container, parentId) {
|
||||||
|
const children = bins.filter(b => b.parent_id === parentId);
|
||||||
|
children.forEach(bin => {
|
||||||
|
const item = document.createElement('div');
|
||||||
|
item.className = `sidebar-item ${state.selectedBin === bin.id ? 'active' : ''}`;
|
||||||
|
item.style.paddingLeft = parentId ? '24px' : '8px';
|
||||||
|
item.textContent = bin.name;
|
||||||
|
item.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
state.selectedBin = state.selectedBin === bin.id ? null : bin.id;
|
||||||
|
renderAssets();
|
||||||
|
// Re-render bins to update active state
|
||||||
|
container.innerHTML = '';
|
||||||
|
renderBins(state.bins, container, null);
|
||||||
|
});
|
||||||
|
container.appendChild(item);
|
||||||
|
// Recurse for nested bins
|
||||||
|
renderBins(bins, container, bin.id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// NAVIGATION
|
// NAVIGATION
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
||||||
function navigateTo(page) {
|
function navigateTo(page) {
|
||||||
if (page === 'capture') {
|
const pages = {
|
||||||
window.location.href = '/capture.html';
|
capture: '/capture.html',
|
||||||
} else if (page === 'upload') {
|
upload: '/upload.html',
|
||||||
window.location.href = '/upload.html';
|
recorders: '/recorders.html',
|
||||||
} else if (page === 'recorders') {
|
};
|
||||||
window.location.href = '/recorders.html';
|
if (pages[page]) window.location.href = pages[page];
|
||||||
} else if (page === 'projects') {
|
|
||||||
alert('Projects page coming soon');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|
@ -625,8 +761,8 @@
|
||||||
|
|
||||||
function updateStatusBar(message = '', isError = false) {
|
function updateStatusBar(message = '', isError = false) {
|
||||||
const indicator = document.getElementById('statusIndicator');
|
const indicator = document.getElementById('statusIndicator');
|
||||||
const text = document.getElementById('statusText');
|
const text = document.getElementById('statusText');
|
||||||
const count = document.getElementById('assetCountItem');
|
const countEl = document.getElementById('assetCountItem');
|
||||||
|
|
||||||
if (message) {
|
if (message) {
|
||||||
text.textContent = message;
|
text.textContent = message;
|
||||||
|
|
@ -636,14 +772,24 @@
|
||||||
text.textContent = 'Connected';
|
text.textContent = 'Connected';
|
||||||
}
|
}
|
||||||
|
|
||||||
const assetCount = state.assets.length;
|
const total = state.assets.length;
|
||||||
if (assetCount > 0) {
|
if (total > 0) {
|
||||||
count.style.display = 'flex';
|
countEl.style.display = 'flex';
|
||||||
document.getElementById('assetCount').textContent = assetCount;
|
document.getElementById('assetCount').textContent = total;
|
||||||
} else {
|
} else {
|
||||||
count.style.display = 'none';
|
countEl.style.display = 'none';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// HELPERS
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
function escapeHtml(str) {
|
||||||
|
const d = document.createElement('div');
|
||||||
|
d.textContent = str;
|
||||||
|
return d.innerHTML;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue