express / frontend /app.js
Raven10492's picture
Upload 11 files
77e074a verified
document.addEventListener("DOMContentLoaded", () => {
// --- DOM Element Selection ---
const generateBtn = document.getElementById("generate-btn");
const saveVideoBtn = document.getElementById("save-video-btn");
const soundToggleBtn = document.getElementById("sound-toggle-btn");
const loader = document.getElementById("loader");
const errorContainer = document.getElementById("error-container");
const placeholder = document.querySelector(".video-container .placeholder");
// Form Inputs
const apiKeyInput = document.getElementById("api-key");
const promptInput = document.getElementById("prompt");
const negativePromptInput = document.getElementById("negative-prompt");
const modelSelect = document.getElementById("model");
const aspectRatioSelect = document.getElementById("aspect-ratio");
const resolutionSelect = document.getElementById("resolution");
const personGenSelect = document.getElementById("person-generation");
const seedInput = document.getElementById("seed");
const sampleCountInput = document.getElementById("sample-count");
const durationInput = document.getElementById("duration");
const generateAudioSwitch = document.getElementById("generate-audio");
const threadCountInput = document.getElementById("thread-count");
const enhancePromptSwitch = document.getElementById("enhance-prompt");
const imagePromptInput = document.getElementById("image-prompt");
const videoPromptInput = document.getElementById("video-prompt");
const imageProcessModeSelect = document.getElementById("image-process-mode");
const imageFillColorGroup = document.getElementById("image-fill-color-group");
const imageFillColorPicker = document.getElementById("image-fill-color-picker");
const imageFillColorInput = document.getElementById("image-fill-color");
// Carousel Elements
const videoCarousel = document.getElementById("video-carousel");
const prevBtn = document.getElementById("prev-btn");
const nextBtn = document.getElementById("next-btn");
const carouselDots = document.getElementById("carousel-dots");
// Prompt Saving Elements
const savePromptBtn = document.getElementById("save-prompt-btn");
const savedPromptsToggle = document.getElementById("saved-prompts-toggle");
const savedPromptsList = document.getElementById("saved-prompts-list");
// Key Saving Elements
const saveKeyBtn = document.getElementById("save-key-btn");
const savedKeysToggle = document.getElementById("saved-keys-toggle");
const savedKeysList = document.getElementById("saved-keys-list");
// --- State ---
let currentIndex = 0;
let videoItems = [];
const SETTINGS_KEY = 'veoGeneratorSettings';
let currentPasteTarget = null; // 'image' or 'video'
let savedVideos = []; // To track URLs of saved videos
const SAVED_PROMPTS_KEY = 'veoGeneratorSavedPrompts';
const SAVED_KEYS_KEY = 'veoGeneratorSavedKeys';
const SOUND_SETTINGS_KEY = 'veoGeneratorSoundEnabled';
let soundEnabled = false; // Default to disabled
const PRESET_KEYS = [];
// --- Notification Sound ---
function playNotificationSound() {
if (!soundEnabled) return; // 如果声音被禁用则直接返回
try {
// 完成音效 - 上升琶音 (C5→E5→G5→C6)
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
const notes = [523.25, 659.25, 783.99, 1046.50]; // C5, E5, G5, C6
notes.forEach((frequency, index) => {
setTimeout(() => {
const oscillator = audioContext.createOscillator();
const gainNode = audioContext.createGain();
oscillator.connect(gainNode);
gainNode.connect(audioContext.destination);
oscillator.frequency.setValueAtTime(frequency, audioContext.currentTime);
oscillator.type = 'sine';
// 最大音量设置
gainNode.gain.setValueAtTime(0, audioContext.currentTime);
gainNode.gain.linearRampToValueAtTime(1.0, audioContext.currentTime + 0.01);
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.3);
oscillator.start(audioContext.currentTime);
oscillator.stop(audioContext.currentTime + 0.3);
}, index * 100);
});
} catch (error) {
console.log('无法播放提示音:', error);
}
}
// --- Sound Toggle Logic ---
function updateSoundButtonState() {
if (soundEnabled) {
soundToggleBtn.classList.remove('disabled');
soundToggleBtn.innerHTML = feather.icons['volume-2'].toSvg();
soundToggleBtn.title = "提示音已开启,点击关闭";
} else {
soundToggleBtn.classList.add('disabled');
soundToggleBtn.innerHTML = feather.icons['volume-x'].toSvg();
soundToggleBtn.title = "提示音已关闭,点击开启";
}
}
function toggleSound() {
soundEnabled = !soundEnabled;
localStorage.setItem(SOUND_SETTINGS_KEY, JSON.stringify(soundEnabled));
updateSoundButtonState();
// 如果开启了声音,播放一次提示音作为反馈
if (soundEnabled) {
playNotificationSound();
}
}
function loadSoundSettings() {
const savedSoundSettings = localStorage.getItem(SOUND_SETTINGS_KEY);
if (savedSoundSettings !== null) {
soundEnabled = JSON.parse(savedSoundSettings);
}
updateSoundButtonState();
}
soundToggleBtn.addEventListener('click', toggleSound);
// --- Functions ---
// --- Saved Prompts Logic ---
function updateSaveButtonState() {
const currentPrompt = promptInput.value.trim();
const prompts = getSavedPrompts();
if (currentPrompt && prompts.includes(currentPrompt)) {
savePromptBtn.innerHTML = feather.icons['check-circle'].toSvg();
savePromptBtn.title = "提示词已保存";
savePromptBtn.disabled = true;
} else {
savePromptBtn.innerHTML = feather.icons['plus-circle'].toSvg();
savePromptBtn.title = "保存当前提示词";
savePromptBtn.disabled = false;
}
}
function getSavedPrompts() {
return JSON.parse(localStorage.getItem(SAVED_PROMPTS_KEY)) || [];
}
function savePrompts(prompts) {
localStorage.setItem(SAVED_PROMPTS_KEY, JSON.stringify(prompts));
}
function renderSavedPrompts() {
const prompts = getSavedPrompts();
savedPromptsList.innerHTML = ''; // Clear the list first
if (prompts.length === 0) {
savedPromptsList.innerHTML = `<div class="saved-prompts-list-empty">没有已保存的提示词</div>`;
return;
}
prompts.forEach((promptText, index) => {
const item = document.createElement('div');
item.className = 'saved-prompt-item';
item.dataset.index = index;
const text = document.createElement('span');
text.className = 'saved-prompt-text';
text.textContent = promptText;
text.title = promptText; // Show full prompt on hover
text.addEventListener('click', () => {
promptInput.value = promptText;
saveSettings(); // Also save to main settings
savedPromptsList.classList.add('hidden');
updateSaveButtonState();
});
const deleteBtn = document.createElement('button');
deleteBtn.className = 'delete-prompt-btn';
deleteBtn.innerHTML = feather.icons['trash-2'].toSvg({ width: 14, height: 14 });
deleteBtn.title = '删除此提示词';
deleteBtn.addEventListener('click', (e) => {
e.stopPropagation(); // Prevent item click event
deletePrompt(index);
});
item.appendChild(text);
item.appendChild(deleteBtn);
savedPromptsList.appendChild(item);
});
}
function addNewPrompt() {
const currentPrompt = promptInput.value.trim();
if (!currentPrompt) {
// Silently return if prompt is empty
return;
}
let prompts = getSavedPrompts();
if (prompts.includes(currentPrompt)) {
// Silently return if prompt already exists
return;
}
prompts.unshift(currentPrompt); // Add to the beginning
savePrompts(prompts);
renderSavedPrompts();
updateSaveButtonState();
}
function deletePrompt(indexToDelete) {
let prompts = getSavedPrompts();
prompts.splice(indexToDelete, 1);
savePrompts(prompts);
renderSavedPrompts();
updateSaveButtonState();
}
savePromptBtn.addEventListener('click', addNewPrompt);
savedPromptsToggle.addEventListener('click', () => {
const isHidden = savedPromptsList.classList.toggle('hidden');
if (!isHidden) {
renderSavedPrompts(); // Re-render when opening
}
});
// Hide dropdown if clicked outside
document.addEventListener('click', (event) => {
if (!savedPromptsToggle.contains(event.target) && !savedPromptsList.contains(event.target)) {
savedPromptsList.classList.add('hidden');
}
});
// --- Saved Keys Logic ---
function getSavedKeys() {
return JSON.parse(localStorage.getItem(SAVED_KEYS_KEY)) || [];
}
function saveKeys(keys) {
localStorage.setItem(SAVED_KEYS_KEY, JSON.stringify(keys));
}
function getAllKeys() {
const customKeys = getSavedKeys();
// Add a 'preset' flag to distinguish them
const presetKeys = PRESET_KEYS.map(k => ({ ...k, preset: true }));
return [...presetKeys, ...customKeys];
}
function renderSavedKeys() {
const keys = getAllKeys();
savedKeysList.innerHTML = ''; // Clear the list
if (keys.length === 0) {
savedKeysList.innerHTML = `<div class="saved-keys-list-empty">没有已保存的 Key</div>`;
return;
}
keys.forEach((keyData, index) => {
const item = document.createElement('div');
item.className = 'saved-key-item';
if (keyData.preset) {
item.classList.add('preset');
}
const alias = document.createElement('span');
alias.className = 'saved-key-alias';
alias.textContent = keyData.alias;
alias.title = keyData.alias;
const value = document.createElement('span');
value.className = 'saved-key-value';
// Show partial key for preview
value.textContent = `...${keyData.value.slice(-6)}`;
value.title = keyData.value;
const textContainer = document.createElement('div');
textContainer.style.display = 'flex';
textContainer.style.alignItems = 'center';
textContainer.style.overflow = 'hidden';
textContainer.style.flexGrow = '1';
textContainer.appendChild(alias);
textContainer.appendChild(value);
item.addEventListener('click', () => {
apiKeyInput.value = keyData.value;
saveSettings();
savedKeysList.classList.add('hidden');
updateSaveKeyButtonState();
});
const deleteBtn = document.createElement('button');
deleteBtn.className = 'delete-key-btn';
deleteBtn.innerHTML = feather.icons['trash-2'].toSvg({ width: 14, height: 14 });
deleteBtn.title = '删除此 Key';
deleteBtn.addEventListener('click', (e) => {
e.stopPropagation();
// We need to find the index in the *custom* keys array
const customKeys = getSavedKeys();
const customIndexToDelete = customKeys.findIndex(k => k.value === keyData.value);
if (customIndexToDelete > -1) {
deleteKey(customIndexToDelete);
}
});
item.appendChild(textContainer);
item.appendChild(deleteBtn);
savedKeysList.appendChild(item);
});
}
function addNewKey() {
const currentKey = apiKeyInput.value.trim();
if (!currentKey) return;
const allKeys = getAllKeys();
if (allKeys.some(k => k.value === currentKey)) {
return; // Key already exists
}
const alias = prompt("为这个新的 Key 输入一个别名:", "");
if (alias === null || alias.trim() === "") {
return; // User cancelled or entered empty alias
}
let customKeys = getSavedKeys();
customKeys.unshift({ alias: alias.trim(), value: currentKey });
saveKeys(customKeys);
renderSavedKeys();
updateSaveKeyButtonState();
}
function deleteKey(indexToDelete) {
let customKeys = getSavedKeys();
customKeys.splice(indexToDelete, 1);
saveKeys(customKeys);
renderSavedKeys();
updateSaveKeyButtonState();
}
function updateSaveKeyButtonState() {
const currentKey = apiKeyInput.value.trim();
const allKeys = getAllKeys();
if (currentKey && allKeys.some(k => k.value === currentKey)) {
saveKeyBtn.innerHTML = feather.icons['check-circle'].toSvg();
saveKeyBtn.title = "Key 已保存";
saveKeyBtn.disabled = true;
} else {
saveKeyBtn.innerHTML = feather.icons['plus-circle'].toSvg();
saveKeyBtn.title = "保存当前 Key";
saveKeyBtn.disabled = false;
}
}
saveKeyBtn.addEventListener('click', addNewKey);
savedKeysToggle.addEventListener('click', () => {
const isHidden = savedKeysList.classList.toggle('hidden');
if (!isHidden) {
renderSavedKeys();
}
});
document.addEventListener('click', (event) => {
if (!savedKeysToggle.contains(event.target) && !savedKeysList.contains(event.target)) {
savedKeysList.classList.add('hidden');
}
});
// --- Settings Persistence ---
function saveSettings() {
const settings = {
apiKey: apiKeyInput.value,
prompt: promptInput.value,
negativePrompt: negativePromptInput.value,
model: modelSelect.value,
aspectRatio: aspectRatioSelect.value,
resolution: resolutionSelect.value,
personGeneration: personGenSelect.value,
seed: seedInput.value,
sampleCount: sampleCountInput.value,
duration: durationInput.value,
generateAudio: generateAudioSwitch.checked,
enhancePrompt: enhancePromptSwitch.checked,
threadCount: threadCountInput.value,
imageProcessMode: imageProcessModeSelect.value,
imageFillColor: imageFillColorInput.value,
};
localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings));
}
function loadSettings() {
const savedSettings = localStorage.getItem(SETTINGS_KEY);
if (savedSettings) {
const settings = JSON.parse(savedSettings);
apiKeyInput.value = settings.apiKey || '';
promptInput.value = settings.prompt || '';
negativePromptInput.value = settings.negativePrompt || '';
modelSelect.value = settings.model || 'veo-3.0-generate-preview';
aspectRatioSelect.value = settings.aspectRatio || '16:9';
resolutionSelect.value = settings.resolution || '1080p';
personGenSelect.value = settings.personGeneration || 'allow_all';
seedInput.value = settings.seed || '';
sampleCountInput.value = settings.sampleCount || '1';
durationInput.value = settings.duration || '8';
generateAudioSwitch.checked = settings.generateAudio !== false; // default to true if undefined
enhancePromptSwitch.checked = settings.enhancePrompt !== false; // default to true if undefined
threadCountInput.value = settings.threadCount || '1';
imageProcessModeSelect.value = settings.imageProcessMode || 'none';
imageFillColorInput.value = settings.imageFillColor || '#000000';
imageFillColorPicker.value = settings.imageFillColor || '#000000';
// Update UI state based on loaded settings
updateImageProcessingState();
updateSaveButtonState();
} else {
// If no settings, load the default preset key
if (PRESET_KEYS.length > 0) {
apiKeyInput.value = PRESET_KEYS[0].value;
saveSettings(); // Save this default state
}
}
updateSaveKeyButtonState(); // Also update key button state on load
}
// Attach listeners to save settings on change
const inputsToSave = [
apiKeyInput, promptInput, negativePromptInput, modelSelect, aspectRatioSelect,
resolutionSelect, personGenSelect, seedInput, sampleCountInput, durationInput,
generateAudioSwitch, threadCountInput, enhancePromptSwitch,
imageProcessModeSelect, imageFillColorInput
];
inputsToSave.forEach(input => {
input.addEventListener('change', saveSettings);
// For text inputs, 'input' event is more responsive
if (input.type === 'textarea' || input.type === 'text' || input.type === 'password' || input.type === 'number') {
input.addEventListener('input', saveSettings);
}
});
promptInput.addEventListener('input', updateSaveButtonState);
apiKeyInput.addEventListener('input', updateSaveKeyButtonState);
// File Drop Zone Logic V2: With Preview and Delete
function setupFileDropZone(zoneId, inputId, previewId) {
const dropZone = document.getElementById(zoneId);
const fileInput = document.getElementById(inputId);
const previewContainer = document.getElementById(previewId);
let currentObjectURL = null;
const handleFile = (file) => {
if (currentObjectURL) {
URL.revokeObjectURL(currentObjectURL);
}
if (!file) {
previewContainer.innerHTML = '';
dropZone.classList.remove('has-file');
fileInput.value = ''; // Ensure file is cleared
return;
}
currentObjectURL = URL.createObjectURL(file);
previewContainer.innerHTML = ''; // Clear previous preview
let previewElement;
if (file.type.startsWith('image/')) {
previewElement = document.createElement('img');
previewElement.src = currentObjectURL;
} else if (file.type.startsWith('video/')) {
previewElement = document.createElement('video');
previewElement.src = currentObjectURL;
previewElement.muted = true;
previewElement.playsInline = true;
previewElement.setAttribute('preload', 'metadata'); // For poster frame
}
if(previewElement) {
previewContainer.appendChild(previewElement);
}
const deleteBtn = document.createElement('button');
deleteBtn.className = 'delete-file-btn';
deleteBtn.innerHTML = feather.icons['x'].toSvg({ width: 16, height: 16 });
deleteBtn.setAttribute('aria-label', 'Remove file');
deleteBtn.addEventListener('click', (e) => {
e.stopPropagation(); // VERY IMPORTANT: Prevents the click from bubbling up to the dropZone
e.preventDefault();
handleFile(null); // Clear the file
});
previewContainer.appendChild(deleteBtn);
dropZone.classList.add('has-file');
};
dropZone.addEventListener("click", (e) => {
// If the click is on the delete button, do nothing.
if (e.target.closest('.delete-file-btn')) {
return;
}
// If the click is on the file input itself, let the browser handle it.
if (e.target === fileInput) {
return;
}
// Otherwise, for any other part of the zone, trigger the file input.
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");
if (e.dataTransfer.files.length) {
fileInput.files = e.dataTransfer.files;
handleFile(fileInput.files[0]);
}
});
fileInput.addEventListener("change", () => {
if (fileInput.files.length > 0) {
handleFile(fileInput.files[0]);
} else {
handleFile(null);
}
});
}
setupFileDropZone("image-drop-zone", "image-prompt", "image-preview");
setupFileDropZone("video-drop-zone", "video-prompt", "video-preview");
// --- Image Processing UI Logic ---
function updateImageProcessingState() {
const isFillMode = imageProcessModeSelect.value === 'fill';
const processGroup = imageProcessModeSelect.closest('.form-group');
// 移除了基于宽高比的禁用限制,现在所有宽高比都可以使用图片处理功能
processGroup.classList.remove('disabled');
imageProcessModeSelect.disabled = false;
// Color picker is enabled only in fill mode
if (isFillMode) {
imageFillColorGroup.classList.remove('disabled');
imageFillColorPicker.disabled = false;
imageFillColorInput.disabled = false;
} else {
imageFillColorGroup.classList.add('disabled');
imageFillColorPicker.disabled = true;
imageFillColorInput.disabled = true;
}
}
aspectRatioSelect.addEventListener('change', updateImageProcessingState);
imageProcessModeSelect.addEventListener('change', updateImageProcessingState);
imageFillColorPicker.addEventListener('input', () => {
imageFillColorInput.value = imageFillColorPicker.value;
saveSettings(); // Save on color change
});
imageFillColorInput.addEventListener('input', () => {
const value = imageFillColorInput.value;
// A simple regex to check for valid hex color format
if (/^#[0-9a-fA-F]{6}$/.test(value)) {
imageFillColorPicker.value = value;
saveSettings(); // Save on text input change
}
});
// Carousel Logic
function updateCarousel() {
videoCarousel.style.transform = `translateX(-${currentIndex * 100}%)`;
updateSaveVideoButtonState(); // Update save button state when carousel changes
const dots = carouselDots.querySelectorAll('.carousel-dot');
dots.forEach((dot, index) => {
dot.classList.toggle('active', index === currentIndex);
});
const videos = videoCarousel.querySelectorAll('video');
videos.forEach((video, index) => {
if (index !== currentIndex) {
video.pause();
}
});
}
function resetCarousel() {
videoCarousel.innerHTML = '';
carouselDots.innerHTML = '';
videoItems = [];
currentIndex = 0;
prevBtn.classList.add('hidden');
nextBtn.classList.add('hidden');
placeholder.style.display = 'flex';
saveVideoBtn.disabled = true; // Disable button when there are no videos
savedVideos = []; // Reset saved videos tracker
}
prevBtn.addEventListener('click', () => {
currentIndex = (currentIndex > 0) ? currentIndex - 1 : videoItems.length - 1;
updateCarousel();
});
nextBtn.addEventListener('click', () => {
currentIndex = (currentIndex < videoItems.length - 1) ? currentIndex + 1 : 0;
updateCarousel();
});
// --- Video Handling ---
function addVideoToCarousel(videoUrl) {
const index = videoItems.length;
videoItems.push(videoUrl);
const item = document.createElement('div');
item.className = 'carousel-item';
const video = document.createElement('video');
video.src = videoUrl;
video.controls = true;
video.addEventListener('click', (e) => {
e.preventDefault();
if (video.paused) {
video.play();
} else {
video.pause();
}
});
item.appendChild(video);
videoCarousel.appendChild(item);
const dot = document.createElement('div');
dot.className = 'carousel-dot';
dot.addEventListener('click', () => {
currentIndex = index;
updateCarousel();
});
carouselDots.appendChild(dot);
if (videoItems.length > 1) {
prevBtn.classList.remove('hidden');
nextBtn.classList.remove('hidden');
}
// If it's the first video, hide loader and update display
if (videoItems.length === 1) {
loader.classList.add("hidden");
placeholder.style.display = "none";
updateCarousel();
saveVideoBtn.disabled = false; // Enable the save button
}
}
// --- Form Submission ---
generateBtn.addEventListener("click", async () => {
resetCarousel();
loader.classList.remove("hidden");
placeholder.style.display = "none";
errorContainer.classList.add("hidden");
const threadCount = parseInt(threadCountInput.value, 10) || 1;
const sampleCount = parseInt(sampleCountInput.value, 10) || 1;
const baseFormData = () => {
const formData = new FormData();
formData.append("api_key", apiKeyInput.value);
formData.append("prompt", promptInput.value);
formData.append("negative_prompt", negativePromptInput.value);
formData.append("model", modelSelect.value);
formData.append("aspect_ratio", aspectRatioSelect.value);
formData.append("duration", durationInput.value);
formData.append("resolution", resolutionSelect.value);
if (seedInput.value) {
formData.append("seed", seedInput.value);
}
formData.append("person_generation", personGenSelect.value);
formData.append("generate_audio", generateAudioSwitch.checked);
formData.append("enhance_prompt", enhancePromptSwitch.checked);
if (imagePromptInput.files[0]) {
formData.append("image", imagePromptInput.files[0]);
}
if (videoPromptInput.files[0]) {
formData.append("video", videoPromptInput.files[0]);
}
// 现在所有宽高比都支持图片处理参数
formData.append("image_process_mode", imageProcessModeSelect.value);
formData.append("image_fill_color", imageFillColorInput.value);
return formData;
};
if (threadCount > 1) {
const requests = [];
for (let i = 0; i < threadCount; i++) {
const formData = baseFormData();
formData.append("sample_count", String(sampleCount)); // 每个线程生成 sampleCount 个视频
const request = fetch("/api/generate_video", {
method: "POST",
body: formData,
})
.then(response => {
if (!response.ok) {
return response.json().then(err => Promise.reject(err));
}
return response.json();
})
.then(data => {
if (data.video_urls && data.video_urls.length > 0) {
// 将该线程返回的全部视频加入轮播
data.video_urls.forEach(addVideoToCarousel);
}
})
.catch(error => {
console.error(`Thread ${i + 1} failed:`, error.detail || error.message);
});
requests.push(request);
}
Promise.allSettled(requests).then(() => {
if (videoItems.length === 0) {
// All requests failed
errorContainer.textContent = "所有视频生成请求均失败。";
errorContainer.classList.remove("hidden");
resetCarousel();
} else {
// At least one video was generated successfully
playNotificationSound();
}
loader.classList.add("hidden");
});
} else {
// --- Single Request Logic (Original Flow) ---
const formData = baseFormData();
formData.append("sample_count", sampleCountInput.value);
try {
const response = await fetch("/api/generate_video", {
method: "POST",
body: formData,
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.detail || "发生未知错误。");
}
const urls = data.video_urls;
if (!urls || !Array.isArray(urls) || urls.length === 0) {
throw new Error("API响应格式不正确或未返回视频。");
}
urls.forEach(addVideoToCarousel);
if (videoItems.length > 0) {
currentIndex = 0;
updateCarousel();
playNotificationSound(); // Play notification sound for successful generation
} else {
resetCarousel(); // No videos were successfully added
}
} catch (error) {
errorContainer.textContent = error.message;
errorContainer.classList.remove("hidden");
resetCarousel();
} finally {
loader.classList.add("hidden");
}
}
});
// --- Save Video Logic ---
function updateSaveVideoButtonState() {
if (videoItems.length === 0) {
saveVideoBtn.disabled = true;
saveVideoBtn.classList.remove('saved');
saveVideoBtn.querySelector('span').textContent = '保存此视频到服务器';
return;
}
const currentVideoUrl = videoItems[currentIndex];
if (savedVideos.includes(currentVideoUrl)) {
saveVideoBtn.disabled = true;
saveVideoBtn.classList.add('saved');
saveVideoBtn.querySelector('span').textContent = '已保存至服务器';
} else {
saveVideoBtn.disabled = false;
saveVideoBtn.classList.remove('saved');
saveVideoBtn.querySelector('span').textContent = '保存此视频到服务器';
}
}
async function saveCurrentVideo() {
if (videoItems.length === 0) return;
const videoUrlToSave = videoItems[currentIndex];
if (savedVideos.includes(videoUrlToSave)) return;
// Temporarily disable to prevent double-clicking
saveVideoBtn.disabled = true;
const originalText = saveVideoBtn.querySelector('span').textContent;
saveVideoBtn.querySelector('span').textContent = '保存中...';
try {
const formData = new FormData();
formData.append('video_url', videoUrlToSave);
const response = await fetch('/api/save_video', {
method: 'POST',
body: formData,
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.detail || '保存失败');
}
// Success
savedVideos.push(videoUrlToSave);
updateSaveVideoButtonState();
} catch (error) {
// Show a temporary error message on the button
saveVideoBtn.querySelector('span').textContent = '保存失败!';
console.error('Save video error:', error);
// Revert button state after a delay
setTimeout(() => {
saveVideoBtn.querySelector('span').textContent = originalText;
updateSaveVideoButtonState(); // Re-evaluates the correct state
}, 2000);
}
}
saveVideoBtn.addEventListener('click', saveCurrentVideo);
// --- Paste to Upload Logic ---
const imageDropZone = document.getElementById("image-drop-zone");
const videoDropZone = document.getElementById("video-drop-zone");
imageDropZone.addEventListener('mouseenter', () => currentPasteTarget = 'image');
imageDropZone.addEventListener('mouseleave', () => currentPasteTarget = null);
videoDropZone.addEventListener('mouseenter', () => currentPasteTarget = 'video');
videoDropZone.addEventListener('mouseleave', () => currentPasteTarget = null);
document.addEventListener('paste', (event) => {
if (!currentPasteTarget) return;
// Don't interfere with text inputs
const activeElement = document.activeElement;
if (activeElement && (activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA')) {
return;
}
const items = event.clipboardData.items;
if (!items) return;
for (let i = 0; i < items.length; i++) {
if (items[i].kind === 'file') {
const file = items[i].getAsFile();
if (!file) continue;
event.preventDefault(); // Prevent default paste behavior
if (currentPasteTarget === 'image' && file.type.startsWith('image/')) {
// Create a new FileList and assign it to the input
const dataTransfer = new DataTransfer();
dataTransfer.items.add(file);
imagePromptInput.files = dataTransfer.files;
// Manually trigger the change event to update the UI
imagePromptInput.dispatchEvent(new Event('change'));
break; // Handle only the first valid file
}
if (currentPasteTarget === 'video' && file.type.startsWith('video/')) {
const dataTransfer = new DataTransfer();
dataTransfer.items.add(file);
videoPromptInput.files = dataTransfer.files;
videoPromptInput.dispatchEvent(new Event('change'));
break; // Handle only the first valid file
}
}
}
});
// --- Initial Load ---
loadSettings();
loadSoundSettings(); // Load sound settings
// Set initial state for image processing UI
updateImageProcessingState();
updateSaveButtonState();
renderSavedKeys(); // Initial render of the key list
});