Phase 2: services/web-ui/public/upload.html
This commit is contained in:
parent
0e86cbb1f3
commit
1ed284eac3
1 changed files with 909 additions and 0 deletions
909
services/web-ui/public/upload.html
Normal file
909
services/web-ui/public/upload.html
Normal file
|
|
@ -0,0 +1,909 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Wild Dragon - Web Uploader</title>
|
||||
<link rel="stylesheet" href="/css/common.css">
|
||||
<style>
|
||||
.upload-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100vh - 110px);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.upload-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: var(--spacing-lg);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.upload-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.upload-header h1 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.upload-controls {
|
||||
display: flex;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.dropzone {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--spacing-lg);
|
||||
min-height: 300px;
|
||||
padding: var(--spacing-xl);
|
||||
background-color: var(--color-bg-tertiary);
|
||||
border: 2px dashed var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-normal);
|
||||
}
|
||||
|
||||
.dropzone:hover {
|
||||
border-color: var(--color-accent-primary);
|
||||
background-color: var(--color-bg-hover);
|
||||
}
|
||||
|
||||
.dropzone.dragover {
|
||||
border-color: var(--color-accent-primary);
|
||||
background-color: rgba(233, 69, 96, 0.1);
|
||||
}
|
||||
|
||||
.dropzone-icon {
|
||||
font-size: 3rem;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.dropzone-text {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.dropzone-text h3 {
|
||||
margin-bottom: var(--spacing-sm);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.dropzone-text p {
|
||||
margin: 0;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.upload-config {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: var(--spacing-lg);
|
||||
padding: var(--spacing-lg);
|
||||
background-color: var(--color-bg-tertiary);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
|
||||
.config-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
.config-section-title {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
.upload-queue {
|
||||
flex: 1;
|
||||
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);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.queue-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-bottom: var(--spacing-md);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.queue-title {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.queue-clear {
|
||||
font-size: 0.85rem;
|
||||
color: var(--color-accent-primary);
|
||||
cursor: pointer;
|
||||
transition: color var(--transition-fast);
|
||||
}
|
||||
|
||||
.queue-clear:hover {
|
||||
color: var(--color-accent-light);
|
||||
}
|
||||
|
||||
.queue-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.queue-item {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
gap: var(--spacing-md);
|
||||
padding: var(--spacing-md);
|
||||
background-color: var(--color-bg-secondary);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.queue-item-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-xs);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.queue-item-name {
|
||||
font-weight: 600;
|
||||
color: var(--color-text-primary);
|
||||
font-size: 0.95rem;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.queue-item-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 0.85rem;
|
||||
color: var(--color-text-tertiary);
|
||||
margin-bottom: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.queue-item-progress {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-xs);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
width: 100%;
|
||||
height: 6px;
|
||||
background-color: var(--color-bg-primary);
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, var(--color-accent-primary), var(--color-accent-light));
|
||||
border-radius: 3px;
|
||||
width: 0%;
|
||||
transition: width var(--transition-fast);
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
font-size: 0.8rem;
|
||||
color: var(--color-text-tertiary);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.queue-item-actions {
|
||||
display: flex;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
|
||||
.queue-item-status {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-secondary);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.queue-item-status.waiting {
|
||||
color: var(--color-text-tertiary);
|
||||
}
|
||||
|
||||
.queue-item-status.uploading {
|
||||
color: var(--color-info);
|
||||
}
|
||||
|
||||
.queue-item-status.processing {
|
||||
color: var(--color-warning);
|
||||
}
|
||||
|
||||
.queue-item-status.complete {
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.queue-item-status.error {
|
||||
color: var(--color-danger);
|
||||
}
|
||||
|
||||
.btn-cancel {
|
||||
padding: var(--spacing-xs) var(--spacing-sm);
|
||||
font-size: 0.8rem;
|
||||
background-color: var(--color-status-error);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-cancel:hover {
|
||||
background-color: #ef5555;
|
||||
}
|
||||
|
||||
.queue-empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 150px;
|
||||
color: var(--color-text-tertiary);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.overall-progress {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-sm);
|
||||
padding: var(--spacing-md);
|
||||
background-color: var(--color-bg-secondary);
|
||||
border-radius: var(--radius-md);
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.overall-progress-label {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.overall-progress-bar {
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
background-color: var(--color-bg-primary);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.overall-progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, var(--color-accent-primary), var(--color-accent-light));
|
||||
width: 0%;
|
||||
transition: width var(--transition-fast);
|
||||
}
|
||||
|
||||
.hidden-file-input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.create-project-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-md);
|
||||
padding: var(--spacing-lg);
|
||||
background-color: var(--color-bg-secondary);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
|
||||
.create-project-form-title {
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.upload-config {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.upload-container {
|
||||
height: calc(100vh - 100px);
|
||||
}
|
||||
}
|
||||
</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 active" data-page="upload">Upload</div>
|
||||
<div class="nav-item" data-page="recorders">Recorders</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="main-content">
|
||||
<div class="content-area">
|
||||
<div class="upload-container">
|
||||
<div class="upload-content">
|
||||
<!-- Header -->
|
||||
<div class="upload-header">
|
||||
<h1>Web Uploader</h1>
|
||||
<div class="upload-controls">
|
||||
<button class="btn btn-secondary" id="backBtn" onclick="navigateTo('assets')">← Back</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dropzone -->
|
||||
<div class="dropzone" id="dropzone">
|
||||
<div class="dropzone-icon">📤</div>
|
||||
<div class="dropzone-text">
|
||||
<h3>Drop files here or click to browse</h3>
|
||||
<p>Supported formats: MOV, MP4, MXF, AVI, MKV, TS, M2T</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Configuration -->
|
||||
<div class="upload-config">
|
||||
<div class="config-section">
|
||||
<div class="config-section-title">Project</div>
|
||||
<select id="projectSelect" class="form-select"></select>
|
||||
<div id="createProjectSection" style="display: none;">
|
||||
<button class="btn btn-secondary" style="width: 100%;" onclick="toggleCreateProject()">+ Create New Project</button>
|
||||
<div class="create-project-form" id="createProjectForm" style="display: none;">
|
||||
<div class="create-project-form-title">New Project</div>
|
||||
<input type="text" id="newProjectName" class="form-input" placeholder="Project name">
|
||||
<div style="display: flex; gap: var(--spacing-sm);">
|
||||
<button class="btn btn-primary btn-sm" style="flex: 1;" onclick="createNewProject()">Create</button>
|
||||
<button class="btn btn-secondary btn-sm" style="flex: 1;" onclick="toggleCreateProject()">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="config-section">
|
||||
<div class="config-section-title">Bin</div>
|
||||
<select id="binSelect" class="form-select"></select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Overall Progress -->
|
||||
<div class="overall-progress" id="overallProgressContainer" style="display: none;">
|
||||
<div class="overall-progress-label">
|
||||
<span>Overall Progress</span>
|
||||
<span id="overallProgressPercent">0%</span>
|
||||
</div>
|
||||
<div class="overall-progress-bar">
|
||||
<div class="overall-progress-fill" id="overallProgressFill"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Upload Queue -->
|
||||
<div class="upload-queue">
|
||||
<div class="queue-header">
|
||||
<div class="queue-title">Upload Queue</div>
|
||||
<div class="queue-clear" id="queueClear" style="display: none;" onclick="clearCompleted()">Clear Completed</div>
|
||||
</div>
|
||||
<div class="queue-list" id="queueList">
|
||||
<div class="queue-empty">No files selected</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">Ready to upload</span>
|
||||
</div>
|
||||
<div class="status-item" id="uploadStatsItem" style="display: none;">
|
||||
<span id="uploadStats">0 files uploaded</span>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<!-- Hidden file input -->
|
||||
<input type="file" id="fileInput" class="hidden-file-input" multiple accept=".mov,.mp4,.mxf,.avi,.mkv,.ts,.m2t">
|
||||
|
||||
<script src="/js/api.js"></script>
|
||||
<script>
|
||||
// ============================================================
|
||||
// STATE MANAGEMENT
|
||||
// ============================================================
|
||||
|
||||
let uploadState = {
|
||||
projects: [],
|
||||
bins: [],
|
||||
selectedProject: null,
|
||||
selectedBin: null,
|
||||
uploadQueue: [],
|
||||
activeUploads: 0,
|
||||
completedUploads: 0,
|
||||
};
|
||||
|
||||
// ============================================================
|
||||
// INITIALIZATION
|
||||
// ============================================================
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
setupEventListeners();
|
||||
await loadProjects();
|
||||
updateStatusBar();
|
||||
});
|
||||
|
||||
function setupEventListeners() {
|
||||
// Navigation
|
||||
document.querySelectorAll('[data-page]').forEach(el => {
|
||||
el.addEventListener('click', (e) => navigateTo(e.target.dataset.page));
|
||||
});
|
||||
|
||||
// Dropzone
|
||||
const dropzone = document.getElementById('dropzone');
|
||||
dropzone.addEventListener('click', () => document.getElementById('fileInput').click());
|
||||
dropzone.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
dropzone.classList.add('dragover');
|
||||
});
|
||||
dropzone.addEventListener('dragleave', () => {
|
||||
dropzone.classList.remove('dragover');
|
||||
});
|
||||
dropzone.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
dropzone.classList.remove('dragover');
|
||||
handleFileSelection(e.dataTransfer.files);
|
||||
});
|
||||
|
||||
// File input
|
||||
document.getElementById('fileInput').addEventListener('change', (e) => {
|
||||
handleFileSelection(e.target.files);
|
||||
});
|
||||
|
||||
// Project selection
|
||||
document.getElementById('projectSelect').addEventListener('change', (e) => {
|
||||
uploadState.selectedProject = e.target.value;
|
||||
loadBins();
|
||||
});
|
||||
|
||||
// Bin selection
|
||||
document.getElementById('binSelect').addEventListener('change', (e) => {
|
||||
uploadState.selectedBin = e.target.value;
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// PROJECT & BIN MANAGEMENT
|
||||
// ============================================================
|
||||
|
||||
async function loadProjects() {
|
||||
try {
|
||||
const result = await getProjects();
|
||||
if (result.success) {
|
||||
uploadState.projects = result.data;
|
||||
renderProjects();
|
||||
if (uploadState.projects.length > 0) {
|
||||
uploadState.selectedProject = uploadState.projects[0].id;
|
||||
document.getElementById('projectSelect').value = uploadState.selectedProject;
|
||||
await loadBins();
|
||||
} else {
|
||||
document.getElementById('createProjectSection').style.display = 'block';
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load projects:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function renderProjects() {
|
||||
const select = document.getElementById('projectSelect');
|
||||
select.innerHTML = '';
|
||||
|
||||
uploadState.projects.forEach(project => {
|
||||
const option = document.createElement('option');
|
||||
option.value = project.id;
|
||||
option.textContent = project.name;
|
||||
select.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
async function loadBins() {
|
||||
if (!uploadState.selectedProject) return;
|
||||
|
||||
try {
|
||||
const result = await getBins(uploadState.selectedProject);
|
||||
if (result.success) {
|
||||
uploadState.bins = result.data;
|
||||
renderBins();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load bins:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function renderBins() {
|
||||
const select = document.getElementById('binSelect');
|
||||
select.innerHTML = '';
|
||||
|
||||
uploadState.bins.forEach(bin => {
|
||||
const option = document.createElement('option');
|
||||
option.value = bin.id;
|
||||
option.textContent = bin.name;
|
||||
select.appendChild(option);
|
||||
});
|
||||
|
||||
if (uploadState.bins.length > 0) {
|
||||
uploadState.selectedBin = uploadState.bins[0].id;
|
||||
document.getElementById('binSelect').value = uploadState.selectedBin;
|
||||
}
|
||||
}
|
||||
|
||||
function toggleCreateProject() {
|
||||
const form = document.getElementById('createProjectForm');
|
||||
form.style.display = form.style.display === 'none' ? 'flex' : 'none';
|
||||
}
|
||||
|
||||
async function createNewProject() {
|
||||
const name = document.getElementById('newProjectName').value.trim();
|
||||
if (!name) {
|
||||
alert('Please enter a project name');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await createProject(name);
|
||||
if (result.success) {
|
||||
await loadProjects();
|
||||
toggleCreateProject();
|
||||
document.getElementById('newProjectName').value = '';
|
||||
updateStatusBar('Project created');
|
||||
} else {
|
||||
alert('Failed to create project');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to create project:', error);
|
||||
alert('Error creating project');
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// FILE HANDLING
|
||||
// ============================================================
|
||||
|
||||
function handleFileSelection(files) {
|
||||
Array.from(files).forEach(file => {
|
||||
if (isValidVideoFile(file)) {
|
||||
addToQueue(file);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function isValidVideoFile(file) {
|
||||
const validTypes = ['.mov', '.mp4', '.mxf', '.avi', '.mkv', '.ts', '.m2t'];
|
||||
const ext = '.' + file.name.split('.').pop().toLowerCase();
|
||||
return validTypes.includes(ext);
|
||||
}
|
||||
|
||||
function addToQueue(file) {
|
||||
const id = 'upload_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
|
||||
const queueItem = {
|
||||
id,
|
||||
file,
|
||||
status: 'waiting',
|
||||
progress: 0,
|
||||
uploadId: null,
|
||||
uploadedBytes: 0,
|
||||
};
|
||||
|
||||
uploadState.uploadQueue.push(queueItem);
|
||||
renderQueue();
|
||||
|
||||
// Auto-start upload if not at max concurrent
|
||||
if (uploadState.activeUploads < 3) {
|
||||
uploadNext();
|
||||
}
|
||||
}
|
||||
|
||||
function renderQueue() {
|
||||
const list = document.getElementById('queueList');
|
||||
const clear = document.getElementById('queueClear');
|
||||
|
||||
if (uploadState.uploadQueue.length === 0) {
|
||||
list.innerHTML = '<div class="queue-empty">No files selected</div>';
|
||||
clear.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
clear.style.display = uploadState.uploadQueue.some(u => u.status === 'complete' || u.status === 'error') ? 'block' : 'none';
|
||||
|
||||
list.innerHTML = '';
|
||||
uploadState.uploadQueue.forEach(item => {
|
||||
const el = createQueueItemElement(item);
|
||||
list.appendChild(el);
|
||||
});
|
||||
}
|
||||
|
||||
function createQueueItemElement(item) {
|
||||
const el = document.createElement('div');
|
||||
el.className = 'queue-item';
|
||||
el.id = `queue-${item.id}`;
|
||||
|
||||
const isComplete = item.status === 'complete';
|
||||
const isError = item.status === 'error';
|
||||
const isProcessing = item.status === 'processing';
|
||||
|
||||
el.innerHTML = `
|
||||
<div class="queue-item-info">
|
||||
<div class="queue-item-name">${item.file.name}</div>
|
||||
<div class="queue-item-meta">
|
||||
<span>${formatFileSize(item.file.size)}</span>
|
||||
<span id="progress-${item.id}">${item.progress}%</span>
|
||||
</div>
|
||||
${item.status !== 'waiting' ? `
|
||||
<div class="queue-item-progress">
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" id="fill-${item.id}" style="width: ${item.progress}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
<div class="queue-item-actions">
|
||||
<div class="queue-item-status ${item.status}">
|
||||
${item.status === 'waiting' ? 'Waiting' :
|
||||
item.status === 'uploading' ? 'Uploading' :
|
||||
item.status === 'processing' ? 'Processing' :
|
||||
item.status === 'complete' ? 'Complete' :
|
||||
item.status === 'error' ? 'Error' : item.status}
|
||||
</div>
|
||||
${item.status === 'waiting' || item.status === 'uploading' ? `
|
||||
<button class="btn btn-cancel btn-sm" onclick="cancelUpload('${item.id}')">Cancel</button>
|
||||
` : ''}
|
||||
</div>
|
||||
`;
|
||||
|
||||
return el;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// UPLOAD LOGIC
|
||||
// ============================================================
|
||||
|
||||
async function uploadNext() {
|
||||
const item = uploadState.uploadQueue.find(u => u.status === 'waiting');
|
||||
if (!item) return;
|
||||
|
||||
uploadState.activeUploads++;
|
||||
item.status = 'uploading';
|
||||
updateQueueItem(item);
|
||||
updateOverallProgress();
|
||||
|
||||
try {
|
||||
const file = item.file;
|
||||
const CHUNK_SIZE = 10 * 1024 * 1024; // 10MB
|
||||
|
||||
if (file.size < 50 * 1024 * 1024) {
|
||||
// Simple upload for small files
|
||||
await simpleUploadFile(item);
|
||||
} else {
|
||||
// Multipart upload for large files
|
||||
await multipartUploadFile(item, CHUNK_SIZE);
|
||||
}
|
||||
|
||||
item.status = 'processing';
|
||||
updateQueueItem(item);
|
||||
updateOverallProgress();
|
||||
|
||||
// Simulate processing completion
|
||||
setTimeout(() => {
|
||||
item.status = 'complete';
|
||||
item.progress = 100;
|
||||
uploadState.completedUploads++;
|
||||
updateQueueItem(item);
|
||||
updateOverallProgress();
|
||||
uploadState.activeUploads--;
|
||||
uploadNext();
|
||||
updateStatusBar();
|
||||
}, 2000);
|
||||
} catch (error) {
|
||||
console.error('Upload error:', error);
|
||||
item.status = 'error';
|
||||
updateQueueItem(item);
|
||||
updateOverallProgress();
|
||||
uploadState.activeUploads--;
|
||||
uploadNext();
|
||||
}
|
||||
}
|
||||
|
||||
async function simpleUploadFile(item) {
|
||||
const formData = new FormData();
|
||||
formData.append('file', item.file);
|
||||
formData.append('project_id', uploadState.selectedProject);
|
||||
formData.append('bin_id', uploadState.selectedBin);
|
||||
|
||||
const result = await simpleUpload(formData);
|
||||
if (!result.success) {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
|
||||
item.progress = 100;
|
||||
updateQueueItem(item);
|
||||
}
|
||||
|
||||
async function multipartUploadFile(item, chunkSize) {
|
||||
const file = item.file;
|
||||
const fileName = file.name;
|
||||
|
||||
// Initialize upload
|
||||
const initResult = await initUpload({
|
||||
filename: fileName,
|
||||
content_type: file.type,
|
||||
project_id: uploadState.selectedProject,
|
||||
bin_id: uploadState.selectedBin,
|
||||
});
|
||||
|
||||
if (!initResult.success) {
|
||||
throw new Error('Failed to initialize upload');
|
||||
}
|
||||
|
||||
item.uploadId = initResult.data.upload_id;
|
||||
const parts = [];
|
||||
const totalChunks = Math.ceil(file.size / chunkSize);
|
||||
|
||||
// Upload chunks
|
||||
for (let i = 0; i < totalChunks; i++) {
|
||||
const start = i * chunkSize;
|
||||
const end = Math.min(start + chunkSize, file.size);
|
||||
const chunk = file.slice(start, end);
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', chunk);
|
||||
formData.append('uploadId', item.uploadId);
|
||||
formData.append('partNumber', i + 1);
|
||||
formData.append('key', fileName);
|
||||
|
||||
const chunkResult = await uploadPart(formData);
|
||||
if (!chunkResult.success) {
|
||||
throw new Error('Failed to upload chunk ' + (i + 1));
|
||||
}
|
||||
|
||||
parts.push({
|
||||
part_number: i + 1,
|
||||
etag: chunkResult.data.etag,
|
||||
});
|
||||
|
||||
item.uploadedBytes = end;
|
||||
item.progress = Math.round((end / file.size) * 100);
|
||||
updateQueueItem(item);
|
||||
}
|
||||
|
||||
// Complete upload
|
||||
const completeResult = await completeUpload({
|
||||
upload_id: item.uploadId,
|
||||
parts,
|
||||
});
|
||||
|
||||
if (!completeResult.success) {
|
||||
throw new Error('Failed to complete upload');
|
||||
}
|
||||
|
||||
item.progress = 100;
|
||||
updateQueueItem(item);
|
||||
}
|
||||
|
||||
function updateQueueItem(item) {
|
||||
const el = document.getElementById(`queue-${item.id}`);
|
||||
if (el) {
|
||||
el.parentNode.replaceChild(createQueueItemElement(item), el);
|
||||
}
|
||||
}
|
||||
|
||||
function cancelUpload(id) {
|
||||
const item = uploadState.uploadQueue.find(u => u.id === id);
|
||||
if (!item) return;
|
||||
|
||||
if (item.uploadId) {
|
||||
abortUpload({ upload_id: item.uploadId }).catch(console.error);
|
||||
}
|
||||
|
||||
uploadState.uploadQueue = uploadState.uploadQueue.filter(u => u.id !== id);
|
||||
renderQueue();
|
||||
updateOverallProgress();
|
||||
|
||||
uploadState.activeUploads--;
|
||||
uploadNext();
|
||||
}
|
||||
|
||||
function clearCompleted() {
|
||||
uploadState.uploadQueue = uploadState.uploadQueue.filter(u => u.status !== 'complete' && u.status !== 'error');
|
||||
renderQueue();
|
||||
updateOverallProgress();
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// UI UPDATES
|
||||
// ============================================================
|
||||
|
||||
function updateOverallProgress() {
|
||||
const container = document.getElementById('overallProgressContainer');
|
||||
const queue = uploadState.uploadQueue;
|
||||
|
||||
if (queue.length === 0) {
|
||||
container.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
const totalProgress = queue.reduce((sum, u) => sum + u.progress, 0) / queue.length;
|
||||
const percent = Math.round(totalProgress);
|
||||
|
||||
container.style.display = 'flex';
|
||||
document.getElementById('overallProgressPercent').textContent = percent + '%';
|
||||
document.getElementById('overallProgressFill').style.width = percent + '%';
|
||||
}
|
||||
|
||||
function updateStatusBar() {
|
||||
const indicator = document.getElementById('statusIndicator');
|
||||
const text = document.getElementById('statusText');
|
||||
const stats = document.getElementById('uploadStatsItem');
|
||||
|
||||
indicator.className = 'status-indicator';
|
||||
|
||||
if (uploadState.activeUploads > 0) {
|
||||
text.textContent = `Uploading ${uploadState.activeUploads} file(s)...`;
|
||||
} else if (uploadState.completedUploads > 0) {
|
||||
text.textContent = `${uploadState.completedUploads} file(s) uploaded`;
|
||||
} else {
|
||||
text.textContent = 'Ready to upload';
|
||||
}
|
||||
|
||||
if (uploadState.completedUploads > 0) {
|
||||
stats.style.display = 'flex';
|
||||
document.getElementById('uploadStats').textContent = uploadState.completedUploads + ' file(s) uploaded';
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// NAVIGATION
|
||||
// ============================================================
|
||||
|
||||
function navigateTo(page) {
|
||||
if (page === 'assets') {
|
||||
window.location.href = '/index.html';
|
||||
} else if (page === 'capture') {
|
||||
window.location.href = '/capture.html';
|
||||
} else if (page === 'recorders') {
|
||||
window.location.href = '/recorders.html';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Reference in a new issue