AutoVideoEditor / templates /index.html
SolarumAsteridion's picture
move status messages to live feed and clean up UI
95fe952
raw
history blame
40 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Auto Video Editor</title>
<link rel="icon"
href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 512 512%22><path fill=%22%2320f3b4%22 d=%22M448 32H64C28.7 32 0 60.7 0 96v320c0 35.3 28.7 64 64 64h384c35.3 0 64-28.7 64-64V96c0-35.3-28.7-64-64-64zm-52.2 64l-32 64H271.8l32-64h92zm-144 0l-32 64H127.8l32-64h92zM64 96h19.8l-32 64H64V96zm384 320H64V192h384v224z%22/></svg>">
<link
href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;500;800&family=Work+Sans:ital,wght@0,100..900;1,100..900&display=swap"
rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css">
<style>
:root {
--glass: rgba(255, 255, 255, 0.03);
--glass-border: rgba(255, 255, 255, 0.12);
--chlorophyll: #20f3b4;
/* Greenish Blue / Teal */
--deep-forest: #071512;
--prism-1: rgba(32, 243, 180, 0.15);
--prism-2: rgba(4, 120, 87, 0.1);
--prism-3: rgba(6, 182, 212, 0.1);
--text-main: #e2e8f0;
--text-dim: #94a3b8;
--font-sans: 'Plus Jakarta Sans', sans-serif;
--font-mono: 'Work Sans', monospace;
--success-color: #00ff9d;
--error-color: #ff4d4d;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
background: var(--deep-forest);
color: var(--text-main);
font-family: var(--font-sans);
height: 100vh;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
background-image:
radial-gradient(circle at 10% 20%, var(--prism-1) 0%, transparent 40%),
radial-gradient(circle at 90% 80%, var(--prism-3) 0%, transparent 40%),
url("https://www.transparenttextures.com/patterns/carbon-fibre.png");
background-attachment: fixed;
}
.grain-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 999;
opacity: 0.05;
}
.main-container {
width: 95vw;
height: 90vh;
display: grid;
grid-template-columns: 380px 1fr 340px;
gap: 20px;
padding: 20px;
z-index: 10;
}
.glass-panel {
background: var(--glass);
backdrop-filter: blur(24px) saturate(180%);
border: 1px solid var(--glass-border);
border-radius: 24px;
padding: 24px;
position: relative;
overflow: hidden;
transition: all 0.4s cubic-bezier(0.23, 1, 0.32, 1);
}
.glass-panel:hover {
border-color: var(--chlorophyll);
box-shadow: 0 0 40px rgba(0, 243, 255, 0.05);
}
.glass-panel::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 2px;
background: linear-gradient(90deg, transparent, var(--chlorophyll), transparent);
opacity: 0.3;
}
.section-label {
font-size: 0.65rem;
text-transform: uppercase;
letter-spacing: 0.2rem;
color: var(--chlorophyll);
margin-bottom: 20px;
font-weight: 800;
display: flex;
align-items: center;
gap: 10px;
}
.section-label span {
width: 4px;
height: 4px;
background: var(--chlorophyll);
border-radius: 50%;
}
.hub-toggle {
display: flex;
background: rgba(0, 0, 0, 0.2);
border-radius: 12px;
padding: 4px;
margin-bottom: 20px;
}
.hub-toggle button {
flex: 1;
background: transparent;
border: none;
color: var(--text-dim);
padding: 8px;
font-size: 0.7rem;
font-weight: 600;
border-radius: 8px;
transition: 0.3s;
text-transform: uppercase;
}
.hub-toggle button.active {
background: var(--glass-border);
color: white;
}
.dropzone-container {
display: flex;
flex-direction: column;
gap: 12px;
}
.dropzone {
border: 1px dashed var(--glass-border);
border-radius: 16px;
height: 100px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
transition: 0.3s;
position: relative;
cursor: pointer;
}
.dropzone:hover {
background: rgba(0, 243, 255, 0.05);
border-color: var(--chlorophyll);
}
.dropzone input[type="file"] {
position: absolute;
width: 100%;
height: 100%;
opacity: 0;
cursor: pointer;
}
.file-info {
font-size: 0.7rem;
font-family: var(--font-mono);
color: var(--text-dim);
text-align: center;
max-width: 80%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.cloud-explorer {
display: none;
flex-direction: column;
gap: 10px;
}
.onedrive-item {
display: flex;
align-items: center;
gap: 12px;
padding: 10px;
border-radius: 10px;
background: rgba(255, 255, 255, 0.02);
font-size: 0.8rem;
cursor: pointer;
transition: 0.2s;
border: 1px solid transparent;
}
.onedrive-item:hover {
background: rgba(0, 243, 255, 0.05);
border-color: var(--glass-border);
}
.onedrive-item.selected {
border-color: var(--chlorophyll);
background: rgba(0, 243, 255, 0.1);
}
.creative-studio {
display: flex;
flex-direction: column;
gap: 20px;
}
.text-console {
flex: 1;
background: rgba(0, 0, 0, 0.1);
border: 1px solid var(--glass-border);
border-radius: 16px;
padding: 30px;
color: var(--text-main);
font-family: var(--font-sans);
font-size: 1.2rem;
font-weight: 300;
resize: none;
outline: none;
line-height: 1.6;
}
.text-console:focus {
border-color: var(--chlorophyll);
background: rgba(0, 0, 0, 0.2);
}
.quick-presets {
display: flex;
gap: 10px;
overflow-x: auto;
padding-bottom: 10px;
}
.preset-chip {
padding: 6px 14px;
background: var(--glass-border);
border-radius: 100px;
font-size: 0.7rem;
white-space: nowrap;
cursor: pointer;
transition: 0.2s;
}
.preset-chip:hover {
background: var(--chlorophyll);
color: var(--deep-forest);
}
.command-center {
display: flex;
flex-direction: column;
gap: 20px;
}
.control-group label {
display: block;
font-size: 0.6rem;
color: var(--text-dim);
margin-bottom: 6px;
font-family: var(--font-mono);
letter-spacing: 1px;
}
.control-input {
width: 100%;
background: rgba(0, 0, 0, 0.2);
border: 1px solid var(--glass-border);
padding: 10px;
border-radius: 10px;
color: white;
font-family: var(--font-mono);
font-size: 0.85rem;
outline: none;
}
.control-input:focus {
border-color: var(--chlorophyll);
}
.toggle-switch {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px;
background: rgba(0, 0, 0, 0.2);
border-radius: 10px;
cursor: pointer;
}
.pulse-feed {
height: 220px;
display: flex;
flex-direction: column;
}
.thinking-logs {
flex: 1;
font-family: var(--font-mono);
font-size: 0.65rem;
color: var(--chlorophyll);
overflow-y: auto;
line-height: 1.6;
opacity: 0.8;
padding-right: 10px;
}
.stage-tracker {
margin-top: 15px;
display: flex;
gap: 4px;
}
.stage-bar {
flex: 1;
height: 3px;
background: var(--glass-border);
border-radius: 2px;
position: relative;
overflow: hidden;
}
.stage-bar.complete {
background: var(--chlorophyll);
}
.stage-bar.active::after {
content: '';
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 50%;
background: var(--chlorophyll);
animation: slide 2s infinite linear;
}
.delivery-hub {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) scale(0.9);
width: 700px;
z-index: 1000;
visibility: hidden;
opacity: 0;
transition: 0.5s cubic-bezier(0.19, 1, 0.22, 1);
}
.delivery-hub.active {
visibility: visible;
opacity: 1;
transform: translate(-50%, -50%) scale(1);
}
#delivery-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
backdrop-filter: blur(8px);
z-index: 999;
display: none;
}
.video-preview {
width: 100%;
height: 300px;
background: #000;
border-radius: 16px;
margin-bottom: 20px;
position: relative;
overflow: hidden;
border: 1px solid var(--glass-border);
}
.video-preview video {
width: 100%;
height: 100%;
object-fit: contain;
}
.status-badge {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
border-radius: 8px;
font-size: 0.65rem;
font-weight: 800;
background: var(--chlorophyll);
color: var(--deep-forest);
text-transform: uppercase;
}
.action-button {
background: var(--chlorophyll);
color: var(--deep-forest);
border: none;
padding: 14px;
border-radius: 12px;
font-weight: 800;
width: 100%;
text-transform: uppercase;
letter-spacing: 1px;
transition: 0.3s;
cursor: pointer;
font-family: var(--font-sans);
}
.action-button:hover:not(:disabled) {
filter: brightness(1.1);
transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(0, 243, 255, 0.2);
}
.action-button:disabled {
background: var(--glass-border);
color: var(--text-dim);
cursor: not-allowed;
}
@keyframes slide {
0% {
left: -100%;
}
100% {
left: 100%;
}
}
::-webkit-scrollbar {
width: 4px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: var(--glass-border);
border-radius: 10px;
}
.prism-refraction {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
pointer-events: none;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.05) 0%, transparent 50%, rgba(0, 243, 255, 0.02) 100%);
}
#onedrive-search {
margin-bottom: 10px;
background: rgba(0, 0, 0, 0.3);
border: 1px solid var(--glass-border);
border-radius: 8px;
padding: 8px;
color: white;
font-size: 0.75rem;
width: 100%;
}
.modal-btn {
text-decoration: none;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
}
#message-area {
position: fixed;
bottom: 20px;
right: 20px;
z-index: 2000;
}
.message {
padding: 12px 20px;
border-radius: 12px;
font-size: 0.8rem;
backdrop-filter: blur(10px);
margin-top: 10px;
border-left: 4px solid;
animation: slideIn 0.3s ease-out;
max-width: 300px;
}
.message.success {
background: rgba(0, 255, 157, 0.1);
border-color: var(--success);
color: var(--success);
}
.message.error {
background: rgba(255, 77, 77, 0.1);
border-color: var(--error);
color: var(--error);
}
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
#onedrive-instruction b {
color: var(--chlorophyll);
}
#onedrive-instruction a {
color: #fff;
text-decoration: underline;
font-weight: bold;
}
.custom-switch {
position: relative;
display: inline-block;
width: 34px;
height: 20px;
}
.custom-switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(255, 255, 255, 0.1);
transition: .4s;
border-radius: 34px;
}
.slider:before {
position: absolute;
content: "";
height: 14px;
width: 14px;
left: 3px;
bottom: 3px;
background-color: var(--text-dim);
transition: .4s;
border-radius: 50%;
}
input:checked+.slider {
background-color: var(--chlorophyll);
}
input:checked+.slider:before {
transform: translateX(14px);
background-color: var(--deep-forest);
}
</style>
</head>
<body>
<svg class="grain-overlay">
<filter id="noise">
<feTurbulence type="fractalNoise" baseFrequency="0.6" numOctaves="3" stitchTiles="stitch" />
<feColorMatrix type="saturate" values="0" />
</filter>
<rect width="100%" height="100%" filter="url(#noise)" />
</svg>
<form id="video-form" style="display: contents;">
<div class="main-container">
<!-- LEFT: Ingestion Hub -->
<div class="glass-panel">
<div class="prism-refraction"></div>
<h2 class="section-label"><span></span> Local Assets</h2>
<div class="dropzone-container">
<div class="dropzone">
<input type="file" id="videos" name="videos[]" accept="video/*" multiple required
onchange="updateFiles(this, 'videos-info')">
<i class="fas fa-video" style="opacity: 0.5;"></i>
<p style="font-size: 0.7rem; color: var(--text-dim)">Source Videos [Required]</p>
<div id="videos-info" class="file-info">Ready for Input</div>
</div>
<div class="dropzone" style="height: 70px;">
<input type="file" id="style_sample" name="style_sample" accept="video/*"
onchange="updateFiles(this, 'style-info')">
<i class="fas fa-eye" style="opacity: 0.5; font-size: 0.8rem;"></i>
<p style="font-size: 0.6rem; color: var(--text-dim)">Visual Reference [Optional]</p>
<div id="style-info" class="file-info">Unlinked</div>
</div>
</div>
<h2 class="section-label" style="margin-top: 25px;"><span></span> Clipchamp Template (OneDrive)</h2>
<div id="hub-cloud" class="cloud-explorer" style="display: flex;">
<button type="button" id="onedrive-login-btn" class="action-button"
style="background: rgba(0, 243, 255, 0.05); color: var(--chlorophyll); border: 1px solid var(--chlorophyll); margin-bottom: 10px;">
<i class="fab fa-microsoft"></i> Connect OneDrive
</button>
<div id="onedrive-instruction"
style="font-size: 0.85rem; padding: 15px; border: 1px solid var(--glass-border); border-radius: 12px; margin-bottom: 10px; display: none; background: rgba(0,0,0,0.2);">
</div>
<div id="onedrive-status"
style="font-size: 0.7rem; color: var(--success-color); margin-bottom: 10px; display: none;">
<i class="fas fa-check-circle"></i> Linked to OneDrive
</div>
<div id="onedrive-project-selection" style="display: none;">
<input type="text" id="onedrive-search" placeholder="Search Project Folders...">
<div id="onedrive-project-list" style="max-height: 150px; overflow-y: auto;">
<!-- Projects -->
</div>
</div>
<input type="hidden" id="onedrive_item_id" name="onedrive_item_id">
<input type="hidden" id="onedrive_access_token" name="onedrive_access_token">
</div>
</div>
<!-- CENTER: Creative Studio -->
<div class="glass-panel creative-studio">
<div class="prism-refraction"></div>
<h2 class="section-label"><span></span>AI Prompt</h2>
<textarea id="style_desc" name="style_desc" class="text-console" required
placeholder="Describe the desired cinematic flow...">{{ default_style_desc | escape }}</textarea>
<div class="quick-presets">
<div class="preset-chip"
onclick="setPreset('Kitchen environment, bright lighting, focus on culinary activity.')">Kitchen
</div>
<div class="preset-chip"
onclick="setPreset('Home decor style, warm lighting, elegant camera pans.')">Home Decor
</div>
<div class="preset-chip"
onclick="setPreset('Lifestyle product showcase, aesthetic setup, dynamic angles.')">Lifestyle
Product</div>
</div>
</div>
<!-- RIGHT: Command & Pulse -->
<div style="display: flex; flex-direction: column; gap: 20px;">
<div class="glass-panel command-center">
<h2 class="section-label"><span></span> Command</h2>
<div class="control-group">
<label>TARGET DURATION</label>
<input type="number" id="duration" name="duration" class="control-input" value="30" min="1">
</div>
<div class="control-group">
<label>LLM MODEL</label>
<select id="model_name" name="model_name" class="control-input">
<option value="gemini-3-flash-preview">Initializing...</option>
</select>
</div>
<div class="toggle-switch" onclick="document.getElementById('mute_audio').click()">
<span style="font-size: 0.65rem; font-weight: 600; font-family: var(--font-mono);">MUTE
AUDIO</span>
<label class="custom-switch" onclick="event.stopPropagation()">
<input type="checkbox" id="mute_audio" name="mute_audio" checked>
<span class="slider"></span>
</label>
</div>
<button type="submit" id="submit-button" class="action-button">Initiate Synthesis</button>
</div>
<div class="glass-panel pulse-feed">
<h2 class="section-label"><span></span> Live-Feed</h2>
<div class="thinking-logs" id="thinking-header">
> Waiting for your input...
</div>
<div id="progress-area" style="display: none; margin-top: auto;">
<div id="progress-message"
style="font-size: 0.65rem; color: var(--chlorophyll); font-family: var(--font-mono); margin-bottom: 5px;">
</div>
<div class="stage-tracker">
<div class="stage-bar" id="stage-1"></div>
<div class="stage-bar" id="stage-2"></div>
<div class="stage-bar" id="stage-3"></div>
<div class="stage-bar" id="stage-4"></div>
</div>
</div>
</div>
</div>
</div>
</form>
<!-- Results Modal -->
<div id="delivery-overlay" onclick="closeHub()"></div>
<div class="glass-panel delivery-hub" id="delivery-hub">
<h2 class="section-label" id="modal-title"><span></span> Delivery Hub</h2>
<div class="video-preview">
<video id="modal-video" controls style="display: none;"></video>
<div id="placeholder-preview"
style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); text-align: center;">
<div style="color: var(--chlorophyll); font-size: 0.8rem; font-family: var(--font-mono);">SYNC COMPLETE
</div>
</div>
</div>
<div style="display: flex; gap: 15px; align-items: center;">
<div id="cloud-sync-badge" class="status-badge" style="display: none;">
<i class="fas fa-cloud-upload-alt"></i> CLOUD SYNC ACTIVE
</div>
<a id="modal-download-link" href="#" class="action-button" download
style="display: flex; align-items: center; justify-content: center; gap: 10px;">
<i class="fas fa-download"></i> Download Asset
</a>
</div>
</div>
<div id="message-area"></div>
<script>
const form = document.getElementById('video-form');
const submitButton = document.getElementById('submit-button');
const progressArea = document.getElementById('progress-area');
const progressMessage = document.getElementById('progress-message');
const thinkingHeader = document.getElementById('thinking-header');
const messageArea = document.getElementById('message-area');
const modelSelect = document.getElementById('model_name');
const deliveryHub = document.getElementById('delivery-hub');
const deliveryOverlay = document.getElementById('delivery-overlay');
const modalTitle = document.getElementById('modal-title');
const modalVideo = document.getElementById('modal-video');
const modalDownloadLink = document.getElementById('modal-download-link');
const syncBadge = document.getElementById('cloud-sync-badge');
// Cookie Helpers
function setCookie(name, value, days) {
let expires = "";
if (days) {
let date = new Date();
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
expires = "; expires=" + date.toUTCString();
}
document.cookie = name + "=" + (value || "") + expires + "; path=/";
}
function getCookie(name) {
let nameEQ = name + "=";
let ca = document.cookie.split(';');
for (let i = 0; i < ca.length; i++) {
let c = ca[i];
while (c.charAt(0) == ' ') c = c.substring(1, c.length);
if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length, c.length);
}
return null;
}
function eraseCookie(name) {
document.cookie = name + '=; Max-Age=-99999999; path=/';
}
let currentRequestId = null;
let progressInterval = null;
const POLLING_INTERVAL = 2000;
function updateFiles(input, infoId) {
const info = document.getElementById(infoId);
if (input.files && input.files.length > 0) {
const count = input.files.length;
info.textContent = count > 1 ? `${count} Files Ready` : input.files[0].name;
info.style.color = 'var(--chlorophyll)';
} else {
info.textContent = 'None';
info.style.color = 'var(--text-dim)';
}
}
function setPreset(text) {
document.getElementById('style_desc').value = text;
}
// --- Model Loader ---
async function loadModels() {
try {
const response = await fetch('/api/models');
const data = await response.json();
if (data.status === 'success') {
modelSelect.innerHTML = '';
data.models.forEach(model => {
const option = document.createElement('option');
option.value = model.name;
option.textContent = model.display_name || model.name;
if (model.name.includes('gemini-3-flash')) option.selected = true;
modelSelect.appendChild(option);
});
}
} catch (error) {
modelSelect.innerHTML = '<option value="gemini-3-flash-preview">Gemini 3 Flash Preview</option>';
}
}
loadModels();
// --- Submission ---
form.addEventListener('submit', async (e) => {
e.preventDefault();
messageArea.innerHTML = '';
if (document.getElementById('videos').files.length === 0) {
showMessage('error', 'Critical Input Missing: Source Videos');
return;
}
submitButton.disabled = true;
submitButton.textContent = 'Synthesis Active';
thinkingHeader.innerHTML = "> PREPARING SEQUENCE...<br>> INITIATING HANDSHAKE...";
thinkingHeader.scrollTop = thinkingHeader.scrollHeight;
const formData = new FormData(form);
try {
const res = await fetch('/generate', { method: 'POST', body: formData });
const result = await res.json();
if (result.status === 'processing_started') {
currentRequestId = result.request_id;
thinkingHeader.innerHTML += "<br>> Handshake established.<br>> Core systems active.";
thinkingHeader.scrollTop = thinkingHeader.scrollHeight;
progressArea.style.display = 'block';
if (progressInterval) clearInterval(progressInterval);
progressInterval = setInterval(pollProgress, POLLING_INTERVAL);
} else {
throw new Error(result.message);
}
} catch (err) {
thinkingHeader.innerHTML += `<br>><span style="color: var(--error-color);"> SYNTHESIS FAILED: ${err.message}</span>`;
thinkingHeader.scrollTop = thinkingHeader.scrollHeight;
submitButton.disabled = false;
submitButton.textContent = 'Initiate Synthesis';
}
});
async function pollProgress() {
if (!currentRequestId) return;
try {
const res = await fetch(`/progress/${currentRequestId}`);
const data = await res.json();
updatePulse(data);
if (data.stage === 'COMPLETED') {
clearInterval(progressInterval);
completeHub(data.result);
} else if (data.stage === 'FAILED') {
clearInterval(progressInterval);
showMessage('error', data.error);
submitButton.disabled = false;
submitButton.textContent = 'Initiate Synthesis';
}
} catch (err) { console.error(err); }
}
function updatePulse(data) {
progressMessage.textContent = data.message;
if (data.thinking_header) {
thinkingHeader.innerHTML += `<br>> ${data.thinking_header}`;
thinkingHeader.scrollTop = thinkingHeader.scrollHeight;
}
const stages = ['INGESTING', 'ANALYZING', 'GENERATING', 'SAVING'];
const currentIdx = stages.indexOf(data.stage);
document.querySelectorAll('.stage-bar').forEach((bar, idx) => {
bar.classList.remove('active', 'complete');
if (idx < currentIdx) bar.classList.add('complete');
if (idx === currentIdx) bar.classList.add('active');
});
}
function completeHub(result) {
submitButton.disabled = false;
submitButton.textContent = 'Initiate Synthesis';
deliveryOverlay.style.display = 'block';
deliveryHub.classList.add('active');
if (result.clipchamp_url) {
modalVideo.style.display = 'none';
document.getElementById('placeholder-preview').style.display = 'block';
modalDownloadLink.href = result.clipchamp_url;
modalDownloadLink.download = result.project_name + '.clipchamp';
if (result.onedrive_uploaded) {
syncBadge.style.display = 'inline-flex';
modalTitle.innerHTML = '<span></span> SYNC COMPLETE';
modalDownloadLink.innerHTML = '<i class="fas fa-cloud"></i> Open Cloud Edit';
} else {
syncBadge.style.display = 'none';
modalTitle.innerHTML = '<span></span> PROJECT READY';
modalDownloadLink.innerHTML = '<i class="fas fa-download"></i> Download .clipchamp';
}
} else {
modalVideo.style.display = 'block';
document.getElementById('placeholder-preview').style.display = 'none';
modalVideo.src = result.video_url;
modalDownloadLink.href = result.video_url;
modalDownloadLink.innerHTML = '<i class="fas fa-download"></i> Download MP4';
syncBadge.style.display = 'none';
}
}
function closeHub() {
deliveryHub.classList.remove('active');
deliveryOverlay.style.display = 'none';
modalVideo.pause();
}
function showMessage(type, text) {
const div = document.createElement('div');
div.className = `message ${type}`;
div.innerHTML = text;
messageArea.appendChild(div);
setTimeout(() => div.remove(), 5000);
}
// --- OneDrive ---
const btnOnedrive = document.getElementById('onedrive-login-btn');
const instructionOnedrive = document.getElementById('onedrive-instruction');
const statusOnedrive = document.getElementById('onedrive-status');
const projectSelectionOnedrive = document.getElementById('onedrive-project-selection');
const projectListOnedrive = document.getElementById('onedrive-project-list');
const searchOnedrive = document.getElementById('onedrive-search');
let onedriveProjects = [];
btnOnedrive.addEventListener('click', async () => {
btnOnedrive.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Linking...';
btnOnedrive.disabled = true;
try {
const res = await fetch('/api/onedrive/login_start', { method: 'POST' });
const data = await res.json();
if (data.status === 'success') {
instructionOnedrive.style.display = 'block';
btnOnedrive.style.display = 'none';
instructionOnedrive.innerHTML = `
<div style="margin-bottom: 12px; color: var(--chlorophyll); font-weight: 800; letter-spacing: 1px;"><i class="fas fa-lock"></i> SIGN-IN REQUIRED</div>
<div style="margin-bottom: 8px;">1. Visit <a href="https://www.microsoft.com/link" target="_blank" style="font-size: 0.95rem;">microsoft.com/link</a></div>
<div>2. Enter code:<br>
<b style="font-size: 1.6rem; letter-spacing: 3px; color: var(--chlorophyll); background: rgba(0,0,0,0.4); padding: 8px 16px; border-radius: 8px; display: inline-block; margin-top: 8px; user-select: text;">${data.user_code}</b>
</div>
`;
const poll = setInterval(async () => {
const sRes = await fetch(`/api/onedrive/login_status/${data.session_id}`);
const sData = await sRes.json();
if (sData.status === 'success') {
clearInterval(poll);
document.getElementById('onedrive_access_token').value = sData.access_token;
setCookie('velocity_onedrive_token', sData.access_token, 7);
instructionOnedrive.style.display = 'none';
btnOnedrive.style.display = 'none';
statusOnedrive.style.display = 'block';
statusOnedrive.innerHTML = '<i class="fab fa-windows"></i> Linked to OneDrive';
statusOnedrive.style.fontSize = '0.9rem';
projectSelectionOnedrive.style.display = 'block';
fetchOneDriveProjects(sData.access_token);
}
}, 5000);
}
} catch (err) {
btnOnedrive.disabled = false;
btnOnedrive.innerHTML = '<i class="fab fa-windows"></i> Retry Link';
}
});
async function fetchOneDriveProjects(token) {
projectListOnedrive.innerHTML = '<div style="padding: 10px; font-size: 0.7rem; color: var(--text-dim);"><i class="fas fa-spinner fa-spin"></i> Fetching Cloud Projects...</div>';
try {
const res = await fetch('/api/onedrive/projects', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ access_token: token })
});
const data = await res.json();
if (data.status === 'success') {
onedriveProjects = data.projects;
renderProjects(onedriveProjects);
} else {
// Token expired or invalid
resetOneDriveLogin();
}
} catch (e) {
resetOneDriveLogin();
}
}
function resetOneDriveLogin() {
eraseCookie('velocity_onedrive_token');
btnOnedrive.style.display = 'block';
btnOnedrive.innerHTML = '<i class="fab fa-windows"></i> Connect OneDrive';
btnOnedrive.disabled = false;
statusOnedrive.style.display = 'none';
projectSelectionOnedrive.style.display = 'none';
projectListOnedrive.innerHTML = '';
}
async function initOneDriveCache() {
const cachedToken = getCookie('velocity_onedrive_token');
if (cachedToken) {
btnOnedrive.style.display = 'none';
statusOnedrive.style.display = 'block';
statusOnedrive.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Restoring Cloud Link...';
document.getElementById('onedrive_access_token').value = cachedToken;
projectSelectionOnedrive.style.display = 'block';
fetchOneDriveProjects(cachedToken).then(() => {
statusOnedrive.innerHTML = '<i class="fab fa-windows"></i> Linked to OneDrive';
statusOnedrive.style.fontSize = '0.9rem';
});
}
}
initOneDriveCache();
function renderProjects(list) {
projectListOnedrive.innerHTML = '';
list.forEach(p => {
const div = document.createElement('div');
div.className = 'onedrive-item';
div.innerHTML = `<i class="fas fa-clapperboard"></i> <span>${p.name}</span>`;
div.onclick = () => {
document.querySelectorAll('.onedrive-item').forEach(e => e.classList.remove('selected'));
div.classList.add('selected');
document.getElementById('onedrive_item_id').value = p.id;
};
projectListOnedrive.appendChild(div);
});
}
searchOnedrive.oninput = (e) => {
const v = e.target.value.toLowerCase();
renderProjects(onedriveProjects.filter(p => p.name.toLowerCase().includes(v)));
};
// Tilt
document.addEventListener('mousemove', (e) => {
const x = (e.clientX / window.innerWidth - 0.5) * 4;
const y = (e.clientY / window.innerHeight - 0.5) * 4;
document.querySelectorAll('.glass-panel').forEach(p => {
p.style.transform = `perspective(1000px) rotateX(${-y}deg) rotateY(${x}deg)`;
});
});
</script>
</body>
</html>