909 lines
31 KiB
HTML
909 lines
31 KiB
HTML
<!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>
|