2026-04-07 21:58:22 -04:00
|
|
|
<!DOCTYPE html>
|
|
|
|
|
<html lang="en">
|
|
|
|
|
<head>
|
|
|
|
|
<meta charset="UTF-8">
|
|
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
|
|
<title>Wild Dragon - Capture Control</title>
|
|
|
|
|
<link rel="stylesheet" href="/css/common.css">
|
|
|
|
|
<style>
|
|
|
|
|
.capture-container {
|
|
|
|
|
display: grid;
|
|
|
|
|
grid-template-columns: 1fr 2fr 1fr;
|
|
|
|
|
gap: var(--spacing-lg);
|
|
|
|
|
height: calc(100vh - 110px);
|
|
|
|
|
padding: var(--spacing-lg);
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.capture-panel {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
gap: var(--spacing-md);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.capture-center {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
gap: var(--spacing-lg);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.recording-display {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: var(--spacing-lg);
|
|
|
|
|
padding: var(--spacing-xl);
|
|
|
|
|
background-color: var(--color-bg-tertiary);
|
|
|
|
|
border: 1px solid var(--color-border);
|
|
|
|
|
border-radius: var(--radius-lg);
|
|
|
|
|
min-height: 250px;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.recording-status {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: var(--spacing-md);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.recording-indicator-large {
|
|
|
|
|
width: 80px;
|
|
|
|
|
height: 80px;
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
font-size: 2rem;
|
|
|
|
|
transition: all var(--transition-normal);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.recording-indicator-large.idle {
|
|
|
|
|
background-color: rgba(16, 185, 129, 0.2);
|
|
|
|
|
color: var(--color-status-ready);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.recording-indicator-large.recording {
|
|
|
|
|
background-color: rgba(239, 68, 68, 0.2);
|
|
|
|
|
color: var(--color-status-error);
|
|
|
|
|
animation: pulse 1.5s infinite;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.recording-info {
|
|
|
|
|
text-align: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.recording-info-item {
|
|
|
|
|
margin-bottom: var(--spacing-sm);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.recording-info-label {
|
|
|
|
|
font-size: 0.8rem;
|
|
|
|
|
color: var(--color-text-tertiary);
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
letter-spacing: 0.3px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.recording-info-value {
|
|
|
|
|
font-size: 1.5rem;
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
color: var(--color-text-primary);
|
|
|
|
|
font-family: 'Courier New', monospace;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.control-buttons {
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: var(--spacing-lg);
|
|
|
|
|
width: 100%;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.btn-record {
|
|
|
|
|
width: 120px;
|
|
|
|
|
height: 120px;
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
font-size: 1.2rem;
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
transition: all var(--transition-normal);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.btn-record:hover {
|
|
|
|
|
transform: scale(1.05);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.btn-record:active:not(:disabled) {
|
|
|
|
|
transform: scale(0.95);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.btn-start {
|
|
|
|
|
background-color: var(--color-accent-primary);
|
|
|
|
|
color: white;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.btn-start:hover:not(:disabled) {
|
|
|
|
|
background-color: var(--color-accent-light);
|
|
|
|
|
box-shadow: 0 8px 24px rgba(233, 69, 96, 0.4);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.btn-stop {
|
|
|
|
|
background-color: var(--color-status-error);
|
|
|
|
|
color: white;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.btn-stop:hover:not(:disabled) {
|
|
|
|
|
background-color: #ef5555;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.recent-captures-list {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
gap: var(--spacing-sm);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.capture-item {
|
|
|
|
|
padding: var(--spacing-md);
|
|
|
|
|
background-color: var(--color-bg-tertiary);
|
|
|
|
|
border: 1px solid var(--color-border);
|
|
|
|
|
border-radius: var(--radius-md);
|
|
|
|
|
transition: all var(--transition-fast);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.capture-item:hover {
|
|
|
|
|
border-color: var(--color-accent-primary);
|
|
|
|
|
background-color: var(--color-bg-hover);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.capture-item-name {
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
color: var(--color-text-primary);
|
|
|
|
|
margin-bottom: var(--spacing-xs);
|
|
|
|
|
font-size: 0.95rem;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.capture-item-meta {
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
font-size: 0.8rem;
|
|
|
|
|
color: var(--color-text-tertiary);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@media (max-width: 1024px) {
|
|
|
|
|
.capture-container {
|
|
|
|
|
grid-template-columns: 1fr;
|
|
|
|
|
height: auto;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.recording-display {
|
|
|
|
|
min-height: 200px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.btn-record {
|
|
|
|
|
width: 100px;
|
|
|
|
|
height: 100px;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.control-section {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
gap: var(--spacing-md);
|
|
|
|
|
padding: var(--spacing-lg);
|
|
|
|
|
background-color: var(--color-bg-tertiary);
|
|
|
|
|
border: 1px solid var(--color-border);
|
|
|
|
|
border-radius: var(--radius-lg);
|
|
|
|
|
}
|
|
|
|
|
</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 active" data-page="capture">Capture</div>
|
|
|
|
|
<div class="nav-item" data-page="projects">Projects</div>
|
|
|
|
|
</nav>
|
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
<!-- Main Content -->
|
|
|
|
|
<div class="main-content">
|
|
|
|
|
<!-- Sidebar -->
|
|
|
|
|
<aside class="sidebar">
|
|
|
|
|
<div class="sidebar-section">
|
|
|
|
|
<div class="sidebar-section-title">Capture Settings</div>
|
|
|
|
|
<div class="control-section">
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
<label class="form-label">Project</label>
|
|
|
|
|
<select id="projectSelect" class="form-select"></select>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
<label class="form-label">Bin</label>
|
|
|
|
|
<select id="binSelect" class="form-select"></select>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
<label class="form-label">Clip Name</label>
|
|
|
|
|
<input type="text" id="clipNameInput" class="form-input" placeholder="e.g., Interview_John_01">
|
|
|
|
|
</div>
|
|
|
|
|
<div class="form-group">
|
|
|
|
|
<label class="form-label">Device</label>
|
|
|
|
|
<select id="deviceSelect" class="form-select"></select>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</aside>
|
|
|
|
|
|
|
|
|
|
<!-- Content Area -->
|
|
|
|
|
<div class="content-area">
|
|
|
|
|
<div class="content-main">
|
|
|
|
|
<div class="capture-container">
|
|
|
|
|
<!-- Left Panel -->
|
|
|
|
|
<div class="capture-panel">
|
|
|
|
|
<div class="control-section">
|
|
|
|
|
<div style="text-align: center;">
|
|
|
|
|
<h3 style="margin-bottom: var(--spacing-md);">Device Status</h3>
|
|
|
|
|
<div id="deviceStatus" style="color: var(--color-text-tertiary); font-size: 0.9rem;">
|
|
|
|
|
Loading devices...
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Center Panel -->
|
|
|
|
|
<div class="capture-center">
|
|
|
|
|
<div class="recording-display">
|
|
|
|
|
<div class="recording-status">
|
|
|
|
|
<div class="recording-indicator-large idle" id="recordingIndicator">●</div>
|
|
|
|
|
<div class="recording-info">
|
|
|
|
|
<div class="recording-info-item">
|
|
|
|
|
<div class="recording-info-label">Timecode</div>
|
|
|
|
|
<div class="recording-info-value" id="timecodeDisplay">00:00:00</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="recording-info-item">
|
|
|
|
|
<div class="recording-info-label">Status</div>
|
|
|
|
|
<div class="recording-info-value" id="statusDisplay" style="color: var(--color-status-ready);">IDLE</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="recording-info-item" id="clipNameDisplay" style="display: none;">
|
|
|
|
|
<div class="recording-info-label">Clip</div>
|
|
|
|
|
<div class="recording-info-value" id="clipNameText"></div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="control-buttons">
|
2026-05-16 00:42:36 -04:00
|
|
|
<button class="btn btn-record btn-start" id="startBtn" onclick="handleStartRecording()">
|
2026-04-07 21:58:22 -04:00
|
|
|
⏺ START
|
|
|
|
|
</button>
|
2026-05-16 00:42:36 -04:00
|
|
|
<button class="btn btn-record btn-stop" id="stopBtn" onclick="handleStopRecording()" disabled>
|
2026-04-07 21:58:22 -04:00
|
|
|
⏹ STOP
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Right Panel -->
|
|
|
|
|
<div class="capture-panel">
|
|
|
|
|
<h3 style="margin-bottom: var(--spacing-md);">Recent Captures</h3>
|
|
|
|
|
<div class="recent-captures-list" id="recentCapturesList">
|
|
|
|
|
<div style="text-align: center; color: var(--color-text-tertiary); padding: var(--spacing-lg);">
|
|
|
|
|
No captures yet
|
|
|
|
|
</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">Initializing...</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="status-item">
|
|
|
|
|
<span id="captureMode">Ready to capture</span>
|
|
|
|
|
</div>
|
|
|
|
|
</footer>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<script src="/js/api.js"></script>
|
|
|
|
|
<script>
|
|
|
|
|
// ============================================================
|
|
|
|
|
// STATE MANAGEMENT
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
let captureState = {
|
|
|
|
|
isRecording: false,
|
|
|
|
|
currentDevice: null,
|
|
|
|
|
currentProject: null,
|
|
|
|
|
currentBin: null,
|
|
|
|
|
currentClipName: '',
|
|
|
|
|
projects: [],
|
|
|
|
|
bins: [],
|
|
|
|
|
devices: [],
|
|
|
|
|
recentCaptures: [],
|
|
|
|
|
recordingTime: 0,
|
|
|
|
|
recordingInterval: null,
|
2026-05-16 00:42:36 -04:00
|
|
|
sessionId: null,
|
2026-04-07 21:58:22 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
// INITIALIZATION
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', async () => {
|
|
|
|
|
setupEventListeners();
|
|
|
|
|
await loadCaptureData();
|
|
|
|
|
updateStatusBar();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
function setupEventListeners() {
|
|
|
|
|
// Navigation
|
|
|
|
|
document.querySelectorAll('[data-page]').forEach(el => {
|
|
|
|
|
el.addEventListener('click', (e) => navigateTo(e.target.dataset.page));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Project selection
|
|
|
|
|
document.getElementById('projectSelect').addEventListener('change', (e) => {
|
|
|
|
|
captureState.currentProject = e.target.value;
|
|
|
|
|
loadBins();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Bin selection
|
|
|
|
|
document.getElementById('binSelect').addEventListener('change', (e) => {
|
|
|
|
|
captureState.currentBin = e.target.value;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Device selection
|
|
|
|
|
document.getElementById('deviceSelect').addEventListener('change', (e) => {
|
|
|
|
|
captureState.currentDevice = e.target.value;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Clip name input
|
|
|
|
|
document.getElementById('clipNameInput').addEventListener('input', (e) => {
|
|
|
|
|
captureState.currentClipName = e.target.value;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function loadCaptureData() {
|
|
|
|
|
try {
|
|
|
|
|
updateStatusBar('Loading capture devices...');
|
|
|
|
|
|
|
|
|
|
const [projectsRes, devicesRes, capturesRes] = await Promise.all([
|
|
|
|
|
getProjects(),
|
|
|
|
|
getCaptureDevices(),
|
|
|
|
|
getRecentCaptures(10),
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
if (projectsRes.success) {
|
|
|
|
|
captureState.projects = projectsRes.data;
|
|
|
|
|
renderProjects();
|
|
|
|
|
if (captureState.projects.length > 0) {
|
|
|
|
|
captureState.currentProject = captureState.projects[0].id;
|
|
|
|
|
document.getElementById('projectSelect').value = captureState.currentProject;
|
|
|
|
|
await loadBins();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (devicesRes.success) {
|
|
|
|
|
captureState.devices = devicesRes.data;
|
|
|
|
|
renderDevices();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (capturesRes.success) {
|
|
|
|
|
captureState.recentCaptures = capturesRes.data;
|
|
|
|
|
renderRecentCaptures();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Fetch current recording status
|
|
|
|
|
const statusRes = await getRecordingStatus();
|
|
|
|
|
if (statusRes.success && statusRes.data.recording) {
|
|
|
|
|
captureState.isRecording = true;
|
|
|
|
|
startTimecodeUpdate();
|
|
|
|
|
updateRecordingUI();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
updateStatusBar();
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Failed to load capture data:', error);
|
|
|
|
|
updateStatusBar('Error loading capture devices', true);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
// RENDERING
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
function renderProjects() {
|
|
|
|
|
const select = document.getElementById('projectSelect');
|
|
|
|
|
select.innerHTML = '';
|
|
|
|
|
|
|
|
|
|
captureState.projects.forEach(project => {
|
|
|
|
|
const option = document.createElement('option');
|
|
|
|
|
option.value = project.id;
|
|
|
|
|
option.textContent = project.name;
|
|
|
|
|
select.appendChild(option);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderDevices() {
|
|
|
|
|
const select = document.getElementById('deviceSelect');
|
|
|
|
|
const statusDiv = document.getElementById('deviceStatus');
|
|
|
|
|
select.innerHTML = '';
|
|
|
|
|
|
|
|
|
|
if (captureState.devices.length === 0) {
|
|
|
|
|
statusDiv.innerHTML = '<span style="color: var(--color-status-error);">No devices found</span>';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
statusDiv.innerHTML = `<span style="color: var(--color-status-ready);">${captureState.devices.length} device(s) available</span>`;
|
|
|
|
|
|
|
|
|
|
captureState.devices.forEach(device => {
|
|
|
|
|
const option = document.createElement('option');
|
|
|
|
|
option.value = device.id;
|
|
|
|
|
option.textContent = `${device.name} (${device.interface})`;
|
|
|
|
|
select.appendChild(option);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (captureState.devices.length > 0) {
|
|
|
|
|
captureState.currentDevice = captureState.devices[0].id;
|
|
|
|
|
document.getElementById('deviceSelect').value = captureState.currentDevice;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function loadBins() {
|
|
|
|
|
if (!captureState.currentProject) return;
|
|
|
|
|
|
|
|
|
|
const result = await getBins(captureState.currentProject);
|
|
|
|
|
if (result.success) {
|
|
|
|
|
captureState.bins = result.data;
|
|
|
|
|
renderBins();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderBins() {
|
|
|
|
|
const select = document.getElementById('binSelect');
|
|
|
|
|
select.innerHTML = '';
|
|
|
|
|
|
|
|
|
|
captureState.bins.forEach(bin => {
|
|
|
|
|
const option = document.createElement('option');
|
|
|
|
|
option.value = bin.id;
|
|
|
|
|
option.textContent = bin.name;
|
|
|
|
|
select.appendChild(option);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (captureState.bins.length > 0) {
|
|
|
|
|
captureState.currentBin = captureState.bins[0].id;
|
|
|
|
|
document.getElementById('binSelect').value = captureState.currentBin;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderRecentCaptures() {
|
|
|
|
|
const container = document.getElementById('recentCapturesList');
|
|
|
|
|
|
|
|
|
|
if (captureState.recentCaptures.length === 0) {
|
|
|
|
|
container.innerHTML = '<div style="text-align: center; color: var(--color-text-tertiary); padding: var(--spacing-lg);">No captures yet</div>';
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
container.innerHTML = '';
|
|
|
|
|
captureState.recentCaptures.slice(0, 10).forEach(capture => {
|
|
|
|
|
const item = document.createElement('div');
|
|
|
|
|
item.className = 'capture-item';
|
|
|
|
|
item.innerHTML = `
|
2026-05-16 00:42:36 -04:00
|
|
|
<div class="capture-item-name">${capture.clip_name || capture.name || 'Untitled'}</div>
|
2026-04-07 21:58:22 -04:00
|
|
|
<div class="capture-item-meta">
|
|
|
|
|
<span>${formatDuration(capture.duration || 0)}</span>
|
2026-05-16 00:42:36 -04:00
|
|
|
<span>${new Date(capture.created_at || capture.captured_at).toLocaleDateString()}</span>
|
2026-04-07 21:58:22 -04:00
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
container.appendChild(item);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
// RECORDING CONTROL
|
2026-05-16 00:42:36 -04:00
|
|
|
// Named handleStartRecording / handleStopRecording to avoid
|
|
|
|
|
// shadowing the api.js startRecording / stopRecording functions.
|
2026-04-07 21:58:22 -04:00
|
|
|
// ============================================================
|
|
|
|
|
|
2026-05-16 00:42:36 -04:00
|
|
|
async function handleStartRecording() {
|
|
|
|
|
if (!captureState.currentProject || !captureState.currentDevice) {
|
|
|
|
|
alert('Please select project and device');
|
2026-04-07 21:58:22 -04:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!captureState.currentClipName) {
|
|
|
|
|
alert('Please enter a clip name');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
updateStatusBar('Starting recording...');
|
|
|
|
|
const result = await startRecording(
|
2026-05-16 00:42:36 -04:00
|
|
|
parseInt(captureState.currentDevice, 10),
|
2026-04-07 21:58:22 -04:00
|
|
|
captureState.currentProject,
|
2026-05-16 00:42:36 -04:00
|
|
|
captureState.currentBin || null,
|
2026-04-07 21:58:22 -04:00
|
|
|
captureState.currentClipName
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (result.success) {
|
|
|
|
|
captureState.isRecording = true;
|
|
|
|
|
captureState.recordingTime = 0;
|
2026-05-16 00:42:36 -04:00
|
|
|
captureState.sessionId = result.data.session_id || null;
|
2026-04-07 21:58:22 -04:00
|
|
|
startTimecodeUpdate();
|
|
|
|
|
updateRecordingUI();
|
|
|
|
|
updateStatusBar();
|
|
|
|
|
} else {
|
|
|
|
|
updateStatusBar('Failed to start recording', true);
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Recording error:', error);
|
|
|
|
|
updateStatusBar('Error starting recording', true);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-16 00:42:36 -04:00
|
|
|
async function handleStopRecording() {
|
2026-04-07 21:58:22 -04:00
|
|
|
try {
|
|
|
|
|
updateStatusBar('Stopping recording...');
|
2026-05-16 00:42:36 -04:00
|
|
|
const result = await stopRecording(captureState.sessionId);
|
2026-04-07 21:58:22 -04:00
|
|
|
|
|
|
|
|
if (result.success) {
|
|
|
|
|
captureState.isRecording = false;
|
2026-05-16 00:42:36 -04:00
|
|
|
captureState.sessionId = null;
|
2026-04-07 21:58:22 -04:00
|
|
|
clearInterval(captureState.recordingInterval);
|
|
|
|
|
updateRecordingUI();
|
|
|
|
|
loadCaptureData();
|
|
|
|
|
updateStatusBar('Recording saved');
|
|
|
|
|
} else {
|
|
|
|
|
updateStatusBar('Failed to stop recording', true);
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Stop error:', error);
|
|
|
|
|
updateStatusBar('Error stopping recording', true);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function startTimecodeUpdate() {
|
|
|
|
|
if (captureState.recordingInterval) {
|
|
|
|
|
clearInterval(captureState.recordingInterval);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
captureState.recordingInterval = setInterval(() => {
|
|
|
|
|
captureState.recordingTime++;
|
|
|
|
|
document.getElementById('timecodeDisplay').textContent = formatDuration(captureState.recordingTime);
|
|
|
|
|
}, 1000);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function updateRecordingUI() {
|
|
|
|
|
const indicator = document.getElementById('recordingIndicator');
|
|
|
|
|
const status = document.getElementById('statusDisplay');
|
|
|
|
|
const startBtn = document.getElementById('startBtn');
|
|
|
|
|
const stopBtn = document.getElementById('stopBtn');
|
|
|
|
|
const clipDisplay = document.getElementById('clipNameDisplay');
|
|
|
|
|
const clipText = document.getElementById('clipNameText');
|
|
|
|
|
|
|
|
|
|
if (captureState.isRecording) {
|
|
|
|
|
indicator.className = 'recording-indicator-large recording';
|
|
|
|
|
status.textContent = 'RECORDING';
|
|
|
|
|
status.style.color = 'var(--color-status-error)';
|
|
|
|
|
startBtn.disabled = true;
|
|
|
|
|
stopBtn.disabled = false;
|
|
|
|
|
clipDisplay.style.display = 'block';
|
|
|
|
|
clipText.textContent = captureState.currentClipName;
|
|
|
|
|
} else {
|
|
|
|
|
indicator.className = 'recording-indicator-large idle';
|
|
|
|
|
status.textContent = 'IDLE';
|
|
|
|
|
status.style.color = 'var(--color-status-ready)';
|
|
|
|
|
startBtn.disabled = false;
|
|
|
|
|
stopBtn.disabled = true;
|
|
|
|
|
clipDisplay.style.display = 'none';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
// NAVIGATION
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
function navigateTo(page) {
|
|
|
|
|
if (page === 'assets') {
|
|
|
|
|
window.location.href = '/index.html';
|
|
|
|
|
} else if (page === 'projects') {
|
|
|
|
|
alert('Projects page coming soon');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============================================================
|
|
|
|
|
// STATUS BAR
|
|
|
|
|
// ============================================================
|
|
|
|
|
|
|
|
|
|
function updateStatusBar(message = '', isError = false) {
|
|
|
|
|
const indicator = document.getElementById('statusIndicator');
|
|
|
|
|
const text = document.getElementById('statusText');
|
|
|
|
|
|
|
|
|
|
if (message) {
|
|
|
|
|
text.textContent = message;
|
|
|
|
|
indicator.className = isError ? 'status-indicator disconnected' : 'status-indicator';
|
|
|
|
|
} else {
|
|
|
|
|
indicator.className = 'status-indicator';
|
|
|
|
|
text.textContent = 'Connected';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</script>
|
|
|
|
|
</body>
|
|
|
|
|
</html>
|