Opera8 commited on
Commit
0fc2724
·
verified ·
1 Parent(s): 900803d

Update templates/ltxvideo.html

Browse files
Files changed (1) hide show
  1. 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">&times;</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">با این کار تمام نتایج فعلی حذف شده و پروژه از ابتدا شروع می‌شود.</p>
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
- const infoContainer = document.createElement('div');
1370
- infoContainer.className = 'video-info-container';
1371
-
1372
- const promptLbl = document.createElement('div');
1373
- promptLbl.className = 'video-prompt-lbl';
1374
- const originalPrompt = proj.prompt || 'تصویر بدون توضیحات متنی';
1375
- const isLongPrompt = originalPrompt.length > 60;
1376
-
1377
- if (isLongPrompt) {
1378
- promptLbl.innerHTML = `
1379
- <strong>دستور ساخت:</strong>
1380
- <span class="prompt-text" style="display: inline-block; max-width: 70%; vertical-align: middle; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${originalPrompt}</span>
1381
- <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>
1382
- `;
1383
- const btn = promptLbl.querySelector('.toggle-prompt-btn');
1384
- const txt = promptLbl.querySelector('.prompt-text');
1385
- btn.addEventListener('click', (e) => {
1386
- e.stopPropagation();
1387
- if (txt.style.whiteSpace === 'nowrap') {
1388
- txt.style.whiteSpace = 'normal';
1389
- txt.style.maxWidth = '100%';
1390
- btn.textContent = 'نمایش کمتر';
1391
- } else {
1392
- txt.style.whiteSpace = 'nowrap';
1393
- txt.style.maxWidth = '70%';
1394
- btn.textContent = 'نمایش بیشتر';
1395
- }
1396
- });
1397
  } else {
1398
- promptLbl.innerHTML = `<strong>دستور ساخت:</strong> <span class="prompt-text">${originalPrompt}</span>`;
 
 
1399
  }
1400
- infoContainer.appendChild(promptLbl);
 
 
 
 
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
- try {
1546
- // شروع آپلود و سپس تغییر آنی تب به سابقه به محض اتمام انتقال فایل
1547
- const uploadExt = await uploadFileToDocker(currentImageFile, newProjectId, 'img');
1548
-
1549
- const newProject = {
1550
- id: newProjectId,
1551
- prompt: promptToUse,
1552
- duration: parseFloat(durationSlider.value),
1553
- status: 'started',
1554
- progress: 10,
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
- promptInput.value = '';
1568
- imageFileInput.value = '';
1569
- if (currentImageObjectURL) { URL.revokeObjectURL(currentImageObjectURL); }
1570
- currentImageObjectURL = null;
1571
- imagePreview.src = '';
1572
- imageDropZone.classList.remove('has-image');
1573
- currentImageFile = null;
1574
- setUiLock(false);
1575
- setGenerateButtonState("ساخت ویدیو جادویی", false);
1576
 
1577
- // آغاز مخابره سیگنال گیت‌هاب اکشن در پس‌زمینه
1578
- proceedWithBackendGeneration(newProject);
 
1579
 
1580
- } catch (err) {
1581
- alert(`خطا در آپلود فایل: ${err.message}`);
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
- renderHistoryGrid();
 
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
- await startGenerationProcess();
 
 
 
 
 
 
 
 
 
 
 
 
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
- 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; }
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">&times;</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
  });