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 = `
没有已保存的提示词
`; 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 = `
没有已保存的 Key
`; 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 });