tts10 / templates /ltxvideo.html
Opera8's picture
Update templates/ltxvideo.html
48bf5a1 verified
Raw
History Blame Contribute Delete
110 kB
<!DOCTYPE html>
<html lang="fa" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI Video Studio</title>
<link href="https://fonts.googleapis.com/css2?family=Vazirmatn:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
<style>
:root {
--app-font: 'Vazirmatn', sans-serif;
--app-bg: #F8F9FC;
--panel-bg: #FFFFFF;
--panel-border: #EAEFF7;
--input-bg: #F6F8FB;
--input-border: #E1E7EF;
--text-primary: #1A202C;
--text-secondary: #626F86;
--text-tertiary: #8A94A6;
--accent-primary: #4A6CFA;
--accent-primary-hover: #3553D6;
--accent-primary-glow: rgba(74, 108, 250, 0.25);
--accent-secondary: #0FD4A8;
--accent-premium: #FFC107;
--accent-premium-glow: rgba(255, 193, 7, 0.3);
--success-color: #38A169;
--danger-color: #e53e3e;
--danger-color-hover: #c53030;
--shadow-sm: 0 1px 2px 0 rgba(26, 32, 44, 0.03);
--shadow-md: 0 4px 6px -1px rgba(26, 32, 44, 0.05), 0 2px 4px -2px rgba(26, 32, 44, 0.04);
--shadow-lg: 0 10px 15px -3px rgba(26, 32, 44, 0.06), 0 4px 6px -4px rgba(26, 32, 44, 0.05);
--shadow-xl: 0 20px 25px -5px rgba(26, 32, 44, 0.07), 0 8px 10px -6px rgba(26, 32, 44, 0.05);
--radius-card: 24px;
--radius-btn: 14px;
--radius-input: 12px;
--transition-smooth: all 0.35s cubic-bezier(0.4, 0, 0.2, 1);
}
@keyframes fadeIn { from { opacity: 0; transform: translateY(15px); } to { opacity: 1; transform: translateY(0); } }
@keyframes core-pulse { 0%, 100% { transform: scale(0.95); box-shadow: 0 0 25px var(--accent-primary-glow); } 50% { transform: scale(1.05); box-shadow: 0 0 50px rgba(74, 108, 250, 0.5); } }
@keyframes ring-rotate { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
@keyframes background-pan { 0% { background-position: 0% 50%; } 50% { background-position: 100% 50%; } 100% { background-position: 0% 50%; } }
@keyframes badge-fade-in { from { opacity: 0; transform: translateY(-10px) scale(0.95); } to { opacity: 1; transform: translateY(0) scale(1); } }
@keyframes slideInUp { from { opacity: 0; transform: translateY(30px); } to { opacity: 1; transform: translateY(0); } }
@keyframes fadeOutShrink { to { opacity: 0; transform: scale(0.8); height: 0; padding: 0; margin: 0; overflow: hidden; border: none; box-shadow: none; } }
.fade-out-shrink { animation: fadeOutShrink 0.5s cubic-bezier(0.4, 0, 0.2, 1) forwards; }
@keyframes shake {
0%, 100% { transform: translateX(0); }
20%, 60% { transform: translateX(-6px); }
40%, 80% { transform: translateX(6px); }
}
.shake-animation {
animation: shake 0.4s cubic-bezier(.36,.07,.19,.97) both !important;
background: linear-gradient(95deg, #e53e3e 0%, #c53030 100%) !important;
box-shadow: 0 6px 12px -3px rgba(229, 62, 62, 0.4) !important;
}
@keyframes modal-fade-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes modal-zoom-in { from { transform: translate(-50%, -50%) scale(0.9); } to { transform: translate(-50%, -50%) scale(1); } }
.modal-overlay { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(26, 32, 44, 0.5); backdrop-filter: blur(8px); z-index: 1000; animation: modal-fade-in 0.3s ease-out; }
.modal-overlay.active { display: block; }
.modal-content { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background-color: var(--panel-bg); padding: 2.5rem; border-radius: var(--radius-card); box-shadow: var(--shadow-xl); border: 1px solid var(--panel-border); width: 90%; max-width: 450px; text-align: center; animation: modal-zoom-in 0.3s cubic-bezier(0.4, 0, 0.2, 1); }
.modal-icon { width: 60px; height: 60px; margin: 0 auto 1.5rem; background: linear-gradient(45deg, var(--accent-primary-glow), rgba(15, 212, 168, 0.2)); border-radius: 50%; display: flex; align-items: center; justify-content: center; color: var(--accent-primary); }
.modal-icon svg { width: 32px; height: 32px; }
.modal-title { font-size: 1.3rem; font-weight: 700; color: var(--text-primary); margin: 0 0 1rem 0; }
.modal-text { font-size: 0.95rem; color: var(--text-secondary); line-height: 1.7; margin: 0 0 2rem 0; }
.modal-buttons { display: flex; gap: 1rem; width: 100%; }
.modal-button { flex: 1; padding: 0.8rem 1.5rem; background: var(--accent-primary); color: white; border: none; border-radius: var(--radius-btn); font-family: var(--app-font); font-weight: 600; font-size: 1rem; cursor: pointer; transition: var(--transition-smooth); }
.modal-button:hover { background: var(--accent-primary-hover); transform: translateY(-2px); }
.modal-button.secondary { background: var(--input-bg); color: var(--text-secondary); border: 1px solid var(--input-border); }
.modal-button.secondary:hover { background-color: var(--panel-border); color: var(--text-primary); }
#confirm-reset-modal .modal-icon, #delete-video-modal .modal-icon, #clear-history-modal .modal-icon { color: var(--danger-color); background: rgba(229, 62, 62, 0.1); }
#confirm-reset-modal .modal-button:not(.secondary), #delete-video-modal .modal-button:not(.secondary), #clear-history-modal .modal-button:not(.secondary) { background-color: var(--danger-color); }
#confirm-reset-modal .modal-button:not(.secondary):hover, #delete-video-modal .modal-button:not(.secondary):hover, #clear-history-modal .modal-button:not(.secondary):hover { background-color: var(--danger-color-hover); }
#report-video-modal .modal-icon { color: var(--danger-color); background: rgba(229, 62, 62, 0.1); }
.report-options-container { display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; margin-bottom: 1.5rem; }
.report-option-btn { background-color: var(--input-bg); color: var(--text-secondary); border: 1px solid var(--input-border); padding: 8px 14px; border-radius: var(--radius-btn); cursor: pointer; transition: all 0.2s ease; }
.report-option-btn:hover { background-color: var(--panel-border); color: var(--text-primary); }
#custom-report-section { display: none; width: 100%; }
#report-video-modal textarea { min-height: 80px; margin-top: 1rem; }
#emoji-container { margin-top: 10px; } .emoji { cursor: pointer; font-size: 24px; margin: 0 5px; }
#report-success-message { color: var(--success-color); font-weight: 600; display: none; margin-top: 1rem; }
body { font-family: var(--app-font); background-color: var(--app-bg); color: var(--text-primary); margin: 0; padding: 2.5rem 1rem 7.5rem 1rem; display: flex; justify-content: center; align-items: flex-start; min-height: 100vh; position: relative; }
.container { max-width: 820px; width: 100%; }
header { position: relative; text-align: center; margin-bottom: 2.5rem; padding: 2rem 0; animation: fadeIn 0.8s 0.1s ease-out backwards; overflow: hidden; }
#neural-network-canvas { position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 1; }
.header-content { position: relative; z-index: 2; }
.ai-orb-container { width: 150px; height: 150px; margin: 0 auto 1rem; position: relative; display: flex; align-items: center; justify-content: center; }
.orb-background { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: radial-gradient(ellipse at center, rgba(74, 108, 250, 0.15) 0%, transparent 70%), linear-gradient(45deg, rgba(15, 212, 168, 0.05), rgba(74, 108, 250, 0.05)); border-radius: 50%; animation: background-pan 12s ease-in-out infinite; background-size: 200% 200%; }
.orb-core { width: 60px; height: 60px; border-radius: 50%; background: radial-gradient(circle, #394A7A 0%, #1A202C 70%); box-shadow: 0 0 25px var(--accent-primary-glow); display: flex; align-items: center; justify-content: center; position: relative; z-index: 2; animation: core-pulse 4s ease-in-out infinite; border: 1px solid rgba(74, 108, 250, 0.3); }
.play-icon { width: 24px; height: 24px; color: var(--accent-secondary); filter: drop-shadow(0 0 8px rgba(15, 212, 168, 0.7)); transition: var(--transition-smooth); }
.orb-core:hover .play-icon { transform: scale(1.1); color: #fff; filter: drop-shadow(0 0 12px rgba(15, 212, 168, 1)); }
.orb-ring { position: absolute; top: 50%; left: 50%; border-style: solid; border-color: transparent; border-radius: 50%; z-index: 1; mix-blend-mode: screen; }
.orb-ring.one { width: 100px; height: 100px; margin-top: -50px; margin-left: -50px; border-width: 2px; border-top-color: var(--accent-primary); border-right-color: var(--accent-primary); animation: ring-rotate 8s linear infinite; filter: blur(1px); }
.orb-ring.two { width: 120px; height: 120px; margin-top: -60px; margin-left: -60px; border-width: 1px; border-bottom-color: var(--accent-secondary); animation: ring-rotate 10s linear infinite reverse; }
.orb-ring.three { width: 140px; height: 140px; margin-top: -70px; margin-left: -70px; border-width: 2px; border-left-color: rgba(255, 255, 255, 0.5); border-right-color: rgba(255, 255, 255, 0.5); animation: ring-rotate 12s linear infinite; filter: blur(1px); opacity: 0.5; }
h1 { font-size: 2.8rem; font-weight: 800; margin: 0; background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; letter-spacing: -1px; }
.subtitle { font-size: 1.1rem; color: var(--text-secondary); margin-top: 0.5rem; }
.instruction { font-size: 0.95rem; color: var(--text-tertiary); margin-top: 0.75rem; font-weight: 500; }
main { padding: 3rem; background-color: var(--panel-bg); border-radius: var(--radius-card); box-shadow: var(--shadow-xl); border: 1px solid var(--panel-border); animation: fadeIn 0.8s 0.3s ease-out backwards; }
.form-group { margin-bottom: 2.5rem; }
.form-group:last-child { margin-bottom: 0; }
.form-label { display: flex; align-items: center; gap: 0.75rem; font-weight: 700; color: var(--text-primary); font-size: 1.2em; margin-bottom: 1.2rem; }
.form-label svg { width: 24px; height: 24px; color: var(--accent-primary); }
#image-drop-zone { position: relative; border: 2px dashed var(--input-border); border-radius: var(--radius-input); padding: 2.5rem; text-align: center; cursor: pointer; transition: var(--transition-smooth); background-color: var(--input-bg); min-height: 200px; display: flex; flex-direction: column; justify-content: center; align-items: center; overflow: hidden; }
#image-drop-zone.drag-over, #image-drop-zone:hover:not(.has-image) { border-color: var(--accent-primary); background-color: #fff; box-shadow: 0 0 15px var(--accent-primary-glow); }
#image-drop-zone.has-image { border-style: solid; border-color: var(--success-color); padding: 0; cursor: default; }
.upload-content { display: flex; flex-direction: column; align-items: center; gap: 1rem; }
.upload-icon svg { width: 48px; height: 48px; color: var(--accent-primary); stroke-width: 1.5; opacity: 0.8; }
#image-drop-zone p { margin: 0; color: var(--text-secondary); font-weight: 500; }
#imagePreview { display: none; width: 100%; height: 100%; object-fit: contain; position: absolute; top: 0; left: 0; }
#image-drop-zone.has-image .upload-content { display: none; }
#image-drop-zone.has-image #imagePreview { display: block; }
#remove-image-btn { position: absolute; top: 12px; left: 12px; width: 32px; height: 32px; background-color: rgba(26, 32, 44, 0.7); color: white; border: none; border-radius: 50%; font-size: 22px; font-weight: bold; line-height: 32px; text-align: center; cursor: pointer; opacity: 0; visibility: hidden; transform: scale(0.8); transition: all 0.25s ease-out; z-index: 10; }
#image-drop-zone.has-image #remove-image-btn { opacity: 1; visibility: visible; transform: scale(1); }
#remove-image-btn:hover { background-color: var(--danger-color); transform: scale(1.1) rotate(90deg); }
textarea { width: 100%; padding: 1rem 1.2rem; border-radius: var(--radius-input); border: 1px solid var(--input-border); background-color: var(--input-bg); color: var(--text-primary); box-shadow: var(--shadow-sm) inset; font-family: var(--app-font); font-size: 1rem; box-sizing: border-box; transition: var(--transition-smooth); min-height: 120px; resize: vertical; }
textarea:focus { outline: none; border-color: var(--accent-primary); box-shadow: 0 0 0 3px var(--accent-primary-glow), var(--shadow-sm) inset; background-color: var(--panel-bg); }
textarea:disabled { background-color: var(--input-border); color: var(--text-tertiary); }
.slider-group { margin-top: 2rem; margin-bottom: 1.5rem; }
.slider-label-container { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; }
.slider-label { font-weight: 600; color: var(--text-secondary); font-size: 0.95rem; display: flex; align-items: center; gap: 8px; }
.info-icon { width: 20px; height: 20px; background-color: var(--input-border); color: var(--text-tertiary); border-radius: 50%; font-size: 14px; font-weight: 700; display: inline-flex; align-items: center; justify-content: center; cursor: pointer; transition: all 0.2s ease; }
.info-icon:hover { background-color: var(--accent-primary); color: white; transform: scale(1.1); }
.slider-value { font-weight: 700; font-size: 1rem; color: var(--accent-primary); background-color: var(--input-bg); padding: 0.25rem 0.75rem; border-radius: 8px; border: 1px solid var(--input-border); min-width: 80px; text-align: center; }
.slider-wrapper { display: flex; align-items: center; gap: 1rem; }
.slider-icon { width: 22px; height: 22px; color: var(--text-tertiary); flex-shrink: 0; opacity: 0.8; }
input[type="range"] { -webkit-appearance: none; appearance: none; flex-grow: 1; width: 100%; height: 12px; background: var(--input-bg); border-radius: var(--radius-input); outline: none; cursor: pointer; direction: rtl; box-shadow: inset 0 1px 3px rgba(0,0,0,0.1); transition: var(--transition-smooth); }
input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 24px; height: 24px; background: linear-gradient(145deg, var(--accent-primary), var(--accent-secondary)); border-radius: 50%; border: 3px solid white; box-shadow: var(--shadow-md); transition: all 0.2s ease-in-out; margin-top: -6px; }
input[type="range"]::-moz-range-thumb { width: 24px; height: 24px; background: linear-gradient(145deg, var(--accent-primary), var(--accent-secondary)); border-radius: 50%; border: 3px solid white; box-shadow: var(--shadow-md); transition: all 0.2s ease-in-out; }
input[type="range"]:hover { box-shadow: inset 0 1px 3px rgba(0,0,0,0.1), 0 0 0 4px var(--accent-primary-glow); }
input[type="range"]:hover::-webkit-slider-thumb { box-shadow: 0 0 0 10px var(--accent-primary-glow), var(--shadow-md); transform: scale(1.1); }
input[type="range"]:active::-webkit-slider-thumb { transform: scale(1.2); box-shadow: 0 0 0 12px var(--accent-primary-glow), var(--shadow-md); }
#generateButton { display: flex; align-items: center; justify-content: center; gap: 0.75rem; width: 100%; padding: 1.1rem; font-size: 1.2rem; font-weight: 700; background: linear-gradient(95deg, var(--accent-secondary) 0%, var(--accent-primary) 100%); color: #fff; border: none; border-radius: var(--radius-btn); cursor: pointer; transition: all 0.3s ease; box-shadow: 0 6px 12px -3px var(--accent-primary-glow); margin-top: 1rem; }
#generateButton svg { width: 24px; height: 24px; margin-left: 4px; filter: drop-shadow(0 0 5px rgba(255,255,255,0.5)); }
#generateButton:hover:not(:disabled) { transform: translateY(-5px) scale(1.02); box-shadow: 0 8px 20px -4px var(--accent-primary-glow); }
#generateButton:disabled { background: var(--text-tertiary); cursor: not-allowed; box-shadow: none; opacity: 0.7; }
#subscription-status-badge { display: none; padding: 6px 16px; border-radius: 20px; font-size: 0.9em; font-weight: 700; margin-top: 1.25rem; letter-spacing: 0.5px; animation: badge-fade-in 0.6s 0.5s ease-out backwards; }
#subscription-status-badge.free-badge { background: linear-gradient(45deg, #6c757d, #495057); color: white; box-shadow: 0 4px 10px rgba(108, 117, 125, 0.3); }
#subscription-status-badge.paid-badge { background: linear-gradient(45deg, var(--accent-premium), #ffca2c); color: #333; box-shadow: 0 4px 10px var(--accent-premium-glow); }
#credit-info-section { margin-top: 1.5rem; margin-bottom: 0.5rem; padding: 1rem; background-color: var(--input-bg); border-radius: var(--radius-input); text-align: center; font-size: 0.95rem; font-weight: 500; color: var(--text-secondary); border: 1px solid var(--input-border); display: none; }
#upgrade-button { margin-top: 1rem; width: 100%; padding: 1rem; font-size: 1.1rem; font-weight: 700; background: linear-gradient(95deg, #FFD54F, #FFC107 100%); color: #212529; border: none; border-radius: var(--radius-btn); cursor: pointer; box-shadow: 0 8px 20px -5px rgba(255, 193, 7, 0.3); display: none; }
/* Processing Container */
#results-container { min-height: 180px; position: relative; padding: 1rem; background-color: var(--input-bg); border-radius: var(--radius-card); border: 2px dashed var(--input-border); box-shadow: var(--shadow-sm) inset; transition: var(--transition-smooth); display: flex; flex-direction: column; align-items: center; justify-content: center; }
#statusSection { display: none; width: 100%; }
#statusSection.active { display: block; animation: fadeIn 0.5s; }
#criticalErrorSection { display: none; width: 100%; }
#criticalErrorSection.active { display: flex; flex-direction: column; align-items: center; justify-content: center; animation: fadeIn 0.5s; }
#statusMessages { max-height: 150px; overflow-y: auto; width: 100%; padding: 0.5rem; margin-bottom: 1rem; }
.status-message { display: flex; align-items: center; gap: 0.75rem; padding: 0.6rem 1rem; margin-bottom: 0.5rem; border-radius: var(--radius-input); font-size: 0.9rem; background: var(--panel-bg); border: 1px solid var(--panel-border); color: var(--text-secondary); }
.status-message.success { border-left: 4px solid var(--success-color); color: var(--success-color); }
.status-message.error { border-left: 4px solid var(--danger-color); color: var(--danger-color); }
.status-icon { width: 18px; height: 18px; }
/* History Section Styling */
.history-section {
margin-top: 1rem;
animation: fadeIn 0.5s ease-out;
}
.history-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
flex-wrap: wrap;
gap: 1rem;
}
.history-title {
font-size: 1.3rem;
font-weight: 800;
color: var(--text-primary);
display: flex;
align-items: center;
gap: 0.75rem;
}
.history-title i {
font-size: 1.5rem;
color: var(--accent-primary);
}
.btn-clear-history {
background: linear-gradient(135deg, rgba(229, 62, 62, 0.08) 0%, rgba(229, 62, 62, 0.03) 100%);
color: var(--danger-color);
border: 1px solid rgba(229, 62, 62, 0.2);
padding: 0.6rem 1.2rem;
border-radius: 12px;
font-family: var(--app-font);
font-weight: 600;
font-size: 0.95rem;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.btn-clear-history:hover {
background: var(--danger-color);
color: white;
box-shadow: 0 6px 15px rgba(229, 62, 62, 0.3);
transform: translateY(-2px);
border-color: var(--danger-color);
}
.history-grid { display: flex; flex-direction: column; gap: 0.75rem; width: 100%; }
.video-result-card {
position: relative;
background: var(--panel-bg);
border-radius: 20px;
padding: 0.75rem 1rem !important;
border: 1px solid var(--panel-border);
box-shadow: var(--shadow-sm);
transition: var(--transition-smooth);
overflow: hidden;
display: flex;
flex-direction: column;
cursor: pointer;
}
.video-result-card:hover {
border-color: rgba(74, 108, 250, 0.2);
box-shadow: var(--shadow-md);
}
.card-header-layout {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
user-select: none;
}
.card-header-left {
display: flex;
align-items: center;
gap: 0.75rem;
min-width: 0;
flex: 1;
}
.card-thumbnail-container {
width: 48px;
height: 48px;
border-radius: 50%;
overflow: hidden;
position: relative;
border: 2px solid #fff;
box-shadow: var(--shadow-sm);
flex-shrink: 0;
cursor: zoom-in;
background-color: var(--input-bg);
display: flex;
align-items: center;
justify-content: center;
}
.card-thumbnail-container img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s ease;
}
.card-thumbnail-container:hover img {
transform: scale(1.1);
}
.card-thumbnail-zoom-overlay {
position: absolute;
inset: 0;
background: rgba(0,0,0,0.4);
opacity: 0;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 0.75rem;
transition: var(--transition-smooth);
}
.card-thumbnail-container:hover .card-thumbnail-zoom-overlay {
opacity: 1;
}
.card-header-titles {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
}
.card-header-title-text {
font-size: 0.9rem;
font-weight: 800;
color: var(--text-primary);
line-height: 1.2;
}
.card-header-meta {
font-size: 0.75rem;
color: var(--text-tertiary);
display: flex;
align-items: center;
gap: 6px;
flex-wrap: wrap;
}
.card-status-badge {
font-size: 0.7rem;
font-weight: 700;
padding: 1px 6px;
border-radius: 6px;
display: inline-block;
}
.status-badge-completed { background-color: rgba(56, 161, 105, 0.08); color: var(--success-color); border: 1px solid rgba(56, 161, 105, 0.1); }
.status-badge-processing { background-color: rgba(217, 119, 6, 0.08); color: #d97706; border: 1px solid rgba(217, 119, 6, 0.1); }
.status-badge-failed { background-color: rgba(229, 62, 62, 0.08); color: var(--danger-color); border: 1px solid rgba(229, 62, 62, 0.1); }
.card-header-actions {
display: flex;
align-items: center;
gap: 16px;
flex-shrink: 0;
}
.chevron-arrow {
color: var(--text-tertiary);
font-size: 0.85rem;
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
order: 1;
}
.video-result-card.expanded .chevron-arrow {
transform: rotate(-90deg);
color: var(--accent-primary);
}
.card-delete-btn {
width: 32px;
height: 32px;
border-radius: 50%;
background-color: var(--input-bg);
border: 1px solid var(--input-border);
color: var(--text-secondary);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: var(--transition-smooth);
order: 2;
}
.card-delete-btn:hover {
background-color: rgba(229, 62, 62, 0.08);
color: var(--danger-color);
border-color: rgba(229, 62, 62, 0.15);
}
.card-expanded-body {
max-height: 0;
overflow: hidden;
opacity: 0;
transition: max-height 0.4s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s ease;
}
.video-result-card.expanded .card-expanded-body {
max-height: 1500px;
opacity: 1;
margin-top: 1.25rem;
padding-top: 1.25rem;
border-top: 1px dashed var(--panel-border);
}
.video-result-card video {
width: 100%;
max-width: 100%;
height: auto;
aspect-ratio: auto;
border-radius: 18px;
margin: 0 auto 1.25rem;
display: block;
background-color: #000;
box-shadow: var(--shadow-lg);
}
.video-info-container { width: 100%; text-align: right; margin-bottom: 1.25rem; display: flex; flex-direction: column; gap: 0.5rem; }
.video-prompt-lbl { font-size: 0.95rem; font-weight: 600; color: var(--text-primary); line-height: 1.6; }
.video-meta-lbl { font-size: 0.8rem; color: var(--text-tertiary); display: flex; gap: 12px; align-items: center; }
.video-controls { display: flex; justify-content: center; width: 100%; gap: 10px; }
.video-button { padding: 0.7rem 1.5rem; border-radius: var(--radius-btn); border: none; cursor: pointer; font-family: var(--app-font); font-weight: 600; font-size: 1rem; transition: var(--transition-smooth); display: inline-flex; align-items: center; justify-content: center; gap: 0.5rem; text-decoration: none;}
.video-button.primary { width: 100%; background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary)); color: white; box-shadow: 0 4px 10px rgba(74, 108, 250, 0.2); }
.video-button.primary:hover:not(:disabled) { box-shadow: 0 6px 15px rgba(74, 108, 250, 0.4); transform: translateY(-2px); }
.video-button:not(.primary):hover:not(:disabled) { background-color: var(--panel-border); color: var(--text-primary); }
.video-button:disabled { cursor: not-allowed; opacity: 0.6; }
#image-lightbox-modal {
display: none;
position: fixed;
top: 0; left: 0; width: 100%; height: 100%;
background-color: rgba(15, 23, 42, 0.9);
backdrop-filter: blur(10px);
z-index: 2000;
justify-content: center;
align-items: center;
}
#image-lightbox-modal.active { display: flex; }
.lightbox-img {
max-width: 90%;
max-height: 80%;
border-radius: 20px;
box-shadow: 0 10px 40px rgba(0,0,0,0.6);
object-fit: contain;
animation: zoomIn 0.35s cubic-bezier(0.4, 0, 0.2, 1);
border: 2px solid rgba(255, 255, 255, 0.1);
}
.lightbox-close {
position: absolute;
top: 24px;
right: 32px;
color: #fff;
font-size: 40px;
font-weight: bold;
cursor: pointer;
transition: var(--transition-smooth);
}
.lightbox-close:hover { color: var(--danger-color); transform: scale(1.1); }
@keyframes zoomIn { from { transform: scale(0.9); opacity: 0; } to { transform: scale(1); opacity: 1; } }
.processing-card-overlay {
position: relative;
width: 100%;
aspect-ratio: 16 / 9;
border-radius: 18px;
overflow: hidden;
background-color: #0d1117;
margin-bottom: 1rem;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
border: 1px solid var(--panel-border);
}
.processing-bg-image {
position: absolute;
top: 0; left: 0; width: 100%; height: 100%;
object-fit: cover;
opacity: 0.3;
filter: blur(4px);
}
.processing-card-content {
position: relative;
z-index: 5;
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
width: 80%;
text-align: center;
}
.processing-spinner {
width: 32px; height: 32px;
border: 3px solid rgba(74, 108, 250, 0.2);
border-top-color: var(--accent-primary);
border-radius: 50%;
animation: ring-rotate 1s linear infinite;
}
.processing-progress-bg {
width: 100%; height: 6px;
background-color: rgba(255,255,255,0.1);
border-radius: 10px;
overflow: hidden;
}
.processing-progress-bar {
height: 100%;
width: 0%;
background: linear-gradient(to right, var(--accent-primary), var(--accent-secondary));
border-radius: 10px;
transition: width 0.5s ease-out;
}
.processing-status-lbl {
font-size: 0.85rem;
font-weight: 700;
color: #fff;
text-shadow: 0 2px 4px rgba(0,0,0,0.8);
}
.failed-card-overlay {
position: relative;
width: 100%;
aspect-ratio: 16 / 9;
border-radius: 18px;
overflow: hidden;
background-color: #1a0f0f;
margin-bottom: 1rem;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.failed-card-content {
z-index: 5;
color: #feb2b2;
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
padding: 1rem;
text-align: center;
}
.failed-card-content i { font-size: 2rem; }
.failed-card-content p { font-size: 0.8rem; margin: 0; line-height: 1.6; }
.bottom-nav-container {
position: fixed;
bottom: 24px;
left: 0;
right: 0;
z-index: 999;
display: flex;
justify-content: center;
pointer-events: none;
padding: 0 1.5rem;
}
.bottom-nav-inner {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(15px);
border: 1px solid var(--panel-border);
border-radius: 40px;
padding: 6px;
display: flex;
gap: 8px;
box-shadow: 0 10px 40px -10px rgba(0,0,0,0.12);
pointer-events: auto;
width: 100%;
max-width: 360px;
height: 72px;
box-sizing: border-box;
align-items: center;
}
.nav-tab {
flex: 1;
background: transparent;
border: none;
height: 100%;
border-radius: 30px;
cursor: pointer;
font-family: var(--app-font);
font-weight: 700;
font-size: 0.85rem;
color: var(--text-secondary);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 4px;
transition: var(--transition-smooth);
position: relative;
outline: none;
}
.nav-tab.active {
color: var(--accent-primary);
background: linear-gradient(to top, rgba(74, 108, 250, 0.05), transparent);
}
.nav-tab.active i {
color: var(--accent-primary);
transform: translateY(-2px);
filter: drop-shadow(0 2px 8px rgba(74, 108, 250, 0.3));
}
.nav-tab.active::after {
content: '';
position: absolute;
bottom: 4px;
width: 5px;
height: 5px;
border-radius: 50%;
background-color: var(--accent-primary);
}
.nav-tab i {
font-size: 1.3rem;
transition: var(--transition-smooth);
}
.nav-badge {
position: absolute;
top: 6px;
right: 28%;
background: var(--danger-color);
color: white;
font-size: 0.7rem;
font-weight: 800;
min-width: 16px;
height: 16px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 0 10px rgba(229, 62, 62, 0.4);
border: 2px solid #fff;
animation: badge-fade-in 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
@keyframes nav-bounce {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.2) translateY(-6px); }
}
.bounce-nav-tab i {
animation: nav-bounce 0.6s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
.fly-orb {
position: fixed;
pointer-events: none;
z-index: 2000;
transition: all 0.85s cubic-bezier(0.175, 0.885, 0.32, 1.275);
display: none;
align-items: center;
justify-content: center;
width: 76px;
height: 76px;
border-radius: 22px;
background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary));
box-shadow: 0 10px 35px rgba(74, 108, 250, 0.5);
color: white;
font-size: 1.8rem;
will-change: top, left, transform, opacity;
}
#aiLoader { display: none; align-items: center; justify-content: center; }
.generator-container { position: relative; width: 400px; max-width: 100%; height: 300px; border: 2px solid #38bdf8; border-radius: 20px; overflow: hidden; box-shadow: 0 0 40px rgba(56, 189, 248, 0.3); animation: pulse-loader 5s infinite cubic-bezier(0.4, 0, 0.6, 1); background-color: #161b22; color: #f0f6fc; }
@keyframes pulse-loader { 0% { box-shadow: 0 0 40px rgba(56, 189, 248, 0.3); } 50% { box-shadow: 0 0 60px rgba(56, 189, 248, 0.7); } 100% { box-shadow: 0 0 40px rgba(56, 189, 248, 0.3); } }
.noise-layer { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100"><rect width="100" height="100" fill="none"/><filter id="noise"><feTurbulence type="fractalNoise" baseFrequency="0.5" numOctaves="4" stitchTiles="stitch"/></filter><rect width="100%" height="100%" filter="url(%23noise)" opacity="0.6"/></svg>') repeat; opacity: 1; animation: fade-noise 7s infinite ease-in-out; } @keyframes fade-noise { 0% { opacity: 1; filter: blur(5px); } 30% { opacity: 0.8; filter: blur(2px); } 100% { opacity: 0; filter: blur(0px); } }
.sketch-layer { position: absolute; top: 0; left: 0; width: 100%; height: 100%; filter: grayscale(1) contrast(1.5) blur(3px); opacity: 0; animation: reveal-sketch 7s infinite ease-in-out; } @keyframes reveal-sketch { 0% { opacity: 0; } 20% { opacity: 1; } 60% { opacity: 0.5; } 100% { opacity: 0; } }
.building-layer { position: absolute; top: 0; left: 0; width: 100%; height: 100%; filter: blur(15px); opacity: 0; animation: denoise-color 7s infinite ease-in-out; } @keyframes denoise-color { 0% { opacity: 0; } 40% { opacity: 0.6; filter: blur(5px); } 100% { opacity: 1; filter: blur(0px); } }
.pixel-grid { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: repeating-linear-gradient(0deg, transparent 0 1px, rgba(255,255,255,0.1) 1px 2px), repeating-linear-gradient(90deg, transparent 0 1px, rgba(255,255,255,0.1) 1px 2px); opacity: 1; animation: dissolve-grid 7s infinite ease-in-out; } @keyframes dissolve-grid { 0% { opacity: 1; } 70% { opacity: 0.5; } 100% { opacity: 0; } }
.particles { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: radial-gradient(circle, rgba(56, 189, 248, 0.2) 0%, transparent 50%); animation: flow-particles 7s infinite cubic-bezier(0.4, 0, 0.6, 1); } @keyframes flow-particles { 0% { transform: translate(0, 0) scale(1); opacity: 0.5; } 50% { transform: translate(10px, -15px) scale(1.05); opacity: 0.8; } 100% { transform: translate(0, 0) scale(1); opacity: 0.5; } }
.text-overlay { position: absolute; top: 45%; left: 50%; transform: translate(-50%, -50%); font-size: 24px; font-weight: 700; text-shadow: 0 0 20px rgba(56, 189, 248, 0.8); animation: glow-text 7s infinite ease-in-out; font-family: var(--app-font); width: 90%; text-align: center;} @keyframes glow-text { 0% { opacity: 0.7; } 50% { opacity: 1; } 100% { opacity: 0.7; } }
.progress-bar { position: absolute; bottom: 0; left: 0; width: 0%; height: 6px; background: linear-gradient(to right, #38bdf8, #bb86fc, #facc15); transition: width 0.3s linear; }
.report-flag-icon { display: inline-block; width: 16px; height: 16px; margin: 0 8px; vertical-align: middle; cursor: pointer; transition: all 0.4s ease; color: var(--text-tertiary); }
.report-flag-icon:hover { color: var(--danger-color); }
@media (max-width: 768px) {
main { padding: 3rem 1.5rem; }
h1 { font-size: 2.2rem; }
.generator-container { height: 250px; }
.text-overlay { font-size: 18px; }
.history-header { flex-direction: column; align-items: flex-start; }
.btn-clear-history { width: 100%; justify-content: center; }
.history-section {
margin-left: -1.5rem;
margin-right: -1.5rem;
padding-left: 0.75rem;
padding-right: 0.75rem;
}
}
</style>
</head>
<body>
<div class="container">
<header>
<canvas id="neural-network-canvas"></canvas>
<div class="header-content">
<div class="ai-orb-container">
<div class="orb-background"></div>
<div class="orb-core">
<svg class="play-icon" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path d="M8 5v14l11-7z"></path></svg>
</div>
<div class="orb-ring one"></div><div class="orb-ring two"></div><div class="orb-ring three"></div>
</div>
<h1>استودیو ویدیو هوش مصنوعی</h1>
<p class="subtitle" id="main-subtitle">تصاویر خود را با قدرت هوش مصنوعی به ویدیو سینمایی تبدیل کنید</p>
<p class="instruction" id="main-instruction">ابتدا تصویر خود را تولید کرده و اینجا آپلود کنید تا ویدیوی خود را بسازید.</p>
<div id="subscription-status-badge" style="display:inline-block;"></div>
</div>
</header>
<div id="fly-orb" class="fly-orb">
<i class="fas fa-wand-magic-sparkles"></i>
</div>
<main>
<div id="tab-home-content">
<div id="main-controls">
<div class="form-group">
<div class="form-label">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4Z"/></svg>
۱. تصویر پروژه
</div>
<label id="image-drop-zone" for="imageFile">
<div class="upload-content">
<div class="upload-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="17 8 12 3 7 8"></polyline><line x1="12" y1="3" x2="12" y2="15"></line></svg>
</div>
<p>فایل تصویر را اینجا بکشید یا برای انتخاب کلیک کنید</p>
</div>
<img id="imagePreview" src="" alt="">
<button id="remove-image-btn" type="button" title="شروع پروژه جدید (حذف تصویر)">&times;</button>
</label>
<input type="file" id="imageFile" accept="image/jpeg, image/png, image/webp" hidden>
</div>
<div class="form-group">
<label for="prompt" class="form-label">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/><path d="m15 5 4 4"/></svg>
۲. دستور ساخت
</label>
<textarea id="prompt" rows="4" placeholder="مثال: زوم آهسته به بیرون در حالی که برگ‌ها در باد می‌رقصند و نور خورشید از بین درختان می‌تابد"></textarea>
<div class="slider-group">
<div class="slider-label-container">
<label for="durationSlider" class="slider-label" id="duration-label">مدت زمان<span class="info-icon" id="duration-info-icon">!</span></label>
<span id="durationValue" class="slider-value">4.0 ثانیه</span>
</div>
<div class="slider-wrapper">
<svg class="slider-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.72"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.72-1.72"></path></svg>
<input type="range" id="durationSlider" min="1.0" max="5.0" value="4.0" step="1.0">
<svg class="slider-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg>
</div>
</div>
<div id="credit-info-section"></div>
<button id="upgrade-button">⭐️ ارتقا به نسخه کامل و نامحدود</button>
<button id="generateButton">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 3L12 8L17 10L12 12L10 17L8 12L3 10L8 8L10 3z"/></svg>
<span>ساخت ویدیو جادویی</span>
</button>
<!-- اضافه شدن پیام هشدار قرمز رنگ زیر دکمه -->
<div id="prompt-warning" style="color: var(--danger-color); font-size: 0.95rem; font-weight: 700; text-align: center; margin-top: 12px; display: none;">لطفاً توضیحات را وارد کنید</div>
</div>
</div>
</div>
<div id="tab-history-content" style="display: none;">
<div class="history-section">
<div class="history-header">
<div class="history-title">
<i class="fas fa-history"></i>
لیست پروژه‌های ویدیویی شما
</div>
<button id="clear-history-btn" class="btn-clear-history">
<i class="fas fa-trash-can"></i>
حذف کل تاریخچه
</button>
</div>
<div id="history-grid" class="history-grid"></div>
</div>
</div>
<div id="processing-container" class="form-group" style="display: none; margin-top: 2.5rem;">
<div id="results-container">
<div id="statusSection">
<div id="statusMessages"></div>
<div id="aiLoader" style="display: none;">
<div class="generator-container">
<div class="noise-layer"></div>
<div class="sketch-layer"></div>
<div class="building-layer"></div>
<div class="pixel-grid"></div>
<div class="particles"></div>
<div class="text-overlay">در حال پردازش ویدیو...</div>
<div class="progress-bar" id="tv-progress-bar"></div>
</div>
</div>
</div>
<div id="criticalErrorSection"></div>
</div>
</div>
</main>
</div>
<div class="bottom-nav-container">
<div class="bottom-nav-inner">
<button id="tab-home-btn" class="nav-tab active">
<i class="fas fa-home"></i>
<span>خانه</span>
</button>
<button id="tab-history-btn" class="nav-tab">
<i class="fas fa-clock-rotate-left"></i>
<span>تاریخچه</span>
<span id="active-badge" class="nav-badge" style="display: none;">0</span>
</button>
</div>
</div>
<div class="modal-overlay" id="image-lightbox-modal">
<span class="lightbox-close" id="close-lightbox-btn">&times;</span>
<img class="lightbox-img" id="lightbox-image-element" src="" alt="Input Image Preview">
</div>
<div class="modal-overlay" id="limit-reached-modal">
<div class="modal-content">
<div class="modal-icon" style="color: var(--accent-premium); background: rgba(255, 193, 7, 0.12)">
<i class="fas fa-triangle-exclamation" style="font-size: 1.8rem;"></i>
</div>
<h3 class="modal-title">تکمیل ظرفیت پردازش همزمان</h3>
<p class="modal-text">شما در حال حاضر دارای ۵ پروژه فعال در حال رندر و پردازش همزمان هستید.<br><br>لطفاً منتظر بمانید تا فرآیند ساخت یکی از ویدیوها به اتمام برسد و مجدداً تلاش کنید.</p>
<div class="modal-buttons">
<button class="modal-button" id="close-limit-modal">متوجه شدم</button>
</div>
</div>
</div>
<div class="modal-overlay" id="duration-modal">
<div class="modal-content">
<div class="modal-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg></div>
<h3 class="modal-title">راهنمای مدت زمان</h3>
<p class="modal-text">گاهی به دلیل شلوغی سرور، ساخت ویدیوهای طولانی با محدودیت مواجه می‌شود.<br><br>اگر با خطا مواجه شدید، لطفا مدت زمان را کاهش داده و دوباره امتحان کنید.</p>
<div class="modal-buttons"><button class="modal-button" id="close-duration-modal">متوجه شدم</button></div>
</div>
</div>
<div class="modal-overlay" id="report-video-modal">
<div class="modal-content">
<div class="modal-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z"></path><line x1="4" y1="22" x2="4" y2="15"></line></svg></div>
<h3 class="modal-title">گزارش ویدیو</h3>
<div id="report-form-content">
<p class="modal-text">لطفا دلیل گزارش خود را انتخاب یا وارد کنید.</p>
<div class="report-options-container">
<button class="report-option-btn" data-reason="محتوای نامناسب">محتوای نامناسب</button>
<button class="report-option-btn" data-reason="محتوای نادرست">محتوای نادرست</button>
<button class="report-option-btn" id="report-other-btn">دیگر</button>
</div>
<div id="custom-report-section">
<textarea id="custom-report-text" placeholder="توضیحات بیشتر..."></textarea>
<div id="emoji-container">
<span class="emoji">😊</span><span class="emoji">😡</span><span class="emoji">😢</span>
</div>
<div class="modal-buttons" style="margin-top: 1.5rem;">
<button class="modal-button" id="send-report-btn">ارسال گزارش</button>
</div>
</div>
</div>
<div id="report-success-message">
<p>گزارش شما با موفقیت ثبت شد. از همکاری شما متشکریم.</p>
</div>
</div>
</div>
<div class="modal-overlay" id="confirm-reset-modal">
<div class="modal-content">
<div class="modal-icon"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path><line x1="12" y1="9" x2="12" y2="13"></line><line x1="12" y1="17" x2="12.01" y2="17"></line></svg></div>
<h3 class="modal-title">آیا مطمئن هستید؟</h3>
<p class="modal-text">با این کار تمام نتایج فعلی حذف شده و پروژه از ابتدا شروع می‌شود.</p>
<div class="modal-buttons">
<button class="modal-button secondary" id="toast-cancel-btn">انصراف</button>
<button class="modal-button" id="toast-confirm-btn">تایید و حذف</button>
</div>
</div>
</div>
<div class="modal-overlay" id="delete-video-modal">
<div class="modal-content">
<div class="modal-icon"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path></svg></div>
<h3 class="modal-title">حذف ویدیو</h3>
<p class="modal-text">آیا از حذف این ویدیو مطمئن هستید؟ این عملیات قابل بازگشت نیست.</p>
<div class="modal-buttons">
<button class="modal-button secondary" id="cancel-delete-btn">انصراف</button>
<button class="modal-button" id="confirm-delete-btn">تایید و حذف</button>
</div>
</div>
</div>
<div class="modal-overlay" id="clear-history-modal">
<div class="modal-content">
<div class="modal-icon"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path></svg></div>
<h3 class="modal-title">حذف کل تاریخچه</h3>
<p class="modal-text">آیا مطمئن هستید که می‌خواهید تمام ویدیوهای ذخیره شده را حذف کنید؟ این عمل غیرقابل بازگشت است.</p>
<div class="modal-buttons">
<button class="modal-button secondary" id="cancel-clear-history-btn">انصراف</button>
<button class="modal-button" id="confirm-clear-history-btn">تایید و حذف</button>
</div>
</div>
</div>
<script>
const MAX_VIDEOS = 20;
const projectDB = {
db: null,
init() {
return new Promise((resolve, reject) => {
const request = indexedDB.open('AIStudioDB_LTX_V3', 2);
request.onupgradeneeded = event => {
const db = event.target.result;
if (!db.objectStoreNames.contains('projects')) {
db.createObjectStore('projects', { keyPath: 'id' });
}
};
request.onsuccess = event => {
this.db = event.target.result;
resolve();
};
request.onerror = event => {
console.error('IndexedDB error:', event.target.errorCode);
reject(event.target.errorCode);
};
});
},
saveProject(project) {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction(['projects'], 'readwrite');
const store = transaction.objectStore('projects');
const request = store.put(project);
request.onsuccess = () => resolve();
request.onerror = e => reject(e.target.error);
});
},
getAllProjects() {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction(['projects'], 'readonly');
const store = transaction.objectStore('projects');
const request = store.getAll();
request.onsuccess = () => resolve(request.result || []);
request.onerror = e => reject(e.target.error);
});
},
deleteProject(id) {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction(['projects'], 'readwrite');
const store = transaction.objectStore('projects');
const request = store.delete(id);
request.onsuccess = () => resolve();
request.onerror = e => reject(e.target.error);
});
},
clearAllProjects() {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction(['projects'], 'readwrite');
const store = transaction.objectStore('projects');
const request = store.clear();
request.onsuccess = () => resolve();
request.onerror = e => reject(e.target.error);
});
}
};
let userSubscriptionStatus = 'free', userFingerprint = null, countdownInterval = null; const PREMIUM_PAGE_ID = '1149636';
async function getBrowserFingerprint() { const components = [navigator.userAgent, navigator.language, screen.width + 'x' + screen.height, new Date().getTimezoneOffset(), "canvas-test"]; const fingerprintString = components.join('---'); let hash = 0; for (let i = 0; i < fingerprintString.length; i++) { hash = ((hash << 5) - hash) + fingerprintString.charCodeAt(i); hash |= 0; } return 'fp_' + Math.abs(hash).toString(16); }
function isUserPaid(userObject) { return userObject?.isLogin && userObject.accessible_pages?.some(p => p == PREMIUM_PAGE_ID); }
function updateUIForSubscriptionStatus(status) {
userSubscriptionStatus = status; const creditInfoDiv = document.getElementById('credit-info-section'); const upgradeBtn = document.getElementById('upgrade-button'); const genBtn = document.getElementById('generateButton'); const subscriptionBadge = document.getElementById('subscription-status-badge');
if (status === 'paid') {
subscriptionBadge.textContent = 'نسخه نامحدود'; subscriptionBadge.className = 'paid-badge'; creditInfoDiv.style.display = 'none'; upgradeBtn.style.display = 'none'; genBtn.disabled = false; if(countdownInterval) clearInterval(countdownInterval);
} else {
subscriptionBadge.textContent = 'نسخه رایگان'; subscriptionBadge.className = 'free-badge'; checkFreeUserCredit();
}
subscriptionBadge.style.display = 'inline-block';
}
async function checkFreeUserCredit() {
if (!userFingerprint) return; const creditInfoDiv = document.getElementById('credit-info-section'); const upgradeBtn = document.getElementById('upgrade-button'); const genBtn = document.getElementById('generateButton');
if (userSubscriptionStatus !== 'paid') { creditInfoDiv.style.display = 'block'; }
const lastGen = localStorage.getItem('ltx_last_gen_time');
if (lastGen && userSubscriptionStatus !== 'paid') {
const lastGenTime = parseInt(lastGen, 10);
const now = Date.now();
const oneDayMs = 24 * 60 * 60 * 1000;
const timeLeft = lastGenTime + oneDayMs - now;
if (timeLeft > 0) {
genBtn.disabled = true;
upgradeBtn.style.display = 'block';
startCountdown(Math.floor((lastGenTime + oneDayMs) / 1000));
return;
}
}
genBtn.disabled = false;
upgradeBtn.style.display = 'none';
if(countdownInterval) clearInterval(countdownInterval);
creditInfoDiv.textContent = `شما ۱ اعتبار ساخت ویدیوی رایگان روزانه دارید.`;
}
function startCountdown(resetTimestamp) {
if (countdownInterval) clearInterval(countdownInterval); const creditInfoDiv = document.getElementById('credit-info-section');
const updateTimer = () => {
const timeLeft = Math.max(0, resetTimestamp - (Date.now() / 1000));
if (timeLeft === 0) { clearInterval(countdownInterval); creditInfoDiv.textContent = 'در حال بروزرسانی اعتبار...'; setTimeout(checkFreeUserCredit, 2000); return; }
const d = Math.floor(timeLeft / 86400); const h = Math.floor((timeLeft % 86400) / 3600); const m = Math.floor((timeLeft % 3600) / 60);
creditInfoDiv.textContent = `اعتبار شما تمام شده. زمان باقی‌مانده: ${d} روز و ${h} ساعت و ${m} دقیقه`;
};
updateTimer(); countdownInterval = setInterval(updateTimer, 60000);
}
window.addEventListener('message', (event) => {
if (event.data?.type === 'USER_DATA_RESPONSE') {
if (event.data.error || !event.data.payload) { updateUIForSubscriptionStatus('free'); return; }
try { const userObject = JSON.parse(event.data.payload); updateUIForSubscriptionStatus(isUserPaid(userObject) ? 'paid' : 'free'); }
catch (e) { updateUIForSubscriptionStatus('free'); }
}
});
document.getElementById('upgrade-button').addEventListener('click', () => { parent.postMessage({ type: 'NAVIGATE_TO_PREMIUM' }, '*'); });
// DOM Elements
const mainControls = document.getElementById('main-controls');
const processingContainer = document.getElementById('processing-container');
const historySection = document.getElementById('tab-history-content');
const historyGrid = document.getElementById('history-grid');
const imageFileInput = document.getElementById('imageFile');
const imagePreview = document.getElementById('imagePreview');
const imageDropZone = document.getElementById('image-drop-zone');
const removeImageBtn = document.getElementById('remove-image-btn');
const promptInput = document.getElementById('prompt');
const generateButton = document.getElementById('generateButton');
const generateButtonText = generateButton.querySelector('span');
const criticalErrorSection = document.getElementById('criticalErrorSection');
const statusSection = document.getElementById('statusSection');
const statusMessagesDiv = document.getElementById('statusMessages');
const aiLoader = document.getElementById('aiLoader');
const loaderTextOverlay = document.querySelector('#aiLoader .text-overlay');
const tvProgressBar = document.getElementById('tv-progress-bar');
const durationSlider = document.getElementById('durationSlider');
const durationValueDisplay = document.getElementById('durationValue');
const durationInfoIcon = document.getElementById('duration-info-icon');
const durationModal = document.getElementById('duration-modal');
const closeDurationModalBtn = document.getElementById('close-duration-modal');
const confirmResetModal = document.getElementById('confirm-reset-modal');
const toastConfirmBtn = document.getElementById('toast-confirm-btn');
const toastCancelBtn = document.getElementById('toast-cancel-btn');
const reportVideoModal = document.getElementById('report-video-modal');
const reportFormContent = document.getElementById('report-form-content');
const customReportSection = document.getElementById('custom-report-section');
const reportSuccessMessage = document.getElementById('report-success-message');
const deleteVideoModal = document.getElementById('delete-video-modal');
const clearHistoryModal = document.getElementById('clear-history-modal');
const limitReachedModal = document.getElementById('limit-reached-modal');
const closeLimitModalBtn = document.getElementById('close-limit-modal');
const tabHomeBtn = document.getElementById('tab-home-btn');
const tabHistoryBtn = document.getElementById('tab-history-btn');
const tabHomeContent = document.getElementById('tab-home-content');
const tabHistoryContent = document.getElementById('tab-history-content');
const activeBadge = document.getElementById('active-badge');
const flyOrb = document.getElementById('fly-orb');
const lightboxModal = document.getElementById('image-lightbox-modal');
const lightboxImg = document.getElementById('lightbox-image-element');
const closeLightboxBtn = document.getElementById('close-lightbox-btn');
let videoToDeleteId = null;
let currentImageFile = null;
let currentImageObjectURL = null;
let globalPollInterval = null;
let currentReportingVideoId = null;
let expandedCardId = null;
let pollTimer = null; // اضافه شدن این متغیر برای رفع خطای ریست فرم
// DOM Elements
// تابع کمکی آپلود فایل به سرور داکر (اضافه شده برای رفع خطا)
async function uploadFileToDocker(file, runId, suffix) {
const formData = new FormData();
const ext = 'png'; // برای سازگاری بهتر با ورودی تصویر LTX، فرمت نهایی را png قرار می‌دهیم
formData.append('run_id', `${runId}_${suffix}`);
formData.append('ext', ext);
formData.append('file', file);
const response = await fetch('/api/webhook/upload', { method: 'POST', body: formData });
if (!response.ok) throw new Error("آپلود تصویر با خطا مواجه شد.");
return ext;
}
// رنگ آمیزی لغزنده مدت زمان
function updateSliderStyle(slider) {
const percentage = ((slider.value - slider.min) / (slider.max - slider.min)) * 100;
const colorStops = `var(--accent-primary), var(--accent-secondary)`;
slider.style.background = `linear-gradient(to left, ${colorStops} ${percentage}%, var(--input-bg) ${percentage}%)`;
}
function setGenerateButtonState(text, disabled) {
generateButtonText.textContent = text;
generateButton.disabled = disabled;
}
function setUiLock(locked) {
generateButton.disabled = locked;
imageFileInput.disabled = locked;
}
function switchTab(target) {
if (target === 'home') {
tabHomeBtn.classList.add('active');
tabHistoryBtn.classList.remove('active');
tabHomeContent.style.display = 'block';
tabHistoryContent.style.display = 'none';
} else {
tabHomeBtn.classList.remove('active');
tabHistoryBtn.classList.add('active');
tabHomeContent.style.display = 'none';
tabHistoryContent.style.display = 'block';
renderHistoryGrid();
}
}
tabHomeBtn.addEventListener('click', () => switchTab('home'));
tabHistoryBtn.addEventListener('click', () => switchTab('history'));
function openLightbox(url) {
lightboxImg.src = url;
lightboxModal.classList.add('active');
}
function closeLightbox() {
lightboxModal.classList.remove('active');
lightboxImg.src = '';
}
closeLightboxBtn.addEventListener('click', closeLightbox);
lightboxModal.addEventListener('click', (e) => {
if (e.target === lightboxModal) closeLightbox();
});
async function getActiveProjectsCount() {
const projects = await projectDB.getAllProjects();
return projects.filter(p => p.status === 'started' || p.status === 'processing').length;
}
function updateActiveBadge() {
projectDB.getAllProjects().then(projects => {
const activeCount = projects.filter(p => p.status === 'started' || p.status === 'processing').length;
if (activeCount > 0) {
activeBadge.textContent = activeCount;
activeBadge.style.display = 'flex';
} else {
activeBadge.style.display = 'none';
}
});
}
function triggerFlyToHistoryAnimation() {
const rectBtn = generateButton.getBoundingClientRect();
const rectTarget = tabHistoryBtn.getBoundingClientRect();
const startX = rectBtn.left + rectBtn.width / 2;
const startY = rectBtn.top + rectBtn.height / 2;
const endX = rectTarget.left + rectTarget.width / 2;
const endY = rectTarget.top + rectTarget.height / 2;
flyOrb.style.display = 'flex';
flyOrb.style.top = `${startY}px`;
flyOrb.style.left = `${startX}px`;
flyOrb.style.transform = 'translate(-50%, -50%) scale(1.5)';
flyOrb.style.opacity = '1';
setTimeout(() => {
flyOrb.style.top = `${endY}px`;
flyOrb.style.left = `${endX}px`;
flyOrb.style.transform = 'translate(-50%, -50%) scale(0.1) rotate(-180deg)';
flyOrb.style.opacity = '0.1';
}, 50);
setTimeout(() => {
flyOrb.style.display = 'none';
tabHistoryBtn.classList.add('bounce-nav-tab');
setTimeout(() => tabHistoryBtn.classList.remove('bounce-nav-tab'), 800);
}, 900);
}
function startGlobalStatusPolling() {
if (globalPollInterval) return;
globalPollInterval = setInterval(async () => {
const projects = await projectDB.getAllProjects();
const activeProjects = projects.filter(p => p.status === 'started' || p.status === 'processing');
if (activeProjects.length === 0) {
clearInterval(globalPollInterval);
globalPollInterval = null;
updateActiveBadge();
return;
}
updateActiveBadge();
for (let proj of activeProjects) {
try {
// ۱. بررسی وضعیت جاری بودن دورهای نجات در پس‌زمینه
let isProcessingInBackend = false;
try {
const procRes = await fetch(`/static/images/${proj.id}_processing.txt`);
if (procRes.ok) {
const procText = await procRes.text();
if (procText.trim() === "processing") {
isProcessingInBackend = true;
}
}
} catch (e) {}
const statusRes = await fetch(`/api/status/${proj.id}`);
const statusData = await statusRes.json();
// اولویت همیشه با تکمیل رندر نهایی ویدیو است و با آماده شدن فایل MP4 کار پایش به اتمام می‌رسد
if (statusData.status === 'ready' && statusData.url && (statusData.url.endsWith('.mp4') || statusData.url.endsWith('.webm'))) {
isProcessingInBackend = false;
}
if (isProcessingInBackend) {
// دورهای نجات پس‌زمینه زنده هستند؛ عکس خطای موقت را نادیده بگیر و به پایش ادامه بده
proj.status = 'processing';
let currentProgress = proj.progress || 10;
if (currentProgress < 98) {
currentProgress += (Math.random() * 0.4) + 0.3; // پیشروی نرم صوری
}
proj.progress = Math.min(Math.round(currentProgress), 98);
await projectDB.saveProject(proj);
const progressElement = document.getElementById(`prog-text-${proj.id}`);
const progressBarElement = document.getElementById(`prog-bar-${proj.id}`);
if (progressElement) progressElement.textContent = `در حال پردازش (مرحله دوم): ${proj.progress}%`;
if (progressBarElement) progressBarElement.style.width = `${proj.progress}%`;
continue; // رد کردن استعلام وضعیت فعلی و ادامه‌ی انتظار
}
let currentProgress = proj.progress || 10;
if (currentProgress < 98) {
currentProgress += (Math.random() * 1.0) + 1.5;
}
proj.progress = Math.min(Math.round(currentProgress), 98);
if (statusData.status === 'ready') {
// تغییر آنی وضعیت محلی به completed برای جلوگیری از پردازش مجدد تیک‌های پایش همزمان
proj.status = 'completed';
proj.progress = 100;
await projectDB.saveProject(proj);
renderHistoryGrid();
try {
const vidRes = await fetch(statusData.url);
const videoBlob = await vidRes.blob();
proj.videoBlob = videoBlob;
proj.remoteUrl = window.location.origin + statusData.url;
await projectDB.saveProject(proj);
renderHistoryGrid();
} catch (downloadErr) {
console.error("Error downloading video file:", downloadErr);
}
if (userSubscriptionStatus === 'free') {
localStorage.setItem('ltx_last_gen_time', Date.now().toString());
checkFreeUserCredit();
}
} else if (statusData.status === 'failed' || statusData.status === 'error') {
proj.status = 'failed';
proj.progress = 0;
proj.error = statusData.message || "خطا در سرور پردازش موازی";
await projectDB.saveProject(proj);
renderHistoryGrid();
} else {
proj.status = 'processing';
await projectDB.saveProject(proj);
const progressElement = document.getElementById(`prog-text-${proj.id}`);
const progressBarElement = document.getElementById(`prog-bar-${proj.id}`);
if (progressElement) progressElement.textContent = `در حال پردازش: ${proj.progress}%`;
if (progressBarElement) progressBarElement.style.width = `${proj.progress}%`;
}
} catch (pollErr) {
console.error(`Polling check failure for ID ${proj.id}:`, pollErr);
}
}
}, 4000);
}
async function renderHistoryGrid() {
const projects = await projectDB.getAllProjects();
projects.sort((a, b) => b.timestamp - a.timestamp);
historyGrid.innerHTML = '';
if (projects.length === 0) {
historyGrid.innerHTML = `<div style="grid-column: 1/-1; text-align: center; padding: 4rem 1rem; color: var(--text-tertiary);"><i class="fas fa-video-slash" style="font-size: 3rem; margin-bottom: 1rem; opacity: 0.5;"></i><p style="font-weight: 500; font-size: 1.1rem; margin: 0;">هنوز هیچ پروژه ویدیویی ثبت نشده است</p></div>`;
return;
}
const totalCount = projects.length;
projects.forEach((proj, index) => {
const videoNumber = totalCount - index;
const card = document.createElement('div');
card.className = `video-result-card ${expandedCardId === proj.id ? 'expanded' : ''}`;
card.id = proj.id;
const headerLayout = document.createElement('div');
headerLayout.className = 'card-header-layout';
const headerLeft = document.createElement('div');
headerLeft.className = 'card-header-left';
const thumbContainer = document.createElement('div');
thumbContainer.className = 'card-thumbnail-container';
thumbContainer.title = 'کلیک برای بزرگنمایی تصویر ورودی';
let imgUrl = '';
if (proj.imageBlob) {
imgUrl = URL.createObjectURL(proj.imageBlob);
const thumbImg = document.createElement('img');
thumbImg.src = imgUrl;
thumbContainer.appendChild(thumbImg);
} else {
thumbContainer.innerHTML = `<i class="fas fa-image" style="font-size: 1.2rem; color: var(--text-tertiary);"></i>`;
}
const zoomOverlay = document.createElement('div');
zoomOverlay.className = 'card-thumbnail-zoom-overlay';
zoomOverlay.innerHTML = `<i class="fas fa-search-plus"></i>`;
thumbContainer.appendChild(zoomOverlay);
thumbContainer.addEventListener('click', (e) => {
e.stopPropagation();
if (imgUrl) {
openLightbox(imgUrl);
}
});
headerLeft.appendChild(thumbContainer);
const titlesDiv = document.createElement('div');
titlesDiv.className = 'card-header-titles';
const titleText = document.createElement('div');
titleText.className = 'card-header-title-text';
titleText.textContent = `ویدیو شماره ${videoNumber}`;
const metaDiv = document.createElement('div');
metaDiv.className = 'card-header-meta';
const fDate = new Date(proj.timestamp).toLocaleDateString('fa-IR');
const dateSpan = document.createElement('span');
dateSpan.innerHTML = `<i class="far fa-calendar" style="margin-left: 4px;"></i>${fDate}`;
metaDiv.appendChild(dateSpan);
const statusBadge = document.createElement('span');
statusBadge.className = 'card-status-badge';
if (proj.status === 'completed') {
statusBadge.classList.add('status-badge-completed');
statusBadge.textContent = 'تکمیل شده';
} else if (proj.status === 'failed') {
statusBadge.classList.add('status-badge-failed');
statusBadge.textContent = 'خطا';
} else {
statusBadge.classList.add('status-badge-processing');
statusBadge.textContent = 'در حال ساخت';
}
metaDiv.appendChild(statusBadge);
titlesDiv.appendChild(titleText);
titlesDiv.appendChild(metaDiv);
headerLeft.appendChild(titlesDiv);
headerLayout.appendChild(headerLeft);
const headerActions = document.createElement('div');
headerActions.className = 'card-header-actions';
const btnDelete = document.createElement('button');
btnDelete.className = 'card-delete-btn';
btnDelete.title = 'حذف ویدیو';
btnDelete.innerHTML = `<i class="fas fa-trash-alt"></i>`;
btnDelete.addEventListener('click', (e) => {
e.stopPropagation();
videoToDeleteId = proj.id;
deleteVideoModal.classList.add('active');
});
const chevron = document.createElement('i');
chevron.className = 'fas fa-chevron-left chevron-arrow';
headerActions.appendChild(btnDelete);
headerActions.appendChild(chevron);
headerLayout.appendChild(headerActions);
card.appendChild(headerLayout);
const expandedBody = document.createElement('div');
expandedBody.className = 'card-expanded-body';
expandedBody.addEventListener('click', (e) => {
e.stopPropagation();
});
if (proj.status === 'started' || proj.status === 'processing') {
const overlayDiv = document.createElement('div');
overlayDiv.className = 'processing-card-overlay';
if (proj.imageBlob) {
const bgImg = document.createElement('img');
bgImg.className = 'processing-bg-image';
bgImg.src = imgUrl;
overlayDiv.appendChild(bgImg);
}
overlayDiv.innerHTML += `
<div class="processing-card-content">
<div class="processing-spinner"></div>
<span class="processing-status-lbl" id="prog-text-${proj.id}">در حال پردازش: ${proj.progress || 10}%</span>
<div class="processing-progress-bg">
<div class="processing-progress-bar" id="prog-bar-${proj.id}" style="width: ${proj.progress || 10}%"></div>
</div>
</div>
`;
expandedBody.appendChild(overlayDiv);
} else if (proj.status === 'failed') {
const failedOverlay = document.createElement('div');
failedOverlay.className = 'failed-card-overlay';
failedOverlay.innerHTML = `
<div class="failed-card-content">
<i class="fas fa-circle-exclamation"></i>
<p>ساخت ویدیو با شکست مواجه شد.</p>
<span style="font-size: 0.75rem; opacity: 0.8; max-width: 90%; display: inline-block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${proj.error || 'خطای اتصال به سرور'}</span>
</div>
`;
expandedBody.appendChild(failedOverlay);
} else {
const video = document.createElement('video');
video.src = URL.createObjectURL(proj.videoBlob);
video.controls = true;
video.playsInline = true;
video.loop = true;
video.autoplay = false;
video.muted = false;
expandedBody.appendChild(video);
}
const infoContainer = document.createElement('div');
infoContainer.className = 'video-info-container';
const promptLbl = document.createElement('div');
promptLbl.className = 'video-prompt-lbl';
const originalPrompt = proj.prompt || 'تصویر بدون توضیحات متنی';
const isLongPrompt = originalPrompt.length > 60;
if (isLongPrompt) {
promptLbl.innerHTML = `
<strong>دستور ساخت:</strong>
<span class="prompt-text" style="display: inline-block; max-width: 70%; vertical-align: middle; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${originalPrompt}</span>
<button class="toggle-prompt-btn" style="display: inline-block; vertical-align: middle; background: none; border: none; color: var(--accent-primary); cursor: pointer; font-family: var(--app-font); font-size: 0.85rem; font-weight: 700; padding: 0 4px; margin-right: 4px; outline: none;">نمایش بیشتر</button>
`;
const btn = promptLbl.querySelector('.toggle-prompt-btn');
const txt = promptLbl.querySelector('.prompt-text');
btn.addEventListener('click', (e) => {
e.stopPropagation();
if (txt.style.whiteSpace === 'nowrap') {
txt.style.whiteSpace = 'normal';
txt.style.maxWidth = '100%';
btn.textContent = 'نمایش کمتر';
} else {
txt.style.whiteSpace = 'nowrap';
txt.style.maxWidth = '70%';
btn.textContent = 'نمایش بیشتر';
}
});
} else {
promptLbl.innerHTML = `<strong>دستور ساخت:</strong> <span class="prompt-text">${originalPrompt}</span>`;
}
infoContainer.appendChild(promptLbl);
const metaLbl = document.createElement('div');
metaLbl.className = 'video-meta-lbl';
metaLbl.innerHTML = `
<span><i class="far fa-calendar" style="margin-left: 4px;"></i>تاریخ ثبت: ${fDate}</span>
<span><i class="far fa-clock" style="margin-left: 4px;"></i>مدت زمان: ${proj.duration} ثانیه</span>
${proj.status === 'completed' ? `<i class="far fa-flag report-flag-icon" data-video-id="${proj.id}" title="گزارش خطای رندر" onclick="handleReportClick(event)"></i>` : ''}
`;
infoContainer.appendChild(metaLbl);
expandedBody.appendChild(infoContainer);
if (proj.status === 'completed') {
const controlsDiv = document.createElement('div');
controlsDiv.className = 'video-controls';
const btnDownload = document.createElement('button');
btnDownload.className = 'video-button primary';
btnDownload.innerHTML = `<i class="fas fa-download"></i> دانلود ویدیو`;
btnDownload.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
const targetUrl = proj.remoteUrl || URL.createObjectURL(proj.videoBlob);
parent.postMessage({ type: 'DOWNLOAD_REQUEST', url: targetUrl }, '*');
});
controlsDiv.appendChild(btnDownload);
expandedBody.appendChild(controlsDiv);
} else if (proj.status === 'failed') {
const retryControls = document.createElement('div');
retryControls.className = 'video-controls';
const btnRetry = document.createElement('button');
btnRetry.className = 'video-button primary';
btnRetry.style.background = 'linear-gradient(135deg, #e53e3e, #c53030)';
btnRetry.innerHTML = `<i class="fas fa-rotate-right"></i> تلاش مجدد`;
btnRetry.addEventListener('click', async (e) => {
e.stopPropagation();
card.remove();
await projectDB.deleteProject(proj.id);
promptInput.value = proj.prompt;
durationSlider.value = proj.duration;
currentImageFile = proj.imageFile;
const dataTransfer = new DataTransfer();
dataTransfer.items.add(proj.imageFile);
imageFileInput.files = dataTransfer.files;
imageFileInput.dispatchEvent(new Event('change'));
switchTab('home');
});
retryControls.appendChild(btnRetry);
expandedBody.appendChild(retryControls);
}
card.appendChild(expandedBody);
card.addEventListener('click', () => {
const isExpanded = card.classList.contains('expanded');
document.querySelectorAll('.video-result-card video').forEach(vid => {
vid.pause();
});
document.querySelectorAll('.video-result-card').forEach(c => {
c.classList.remove('expanded');
});
document.querySelectorAll('.video-result-card').forEach(c => {
const txt = c.querySelector('.prompt-text');
const btn = c.querySelector('.toggle-prompt-btn');
if (txt && btn) {
txt.style.whiteSpace = 'nowrap';
txt.style.maxWidth = '70%';
btn.textContent = 'نمایش بیشتر';
}
});
if (isExpanded) {
card.classList.remove('expanded');
expandedCardId = null;
} else {
card.classList.add('expanded');
expandedCardId = proj.id;
}
});
historyGrid.appendChild(card);
});
}
async function startGenerationProcess() {
let promptToUse = promptInput.value.trim();
const warningEl = document.getElementById('prompt-warning');
// بررسی خالی بودن متن کادر توضیحات
if (!promptToUse) {
if (warningEl) {
warningEl.style.display = 'block';
}
setUiLock(false);
return;
} else {
if (warningEl) {
warningEl.style.display = 'none';
}
}
const promptToUseLower = promptToUse.toLowerCase();
const forbiddenWords = ["sex", "sexy", "porn", "nude", "erotic", "سکس", "سکسی", "پورن", "شهوانی", "برهنه", "لخت"];
if (forbiddenWords.some(word => promptToUseLower.includes(word))) {
alert("متن ورودی شما حاوی کلمات نامناسب است. لطفاً آن را اصلاح کرده و مجدداً تلاش کنید.");
setUiLock(false);
return;
}
if (!currentImageFile) {
alert('لطفاً یک تصویر برای شروع پروژه انتخاب کنید.');
setUiLock(false);
return;
}
const activeCount = await getActiveProjectsCount();
const maxAllowedActive = (userSubscriptionStatus === 'paid') ? 5 : 2;
if (activeCount >= maxAllowedActive) {
const modalText = limitReachedModal.querySelector('.modal-text');
if (modalText) {
modalText.innerHTML = `شما در حال حاضر دارای ${maxAllowedActive} پروژه فعال در حال رندر و پردازش همزمان هستید.<br><br>لطفاً منتظر بمانید تا فرآیند ساخت یکی از ویدیوها به اتمام برسد و مجدداً تلاش کنید.`;
}
limitReachedModal.classList.add('active');
setUiLock(false);
return;
}
if (userSubscriptionStatus !== 'paid') {
const lastGen = localStorage.getItem('ltx_last_gen_time');
if (lastGen) {
const lastGenTime = parseInt(lastGen, 10);
const now = Date.now();
const oneDayMs = 24 * 60 * 60 * 1000;
if (lastGenTime + oneDayMs > now) {
alert("محدودیت ساخت ویدیوی رایگان شما برای امروز به پایان رسیده است.");
setUiLock(false);
return;
}
}
}
// قفل کردن کلیدها و تغییر آنی متن به در حال آپلود تصویر
setUiLock(true);
setGenerateButtonState("در حال آپلود تصویر...", true);
const newProjectId = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
const r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
try {
// شروع آپلود و سپس تغییر آنی تب به سابقه به محض اتمام انتقال فایل
const uploadExt = await uploadFileToDocker(currentImageFile, newProjectId, 'img');
const newProject = {
id: newProjectId,
prompt: promptToUse,
duration: parseFloat(durationSlider.value),
status: 'started',
progress: 10,
timestamp: Date.now(),
imageFile: currentImageFile,
imageBlob: currentImageFile
};
try {
// تلاش برای ذخیره اولیه به همراه شیء تصویر
await projectDB.saveProject(newProject);
} catch (dbErr) {
console.warn("ذخیره اولیه به دلیل محدودیت مرورگر با خطا مواجه شد. در حال ذخیره متنی...", dbErr);
// حذف تصویر جهت پیشگیری از لغو ذخیره‌سازی در حالت خصوصی مرورگر
delete newProject.imageFile;
delete newProject.imageBlob;
await projectDB.saveProject(newProject);
}
triggerFlyToHistoryAnimation();
updateActiveBadge();
switchTab('history');
// ریست کردن فرم خانه برای سفارش بعدی
promptInput.value = '';
imageFileInput.value = '';
if (currentImageObjectURL) { URL.revokeObjectURL(currentImageObjectURL); }
currentImageObjectURL = null;
imagePreview.src = '';
imageDropZone.classList.remove('has-image');
currentImageFile = null;
durationSlider.value = "4.0"; // بازگشت اسلایدر به ۴ ثانیه
durationValueDisplay.textContent = "4.0 ثانیه"; // بازگشت متن به ۴ ثانیه
updateSliderStyle(durationSlider); // اصلاح رنگ خط اسلایدر
setUiLock(false);
setGenerateButtonState("ساخت ویدیو جادویی", false);
// آغاز مخابره سیگنال گیت‌هاب اکشن در پس‌زمینه
proceedWithBackendGeneration(newProject);
} catch (err) {
alert(`خطا در آپلود فایل: ${err.message}`);
setUiLock(false);
setGenerateButtonState("ساخت ویدیو جادویی", false);
}
}
async function proceedWithBackendGeneration(projectObj) {
try {
const imageUrl = window.location.origin + `/static/images/${projectObj.id}_img.png`;
const ltxPayload = {
image_path: imageUrl,
prompt: projectObj.prompt,
seconds: projectObj.duration < 5.0 ? projectObj.duration + 0.1 : projectObj.duration,
seed: -1
};
const base64Payload = btoa(unescape(encodeURIComponent(JSON.stringify(ltxPayload))));
const finalPrompt = `VOICECONFIG_LTX_${base64Payload}`;
const genRes = await fetch('/api/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
prompt: finalPrompt,
action_name: "ltxvideo"
})
});
const data = await genRes.json();
if (data.status === 'success') {
const actualRunId = data.run_id;
const oldId = projectObj.id; // ذخیره آیدی موقت قبلی
projectObj.id = actualRunId;
projectObj.status = 'processing';
projectObj.progress = 20;
try {
// ۱. ابتدا پروژه را با آیدی اصلی ذخیره می‌کنیم تا از لیست حذف نشود
await projectDB.saveProject(projectObj);
} catch (dbErr) {
console.warn("ذخیره فایل خام در این مرورگر پشتیبانی نمی‌شود. ذخیره بدون فایل انجام می‌شود...", dbErr);
// حذف اشیاء سنگین فایل در صورت ناسازگاری با حافظه مرورگر برای جلوگیری از کرش پایگاه داده
delete projectObj.imageFile;
delete projectObj.imageBlob;
await projectDB.saveProject(projectObj);
}
// ۲. پس از ذخیره موفق با آیدی اصلی، حالا آیدی موقت قبلی را با اطمینان پاک می‌کنیم
await projectDB.deleteProject(oldId);
renderHistoryGrid();
startGlobalStatusPolling();
} else {
throw new Error(data.message || 'خطا در ثبت نوبت صف.');
}
} catch (error) {
projectObj.status = 'failed';
projectObj.error = error.message;
try {
// ایمن‌سازی فرآیند ثبت خطا در دیتابیس برای مرورگرهای دارای محدودیت
await projectDB.saveProject(projectObj);
} catch (dbErr) {
delete projectObj.imageFile;
delete projectObj.imageBlob;
await projectDB.saveProject(projectObj);
}
renderHistoryGrid();
}
}
generateButton.addEventListener('click', async () => {
if (generateButton.disabled) return;
await startGenerationProcess();
});
async function initializeForm() {
if(pollTimer) clearInterval(pollTimer);
// پنهان کردن خطا در زمان ریست فرم
const warningEl = document.getElementById('prompt-warning');
if (warningEl) warningEl.style.display = 'none';
// ریست کردن فرم خانه برای سفارش بعدی
promptInput.value = '';
imageFileInput.value = '';
if (currentImageObjectURL) { URL.revokeObjectURL(currentImageObjectURL); }
currentImageObjectURL = null;
imagePreview.src = '';
imageDropZone.classList.remove('has-image');
currentImageFile = null;
durationSlider.value = "4.0"; // بازگشت اسلایدر به ۴ ثانیه
durationValueDisplay.textContent = "4.0 ثانیه"; // بازگشت متن به ۴ ثانیه
setUiLock(false);
setGenerateButtonState("ساخت ویدیو جادویی", false);
updateSliderStyle(durationSlider); // اصلاح رنگ خط اسلایدر
}
document.getElementById('confirm-delete-btn').addEventListener('click', async () => {
if (videoToDeleteId) {
deleteVideoModal.classList.remove('active');
const card = document.getElementById(videoToDeleteId);
if (card) {
card.classList.add('fade-out-shrink');
setTimeout(() => card.remove(), 500);
}
await projectDB.deleteProject(videoToDeleteId);
videoToDeleteId = null;
updateActiveBadge();
setTimeout(() => {
projectDB.getAllProjects().then(projs => {
if (projs.length === 0) renderHistoryGrid();
});
}, 600);
}
});
document.getElementById('cancel-delete-btn').addEventListener('click', () => { deleteVideoModal.classList.remove('active'); videoToDeleteId = null; });
document.getElementById('clear-history-btn').addEventListener('click', () => {
clearHistoryModal.classList.add('active');
});
document.getElementById('cancel-clear-history-btn').addEventListener('click', () => clearHistoryModal.classList.remove('active'));
closeLimitModalBtn.addEventListener('click', () => limitReachedModal.classList.remove('active'));
document.getElementById('confirm-clear-history-btn').addEventListener('click', async () => {
clearHistoryModal.classList.remove('active');
await projectDB.clearAllProjects();
renderHistoryGrid();
updateActiveBadge();
});
toastConfirmBtn.addEventListener('click', async () => {
confirmResetModal.classList.remove('active');
await initializeForm();
});
toastCancelBtn.addEventListener('click', () => confirmResetModal.classList.remove('active'));
removeImageBtn.addEventListener('click', async (e) => {
e.preventDefault();
e.stopPropagation();
await initializeForm();
});
imageFileInput.addEventListener('change', async function(event) {
const file = event.target.files[0];
if (!file) return;
generateButton.disabled = true;
if (!file.type.startsWith('image/')) {
alert('فرمت فایل نامعتبر است. لطفاً یک تصویر انتخاب کنید.');
currentImageFile = null; imageFileInput.value = ''; generateButton.disabled = false;
return;
}
currentImageFile = file;
if (currentImageObjectURL) URL.revokeObjectURL(currentImageObjectURL);
currentImageObjectURL = URL.createObjectURL(file);
imagePreview.onload = () => {
imageDropZone.classList.add('has-image');
setUiLock(false);
};
imagePreview.src = currentImageObjectURL;
});
document.addEventListener('DOMContentLoaded', async () => {
updateSliderStyle(durationSlider);
userFingerprint = await getBrowserFingerprint();
parent.postMessage({ type: 'REQUEST_USER_DATA' }, '*');
try {
await projectDB.init();
await renderHistoryGrid();
updateActiveBadge();
startGlobalStatusPolling();
}
catch (error) {
console.error("Failed to init state database:", error);
}
});
durationSlider.addEventListener('input', () => {
const value = parseFloat(durationSlider.value);
durationValueDisplay.textContent = `${value.toFixed(1)} ثانیه`;
updateSliderStyle(durationSlider);
});
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(e => imageDropZone.addEventListener(e, ev => ev.preventDefault()));
['dragenter', 'dragover'].forEach(e => imageDropZone.addEventListener(e, () => { if (!imageDropZone.classList.contains('has-image')) imageDropZone.classList.add('drag-over'); }));
['dragleave', 'drop'].forEach(e => imageDropZone.addEventListener(e, () => imageDropZone.classList.remove('drag-over')));
imageDropZone.addEventListener('drop', e => { if (e.dataTransfer.files.length > 0) { imageFileInput.files = e.dataTransfer.files; imageFileInput.dispatchEvent(new Event('change')); } });
durationInfoIcon.addEventListener('click', () => durationModal.classList.add('active'));
closeDurationModalBtn.addEventListener('click', () => durationModal.classList.remove('active'));
function handleReportClick(event) {
currentReportingVideoId = event.currentTarget.dataset.videoId;
reportFormContent.style.display = 'block';
reportSuccessMessage.style.display = 'none';
customReportSection.style.display = 'none';
reportVideoModal.querySelector('#custom-report-text').value = '';
reportVideoModal.classList.add('active');
}
reportVideoModal.querySelectorAll('.report-option-btn').forEach(btn => {
btn.addEventListener('click', () => {
if (btn.id === 'report-other-btn') { customReportSection.style.display = 'block'; return; }
reportFormContent.style.display = 'none';
reportSuccessMessage.style.display = 'block';
setTimeout(() => reportVideoModal.classList.remove('active'), 2000);
});
});
reportVideoModal.querySelector('#send-report-btn').addEventListener('click', () => {
const customText = reportVideoModal.querySelector('#custom-report-text').value.trim();
if (customText && currentReportingVideoId) {
reportFormContent.style.display = 'none';
reportSuccessMessage.style.display = 'block';
setTimeout(() => reportVideoModal.classList.remove('active'), 2000);
}
});
reportVideoModal.addEventListener('click', (e) => { if(e.target === reportVideoModal) reportVideoModal.classList.remove('active'); });
reportVideoModal.querySelectorAll('.emoji').forEach(emoji => { emoji.addEventListener('click', () => { reportVideoModal.querySelector('#custom-report-text').value += emoji.textContent; }); });
document.addEventListener('DOMContentLoaded', () => {
const canvas = document.getElementById('neural-network-canvas');
if (!canvas) return; const header = canvas.parentElement; const ctx = canvas.getContext('2d');
let particles = []; const particleCount = 20; const maxDistance = 100;
const computedStyles = getComputedStyle(document.documentElement);
const particleColor = computedStyles.getPropertyValue('--accent-primary').trim() || '#4A6CFA';
const lineColor = computedStyles.getPropertyValue('--text-tertiary').trim() || '#8A94A6';
function resizeCanvas() { canvas.width = header.clientWidth; canvas.height = header.clientHeight; init(); }
class Particle { constructor() { this.x = Math.random() * canvas.width; this.y = Math.random() * canvas.height; this.vx = (Math.random() - 0.5) * 0.3; this.vy = (Math.random() - 0.5) * 0.3; this.radius = 1.2; } update() { this.x += this.vx; this.y += this.vy; if (this.x < 0 || this.x > canvas.width) this.vx *= -1; if (this.y < 0 || this.y > canvas.height) this.vy *= -1; } draw() { ctx.beginPath(); ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2); ctx.fillStyle = particleColor; ctx.fill(); } }
function init() { particles = []; for (let i = 0; i < particleCount; i++) { particles.push(new Particle()); } }
const connectParticles = function() { for (let i = 0; i < particles.length; i++) { for (let j = i + 1; j < particles.length; j++) { const dx = particles[i].x - particles[j].x; const dy = particles[i].y - particles[j].y; const distance = Math.sqrt(dx * dx + dy * dy); if (distance < maxDistance) { ctx.beginPath(); ctx.moveTo(particles[i].x, particles[i].y); ctx.lineTo(particles[j].x, particles[j].y); ctx.strokeStyle = lineColor; ctx.lineWidth = 0.2; ctx.globalAlpha = 1 - distance / maxDistance; ctx.stroke(); } } } ctx.globalAlpha = 1; }
function animate() { ctx.clearRect(0, 0, canvas.width, canvas.height); particles.forEach(particle => { particle.update(); particle.draw(); }); connectParticles(); requestAnimationFrame(animate); }
window.addEventListener('resize', resizeCanvas); resizeCanvas(); animate();
});
</script>
</body>
</html>