Update templates/ltxvideo.html
Browse files- templates/ltxvideo.html +170 -97
templates/ltxvideo.html
CHANGED
|
@@ -5,6 +5,7 @@
|
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
<title>AI Video Studio</title>
|
| 7 |
<link href="https://fonts.googleapis.com/css2?family=Vazirmatn:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
|
|
|
| 8 |
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
|
| 9 |
<style>
|
| 10 |
:root {
|
|
@@ -214,6 +215,7 @@
|
|
| 214 |
border-color: var(--danger-color);
|
| 215 |
}
|
| 216 |
|
|
|
|
| 217 |
.history-grid { display: flex; flex-direction: column; gap: 0.75rem; width: 100%; }
|
| 218 |
|
| 219 |
.video-result-card {
|
|
@@ -234,6 +236,7 @@
|
|
| 234 |
box-shadow: var(--shadow-md);
|
| 235 |
}
|
| 236 |
|
|
|
|
| 237 |
.card-header-layout {
|
| 238 |
display: flex;
|
| 239 |
align-items: center;
|
|
@@ -249,6 +252,7 @@
|
|
| 249 |
flex: 1;
|
| 250 |
}
|
| 251 |
|
|
|
|
| 252 |
.card-thumbnail-container {
|
| 253 |
width: 48px;
|
| 254 |
height: 48px;
|
|
@@ -310,6 +314,7 @@
|
|
| 310 |
flex-wrap: wrap;
|
| 311 |
}
|
| 312 |
|
|
|
|
| 313 |
.card-status-badge {
|
| 314 |
font-size: 0.7rem;
|
| 315 |
font-weight: 700;
|
|
@@ -331,13 +336,14 @@
|
|
| 331 |
color: var(--text-tertiary);
|
| 332 |
font-size: 0.85rem;
|
| 333 |
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
| 334 |
-
order: 1;
|
| 335 |
}
|
| 336 |
.video-result-card.expanded .chevron-arrow {
|
| 337 |
transform: rotate(-90deg);
|
| 338 |
color: var(--accent-primary);
|
| 339 |
}
|
| 340 |
|
|
|
|
| 341 |
.card-delete-btn {
|
| 342 |
width: 32px;
|
| 343 |
height: 32px;
|
|
@@ -350,7 +356,7 @@
|
|
| 350 |
justify-content: center;
|
| 351 |
cursor: pointer;
|
| 352 |
transition: var(--transition-smooth);
|
| 353 |
-
order: 2;
|
| 354 |
}
|
| 355 |
.card-delete-btn:hover {
|
| 356 |
background-color: rgba(229, 62, 62, 0.08);
|
|
@@ -358,6 +364,7 @@
|
|
| 358 |
border-color: rgba(229, 62, 62, 0.15);
|
| 359 |
}
|
| 360 |
|
|
|
|
| 361 |
.card-expanded-body {
|
| 362 |
max-height: 0;
|
| 363 |
overflow: hidden;
|
|
@@ -365,13 +372,14 @@
|
|
| 365 |
transition: max-height 0.4s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s ease;
|
| 366 |
}
|
| 367 |
.video-result-card.expanded .card-expanded-body {
|
| 368 |
-
max-height: 1500px;
|
| 369 |
opacity: 1;
|
| 370 |
margin-top: 1.25rem;
|
| 371 |
padding-top: 1.25rem;
|
| 372 |
border-top: 1px dashed var(--panel-border);
|
| 373 |
}
|
| 374 |
|
|
|
|
| 375 |
.video-result-card video {
|
| 376 |
width: 100%;
|
| 377 |
max-width: 100%;
|
|
@@ -396,6 +404,7 @@
|
|
| 396 |
.video-button:not(.primary):hover:not(:disabled) { background-color: var(--panel-border); color: var(--text-primary); }
|
| 397 |
.video-button:disabled { cursor: not-allowed; opacity: 0.6; }
|
| 398 |
|
|
|
|
| 399 |
#image-lightbox-modal {
|
| 400 |
display: none;
|
| 401 |
position: fixed;
|
|
@@ -429,6 +438,7 @@
|
|
| 429 |
.lightbox-close:hover { color: var(--danger-color); transform: scale(1.1); }
|
| 430 |
@keyframes zoomIn { from { transform: scale(0.9); opacity: 0; } to { transform: scale(1); opacity: 1; } }
|
| 431 |
|
|
|
|
| 432 |
.processing-card-overlay {
|
| 433 |
position: relative;
|
| 434 |
width: 100%;
|
|
@@ -487,6 +497,7 @@
|
|
| 487 |
text-shadow: 0 2px 4px rgba(0,0,0,0.8);
|
| 488 |
}
|
| 489 |
|
|
|
|
| 490 |
.failed-card-overlay {
|
| 491 |
position: relative;
|
| 492 |
width: 100%;
|
|
@@ -513,6 +524,7 @@
|
|
| 513 |
.failed-card-content i { font-size: 2rem; }
|
| 514 |
.failed-card-content p { font-size: 0.8rem; margin: 0; line-height: 1.6; }
|
| 515 |
|
|
|
|
| 516 |
.bottom-nav-container {
|
| 517 |
position: fixed;
|
| 518 |
bottom: 24px;
|
|
@@ -583,6 +595,7 @@
|
|
| 583 |
transition: var(--transition-smooth);
|
| 584 |
}
|
| 585 |
|
|
|
|
| 586 |
.nav-badge {
|
| 587 |
position: absolute;
|
| 588 |
top: 6px;
|
|
@@ -602,6 +615,7 @@
|
|
| 602 |
animation: badge-fade-in 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
| 603 |
}
|
| 604 |
|
|
|
|
| 605 |
@keyframes nav-bounce {
|
| 606 |
0%, 100% { transform: scale(1); }
|
| 607 |
50% { transform: scale(1.2) translateY(-6px); }
|
|
@@ -610,6 +624,7 @@
|
|
| 610 |
animation: nav-bounce 0.6s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
| 611 |
}
|
| 612 |
|
|
|
|
| 613 |
.fly-orb {
|
| 614 |
position: fixed;
|
| 615 |
pointer-events: none;
|
|
@@ -650,6 +665,7 @@
|
|
| 650 |
.history-header { flex-direction: column; align-items: flex-start; }
|
| 651 |
.btn-clear-history { width: 100%; justify-content: center; }
|
| 652 |
|
|
|
|
| 653 |
.history-section {
|
| 654 |
margin-left: -1.5rem;
|
| 655 |
margin-right: -1.5rem;
|
|
@@ -678,11 +694,13 @@
|
|
| 678 |
</div>
|
| 679 |
</header>
|
| 680 |
|
|
|
|
| 681 |
<div id="fly-orb" class="fly-orb">
|
| 682 |
<i class="fas fa-wand-magic-sparkles"></i>
|
| 683 |
</div>
|
| 684 |
|
| 685 |
<main>
|
|
|
|
| 686 |
<div id="tab-home-content">
|
| 687 |
<div id="main-controls">
|
| 688 |
<div class="form-group">
|
|
@@ -732,6 +750,7 @@
|
|
| 732 |
</div>
|
| 733 |
</div>
|
| 734 |
|
|
|
|
| 735 |
<div id="tab-history-content" style="display: none;">
|
| 736 |
<div class="history-section">
|
| 737 |
<div class="history-header">
|
|
@@ -745,10 +764,12 @@
|
|
| 745 |
</button>
|
| 746 |
</div>
|
| 747 |
|
|
|
|
| 748 |
<div id="history-grid" class="history-grid"></div>
|
| 749 |
</div>
|
| 750 |
</div>
|
| 751 |
|
|
|
|
| 752 |
<div id="processing-container" class="form-group" style="display: none; margin-top: 2.5rem;">
|
| 753 |
<div id="results-container">
|
| 754 |
<div id="statusSection">
|
|
@@ -771,6 +792,7 @@
|
|
| 771 |
</main>
|
| 772 |
</div>
|
| 773 |
|
|
|
|
| 774 |
<div class="bottom-nav-container">
|
| 775 |
<div class="bottom-nav-inner">
|
| 776 |
<button id="tab-home-btn" class="nav-tab active">
|
|
@@ -780,16 +802,19 @@
|
|
| 780 |
<button id="tab-history-btn" class="nav-tab">
|
| 781 |
<i class="fas fa-clock-rotate-left"></i>
|
| 782 |
<span>تاریخچه</span>
|
|
|
|
| 783 |
<span id="active-badge" class="nav-badge" style="display: none;">0</span>
|
| 784 |
</button>
|
| 785 |
</div>
|
| 786 |
</div>
|
| 787 |
|
|
|
|
| 788 |
<div class="modal-overlay" id="image-lightbox-modal">
|
| 789 |
<span class="lightbox-close" id="close-lightbox-btn">×</span>
|
| 790 |
<img class="lightbox-img" id="lightbox-image-element" src="" alt="Input Image Preview">
|
| 791 |
</div>
|
| 792 |
|
|
|
|
| 793 |
<div class="modal-overlay" id="limit-reached-modal">
|
| 794 |
<div class="modal-content">
|
| 795 |
<div class="modal-icon" style="color: var(--accent-premium); background: rgba(255, 193, 7, 0.12)">
|
|
@@ -843,7 +868,7 @@
|
|
| 843 |
<div class="modal-content">
|
| 844 |
<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>
|
| 845 |
<h3 class="modal-title">آیا مطمئن هستید؟</h3>
|
| 846 |
-
<p class="modal-text">با این کار تمام
|
| 847 |
<div class="modal-buttons">
|
| 848 |
<button class="modal-button secondary" id="toast-cancel-btn">انصراف</button>
|
| 849 |
<button class="modal-button" id="toast-confirm-btn">تایید و حذف</button>
|
|
@@ -878,6 +903,7 @@
|
|
| 878 |
<script>
|
| 879 |
const MAX_VIDEOS = 20;
|
| 880 |
|
|
|
|
| 881 |
const projectDB = {
|
| 882 |
db: null,
|
| 883 |
init() {
|
|
@@ -1003,6 +1029,7 @@
|
|
| 1003 |
const durationValueDisplay = document.getElementById('durationValue');
|
| 1004 |
const durationInfoIcon = document.getElementById('duration-info-icon');
|
| 1005 |
const durationModal = document.getElementById('duration-modal');
|
|
|
|
| 1006 |
const closeDurationModalBtn = document.getElementById('close-duration-modal');
|
| 1007 |
const confirmResetModal = document.getElementById('confirm-reset-modal');
|
| 1008 |
const toastConfirmBtn = document.getElementById('toast-confirm-btn');
|
|
@@ -1024,6 +1051,7 @@
|
|
| 1024 |
const activeBadge = document.getElementById('active-badge');
|
| 1025 |
const flyOrb = document.getElementById('fly-orb');
|
| 1026 |
|
|
|
|
| 1027 |
const lightboxModal = document.getElementById('image-lightbox-modal');
|
| 1028 |
const lightboxImg = document.getElementById('lightbox-image-element');
|
| 1029 |
const closeLightboxBtn = document.getElementById('close-lightbox-btn');
|
|
@@ -1033,8 +1061,9 @@
|
|
| 1033 |
let currentImageObjectURL = null;
|
| 1034 |
let globalPollInterval = null;
|
| 1035 |
let currentReportingVideoId = null;
|
| 1036 |
-
let expandedCardId = null;
|
| 1037 |
|
|
|
|
| 1038 |
function updateSliderStyle(slider) {
|
| 1039 |
const percentage = ((slider.value - slider.min) / (slider.max - slider.min)) * 100;
|
| 1040 |
const colorStops = `var(--accent-primary), var(--accent-secondary)`;
|
|
@@ -1051,6 +1080,7 @@
|
|
| 1051 |
imageFileInput.disabled = locked;
|
| 1052 |
}
|
| 1053 |
|
|
|
|
| 1054 |
function switchTab(target) {
|
| 1055 |
if (target === 'home') {
|
| 1056 |
tabHomeBtn.classList.add('active');
|
|
@@ -1062,13 +1092,14 @@
|
|
| 1062 |
tabHistoryBtn.classList.add('active');
|
| 1063 |
tabHomeContent.style.display = 'none';
|
| 1064 |
tabHistoryContent.style.display = 'block';
|
| 1065 |
-
renderHistoryGrid();
|
| 1066 |
}
|
| 1067 |
}
|
| 1068 |
|
| 1069 |
tabHomeBtn.addEventListener('click', () => switchTab('home'));
|
| 1070 |
tabHistoryBtn.addEventListener('click', () => switchTab('history'));
|
| 1071 |
|
|
|
|
| 1072 |
function openLightbox(url) {
|
| 1073 |
lightboxImg.src = url;
|
| 1074 |
lightboxModal.classList.add('active');
|
|
@@ -1082,6 +1113,9 @@
|
|
| 1082 |
if (e.target === lightboxModal) closeLightbox();
|
| 1083 |
});
|
| 1084 |
|
|
|
|
|
|
|
|
|
|
| 1085 |
async function getActiveProjectsCount() {
|
| 1086 |
const projects = await projectDB.getAllProjects();
|
| 1087 |
return projects.filter(p => p.status === 'started' || p.status === 'processing').length;
|
|
@@ -1099,6 +1133,7 @@
|
|
| 1099 |
});
|
| 1100 |
}
|
| 1101 |
|
|
|
|
| 1102 |
function triggerFlyToHistoryAnimation() {
|
| 1103 |
const rectBtn = generateButton.getBoundingClientRect();
|
| 1104 |
const rectTarget = tabHistoryBtn.getBoundingClientRect();
|
|
@@ -1108,12 +1143,14 @@
|
|
| 1108 |
const endX = rectTarget.left + rectTarget.width / 2;
|
| 1109 |
const endY = rectTarget.top + rectTarget.height / 2;
|
| 1110 |
|
|
|
|
| 1111 |
flyOrb.style.display = 'flex';
|
| 1112 |
flyOrb.style.top = `${startY}px`;
|
| 1113 |
flyOrb.style.left = `${startX}px`;
|
| 1114 |
flyOrb.style.transform = 'translate(-50%, -50%) scale(1.5)';
|
| 1115 |
flyOrb.style.opacity = '1';
|
| 1116 |
|
|
|
|
| 1117 |
setTimeout(() => {
|
| 1118 |
flyOrb.style.top = `${endY}px`;
|
| 1119 |
flyOrb.style.left = `${endX}px`;
|
|
@@ -1121,6 +1158,7 @@
|
|
| 1121 |
flyOrb.style.opacity = '0.1';
|
| 1122 |
}, 50);
|
| 1123 |
|
|
|
|
| 1124 |
setTimeout(() => {
|
| 1125 |
flyOrb.style.display = 'none';
|
| 1126 |
tabHistoryBtn.classList.add('bounce-nav-tab');
|
|
@@ -1128,6 +1166,9 @@
|
|
| 1128 |
}, 900);
|
| 1129 |
}
|
| 1130 |
|
|
|
|
|
|
|
|
|
|
| 1131 |
function startGlobalStatusPolling() {
|
| 1132 |
if (globalPollInterval) return;
|
| 1133 |
|
|
@@ -1149,31 +1190,28 @@
|
|
| 1149 |
const statusRes = await fetch(`/api/status/${proj.id}`);
|
| 1150 |
const statusData = await statusRes.json();
|
| 1151 |
|
|
|
|
| 1152 |
let currentProgress = proj.progress || 10;
|
| 1153 |
if (currentProgress < 98) {
|
|
|
|
| 1154 |
currentProgress += (Math.random() * 1.0) + 1.5;
|
| 1155 |
}
|
| 1156 |
proj.progress = Math.min(Math.round(currentProgress), 98);
|
| 1157 |
|
| 1158 |
if (statusData.status === 'ready') {
|
| 1159 |
-
// تغییر آنی وضعیت محلی به completed برای جلوگیری از پردازش مجدد تیکهای پایش همزمان
|
| 1160 |
proj.status = 'completed';
|
| 1161 |
proj.progress = 100;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1162 |
await projectDB.saveProject(proj);
|
| 1163 |
renderHistoryGrid();
|
| 1164 |
-
|
| 1165 |
-
try {
|
| 1166 |
-
const vidRes = await fetch(statusData.url);
|
| 1167 |
-
const videoBlob = await vidRes.blob();
|
| 1168 |
-
proj.videoBlob = videoBlob;
|
| 1169 |
-
proj.remoteUrl = window.location.origin + statusData.url;
|
| 1170 |
-
|
| 1171 |
-
await projectDB.saveProject(proj);
|
| 1172 |
-
renderHistoryGrid();
|
| 1173 |
-
} catch (downloadErr) {
|
| 1174 |
-
console.error("Error downloading video file:", downloadErr);
|
| 1175 |
-
}
|
| 1176 |
|
|
|
|
| 1177 |
if (userSubscriptionStatus === 'free') {
|
| 1178 |
await fetch('/api/use-credit', {
|
| 1179 |
method: 'POST',
|
|
@@ -1194,6 +1232,7 @@
|
|
| 1194 |
proj.status = 'processing';
|
| 1195 |
await projectDB.saveProject(proj);
|
| 1196 |
|
|
|
|
| 1197 |
const progressElement = document.getElementById(`prog-text-${proj.id}`);
|
| 1198 |
const progressBarElement = document.getElementById(`prog-bar-${proj.id}`);
|
| 1199 |
if (progressElement) progressElement.textContent = `در حال پردازش: ${proj.progress}%`;
|
|
@@ -1206,8 +1245,10 @@
|
|
| 1206 |
}, 4000);
|
| 1207 |
}
|
| 1208 |
|
|
|
|
| 1209 |
async function renderHistoryGrid() {
|
| 1210 |
const projects = await projectDB.getAllProjects();
|
|
|
|
| 1211 |
projects.sort((a, b) => b.timestamp - a.timestamp);
|
| 1212 |
|
| 1213 |
historyGrid.innerHTML = '';
|
|
@@ -1220,18 +1261,21 @@
|
|
| 1220 |
const totalCount = projects.length;
|
| 1221 |
|
| 1222 |
projects.forEach((proj, index) => {
|
|
|
|
| 1223 |
const videoNumber = totalCount - index;
|
| 1224 |
|
| 1225 |
const card = document.createElement('div');
|
| 1226 |
card.className = `video-result-card ${expandedCardId === proj.id ? 'expanded' : ''}`;
|
| 1227 |
card.id = proj.id;
|
| 1228 |
|
|
|
|
| 1229 |
const headerLayout = document.createElement('div');
|
| 1230 |
headerLayout.className = 'card-header-layout';
|
| 1231 |
|
| 1232 |
const headerLeft = document.createElement('div');
|
| 1233 |
headerLeft.className = 'card-header-left';
|
| 1234 |
|
|
|
|
| 1235 |
const thumbContainer = document.createElement('div');
|
| 1236 |
thumbContainer.className = 'card-thumbnail-container';
|
| 1237 |
thumbContainer.title = 'کلیک برای بزرگنمایی تصویر ورودی';
|
|
@@ -1246,11 +1290,13 @@
|
|
| 1246 |
thumbContainer.innerHTML = `<i class="fas fa-image" style="font-size: 1.2rem; color: var(--text-tertiary);"></i>`;
|
| 1247 |
}
|
| 1248 |
|
|
|
|
| 1249 |
const zoomOverlay = document.createElement('div');
|
| 1250 |
zoomOverlay.className = 'card-thumbnail-zoom-overlay';
|
| 1251 |
zoomOverlay.innerHTML = `<i class="fas fa-search-plus"></i>`;
|
| 1252 |
thumbContainer.appendChild(zoomOverlay);
|
| 1253 |
|
|
|
|
| 1254 |
thumbContainer.addEventListener('click', (e) => {
|
| 1255 |
e.stopPropagation();
|
| 1256 |
if (imgUrl) {
|
|
@@ -1259,6 +1305,7 @@
|
|
| 1259 |
});
|
| 1260 |
headerLeft.appendChild(thumbContainer);
|
| 1261 |
|
|
|
|
| 1262 |
const titlesDiv = document.createElement('div');
|
| 1263 |
titlesDiv.className = 'card-header-titles';
|
| 1264 |
|
|
@@ -1274,6 +1321,7 @@
|
|
| 1274 |
dateSpan.innerHTML = `<i class="far fa-calendar" style="margin-left: 4px;"></i>${fDate}`;
|
| 1275 |
metaDiv.appendChild(dateSpan);
|
| 1276 |
|
|
|
|
| 1277 |
const statusBadge = document.createElement('span');
|
| 1278 |
statusBadge.className = 'card-status-badge';
|
| 1279 |
if (proj.status === 'completed') {
|
|
@@ -1293,6 +1341,7 @@
|
|
| 1293 |
headerLeft.appendChild(titlesDiv);
|
| 1294 |
headerLayout.appendChild(headerLeft);
|
| 1295 |
|
|
|
|
| 1296 |
const headerActions = document.createElement('div');
|
| 1297 |
headerActions.className = 'card-header-actions';
|
| 1298 |
|
|
@@ -1301,7 +1350,7 @@
|
|
| 1301 |
btnDelete.title = 'حذف ویدیو';
|
| 1302 |
btnDelete.innerHTML = `<i class="fas fa-trash-alt"></i>`;
|
| 1303 |
btnDelete.addEventListener('click', (e) => {
|
| 1304 |
-
e.stopPropagation();
|
| 1305 |
videoToDeleteId = proj.id;
|
| 1306 |
deleteVideoModal.classList.add('active');
|
| 1307 |
});
|
|
@@ -1314,9 +1363,11 @@
|
|
| 1314 |
headerLayout.appendChild(headerActions);
|
| 1315 |
card.appendChild(headerLayout);
|
| 1316 |
|
|
|
|
| 1317 |
const expandedBody = document.createElement('div');
|
| 1318 |
expandedBody.className = 'card-expanded-body';
|
| 1319 |
|
|
|
|
| 1320 |
expandedBody.addEventListener('click', (e) => {
|
| 1321 |
e.stopPropagation();
|
| 1322 |
});
|
|
@@ -1356,6 +1407,7 @@
|
|
| 1356 |
expandedBody.appendChild(failedOverlay);
|
| 1357 |
|
| 1358 |
} else {
|
|
|
|
| 1359 |
const video = document.createElement('video');
|
| 1360 |
video.src = URL.createObjectURL(proj.videoBlob);
|
| 1361 |
video.controls = true;
|
|
@@ -1366,38 +1418,39 @@
|
|
| 1366 |
expandedBody.appendChild(video);
|
| 1367 |
}
|
| 1368 |
|
| 1369 |
-
|
| 1370 |
-
|
| 1371 |
-
|
| 1372 |
-
|
| 1373 |
-
|
| 1374 |
-
|
| 1375 |
-
|
| 1376 |
-
|
| 1377 |
-
|
| 1378 |
-
|
| 1379 |
-
|
| 1380 |
-
|
| 1381 |
-
|
| 1382 |
-
|
| 1383 |
-
|
| 1384 |
-
|
| 1385 |
-
|
| 1386 |
-
|
| 1387 |
-
|
| 1388 |
-
|
| 1389 |
-
|
| 1390 |
-
|
| 1391 |
-
|
| 1392 |
-
txt.style.whiteSpace = 'nowrap';
|
| 1393 |
-
txt.style.maxWidth = '70%';
|
| 1394 |
-
btn.textContent = 'نمایش بیشتر';
|
| 1395 |
-
}
|
| 1396 |
-
});
|
| 1397 |
} else {
|
| 1398 |
-
|
|
|
|
|
|
|
| 1399 |
}
|
| 1400 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1401 |
|
| 1402 |
const metaLbl = document.createElement('div');
|
| 1403 |
metaLbl.className = 'video-meta-lbl';
|
|
@@ -1409,13 +1462,14 @@
|
|
| 1409 |
infoContainer.appendChild(metaLbl);
|
| 1410 |
expandedBody.appendChild(infoContainer);
|
| 1411 |
|
|
|
|
| 1412 |
if (proj.status === 'completed') {
|
| 1413 |
const controlsDiv = document.createElement('div');
|
| 1414 |
controlsDiv.className = 'video-controls';
|
| 1415 |
|
| 1416 |
const btnDownload = document.createElement('button');
|
| 1417 |
btnDownload.className = 'video-button primary';
|
| 1418 |
-
btnDownload.innerHTML = `<i class="fas fa-download"></i> دانلود ویدیو`;
|
| 1419 |
btnDownload.addEventListener('click', (e) => {
|
| 1420 |
e.preventDefault();
|
| 1421 |
e.stopPropagation();
|
|
@@ -1453,17 +1507,21 @@
|
|
| 1453 |
|
| 1454 |
card.appendChild(expandedBody);
|
| 1455 |
|
|
|
|
| 1456 |
card.addEventListener('click', () => {
|
| 1457 |
const isExpanded = card.classList.contains('expanded');
|
| 1458 |
|
|
|
|
| 1459 |
document.querySelectorAll('.video-result-card video').forEach(vid => {
|
| 1460 |
vid.pause();
|
| 1461 |
});
|
| 1462 |
|
|
|
|
| 1463 |
document.querySelectorAll('.video-result-card').forEach(c => {
|
| 1464 |
c.classList.remove('expanded');
|
| 1465 |
});
|
| 1466 |
|
|
|
|
| 1467 |
document.querySelectorAll('.video-result-card').forEach(c => {
|
| 1468 |
const txt = c.querySelector('.prompt-text');
|
| 1469 |
const btn = c.querySelector('.toggle-prompt-btn');
|
|
@@ -1487,6 +1545,7 @@
|
|
| 1487 |
});
|
| 1488 |
}
|
| 1489 |
|
|
|
|
| 1490 |
async function startGenerationProcess() {
|
| 1491 |
let promptToUse = promptInput.value.trim();
|
| 1492 |
const promptToUseLower = promptToUse.toLowerCase();
|
|
@@ -1504,9 +1563,11 @@
|
|
| 1504 |
return;
|
| 1505 |
}
|
| 1506 |
|
|
|
|
| 1507 |
const activeCount = await getActiveProjectsCount();
|
| 1508 |
const maxAllowedActive = (userSubscriptionStatus === 'paid') ? 5 : 2;
|
| 1509 |
if (activeCount >= maxAllowedActive) {
|
|
|
|
| 1510 |
const modalText = limitReachedModal.querySelector('.modal-text');
|
| 1511 |
if (modalText) {
|
| 1512 |
modalText.innerHTML = `شما در حال حاضر دارای ${maxAllowedActive} پروژه فعال در حال رندر و پردازش همزمان هستید.<br><br>لطفاً منتظر بمانید تا فرآیند ساخت یکی از ویدیوها به اتمام برسد و مجدداً تلاش کنید.`;
|
|
@@ -1516,6 +1577,7 @@
|
|
| 1516 |
return;
|
| 1517 |
}
|
| 1518 |
|
|
|
|
| 1519 |
if (userSubscriptionStatus !== 'paid') {
|
| 1520 |
try {
|
| 1521 |
const response = await fetch('/api/check-credit', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ fingerprint: userFingerprint }) });
|
|
@@ -1533,64 +1595,52 @@
|
|
| 1533 |
}
|
| 1534 |
}
|
| 1535 |
|
| 1536 |
-
//
|
| 1537 |
-
setUiLock(true);
|
| 1538 |
-
setGenerateButtonState("در حال آپلود تصویر...", true);
|
| 1539 |
-
|
| 1540 |
const newProjectId = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
| 1541 |
const r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
|
| 1542 |
return v.toString(16);
|
| 1543 |
});
|
| 1544 |
|
| 1545 |
-
|
| 1546 |
-
|
| 1547 |
-
|
| 1548 |
-
|
| 1549 |
-
|
| 1550 |
-
|
| 1551 |
-
|
| 1552 |
-
|
| 1553 |
-
|
| 1554 |
-
|
| 1555 |
-
timestamp: Date.now(),
|
| 1556 |
-
imageFile: currentImageFile,
|
| 1557 |
-
imageBlob: currentImageFile
|
| 1558 |
-
};
|
| 1559 |
-
|
| 1560 |
-
await projectDB.saveProject(newProject);
|
| 1561 |
-
|
| 1562 |
-
triggerFlyToHistoryAnimation();
|
| 1563 |
-
updateActiveBadge();
|
| 1564 |
-
switchTab('history');
|
| 1565 |
|
| 1566 |
-
|
| 1567 |
-
|
| 1568 |
-
|
| 1569 |
-
|
| 1570 |
-
currentImageObjectURL = null;
|
| 1571 |
-
imagePreview.src = '';
|
| 1572 |
-
imageDropZone.classList.remove('has-image');
|
| 1573 |
-
currentImageFile = null;
|
| 1574 |
-
setUiLock(false);
|
| 1575 |
-
setGenerateButtonState("ساخت ویدیو جادویی", false);
|
| 1576 |
|
| 1577 |
-
|
| 1578 |
-
|
|
|
|
| 1579 |
|
| 1580 |
-
|
| 1581 |
-
|
| 1582 |
-
setUiLock(false);
|
| 1583 |
-
setGenerateButtonState("ساخت ویدیو جادویی", false);
|
| 1584 |
-
}
|
| 1585 |
}
|
| 1586 |
|
| 1587 |
async function proceedWithBackendGeneration(projectObj) {
|
| 1588 |
try {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1589 |
const imageUrl = window.location.origin + `/static/images/${projectObj.id}_img.png`;
|
| 1590 |
|
| 1591 |
const ltxPayload = {
|
| 1592 |
image_path: imageUrl,
|
| 1593 |
prompt: projectObj.prompt,
|
|
|
|
| 1594 |
seconds: projectObj.duration < 5.0 ? projectObj.duration + 0.1 : projectObj.duration,
|
| 1595 |
seed: -1
|
| 1596 |
};
|
|
@@ -1611,17 +1661,22 @@
|
|
| 1611 |
|
| 1612 |
if (data.status === 'success') {
|
| 1613 |
const actualRunId = data.run_id;
|
|
|
|
|
|
|
| 1614 |
await projectDB.deleteProject(projectObj.id);
|
| 1615 |
|
|
|
|
| 1616 |
projectObj.id = actualRunId;
|
| 1617 |
projectObj.status = 'processing';
|
| 1618 |
projectObj.progress = 20;
|
| 1619 |
|
|
|
|
| 1620 |
await projectDB.saveProject(projectObj);
|
| 1621 |
-
|
|
|
|
| 1622 |
startGlobalStatusPolling();
|
| 1623 |
} else {
|
| 1624 |
-
throw new Error(data.message || 'خطا در
|
| 1625 |
}
|
| 1626 |
} catch (error) {
|
| 1627 |
projectObj.status = 'failed';
|
|
@@ -1633,11 +1688,22 @@
|
|
| 1633 |
|
| 1634 |
generateButton.addEventListener('click', async () => {
|
| 1635 |
if (generateButton.disabled) return;
|
| 1636 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1637 |
});
|
| 1638 |
|
| 1639 |
async function initializeForm() {
|
| 1640 |
-
if(pollTimer) clearInterval(pollTimer);
|
| 1641 |
imageFileInput.value = '';
|
| 1642 |
if (currentImageObjectURL) { URL.revokeObjectURL(currentImageObjectURL); }
|
| 1643 |
currentImageObjectURL = null;
|
|
@@ -1650,6 +1716,7 @@
|
|
| 1650 |
updateSliderStyle(durationSlider);
|
| 1651 |
}
|
| 1652 |
|
|
|
|
| 1653 |
document.getElementById('confirm-delete-btn').addEventListener('click', async () => {
|
| 1654 |
if (videoToDeleteId) {
|
| 1655 |
deleteVideoModal.classList.remove('active');
|
|
@@ -1671,6 +1738,7 @@
|
|
| 1671 |
|
| 1672 |
document.getElementById('cancel-delete-btn').addEventListener('click', () => { deleteVideoModal.classList.remove('active'); videoToDeleteId = null; });
|
| 1673 |
|
|
|
|
| 1674 |
document.getElementById('clear-history-btn').addEventListener('click', () => {
|
| 1675 |
clearHistoryModal.classList.add('active');
|
| 1676 |
});
|
|
@@ -1685,6 +1753,7 @@
|
|
| 1685 |
updateActiveBadge();
|
| 1686 |
});
|
| 1687 |
|
|
|
|
| 1688 |
toastConfirmBtn.addEventListener('click', async () => {
|
| 1689 |
confirmResetModal.classList.remove('active');
|
| 1690 |
await initializeForm();
|
|
@@ -1720,6 +1789,7 @@
|
|
| 1720 |
imagePreview.src = currentImageObjectURL;
|
| 1721 |
});
|
| 1722 |
|
|
|
|
| 1723 |
document.addEventListener('DOMContentLoaded', async () => {
|
| 1724 |
updateSliderStyle(durationSlider);
|
| 1725 |
userFingerprint = await getBrowserFingerprint();
|
|
@@ -1727,8 +1797,10 @@
|
|
| 1727 |
|
| 1728 |
try {
|
| 1729 |
await projectDB.init();
|
|
|
|
| 1730 |
await renderHistoryGrid();
|
| 1731 |
updateActiveBadge();
|
|
|
|
| 1732 |
startGlobalStatusPolling();
|
| 1733 |
}
|
| 1734 |
catch (error) {
|
|
@@ -1778,6 +1850,7 @@
|
|
| 1778 |
reportVideoModal.addEventListener('click', (e) => { if(e.target === reportVideoModal) reportVideoModal.classList.remove('active'); });
|
| 1779 |
reportVideoModal.querySelectorAll('.emoji').forEach(emoji => { emoji.addEventListener('click', () => { reportVideoModal.querySelector('#custom-report-text').value += emoji.textContent; }); });
|
| 1780 |
|
|
|
|
| 1781 |
document.addEventListener('DOMContentLoaded', () => {
|
| 1782 |
const canvas = document.getElementById('neural-network-canvas');
|
| 1783 |
if (!canvas) return; const header = canvas.parentElement; const ctx = canvas.getContext('2d');
|
|
@@ -1788,7 +1861,7 @@
|
|
| 1788 |
function resizeCanvas() { canvas.width = header.clientWidth; canvas.height = header.clientHeight; init(); }
|
| 1789 |
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(); } }
|
| 1790 |
function init() { particles = []; for (let i = 0; i < particleCount; i++) { particles.push(new Particle()); } }
|
| 1791 |
-
|
| 1792 |
function animate() { ctx.clearRect(0, 0, canvas.width, canvas.height); particles.forEach(particle => { particle.update(); particle.draw(); }); connectParticles(); requestAnimationFrame(animate); }
|
| 1793 |
window.addEventListener('resize', resizeCanvas); resizeCanvas(); animate();
|
| 1794 |
});
|
|
|
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
<title>AI Video Studio</title>
|
| 7 |
<link href="https://fonts.googleapis.com/css2?family=Vazirmatn:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
| 8 |
+
<!-- آیکونهای جذاب FontAwesome مشابه استودیو صوتی -->
|
| 9 |
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
|
| 10 |
<style>
|
| 11 |
:root {
|
|
|
|
| 215 |
border-color: var(--danger-color);
|
| 216 |
}
|
| 217 |
|
| 218 |
+
/* استایلهای ارتقا یافته کشویی (Accordion) جهت تاریخچه عریض و حرفهای */
|
| 219 |
.history-grid { display: flex; flex-direction: column; gap: 0.75rem; width: 100%; }
|
| 220 |
|
| 221 |
.video-result-card {
|
|
|
|
| 236 |
box-shadow: var(--shadow-md);
|
| 237 |
}
|
| 238 |
|
| 239 |
+
/* هدر جمعوجور و آراسته به سبک استودیو صوتی */
|
| 240 |
.card-header-layout {
|
| 241 |
display: flex;
|
| 242 |
align-items: center;
|
|
|
|
| 252 |
flex: 1;
|
| 253 |
}
|
| 254 |
|
| 255 |
+
/* دایرهای کردن پیشنمایش تصویر و فشردهسازی ابعاد کادر بسته */
|
| 256 |
.card-thumbnail-container {
|
| 257 |
width: 48px;
|
| 258 |
height: 48px;
|
|
|
|
| 314 |
flex-wrap: wrap;
|
| 315 |
}
|
| 316 |
|
| 317 |
+
/* بهینهسازی تگ وضعیت رندر همگام با تاریخچه صوتی */
|
| 318 |
.card-status-badge {
|
| 319 |
font-size: 0.7rem;
|
| 320 |
font-weight: 700;
|
|
|
|
| 336 |
color: var(--text-tertiary);
|
| 337 |
font-size: 0.85rem;
|
| 338 |
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
| 339 |
+
order: 1; /* فلش را در سمت راستترین حالت چیدمان قرار میدهد */
|
| 340 |
}
|
| 341 |
.video-result-card.expanded .chevron-arrow {
|
| 342 |
transform: rotate(-90deg);
|
| 343 |
color: var(--accent-primary);
|
| 344 |
}
|
| 345 |
|
| 346 |
+
/* کاهش ابعاد دکمه حذف به ابعاد استاندارد ۳۲پیکسلی در حالت بسته */
|
| 347 |
.card-delete-btn {
|
| 348 |
width: 32px;
|
| 349 |
height: 32px;
|
|
|
|
| 356 |
justify-content: center;
|
| 357 |
cursor: pointer;
|
| 358 |
transition: var(--transition-smooth);
|
| 359 |
+
order: 2; /* دکمه حذف بعد از فلش قرار میگیرد تا همردیف هدر صوتی شود */
|
| 360 |
}
|
| 361 |
.card-delete-btn:hover {
|
| 362 |
background-color: rgba(229, 62, 62, 0.08);
|
|
|
|
| 364 |
border-color: rgba(229, 62, 62, 0.15);
|
| 365 |
}
|
| 366 |
|
| 367 |
+
/* بدنه تاشو آکاردئون */
|
| 368 |
.card-expanded-body {
|
| 369 |
max-height: 0;
|
| 370 |
overflow: hidden;
|
|
|
|
| 372 |
transition: max-height 0.4s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.3s ease;
|
| 373 |
}
|
| 374 |
.video-result-card.expanded .card-expanded-body {
|
| 375 |
+
max-height: 1500px; /* اجازه بزرگ شدن تا ابعاد واقعی ویدیو */
|
| 376 |
opacity: 1;
|
| 377 |
margin-top: 1.25rem;
|
| 378 |
padding-top: 1.25rem;
|
| 379 |
border-top: 1px dashed var(--panel-border);
|
| 380 |
}
|
| 381 |
|
| 382 |
+
/* نمایش ویدیو در ابعاد بزرگ، کاملاً اورجینال و بدون کوچک شدن */
|
| 383 |
.video-result-card video {
|
| 384 |
width: 100%;
|
| 385 |
max-width: 100%;
|
|
|
|
| 404 |
.video-button:not(.primary):hover:not(:disabled) { background-color: var(--panel-border); color: var(--text-primary); }
|
| 405 |
.video-button:disabled { cursor: not-allowed; opacity: 0.6; }
|
| 406 |
|
| 407 |
+
/* استایل لایتباکس بزرگنمایی تصویر ورودی */
|
| 408 |
#image-lightbox-modal {
|
| 409 |
display: none;
|
| 410 |
position: fixed;
|
|
|
|
| 438 |
.lightbox-close:hover { color: var(--danger-color); transform: scale(1.1); }
|
| 439 |
@keyframes zoomIn { from { transform: scale(0.9); opacity: 0; } to { transform: scale(1); opacity: 1; } }
|
| 440 |
|
| 441 |
+
/* کارت ویدیویی در حال پردازش موازی در تاریخچه */
|
| 442 |
.processing-card-overlay {
|
| 443 |
position: relative;
|
| 444 |
width: 100%;
|
|
|
|
| 497 |
text-shadow: 0 2px 4px rgba(0,0,0,0.8);
|
| 498 |
}
|
| 499 |
|
| 500 |
+
/* کارت شکست خورده در تاریخچه */
|
| 501 |
.failed-card-overlay {
|
| 502 |
position: relative;
|
| 503 |
width: 100%;
|
|
|
|
| 524 |
.failed-card-content i { font-size: 2rem; }
|
| 525 |
.failed-card-content p { font-size: 0.8rem; margin: 0; line-height: 1.6; }
|
| 526 |
|
| 527 |
+
/* نوار ناوبری شناور در پایین صفحه - کاملا مشابه برنامه تغییر صدا */
|
| 528 |
.bottom-nav-container {
|
| 529 |
position: fixed;
|
| 530 |
bottom: 24px;
|
|
|
|
| 595 |
transition: var(--transition-smooth);
|
| 596 |
}
|
| 597 |
|
| 598 |
+
/* نشانگر قرمز تعداد ویدیوهای در حال ساخت روی تب تاریخچه */
|
| 599 |
.nav-badge {
|
| 600 |
position: absolute;
|
| 601 |
top: 6px;
|
|
|
|
| 615 |
animation: badge-fade-in 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
| 616 |
}
|
| 617 |
|
| 618 |
+
/* انیمیشن لرزش ملایم در زمان دریافت پروژه جدید */
|
| 619 |
@keyframes nav-bounce {
|
| 620 |
0%, 100% { transform: scale(1); }
|
| 621 |
50% { transform: scale(1.2) translateY(-6px); }
|
|
|
|
| 624 |
animation: nav-bounce 0.6s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
| 625 |
}
|
| 626 |
|
| 627 |
+
/* المان پرواز درخشان (Fly to History) */
|
| 628 |
.fly-orb {
|
| 629 |
position: fixed;
|
| 630 |
pointer-events: none;
|
|
|
|
| 665 |
.history-header { flex-direction: column; align-items: flex-start; }
|
| 666 |
.btn-clear-history { width: 100%; justify-content: center; }
|
| 667 |
|
| 668 |
+
/* اعمال تغییر فاصله فقط و فقط روی کادرهای تاریخچه بدون تغییر در ساختار اصلی هومپج */
|
| 669 |
.history-section {
|
| 670 |
margin-left: -1.5rem;
|
| 671 |
margin-right: -1.5rem;
|
|
|
|
| 694 |
</div>
|
| 695 |
</header>
|
| 696 |
|
| 697 |
+
<!-- المان شناور پرواز برای انیمیشن ساخت ویدیو -->
|
| 698 |
<div id="fly-orb" class="fly-orb">
|
| 699 |
<i class="fas fa-wand-magic-sparkles"></i>
|
| 700 |
</div>
|
| 701 |
|
| 702 |
<main>
|
| 703 |
+
<!-- بخش خانه (سفارش ویدیو جدید) -->
|
| 704 |
<div id="tab-home-content">
|
| 705 |
<div id="main-controls">
|
| 706 |
<div class="form-group">
|
|
|
|
| 750 |
</div>
|
| 751 |
</div>
|
| 752 |
|
| 753 |
+
<!-- بخش تاریخچه پروژهها (کشویی، حرفهای و مستقل) -->
|
| 754 |
<div id="tab-history-content" style="display: none;">
|
| 755 |
<div class="history-section">
|
| 756 |
<div class="history-header">
|
|
|
|
| 764 |
</button>
|
| 765 |
</div>
|
| 766 |
|
| 767 |
+
<!-- گرید پیشرفته نمایش کارت ویدیوها یا وضعیتهای پردازش فعال -->
|
| 768 |
<div id="history-grid" class="history-grid"></div>
|
| 769 |
</div>
|
| 770 |
</div>
|
| 771 |
|
| 772 |
+
<!-- بخش باکس متحرک رندر (این بخش اکنون برای پروژههای مجزا در کارتهای تاریخچه رندر میشود و برای کاربردهای کلی نیست) -->
|
| 773 |
<div id="processing-container" class="form-group" style="display: none; margin-top: 2.5rem;">
|
| 774 |
<div id="results-container">
|
| 775 |
<div id="statusSection">
|
|
|
|
| 792 |
</main>
|
| 793 |
</div>
|
| 794 |
|
| 795 |
+
<!-- نوار ناوبری شناور در پایین کل صفحه (خانه و تاریخچه) -->
|
| 796 |
<div class="bottom-nav-container">
|
| 797 |
<div class="bottom-nav-inner">
|
| 798 |
<button id="tab-home-btn" class="nav-tab active">
|
|
|
|
| 802 |
<button id="tab-history-btn" class="nav-tab">
|
| 803 |
<i class="fas fa-clock-rotate-left"></i>
|
| 804 |
<span>تاریخچه</span>
|
| 805 |
+
<!-- نشانگر فعال پروژههای در حال پردازش موازی -->
|
| 806 |
<span id="active-badge" class="nav-badge" style="display: none;">0</span>
|
| 807 |
</button>
|
| 808 |
</div>
|
| 809 |
</div>
|
| 810 |
|
| 811 |
+
<!-- مودال بزرگنمایی تصویر ورودی (Lightbox) -->
|
| 812 |
<div class="modal-overlay" id="image-lightbox-modal">
|
| 813 |
<span class="lightbox-close" id="close-lightbox-btn">×</span>
|
| 814 |
<img class="lightbox-img" id="lightbox-image-element" src="" alt="Input Image Preview">
|
| 815 |
</div>
|
| 816 |
|
| 817 |
+
<!-- مودال اختصاصی و زیبای محدودیت ظرفیت ۵ پروژه موازی -->
|
| 818 |
<div class="modal-overlay" id="limit-reached-modal">
|
| 819 |
<div class="modal-content">
|
| 820 |
<div class="modal-icon" style="color: var(--accent-premium); background: rgba(255, 193, 7, 0.12)">
|
|
|
|
| 868 |
<div class="modal-content">
|
| 869 |
<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>
|
| 870 |
<h3 class="modal-title">آیا مطمئن هستید؟</h3>
|
| 871 |
+
<p class="modal-text">با این کار تمام تصویر آپلودی حذف شده و پر��ژه از ابتدا شروع میشود.</p>
|
| 872 |
<div class="modal-buttons">
|
| 873 |
<button class="modal-button secondary" id="toast-cancel-btn">انصراف</button>
|
| 874 |
<button class="modal-button" id="toast-confirm-btn">تایید و حذف</button>
|
|
|
|
| 903 |
<script>
|
| 904 |
const MAX_VIDEOS = 20;
|
| 905 |
|
| 906 |
+
// توسعه سیستم پایگاهداده محلی پیشرفته IndexedDB برای ذخیرهسازی دادهها و تصاویر به تفکیک پروژه
|
| 907 |
const projectDB = {
|
| 908 |
db: null,
|
| 909 |
init() {
|
|
|
|
| 1029 |
const durationValueDisplay = document.getElementById('durationValue');
|
| 1030 |
const durationInfoIcon = document.getElementById('duration-info-icon');
|
| 1031 |
const durationModal = document.getElementById('duration-modal');
|
| 1032 |
+
|
| 1033 |
const closeDurationModalBtn = document.getElementById('close-duration-modal');
|
| 1034 |
const confirmResetModal = document.getElementById('confirm-reset-modal');
|
| 1035 |
const toastConfirmBtn = document.getElementById('toast-confirm-btn');
|
|
|
|
| 1051 |
const activeBadge = document.getElementById('active-badge');
|
| 1052 |
const flyOrb = document.getElementById('fly-orb');
|
| 1053 |
|
| 1054 |
+
// المانهای بزرگنمایی تصویر (Lightbox)
|
| 1055 |
const lightboxModal = document.getElementById('image-lightbox-modal');
|
| 1056 |
const lightboxImg = document.getElementById('lightbox-image-element');
|
| 1057 |
const closeLightboxBtn = document.getElementById('close-lightbox-btn');
|
|
|
|
| 1061 |
let currentImageObjectURL = null;
|
| 1062 |
let globalPollInterval = null;
|
| 1063 |
let currentReportingVideoId = null;
|
| 1064 |
+
let expandedCardId = null; // ذخیره شناسه کادر باز شده جاری
|
| 1065 |
|
| 1066 |
+
// رنگ آمیزی لغزنده مدت زمان
|
| 1067 |
function updateSliderStyle(slider) {
|
| 1068 |
const percentage = ((slider.value - slider.min) / (slider.max - slider.min)) * 100;
|
| 1069 |
const colorStops = `var(--accent-primary), var(--accent-secondary)`;
|
|
|
|
| 1080 |
imageFileInput.disabled = locked;
|
| 1081 |
}
|
| 1082 |
|
| 1083 |
+
// ناوبری بین تب ها
|
| 1084 |
function switchTab(target) {
|
| 1085 |
if (target === 'home') {
|
| 1086 |
tabHomeBtn.classList.add('active');
|
|
|
|
| 1092 |
tabHistoryBtn.classList.add('active');
|
| 1093 |
tabHomeContent.style.display = 'none';
|
| 1094 |
tabHistoryContent.style.display = 'block';
|
| 1095 |
+
renderHistoryGrid(); // رندر بهروز تاریخچه در زمان کلیک
|
| 1096 |
}
|
| 1097 |
}
|
| 1098 |
|
| 1099 |
tabHomeBtn.addEventListener('click', () => switchTab('home'));
|
| 1100 |
tabHistoryBtn.addEventListener('click', () => switchTab('history'));
|
| 1101 |
|
| 1102 |
+
// باز و بسته کردن لایت باکس تصویر
|
| 1103 |
function openLightbox(url) {
|
| 1104 |
lightboxImg.src = url;
|
| 1105 |
lightboxModal.classList.add('active');
|
|
|
|
| 1113 |
if (e.target === lightboxModal) closeLightbox();
|
| 1114 |
});
|
| 1115 |
|
| 1116 |
+
// ==========================================
|
| 1117 |
+
// سیستم پردازش چندوظیفهای موازی (تا ۵ پروژه)
|
| 1118 |
+
// ==========================================
|
| 1119 |
async function getActiveProjectsCount() {
|
| 1120 |
const projects = await projectDB.getAllProjects();
|
| 1121 |
return projects.filter(p => p.status === 'started' || p.status === 'processing').length;
|
|
|
|
| 1133 |
});
|
| 1134 |
}
|
| 1135 |
|
| 1136 |
+
// انیمیشن سهبعدی زیبای پرواز به تاریخچه (Fly to History)
|
| 1137 |
function triggerFlyToHistoryAnimation() {
|
| 1138 |
const rectBtn = generateButton.getBoundingClientRect();
|
| 1139 |
const rectTarget = tabHistoryBtn.getBoundingClientRect();
|
|
|
|
| 1143 |
const endX = rectTarget.left + rectTarget.width / 2;
|
| 1144 |
const endY = rectTarget.top + rectTarget.height / 2;
|
| 1145 |
|
| 1146 |
+
// بازنشانی موقعیت اورب به دکمه ساخت
|
| 1147 |
flyOrb.style.display = 'flex';
|
| 1148 |
flyOrb.style.top = `${startY}px`;
|
| 1149 |
flyOrb.style.left = `${startX}px`;
|
| 1150 |
flyOrb.style.transform = 'translate(-50%, -50%) scale(1.5)';
|
| 1151 |
flyOrb.style.opacity = '1';
|
| 1152 |
|
| 1153 |
+
// آغاز پرواز شتابدار بعد از مکث کوتاه
|
| 1154 |
setTimeout(() => {
|
| 1155 |
flyOrb.style.top = `${endY}px`;
|
| 1156 |
flyOrb.style.left = `${endX}px`;
|
|
|
|
| 1158 |
flyOrb.style.opacity = '0.1';
|
| 1159 |
}, 50);
|
| 1160 |
|
| 1161 |
+
// خاتمه انیمیشن و لرزاندن ملایم آیکون تاریخچه در زمان فرود
|
| 1162 |
setTimeout(() => {
|
| 1163 |
flyOrb.style.display = 'none';
|
| 1164 |
tabHistoryBtn.classList.add('bounce-nav-tab');
|
|
|
|
| 1166 |
}, 900);
|
| 1167 |
}
|
| 1168 |
|
| 1169 |
+
// ==========================================
|
| 1170 |
+
// موتور بررسی وضعیت همزمان (Unified Multi-Poll)
|
| 1171 |
+
// ==========================================
|
| 1172 |
function startGlobalStatusPolling() {
|
| 1173 |
if (globalPollInterval) return;
|
| 1174 |
|
|
|
|
| 1190 |
const statusRes = await fetch(`/api/status/${proj.id}`);
|
| 1191 |
const statusData = await statusRes.json();
|
| 1192 |
|
| 1193 |
+
// افزایش مجازی پیشرفت (طراحی شده برای رسیدن به ۹۸٪ در حدود ۳ دقیقه و توقف در آنجا)
|
| 1194 |
let currentProgress = proj.progress || 10;
|
| 1195 |
if (currentProgress < 98) {
|
| 1196 |
+
// افزایش ملایم حدود ۲ درصد در هر نوبت بررسی (هر ۴ ثانیه) جهت توزیع در بازه ۱۸۰ ثانیهای
|
| 1197 |
currentProgress += (Math.random() * 1.0) + 1.5;
|
| 1198 |
}
|
| 1199 |
proj.progress = Math.min(Math.round(currentProgress), 98);
|
| 1200 |
|
| 1201 |
if (statusData.status === 'ready') {
|
|
|
|
| 1202 |
proj.status = 'completed';
|
| 1203 |
proj.progress = 100;
|
| 1204 |
+
|
| 1205 |
+
// واکشی خودکار فایل ویدیو آماده شده به عنوان باینری
|
| 1206 |
+
const vidRes = await fetch(statusData.url);
|
| 1207 |
+
const videoBlob = await vidRes.blob();
|
| 1208 |
+
proj.videoBlob = videoBlob;
|
| 1209 |
+
proj.remoteUrl = window.location.origin + statusData.url;
|
| 1210 |
+
|
| 1211 |
await projectDB.saveProject(proj);
|
| 1212 |
renderHistoryGrid();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1213 |
|
| 1214 |
+
// کاهش خودکار موجودی اعتبار برای کاربران رایگان به ازای ویدیوی تکمیلی موفق
|
| 1215 |
if (userSubscriptionStatus === 'free') {
|
| 1216 |
await fetch('/api/use-credit', {
|
| 1217 |
method: 'POST',
|
|
|
|
| 1232 |
proj.status = 'processing';
|
| 1233 |
await projectDB.saveProject(proj);
|
| 1234 |
|
| 1235 |
+
// بهروزرسانی زنده درصد روی کارت رندر فعال
|
| 1236 |
const progressElement = document.getElementById(`prog-text-${proj.id}`);
|
| 1237 |
const progressBarElement = document.getElementById(`prog-bar-${proj.id}`);
|
| 1238 |
if (progressElement) progressElement.textContent = `در حال پردازش: ${proj.progress}%`;
|
|
|
|
| 1245 |
}, 4000);
|
| 1246 |
}
|
| 1247 |
|
| 1248 |
+
// رندر و ساخت پویا کارت های تاریخچه (کشویی، کاملا منطبق با استودیو صوتی و دارای لایت باکس بزرگنمایی)
|
| 1249 |
async function renderHistoryGrid() {
|
| 1250 |
const projects = await projectDB.getAllProjects();
|
| 1251 |
+
// مرتب سازی از جدیدترین پروژه به قدیمیترین
|
| 1252 |
projects.sort((a, b) => b.timestamp - a.timestamp);
|
| 1253 |
|
| 1254 |
historyGrid.innerHTML = '';
|
|
|
|
| 1261 |
const totalCount = projects.length;
|
| 1262 |
|
| 1263 |
projects.forEach((proj, index) => {
|
| 1264 |
+
// تعیین شماره ویدیو به ترتیب معکوس (قدیمیترین=ویدیو شماره ۱، جدیدترین=ویدیو شماره N)
|
| 1265 |
const videoNumber = totalCount - index;
|
| 1266 |
|
| 1267 |
const card = document.createElement('div');
|
| 1268 |
card.className = `video-result-card ${expandedCardId === proj.id ? 'expanded' : ''}`;
|
| 1269 |
card.id = proj.id;
|
| 1270 |
|
| 1271 |
+
// ۱. هدر آکاردئون (همیشه قابل مشاهده است)
|
| 1272 |
const headerLayout = document.createElement('div');
|
| 1273 |
headerLayout.className = 'card-header-layout';
|
| 1274 |
|
| 1275 |
const headerLeft = document.createElement('div');
|
| 1276 |
headerLeft.className = 'card-header-left';
|
| 1277 |
|
| 1278 |
+
// کانتینر تصویر ورودی کوچک شده (با قابلیت کلیک برای زوم)
|
| 1279 |
const thumbContainer = document.createElement('div');
|
| 1280 |
thumbContainer.className = 'card-thumbnail-container';
|
| 1281 |
thumbContainer.title = 'کلیک برای بزرگنمایی تصویر ورودی';
|
|
|
|
| 1290 |
thumbContainer.innerHTML = `<i class="fas fa-image" style="font-size: 1.2rem; color: var(--text-tertiary);"></i>`;
|
| 1291 |
}
|
| 1292 |
|
| 1293 |
+
// افکت ذره بین روی تصویر
|
| 1294 |
const zoomOverlay = document.createElement('div');
|
| 1295 |
zoomOverlay.className = 'card-thumbnail-zoom-overlay';
|
| 1296 |
zoomOverlay.innerHTML = `<i class="fas fa-search-plus"></i>`;
|
| 1297 |
thumbContainer.appendChild(zoomOverlay);
|
| 1298 |
|
| 1299 |
+
// ممانعت از باز شدن کادر در زمان کلیک روی تصویر جهت زوم لایتباکس
|
| 1300 |
thumbContainer.addEventListener('click', (e) => {
|
| 1301 |
e.stopPropagation();
|
| 1302 |
if (imgUrl) {
|
|
|
|
| 1305 |
});
|
| 1306 |
headerLeft.appendChild(thumbContainer);
|
| 1307 |
|
| 1308 |
+
// متون و مشخصات اصلی هدر
|
| 1309 |
const titlesDiv = document.createElement('div');
|
| 1310 |
titlesDiv.className = 'card-header-titles';
|
| 1311 |
|
|
|
|
| 1321 |
dateSpan.innerHTML = `<i class="far fa-calendar" style="margin-left: 4px;"></i>${fDate}`;
|
| 1322 |
metaDiv.appendChild(dateSpan);
|
| 1323 |
|
| 1324 |
+
// تگ وضعیت پردازش در هدر کادر
|
| 1325 |
const statusBadge = document.createElement('span');
|
| 1326 |
statusBadge.className = 'card-status-badge';
|
| 1327 |
if (proj.status === 'completed') {
|
|
|
|
| 1341 |
headerLeft.appendChild(titlesDiv);
|
| 1342 |
headerLayout.appendChild(headerLeft);
|
| 1343 |
|
| 1344 |
+
// دکمههای حذفی و آیکون چرخشی چوران
|
| 1345 |
const headerActions = document.createElement('div');
|
| 1346 |
headerActions.className = 'card-header-actions';
|
| 1347 |
|
|
|
|
| 1350 |
btnDelete.title = 'حذف ویدیو';
|
| 1351 |
btnDelete.innerHTML = `<i class="fas fa-trash-alt"></i>`;
|
| 1352 |
btnDelete.addEventListener('click', (e) => {
|
| 1353 |
+
e.stopPropagation(); // ممانعت از باز شدن آکاردئون در زمان حذف
|
| 1354 |
videoToDeleteId = proj.id;
|
| 1355 |
deleteVideoModal.classList.add('active');
|
| 1356 |
});
|
|
|
|
| 1363 |
headerLayout.appendChild(headerActions);
|
| 1364 |
card.appendChild(headerLayout);
|
| 1365 |
|
| 1366 |
+
// ۲. بدنه تاشوی آکاردئون (تنها در صورت کلیک و بسط یافتن فعال میشود)
|
| 1367 |
const expandedBody = document.createElement('div');
|
| 1368 |
expandedBody.className = 'card-expanded-body';
|
| 1369 |
|
| 1370 |
+
// جلوگیری از بسته شدن کارت هنگام کلیک روی هر کجای بدنه داخلی (ویدیو، متون، دکمهها و غیره)
|
| 1371 |
expandedBody.addEventListener('click', (e) => {
|
| 1372 |
e.stopPropagation();
|
| 1373 |
});
|
|
|
|
| 1407 |
expandedBody.appendChild(failedOverlay);
|
| 1408 |
|
| 1409 |
} else {
|
| 1410 |
+
// پخش کننده ویدیو در ابعاد واقعی و بزرگ اورجینال
|
| 1411 |
const video = document.createElement('video');
|
| 1412 |
video.src = URL.createObjectURL(proj.videoBlob);
|
| 1413 |
video.controls = true;
|
|
|
|
| 1418 |
expandedBody.appendChild(video);
|
| 1419 |
}
|
| 1420 |
|
| 1421 |
+
// مشخصات داخلی و متن دستور ساخت ویدیو
|
| 1422 |
+
const infoContainer = document.createElement('div');
|
| 1423 |
+
infoContainer.className = 'video-info-container';
|
| 1424 |
+
|
| 1425 |
+
const promptLbl = document.createElement('div');
|
| 1426 |
+
promptLbl.className = 'video-prompt-lbl';
|
| 1427 |
+
const originalPrompt = proj.prompt || 'تصویر بدون توضیحات متنی';
|
| 1428 |
+
const isLongPrompt = originalPrompt.length > 60; // تعیین حد نصاب طول کاراکتر برای نمایش دکمه
|
| 1429 |
+
|
| 1430 |
+
if (isLongPrompt) {
|
| 1431 |
+
promptLbl.innerHTML = `
|
| 1432 |
+
<strong>دستور ساخت:</strong>
|
| 1433 |
+
<span class="prompt-text" style="display: inline-block; max-width: 70%; vertical-align: middle; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${originalPrompt}</span>
|
| 1434 |
+
<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>
|
| 1435 |
+
`;
|
| 1436 |
+
const btn = promptLbl.querySelector('.toggle-prompt-btn');
|
| 1437 |
+
const txt = promptLbl.querySelector('.prompt-text');
|
| 1438 |
+
btn.addEventListener('click', (e) => {
|
| 1439 |
+
e.stopPropagation(); // جلوگیری از بسته شدن خودکار کادر آکاردئون
|
| 1440 |
+
if (txt.style.whiteSpace === 'nowrap') {
|
| 1441 |
+
txt.style.whiteSpace = 'normal';
|
| 1442 |
+
txt.style.maxWidth = '100%';
|
| 1443 |
+
btn.textContent = 'نمایش کمتر';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1444 |
} else {
|
| 1445 |
+
txt.style.whiteSpace = 'nowrap';
|
| 1446 |
+
txt.style.maxWidth = '70%';
|
| 1447 |
+
btn.textContent = 'نمایش بیشتر';
|
| 1448 |
}
|
| 1449 |
+
});
|
| 1450 |
+
} else {
|
| 1451 |
+
promptLbl.innerHTML = `<strong>دستور ساخت:</strong> <span class="prompt-text">${originalPrompt}</span>`;
|
| 1452 |
+
}
|
| 1453 |
+
infoContainer.appendChild(promptLbl);
|
| 1454 |
|
| 1455 |
const metaLbl = document.createElement('div');
|
| 1456 |
metaLbl.className = 'video-meta-lbl';
|
|
|
|
| 1462 |
infoContainer.appendChild(metaLbl);
|
| 1463 |
expandedBody.appendChild(infoContainer);
|
| 1464 |
|
| 1465 |
+
// بخش دانلود یا رندر کارت تاشو
|
| 1466 |
if (proj.status === 'completed') {
|
| 1467 |
const controlsDiv = document.createElement('div');
|
| 1468 |
controlsDiv.className = 'video-controls';
|
| 1469 |
|
| 1470 |
const btnDownload = document.createElement('button');
|
| 1471 |
btnDownload.className = 'video-button primary';
|
| 1472 |
+
btnDownload.innerHTML = `<i class="fas fa-download"></i> دانلود ویدیو سینمایی`;
|
| 1473 |
btnDownload.addEventListener('click', (e) => {
|
| 1474 |
e.preventDefault();
|
| 1475 |
e.stopPropagation();
|
|
|
|
| 1507 |
|
| 1508 |
card.appendChild(expandedBody);
|
| 1509 |
|
| 1510 |
+
// هندل اکشن باز و بسته شدن کشویی با کلیک روی کارت
|
| 1511 |
card.addEventListener('click', () => {
|
| 1512 |
const isExpanded = card.classList.contains('expanded');
|
| 1513 |
|
| 1514 |
+
// متوقف کردن پخش تمامی ویدیوها در زمان بستن یا تغییر کادرها
|
| 1515 |
document.querySelectorAll('.video-result-card video').forEach(vid => {
|
| 1516 |
vid.pause();
|
| 1517 |
});
|
| 1518 |
|
| 1519 |
+
// بستن تمام کارتهای باز دیگر
|
| 1520 |
document.querySelectorAll('.video-result-card').forEach(c => {
|
| 1521 |
c.classList.remove('expanded');
|
| 1522 |
});
|
| 1523 |
|
| 1524 |
+
// بازنشانی وضعیت نمایش متنها به حالت خلاصه (کوتاه شده)
|
| 1525 |
document.querySelectorAll('.video-result-card').forEach(c => {
|
| 1526 |
const txt = c.querySelector('.prompt-text');
|
| 1527 |
const btn = c.querySelector('.toggle-prompt-btn');
|
|
|
|
| 1545 |
});
|
| 1546 |
}
|
| 1547 |
|
| 1548 |
+
// آغاز مکانیزم ساخت سفارش جدید در فرانتاند
|
| 1549 |
async function startGenerationProcess() {
|
| 1550 |
let promptToUse = promptInput.value.trim();
|
| 1551 |
const promptToUseLower = promptToUse.toLowerCase();
|
|
|
|
| 1563 |
return;
|
| 1564 |
}
|
| 1565 |
|
| 1566 |
+
// ۱. بررسی سقف موازی پردازش فعال بر اساس نوع اشتراک (۵ برای پولی، ۲ برای رایگان)
|
| 1567 |
const activeCount = await getActiveProjectsCount();
|
| 1568 |
const maxAllowedActive = (userSubscriptionStatus === 'paid') ? 5 : 2;
|
| 1569 |
if (activeCount >= maxAllowedActive) {
|
| 1570 |
+
// پویا کردن متن درون مودال متناسب با سقف مجاز کاربر
|
| 1571 |
const modalText = limitReachedModal.querySelector('.modal-text');
|
| 1572 |
if (modalText) {
|
| 1573 |
modalText.innerHTML = `شما در حال حاضر دارای ${maxAllowedActive} پروژه فعال در حال رندر و پردازش همزمان هستید.<br><br>لطفاً منتظر بمانید تا فرآیند ساخت یکی از ویدیوها به اتمام برسد و مجدداً تلاش کنید.`;
|
|
|
|
| 1577 |
return;
|
| 1578 |
}
|
| 1579 |
|
| 1580 |
+
// ۲. بررسی اعتبار حسابهای کاربری رایگان
|
| 1581 |
if (userSubscriptionStatus !== 'paid') {
|
| 1582 |
try {
|
| 1583 |
const response = await fetch('/api/check-credit', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ fingerprint: userFingerprint }) });
|
|
|
|
| 1595 |
}
|
| 1596 |
}
|
| 1597 |
|
| 1598 |
+
// ۳. ایجاد آبجکت پروژه جدید موقت در دیتابیس
|
|
|
|
|
|
|
|
|
|
| 1599 |
const newProjectId = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
| 1600 |
const r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
|
| 1601 |
return v.toString(16);
|
| 1602 |
});
|
| 1603 |
|
| 1604 |
+
const newProject = {
|
| 1605 |
+
id: newProjectId,
|
| 1606 |
+
prompt: promptToUse,
|
| 1607 |
+
duration: parseFloat(durationSlider.value),
|
| 1608 |
+
status: 'started',
|
| 1609 |
+
progress: 10,
|
| 1610 |
+
timestamp: Date.now(),
|
| 1611 |
+
imageFile: currentImageFile,
|
| 1612 |
+
imageBlob: currentImageFile // ذخیره سازی موازی تصویر باینری
|
| 1613 |
+
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1614 |
|
| 1615 |
+
await projectDB.saveProject(newProject);
|
| 1616 |
+
|
| 1617 |
+
// اجرای انیمیشن بینظیر پرواز همزمان با ثبت
|
| 1618 |
+
triggerFlyToHistoryAnimation();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1619 |
|
| 1620 |
+
// شروع بلافاصله بررسی سرور برای پروژه تازه متولد شده
|
| 1621 |
+
updateActiveBadge();
|
| 1622 |
+
switchTab('home');
|
| 1623 |
|
| 1624 |
+
// منتظر ماندن برای اتمام کامل ثبت بکباند جهت جلوگیری از تداخل
|
| 1625 |
+
await proceedWithBackendGeneration(newProject);
|
|
|
|
|
|
|
|
|
|
| 1626 |
}
|
| 1627 |
|
| 1628 |
async function proceedWithBackendGeneration(projectObj) {
|
| 1629 |
try {
|
| 1630 |
+
const formData = new FormData();
|
| 1631 |
+
formData.append('run_id', projectObj.id + '_img');
|
| 1632 |
+
formData.append('ext', 'png');
|
| 1633 |
+
formData.append('file', projectObj.imageFile);
|
| 1634 |
+
|
| 1635 |
+
const uploadRes = await fetch('/api/webhook/upload', { method: 'POST', body: formData });
|
| 1636 |
+
if (!uploadRes.ok) throw new Error("آپلود تصویر با خطا مواجه شد.");
|
| 1637 |
+
|
| 1638 |
const imageUrl = window.location.origin + `/static/images/${projectObj.id}_img.png`;
|
| 1639 |
|
| 1640 |
const ltxPayload = {
|
| 1641 |
image_path: imageUrl,
|
| 1642 |
prompt: projectObj.prompt,
|
| 1643 |
+
// اضافه کردن 0.1 ثانیه فقط برای مدت زمانهای کمتر از 5 ثانیه
|
| 1644 |
seconds: projectObj.duration < 5.0 ? projectObj.duration + 0.1 : projectObj.duration,
|
| 1645 |
seed: -1
|
| 1646 |
};
|
|
|
|
| 1661 |
|
| 1662 |
if (data.status === 'success') {
|
| 1663 |
const actualRunId = data.run_id;
|
| 1664 |
+
|
| 1665 |
+
// حذف شناسه موقت قدیمی از دیتابیس
|
| 1666 |
await projectDB.deleteProject(projectObj.id);
|
| 1667 |
|
| 1668 |
+
// جایگذاری شناسه اصلی و واقعی رندر سرور
|
| 1669 |
projectObj.id = actualRunId;
|
| 1670 |
projectObj.status = 'processing';
|
| 1671 |
projectObj.progress = 20;
|
| 1672 |
|
| 1673 |
+
// ذخیره مجدد با شناسه اصلی رندر برای شروع صحیح فرآیند پولینگ
|
| 1674 |
await projectDB.saveProject(projectObj);
|
| 1675 |
+
|
| 1676 |
+
// فراخوانی مجدد لوپ پولینگ برای پایش دائمی رندرها
|
| 1677 |
startGlobalStatusPolling();
|
| 1678 |
} else {
|
| 1679 |
+
throw new Error(data.message || 'خطا در صف پردازش سرور موازی.');
|
| 1680 |
}
|
| 1681 |
} catch (error) {
|
| 1682 |
projectObj.status = 'failed';
|
|
|
|
| 1688 |
|
| 1689 |
generateButton.addEventListener('click', async () => {
|
| 1690 |
if (generateButton.disabled) return;
|
| 1691 |
+
setUiLock(true);
|
| 1692 |
+
try {
|
| 1693 |
+
// منتظر ماندن واقعی تا ثبت کامل پروژه در سرور و دیتابیس
|
| 1694 |
+
await startGenerationProcess();
|
| 1695 |
+
// ریست فرم خانه پس از ثبت موفقیتآمیز
|
| 1696 |
+
promptInput.value = '';
|
| 1697 |
+
removeImageBtn.click();
|
| 1698 |
+
} catch (err) {
|
| 1699 |
+
console.error("خطا در ثبت پروژه:", err);
|
| 1700 |
+
} finally {
|
| 1701 |
+
// فعالسازی مجدد دکمه فقط و فقط پس از اتمام کامل فرآیند ثبت سرور
|
| 1702 |
+
setUiLock(false);
|
| 1703 |
+
}
|
| 1704 |
});
|
| 1705 |
|
| 1706 |
async function initializeForm() {
|
|
|
|
| 1707 |
imageFileInput.value = '';
|
| 1708 |
if (currentImageObjectURL) { URL.revokeObjectURL(currentImageObjectURL); }
|
| 1709 |
currentImageObjectURL = null;
|
|
|
|
| 1716 |
updateSliderStyle(durationSlider);
|
| 1717 |
}
|
| 1718 |
|
| 1719 |
+
// اعمال اکشن حذف تکی روی مودال
|
| 1720 |
document.getElementById('confirm-delete-btn').addEventListener('click', async () => {
|
| 1721 |
if (videoToDeleteId) {
|
| 1722 |
deleteVideoModal.classList.remove('active');
|
|
|
|
| 1738 |
|
| 1739 |
document.getElementById('cancel-delete-btn').addEventListener('click', () => { deleteVideoModal.classList.remove('active'); videoToDeleteId = null; });
|
| 1740 |
|
| 1741 |
+
// فعالسازی و نمایش مودال تایید در زمان کلیک روی دکمه حذف کل تاریخچه
|
| 1742 |
document.getElementById('clear-history-btn').addEventListener('click', () => {
|
| 1743 |
clearHistoryModal.classList.add('active');
|
| 1744 |
});
|
|
|
|
| 1753 |
updateActiveBadge();
|
| 1754 |
});
|
| 1755 |
|
| 1756 |
+
// لغو ریست یا ریست کلی پروژه جاری تصویر
|
| 1757 |
toastConfirmBtn.addEventListener('click', async () => {
|
| 1758 |
confirmResetModal.classList.remove('active');
|
| 1759 |
await initializeForm();
|
|
|
|
| 1789 |
imagePreview.src = currentImageObjectURL;
|
| 1790 |
});
|
| 1791 |
|
| 1792 |
+
// ران تایم اولیه بالا آمدن داکیومنت
|
| 1793 |
document.addEventListener('DOMContentLoaded', async () => {
|
| 1794 |
updateSliderStyle(durationSlider);
|
| 1795 |
userFingerprint = await getBrowserFingerprint();
|
|
|
|
| 1797 |
|
| 1798 |
try {
|
| 1799 |
await projectDB.init();
|
| 1800 |
+
// رندر آغازین پروژهها از دیتابیس لوکال آفلاین
|
| 1801 |
await renderHistoryGrid();
|
| 1802 |
updateActiveBadge();
|
| 1803 |
+
// فعال کردن وضعیت پولینگ خودکار در صورت وجود رندرهای نیمه کاره
|
| 1804 |
startGlobalStatusPolling();
|
| 1805 |
}
|
| 1806 |
catch (error) {
|
|
|
|
| 1850 |
reportVideoModal.addEventListener('click', (e) => { if(e.target === reportVideoModal) reportVideoModal.classList.remove('active'); });
|
| 1851 |
reportVideoModal.querySelectorAll('.emoji').forEach(emoji => { emoji.addEventListener('click', () => { reportVideoModal.querySelector('#custom-report-text').value += emoji.textContent; }); });
|
| 1852 |
|
| 1853 |
+
// پیاده سازی انیمیشن پس زمینه هدر
|
| 1854 |
document.addEventListener('DOMContentLoaded', () => {
|
| 1855 |
const canvas = document.getElementById('neural-network-canvas');
|
| 1856 |
if (!canvas) return; const header = canvas.parentElement; const ctx = canvas.getContext('2d');
|
|
|
|
| 1861 |
function resizeCanvas() { canvas.width = header.clientWidth; canvas.height = header.clientHeight; init(); }
|
| 1862 |
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(); } }
|
| 1863 |
function init() { particles = []; for (let i = 0; i < particleCount; i++) { particles.push(new Particle()); } }
|
| 1864 |
+
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; }
|
| 1865 |
function animate() { ctx.clearRect(0, 0, canvas.width, canvas.height); particles.forEach(particle => { particle.update(); particle.draw(); }); connectParticles(); requestAnimationFrame(animate); }
|
| 1866 |
window.addEventListener('resize', resizeCanvas); resizeCanvas(); animate();
|
| 1867 |
});
|