QuranReels / UI.html
MEMO999's picture
Upload 11 files
4ade857 verified
Raw
History Blame Contribute Delete
120 kB
<!DOCTYPE html>
<html lang="ar" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>صانع الريلز القرآني</title>
<meta name="theme-color" content="#0f0f0f">
<meta property="og:title" content="صانع الريلز القرآني 🎬">
<meta property="og:description" content="أنشئ فيديوهات قرآنية احترافية في ثوانٍ">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Amiri:wght@400;700&family=IBM+Plex+Sans+Arabic:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<style>
/* ═══════════════════════════════════════
🎨 CSS Variables & Base
═══════════════════════════════════════ */
:root {
/* Dark Theme (Default) */
--bg-primary: #0f0f0f;
--bg-secondary: #1a1a1a;
--bg-card: #1f1f1f;
--bg-input: #2a2a2a;
--text-primary: #ffffff;
--text-secondary: #a0a0a0;
--text-muted: #666666;
--accent: #c9a227;
--accent-light: #e6c458;
--accent-dark: #9a7b1c;
--success: #22c55e;
--danger: #ef4444;
--warning: #f59e0b;
--border: rgba(255,255,255,0.08);
--shadow: 0 4px 24px rgba(0,0,0,0.4);
--radius: 16px;
--radius-sm: 10px;
}
.light-theme {
--bg-primary: #f8f8f8;
--bg-secondary: #ffffff;
--bg-card: #ffffff;
--bg-input: #f0f0f0;
--text-primary: #1a1a1a;
--text-secondary: #555555;
--text-muted: #999999;
--accent: #b8941f;
--accent-light: #d4b030;
--accent-dark: #8a6d15;
--border: rgba(0,0,0,0.08);
--shadow: 0 4px 24px rgba(0,0,0,0.1);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
-webkit-tap-highlight-color: transparent;
}
body {
font-family: 'IBM Plex Sans Arabic', sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
min-height: 100dvh;
transition: background 0.3s, color 0.3s;
}
/* ═══════════════════════════════════════
📱 App Container
═══════════════════════════════════════ */
.app {
max-width: 480px;
margin: 0 auto;
min-height: 100dvh;
display: flex;
flex-direction: column;
}
/* ═══════════════════════════════════════
🔝 Header
═══════════════════════════════════════ */
.header {
padding: 20px 20px 16px;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid var(--border);
}
.logo {
display: flex;
align-items: center;
gap: 12px;
}
.logo-icon {
width: 44px;
height: 44px;
background: linear-gradient(135deg, var(--accent), var(--accent-light));
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 22px;
}
.logo-text h1 {
font-family: 'Amiri', serif;
font-size: 1.4rem;
background: linear-gradient(135deg, var(--accent-light), var(--accent));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.logo-text span {
font-size: 0.7rem;
color: var(--text-muted);
}
.header-actions {
display: flex;
gap: 8px;
}
.icon-btn {
width: 42px;
height: 42px;
border-radius: var(--radius-sm);
border: 1px solid var(--border);
background: var(--bg-card);
color: var(--text-secondary);
font-size: 1.1rem;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
}
.icon-btn:hover {
border-color: var(--accent);
color: var(--accent);
}
/* ═══════════════════════════════════════
📑 Tabs Navigation
═══════════════════════════════════════ */
.tabs-nav {
display: flex;
padding: 0 20px;
gap: 4px;
border-bottom: 1px solid var(--border);
background: var(--bg-secondary);
}
.tab-btn {
flex: 1;
padding: 14px 16px;
background: none;
border: none;
color: var(--text-muted);
font-family: inherit;
font-size: 0.9rem;
font-weight: 500;
cursor: pointer;
position: relative;
transition: color 0.2s;
}
.tab-btn.active {
color: var(--accent);
}
.tab-btn::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 0;
height: 2px;
background: var(--accent);
border-radius: 2px;
transition: width 0.2s;
}
.tab-btn.active::after {
width: 60%;
}
/* ═══════════════════════════════════════
📄 Tab Content
═══════════════════════════════════════ */
.tab-content {
flex: 1;
overflow-y: auto;
padding: 20px;
}
.tab-panel {
display: none;
animation: fadeIn 0.3s ease;
}
.tab-panel.active {
display: block;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
/* ═══════════════════════════════════════
📝 Form Elements
═══════════════════════════════════════ */
.form-group {
margin-bottom: 18px;
}
.form-label {
display: block;
font-size: 0.85rem;
font-weight: 500;
color: var(--text-secondary);
margin-bottom: 8px;
}
.form-select, .form-input {
width: 100%;
height: 52px;
padding: 0 16px;
background: var(--bg-input);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
color: var(--text-primary);
font-family: inherit;
font-size: 1rem;
outline: none;
transition: border-color 0.2s, box-shadow 0.2s;
}
.form-select:focus, .form-input:focus {
border-color: var(--accent);
box-shadow: 0 0 0 3px rgba(201, 162, 39, 0.15);
}
/* ═══════════════════════════════════════
🔢 Ayah Selection
═══════════════════════════════════════ */
.ayah-selector {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
padding: 16px;
margin-bottom: 18px;
}
.ayah-selector-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
.ayah-selector-title {
font-size: 0.85rem;
font-weight: 500;
color: var(--text-secondary);
}
.ayah-selector-range {
display: flex;
align-items: center;
gap: 8px;
}
.ayah-num-display {
display: flex;
align-items: center;
gap: 6px;
}
.ayah-num-display input {
width: 60px;
height: 44px;
background: var(--bg-input);
border: 1px solid var(--border);
border-radius: 8px;
color: var(--text-primary);
font-family: inherit;
font-size: 1.1rem;
font-weight: 700;
text-align: center;
outline: none;
transition: border-color 0.2s;
}
.ayah-num-display input:focus {
border-color: var(--accent);
}
.ayah-num-display button {
width: 36px;
height: 36px;
background: var(--bg-input);
border: 1px solid var(--border);
border-radius: 8px;
color: var(--accent);
font-size: 1.2rem;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
}
.ayah-num-display button:hover {
background: var(--accent);
color: #000;
}
.ayah-separator {
color: var(--text-muted);
font-size: 0.9rem;
padding: 0 4px;
}
.ayah-label {
font-size: 0.75rem;
color: var(--text-muted);
text-align: center;
margin-top: 4px;
}
.ayah-row {
display: flex;
gap: 12px;
}
.ayah-row .form-group {
flex: 1;
}
/* ═══════════════════════════════════════
🔘 Buttons
═══════════════════════════════════════ */
.btn-primary {
width: 100%;
height: 56px;
background: linear-gradient(135deg, var(--accent), var(--accent-light));
border: none;
border-radius: var(--radius-sm);
color: #000;
font-family: inherit;
font-size: 1.05rem;
font-weight: 700;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
transition: transform 0.2s, box-shadow 0.2s;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(201, 162, 39, 0.3);
}
.btn-primary:active {
transform: translateY(0);
}
.btn-secondary {
flex: 1;
height: 48px;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
color: var(--text-primary);
font-family: inherit;
font-size: 0.95rem;
font-weight: 500;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
transition: all 0.2s;
}
.btn-secondary:hover {
border-color: var(--accent);
color: var(--accent);
}
.btn-row {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.btn-stop {
background: var(--danger);
border: none;
color: #fff;
}
.btn-stop:hover {
background: #dc2626;
color: #fff;
}
.random-btn {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 8px;
color: var(--accent);
font-size: 0.75rem;
padding: 4px 10px;
cursor: pointer;
transition: all 0.2s;
}
.random-btn:hover {
background: var(--accent);
color: #000;
border-color: var(--accent);
}
/* ═══════════════════════════════════════
🎚️ Toggle Switches
═══════════════════════════════════════ */
.toggle-group {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
overflow: hidden;
}
.toggle-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 16px;
border-bottom: 1px solid var(--border);
}
.toggle-item:last-child {
border-bottom: none;
}
.toggle-info h4 {
font-size: 0.95rem;
font-weight: 500;
margin-bottom: 2px;
}
.toggle-info span {
font-size: 0.75rem;
color: var(--text-muted);
}
.switch {
position: relative;
width: 48px;
height: 26px;
}
.switch input {
opacity: 0;
width: 0;
height: 0;
}
.switch-slider {
position: absolute;
inset: 0;
background: var(--bg-input);
border-radius: 26px;
cursor: pointer;
transition: background 0.3s;
}
.switch-slider::before {
content: '';
position: absolute;
width: 20px;
height: 20px;
left: 3px;
bottom: 3px;
background: #fff;
border-radius: 50%;
transition: transform 0.3s;
}
.switch input:checked + .switch-slider {
background: var(--accent);
}
.switch input:checked + .switch-slider::before {
transform: translateX(22px);
}
/* ═══════════════════════════════════════
📊 Progress Section
═══════════════════════════════════════ */
.progress-section {
display: none;
text-align: center;
padding: 30px 20px;
}
.progress-section.active {
display: block;
}
.progress-ring {
width: 140px;
height: 140px;
margin: 0 auto 20px;
position: relative;
}
.progress-ring svg {
transform: rotate(-90deg);
}
.progress-ring circle {
fill: none;
stroke-width: 8;
stroke-linecap: round;
}
.progress-ring .bg {
stroke: var(--bg-input);
}
.progress-ring .fill {
stroke: var(--accent);
stroke-dasharray: 408;
stroke-dashoffset: 408;
transition: stroke-dashoffset 0.5s ease;
}
.progress-percent {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
font-size: 2rem;
font-weight: 700;
color: var(--text-primary);
}
.progress-status {
font-size: 0.95rem;
color: var(--text-secondary);
margin-bottom: 8px;
}
.progress-eta {
font-size: 0.85rem;
color: var(--accent);
font-family: monospace;
}
/* ═══════════════════════════════════════
✅ Result Section
═══════════════════════════════════════ */
.result-section {
display: none;
text-align: center;
}
.result-section.active {
display: block;
}
.result-video {
width: 100%;
border-radius: var(--radius);
background: #000;
margin-bottom: 16px;
}
.result-actions {
display: flex;
gap: 10px;
}
.result-actions .btn-secondary {
flex: 1;
}
.btn-download {
background: var(--success);
border: none;
color: #fff;
}
.btn-download:hover {
background: #16a34a;
color: #fff;
}
/* ═══════════════════════════════════════
📜 History Section
═══════════════════════════════════════ */
.history-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
}
.history-header h3 {
font-size: 1rem;
font-weight: 600;
}
.history-empty {
text-align: center;
padding: 40px 20px;
color: var(--text-muted);
}
.history-empty-icon {
font-size: 3rem;
margin-bottom: 12px;
opacity: 0.5;
}
.history-item {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
padding: 14px;
margin-bottom: 10px;
}
.history-item-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 8px;
}
.history-item-title {
font-weight: 600;
font-size: 0.95rem;
}
.history-item-status {
font-size: 0.85rem;
}
.history-item-meta {
font-size: 0.8rem;
color: var(--text-muted);
margin-bottom: 10px;
}
.history-item-actions {
display: flex;
gap: 8px;
}
.history-item-actions button,
.history-item-actions a {
flex: 1;
height: 38px;
background: var(--bg-input);
border: 1px solid var(--border);
border-radius: 8px;
color: var(--text-primary);
font-family: inherit;
font-size: 0.85rem;
font-weight: 500;
cursor: pointer;
text-decoration: none;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
transition: all 0.2s;
}
.history-item-actions button:hover,
.history-item-actions a:hover {
border-color: var(--accent);
color: var(--accent);
}
.history-item-actions .btn-delete {
color: var(--danger);
border-color: rgba(239, 68, 68, 0.3);
}
.history-item-actions .btn-delete:hover {
background: rgba(239, 68, 68, 0.1);
border-color: var(--danger);
}
.history-timer {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 3px 8px;
background: rgba(245, 158, 11, 0.1);
border-radius: 6px;
font-size: 0.75rem;
color: var(--warning);
margin-right: 8px;
}
.history-timer.urgent {
background: rgba(239, 68, 68, 0.1);
color: var(--danger);
}
/* ═══════════════════════════════════════
⚙️ Settings Panel
═══════════════════════════════════════ */
.settings-section {
margin-bottom: 24px;
}
.settings-section h4 {
font-size: 0.85rem;
font-weight: 600;
color: var(--accent);
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 1px solid var(--border);
}
.settings-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 0;
border-bottom: 1px solid var(--border);
}
.settings-row:last-child {
border-bottom: none;
}
.settings-label {
font-size: 0.9rem;
color: var(--text-primary);
}
.settings-controls {
display: flex;
align-items: center;
gap: 10px;
}
.color-picker {
width: 36px;
height: 36px;
border: 2px solid var(--border);
border-radius: 8px;
cursor: pointer;
overflow: hidden;
}
.color-picker::-webkit-color-swatch-wrapper {
padding: 0;
}
.color-picker::-webkit-color-swatch {
border: none;
border-radius: 6px;
}
.size-slider {
width: 80px;
height: 4px;
accent-color: var(--accent);
}
/* ═══════════════════════════════════════
🔔 Toast Notifications
═══════════════════════════════════════ */
.toast-container {
position: fixed;
bottom: 100px;
left: 50%;
transform: translateX(-50%);
z-index: 1000;
display: flex;
flex-direction: column;
gap: 8px;
pointer-events: none;
}
.toast {
padding: 12px 20px;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
box-shadow: var(--shadow);
font-size: 0.9rem;
animation: toastIn 0.3s ease, toastOut 0.3s ease 2.7s forwards;
pointer-events: auto;
}
.toast.success {
border-color: var(--success);
color: var(--success);
}
.toast.error {
border-color: var(--danger);
color: var(--danger);
}
@keyframes toastIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes toastOut {
from { opacity: 1; }
to { opacity: 0; }
}
/* ═══════════════════════════════════════
📱 Preview Box (Settings)
═══════════════════════════════════════ */
.preview-box {
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
border-radius: var(--radius);
padding: 30px 20px;
text-align: center;
margin-bottom: 20px;
position: relative;
overflow: hidden;
}
.preview-box::before {
content: '';
position: absolute;
inset: 0;
background: radial-gradient(circle at 50% 50%, transparent 30%, rgba(0,0,0,0.6) 100%);
pointer-events: none;
}
.preview-ar {
font-family: 'Amiri', serif;
font-size: 1.5rem;
margin-bottom: 10px;
position: relative;
z-index: 1;
}
.preview-en {
font-size: 0.85rem;
color: #ccc;
position: relative;
z-index: 1;
}
/* ═══════════════════════════════════════
🔧 Advanced Options
═══════════════════════════════════════ */
.advanced-toggle {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 12px;
background: var(--bg-card);
border: 1px dashed var(--border);
border-radius: var(--radius-sm);
color: var(--text-muted);
font-size: 0.85rem;
cursor: pointer;
margin-bottom: 16px;
transition: all 0.2s;
}
.advanced-toggle:hover {
border-color: var(--accent);
color: var(--accent);
}
.advanced-options {
display: none;
}
.advanced-options.show {
display: block;
}
/* ═══════════════════════════════════════
📱 Responsive
═══════════════════════════════════════ */
@media (max-width: 400px) {
.header {
padding: 16px 16px 12px;
}
.tab-content {
padding: 16px;
}
.logo-text h1 {
font-size: 1.2rem;
}
}
</style>
</head>
<body>
<div class="app">
<!-- ═══════════════════════════════════
🔝 Header
═════════════════════════════════════ -->
<header class="header">
<div class="logo">
<div class="logo-icon">🎬</div>
<div class="logo-text">
<h1>صانع الريلز القرآني</h1>
<span>نسخة التوربو</span>
</div>
</div>
<div class="header-actions">
<button class="icon-btn" onclick="toggleTheme()" id="themeBtn">🌙</button>
</div>
</header>
<!-- ═══════════════════════════════════
📑 Tabs Navigation
═════════════════════════════════════ -->
<nav class="tabs-nav">
<button class="tab-btn active" data-tab="main">🏠 الرئيسية</button>
<button class="tab-btn" data-tab="batch">📦 دفعة متعددة</button>
<button class="tab-btn" data-tab="history">📜 السجل</button>
<button class="tab-btn" data-tab="settings">⚙️ إعدادات</button>
</nav>
<!-- ═══════════════════════════════════
📄 Tab Content
═════════════════════════════════════ -->
<main class="tab-content">
<!-- 🏠 Main Tab -->
<div class="tab-panel active" id="main-panel">
<form id="videoForm">
<!-- القارئ -->
<div class="form-group">
<label class="form-label">🎤 القارئ</label>
<select class="form-select" id="reciterSelect"></select>
</div>
<!-- السورة -->
<div class="form-group">
<label class="form-label">📖 السورة</label>
<select class="form-select" id="surahSelect"></select>
</div>
<!-- الآيات -->
<div class="ayah-selector">
<div class="ayah-selector-header">
<span class="ayah-selector-title">📍 نطاق الآيات</span>
<span id="maxAyahHint" style="font-size: 0.75rem; color: var(--text-muted);"></span>
</div>
<div class="ayah-selector-range">
<div class="ayah-num-display">
<button type="button" onclick="adjustAyah('startAyah', -1)"></button>
<input type="number" id="startAyah" value="1" min="1" max="286">
<button type="button" onclick="adjustAyah('startAyah', 1)">+</button>
</div>
<span class="ayah-separator"></span>
<div class="ayah-num-display">
<button type="button" onclick="adjustAyah('endAyah', -1)"></button>
<input type="number" id="endAyah" value="5" min="1" max="286">
<button type="button" onclick="adjustAyah('endAyah', 1)">+</button>
</div>
</div>
<div style="display: flex; justify-content: space-between; margin-top: 6px;">
<span class="ayah-label">من آية</span>
<span class="ayah-label">إلى آية</span>
</div>
<!-- عرض الوقت التقريبي -->
<div id="estimatedTimeDisplay" style="text-align: center; margin-top: 10px; padding: 8px; background: var(--bg-input); border-radius: 8px; font-size: 0.85rem;">
⏱️ الوقت التقريبي: <span id="estimatedTime" style="color: var(--accent); font-weight: 600;">--:--</span>
</div>
</div>
<!-- خيارات سريعة -->
<div class="toggle-group" style="margin-bottom: 18px;">
<div class="toggle-item">
<div class="toggle-info">
<h4>تغيير الخلفية مع الآيات</h4>
<span>كل آية بخلفية مختلفة</span>
</div>
<label class="switch">
<input type="checkbox" id="dynamicBg" checked>
<span class="switch-slider"></span>
</label>
</div>
<div class="toggle-item">
<div class="toggle-info">
<h4>توهج النص</h4>
<span>تأثير هالة حول النص</span>
</div>
<label class="switch">
<input type="checkbox" id="useGlow" checked>
<span class="switch-slider"></span>
</label>
</div>
<div class="toggle-item">
<div class="toggle-info">
<h4>تعتيم سينمائي</h4>
<span>Vignette effect</span>
</div>
<label class="switch">
<input type="checkbox" id="useVignette" checked>
<span class="switch-slider"></span>
</label>
</div>
</div>
<!-- 🎛️ تأثيرات صوتية -->
<div class="form-group" style="margin-top: 18px;">
<label class="form-label">🎵 تأثير الصوت (Reverb)</label>
<select class="form-select" id="reverbLevel">
<option value="none">🔊 بدون تأثير (نظيف)</option>
<option value="light">🌬️ صداء خفيف</option>
<option value="medium">🎙️ صداء متوسط</option>
<option value="mosque">🕌 تأثير مسجد</option>
</select>
</div>
<!-- خيارات متقدمة -->
<div class="advanced-toggle" onclick="toggleAdvanced()">
<span></span>
<span>خيارات متقدمة</span>
</div>
<div class="advanced-options" id="advancedOptions">
<div class="form-group">
<label class="form-label">سلاسة الفيديو (FPS)</label>
<select class="form-select" id="fpsSelect">
<option value="15">🚀 15 FPS - سريع جداً</option>
<option value="20" selected>⚖️ 20 FPS - متوازن</option>
<option value="24">🎬 24 FPS - سينمائي</option>
<option value="30">💎 30 FPS - الأكثر سلاسة</option>
</select>
</div>
<div class="form-group">
<label class="form-label">جودة الفيديو</label>
<select class="form-select" id="qualitySelect">
<option value="720">⚡ 720p HD - الأسرع</option>
<option value="1080">🌟 1080p FHD</option>
</select>
</div>
<div class="toggle-item" style="border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 12px 16px;">
<div class="toggle-info">
<h4>بحث خلفية مخصص</h4>
</div>
<label class="switch">
<input type="checkbox" id="useSearch" onchange="toggleSearchInput()">
<span class="switch-slider"></span>
</label>
</div>
<div class="form-group" id="searchInputGroup" style="display: none;">
<input type="text" class="form-input" id="bgQuery" placeholder="اكتب: بحر، سماء، جبال...">
</div>
</div>
<!-- أزرار التحكم -->
<div class="btn-row">
<button type="button" class="btn-secondary" onclick="randomize()">🎲 عشوائي</button>
</div>
<button type="submit" class="btn-primary" id="submitBtn">✨ إنشاء الفيديو</button>
<button type="button" class="btn-primary btn-stop" id="stopBtn" style="display: none;">🛑 إيقاف</button>
</form>
<!-- Progress Section -->
<div class="progress-section" id="progressSection">
<div class="progress-ring">
<svg width="140" height="140">
<circle class="bg" cx="70" cy="70" r="65"></circle>
<circle class="fill" id="progressCircle" cx="70" cy="70" r="65"></circle>
</svg>
<div class="progress-percent" id="progressPercent">0%</div>
</div>
<div class="progress-status" id="progressStatus">جاري التحضير...</div>
<div class="progress-eta" id="progressEta" style="display: none;">
⏳ متبقي: <span id="etaTime">--:--</span>
</div>
</div>
<!-- Result Section -->
<div class="result-section" id="resultSection">
<video class="result-video" id="videoPlayer" controls playsinline></video>
<div class="result-actions">
<a class="btn-secondary btn-download" id="downloadLink" download>⬇️ تحميل</a>
<button class="btn-secondary" onclick="resetForm()">🔄 فيديو جديد</button>
</div>
</div>
</div>
<!-- 📦 Batch Tab -->
<div class="tab-panel" id="batch-panel">
<div class="batch-header">
<h3>📦 إنشاء دفعة فيديوهات</h3>
<p style="color: var(--text-muted); font-size: 0.85rem; margin-top: 4px;">
أضف فيديوهات للقائمة ثم ابدأ الإنشاء
</p>
</div>
<!-- 🎤 القارئ -->
<div class="form-group">
<label class="form-label">🎤 القارئ</label>
<select class="form-select" id="batchReciterSelect"></select>
</div>
<!-- 📖 السورة -->
<div class="form-group">
<label class="form-label">📖 السورة</label>
<select class="form-select" id="batchSurahSelect"></select>
</div>
<!-- 📍 الآيات -->
<div class="ayah-selector">
<div class="ayah-selector-header">
<span class="ayah-selector-title">📍 نطاق الآيات</span>
</div>
<div class="ayah-selector-range">
<div class="ayah-num-display">
<button type="button" onclick="adjustBatchAyah('batchStartAyah', -1)"></button>
<input type="number" id="batchStartAyah" value="1" min="1">
<button type="button" onclick="adjustBatchAyah('batchStartAyah', 1)">+</button>
</div>
<span class="ayah-separator"></span>
<div class="ayah-num-display">
<button type="button" onclick="adjustBatchAyah('batchEndAyah', -1)"></button>
<input type="number" id="batchEndAyah" value="5" min="1">
<button type="button" onclick="adjustBatchAyah('batchEndAyah', 1)">+</button>
</div>
</div>
<div style="display: flex; justify-content: space-between; margin-top: 6px;">
<span class="ayah-label">من آية</span>
<span class="ayah-label">إلى آية</span>
</div>
<!-- عرض الوقت التقريبي للدفعة -->
<div id="batchEstimatedTimeDisplay" style="text-align: center; margin-top: 10px; padding: 8px; background: var(--bg-input); border-radius: 8px; font-size: 0.85rem;">
⏱️ الوقت التقريبي: <span id="batchEstimatedTime" style="color: var(--accent); font-weight: 600;">--:--</span>
</div>
</div>
<!-- ✨ Effects -->
<div class="toggle-group" style="margin-bottom: 18px;">
<div class="toggle-item">
<div class="toggle-info">
<h4>تغيير الخلفية</h4>
<span>كل آية بخلفية مختلفة</span>
</div>
<label class="switch">
<input type="checkbox" id="batchDynamicBg" checked>
<span class="switch-slider"></span>
</label>
</div>
<div class="toggle-item">
<div class="toggle-info">
<h4>توهج النص</h4>
</div>
<label class="switch">
<input type="checkbox" id="batchUseGlow" checked>
<span class="switch-slider"></span>
</label>
</div>
<div class="toggle-item">
<div class="toggle-info">
<h4>تعتيم سينمائي</h4>
</div>
<label class="switch">
<input type="checkbox" id="batchUseVignette" checked>
<span class="switch-slider"></span>
</label>
</div>
</div>
<!-- أزرار التحكم -->
<div class="btn-row">
<button type="button" class="btn-secondary" onclick="randomizeBatch()">🎲 عشوائي</button>
</div>
<!-- زر الإضافة -->
<button type="button" class="btn-primary" onclick="addToBatchList()">
➕ إضافة للقائمة
</button>
<!-- قائمة الفيديوهات -->
<div class="batch-list-section" style="margin-top: 24px;">
<div class="history-header" style="margin-bottom: 12px;">
<h4 style="font-size: 0.95rem;">📋 قائمة الفيديوهات (<span id="batchCount">0</span>/15)</h4>
<button class="btn-secondary" style="width: auto; height: 32px; padding: 0 10px; font-size: 0.75rem;" onclick="clearBatchList()">🗑️ مسح الكل</button>
</div>
<div id="batchList" style="max-height: 350px; overflow-y: auto;">
<div class="history-empty" id="batchListEmpty">
<p style="font-size: 0.85rem;">لم تتم إضافة أي فيديوهات</p>
</div>
</div>
</div>
<!-- الوقت الكلي للدفعة -->
<div id="batchTotalTimeDisplay" style="text-align: center; margin-top: 16px; padding: 12px; background: var(--bg-card); border: 1px solid var(--border); border-radius: 10px; display: none;">
⏱️ الوقت الكلي التقريبي: <span id="batchTotalTime" style="color: var(--accent); font-weight: 600; font-size: 1.1rem;">--:--</span>
</div>
<!-- زر إنشاء الدفعة -->
<button type="button" class="btn-primary" style="margin-top: 20px; background: linear-gradient(135deg, #22c55e, #16a34a);" onclick="startBatchGeneration()">
🚀 إنشاء الدفعة (<span id="batchTotalVideos">0</span> فيديو)
</button>
<!-- حالة الدفعة -->
<div id="batchStatusSection" style="display: none; margin-top: 20px;">
<div class="progress-section active">
<div class="progress-ring">
<svg width="140" height="140">
<circle class="bg" cx="70" cy="70" r="65"></circle>
<circle class="fill" id="batchProgressCircle" cx="70" cy="70" r="65"></circle>
</svg>
<div class="progress-percent" id="batchProgressPercent">0%</div>
</div>
<div class="progress-status" id="batchProgressStatus">جاري التحضير...</div>
<div style="margin-top: 12px; font-size: 0.85rem; color: var(--text-muted);">
<span id="batchCompletedCount">0</span> / <span id="batchTotalCount">0</span> فيديو
</div>
<button type="button" class="btn-secondary btn-stop" style="margin-top: 12px;" onclick="cancelBatch()">
🛑 إلغاء الدفعة
</button>
</div>
</div>
</div>
<!-- 📜 History Tab -->
<div class="tab-panel" id="history-panel">
<div class="history-header">
<h3>سجل الفيديوهات</h3>
<button class="btn-secondary" style="width: auto; height: 36px; padding: 0 12px; font-size: 0.8rem;" onclick="clearAllHistory()">🗑️ مسح الكل</button>
</div>
<div class="history-empty" id="historyEmpty">
<div class="history-empty-icon">📜</div>
<p>لا توجد فيديوهات في السجل</p>
</div>
<div id="historyList"></div>
</div>
<!-- ⚙️ Settings Tab -->
<div class="tab-panel" id="settings-panel">
<!-- Preview -->
<div class="preview-box">
<div class="preview-ar" id="previewAr">بِسْمِ اللَّهِ الرَّحْمَٰنِ الرَّحِيمِ</div>
<div class="preview-en" id="previewEn">In the name of Allah, the Entirely Merciful</div>
</div>
<!-- Arabic Settings -->
<div class="settings-section">
<h4>النص العربي</h4>
<div class="settings-row">
<span class="settings-label">اللون</span>
<div class="settings-controls">
<input type="color" class="color-picker" id="arColor" value="#ffffff" onchange="updatePreview()">
</div>
</div>
<div class="settings-row">
<span class="settings-label">الحجم</span>
<div class="settings-controls">
<input type="range" class="size-slider" id="arSize" min="0.5" max="2" step="0.1" value="1" onchange="updatePreview()">
</div>
</div>
<div class="settings-row">
<span class="settings-label">لون الإطار</span>
<div class="settings-controls">
<input type="color" class="color-picker" id="arOutlineColor" value="#000000" onchange="updatePreview()">
</div>
</div>
<div class="settings-row">
<span class="settings-label">سمك الإطار</span>
<div class="settings-controls">
<input type="range" class="size-slider" id="arOutlineWidth" min="0" max="10" step="1" value="4" onchange="updatePreview()">
</div>
</div>
</div>
<!-- English Settings -->
<div class="settings-section">
<h4>الترجمة الإنجليزية</h4>
<div class="settings-row">
<span class="settings-label">اللون</span>
<div class="settings-controls">
<input type="color" class="color-picker" id="enColor" value="#FFD700" onchange="updatePreview()">
</div>
</div>
<div class="settings-row">
<span class="settings-label">الحجم</span>
<div class="settings-controls">
<input type="range" class="size-slider" id="enSize" min="0.5" max="2" step="0.1" value="1" onchange="updatePreview()">
</div>
</div>
<div class="settings-row">
<span class="settings-label">لون الإطار</span>
<div class="settings-controls">
<input type="color" class="color-picker" id="enOutlineColor" value="#000000" onchange="updatePreview()">
</div>
</div>
<div class="settings-row">
<span class="settings-label">سمك الإطار</span>
<div class="settings-controls">
<input type="range" class="size-slider" id="enOutlineWidth" min="0" max="10" step="1" value="3" onchange="updatePreview()">
</div>
</div>
</div>
<!-- Pexels Key -->
<div class="settings-section">
<h4>مفتاح Pexels (اختياري)</h4>
<input type="text" class="form-input" id="pexelsKey" placeholder="اتركه فارغاً للاستخدام الافتراضي" style="margin-top: 8px;">
</div>
<button class="btn-primary" onclick="saveAllSettings()" style="margin-top: 10px;">💾 حفظ الإعدادات</button>
<button class="btn-secondary" onclick="resetAllSettings()" style="margin-top: 10px;">استعادة الافتراضي</button>
</div>
</main>
</div>
<!-- Toast Container -->
<div class="toast-container" id="toastContainer"></div>
<script>
// ═══════════════════════════════════════
// 📦 State & Config
// ═══════════════════════════════════════
let VERSE_COUNTS = {};
let currentJobId = null;
let pollInterval = null;
let SESSION_ID = null;
const defaultSettings = {
arColor: '#ffffff',
arSize: '1',
arOutlineColor: '#000000',
arOutlineWidth: '4',
enColor: '#FFD700',
enSize: '1',
enOutlineColor: '#000000',
enOutlineWidth: '3'
};
// ═══════════════════════════════════════
// 🔑 Session Management
// ═══════════════════════════════════════
function getOrCreateSessionId() {
let sessionId = localStorage.getItem('quran_session_id');
const sessionAge = localStorage.getItem('quran_session_age');
const thirtyDays = 30 * 24 * 60 * 60 * 1000;
if (!sessionId || !sessionAge || (Date.now() - parseInt(sessionAge)) > thirtyDays) {
sessionId = 'sess_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
localStorage.setItem('quran_session_id', sessionId);
localStorage.setItem('quran_session_age', Date.now().toString());
console.log('🆕 New session created:', sessionId);
}
return sessionId;
}
// ═══════════════════════════════════════
// 🚀 Initialization
// ═══════════════════════════════════════
window.onload = async () => {
SESSION_ID = getOrCreateSessionId();
console.log('📱 Session ID:', SESSION_ID);
initTabs();
loadSettings();
// تحميل الكونفج الأول عشان VERSE_COUNTS
await loadConfig();
// بعدين نعمل validation بعد ما البيانات تتحمل
initAyahValidation();
await loadHistory();
await checkRunningJob();
// تحديث الوقت التقريبي عند التحميل
setTimeout(() => {
updateEstimatedTime();
updateBatchEstimatedTime();
}, 500);
};
function initAyahValidation() {
const startInput = document.getElementById('startAyah');
const endInput = document.getElementById('endAyah');
const surahInput = document.getElementById('surahSelect');
const maxHint = document.getElementById('maxAyahHint');
// تحديث الحد الأقصى عند تغيير السورة
const updateMaxAyah = () => {
const surah = parseInt(surahInput?.value) || 1;
const max = VERSE_COUNTS[surah] || 286;
// تحديث الـ max attribute
if (startInput) startInput.max = max;
if (endInput) endInput.max = max;
// عرض تلميح الحد الأقصى
if (maxHint) maxHint.textContent = `(عدد الآيات: ${max})`;
// تصحيح القيم الحالية
if (parseInt(startInput?.value) > max) startInput.value = 1;
if (parseInt(endInput?.value) > max) endInput.value = Math.min(5, max);
};
// التحقق الفوري أثناء الكتابة
const validateOnInput = (input) => {
const surah = parseInt(surahInput?.value) || 1;
const max = VERSE_COUNTS[surah] || 286;
let val = parseInt(input.value) || 1;
if (val < 1) input.value = 1;
if (val > max) {
input.value = max;
showToast(`هذه السورة بها ${max} آية فقط`, 'error');
}
};
startInput?.addEventListener('input', () => validateOnInput(startInput));
endInput?.addEventListener('input', () => validateOnInput(endInput));
startInput?.addEventListener('blur', () => validateAyahInput('startAyah'));
endInput?.addEventListener('blur', () => validateAyahInput('endAyah'));
startInput?.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
validateAyahInput('startAyah');
endInput.focus();
}
});
endInput?.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
validateAyahInput('endAyah');
}
});
surahInput?.addEventListener('change', () => {
updateMaxAyah();
const max = VERSE_COUNTS[parseInt(surahInput.value)] || 286;
startInput.value = 1;
endInput.value = Math.min(5, max);
validateAyahRange();
});
// تحديث الحد الأقصى عند تحميل البيانات
setTimeout(updateMaxAyah, 500);
}
function initTabs() {
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.addEventListener('click', () => {
const tabId = btn.dataset.tab;
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
btn.classList.add('active');
document.getElementById(`${tabId}-panel`).classList.add('active');
});
});
}
async function loadConfig() {
try {
const res = await fetch('/api/config');
const data = await res.json();
VERSE_COUNTS = data.verseCounts;
data.surahs.forEach((name, i) => {
surahSelect.add(new Option(`${i + 1}. ${name}`, i + 1));
});
Object.entries(data.reciters).forEach(([name, id]) => {
reciterSelect.add(new Option(name, id));
});
// تحديث الحد الأقصى للآيات بناءً على السورة الأولى
const firstSurahMax = VERSE_COUNTS[1] || 7;
const startAyahInput = document.getElementById('startAyah');
const endAyahInput = document.getElementById('endAyah');
if (startAyahInput) startAyahInput.max = firstSurahMax;
if (endAyahInput) endAyahInput.max = firstSurahMax;
// إضافة event listener لتحديث الحد الأقصى عند تغيير السورة
surahSelect.addEventListener('change', function() {
const surah = parseInt(this.value) || 1;
const max = VERSE_COUNTS[surah] || 286;
if (startAyahInput) {
startAyahInput.max = max;
if (parseInt(startAyahInput.value) > max) startAyahInput.value = 1;
}
if (endAyahInput) {
endAyahInput.max = max;
if (parseInt(endAyahInput.value) > max) endAyahInput.value = Math.min(5, max);
}
// تحديث الوقت التقريبي
updateEstimatedTime();
});
// تحديث الوقت التقريبي
updateEstimatedTime();
} catch (err) {
console.error('Config load error:', err);
showToast('فشل تحميل الإعدادات', 'error');
}
}
// ═══════════════════════════════════════
// 📝 Form Handling
// ═══════════════════════════════════════
document.getElementById('videoForm').addEventListener('submit', async (e) => {
e.preventDefault();
await startGeneration();
});
function adjustAyah(id, delta) {
const input = document.getElementById(id);
const surah = parseInt(surahSelect.value);
const max = VERSE_COUNTS[surah] || 286;
let val = parseInt(input.value) + delta;
if (val < 1) val = 1;
if (val > max) val = max;
input.value = val;
validateAyahRange();
}
function validateAyahInput(id) {
const input = document.getElementById(id);
const surah = parseInt(surahSelect.value);
const max = VERSE_COUNTS[surah] || 286;
let val = parseInt(input.value);
if (isNaN(val) || val < 1) {
val = 1;
}
if (val > max) {
val = max;
showToast(`هذه السورة بها ${max} آية فقط`, 'error');
}
input.value = val;
validateAyahRange();
}
function validateAyahRange() {
const surah = parseInt(surahSelect.value);
const max = VERSE_COUNTS[surah] || 286;
const start = parseInt(startAyah.value);
const end = parseInt(endAyah.value);
if (start > end) {
endAyah.value = start;
}
if (end > max) {
endAyah.value = max;
}
// تحديث الوقت التقريبي
updateEstimatedTime();
}
// ═══════════════════════════════════════
// ⏱️ Estimated Time Calculator (Real Duration from API)
// ═══════════════════════════════════════
// Cache لتخزين مدة الآيات
let durationCache = {};
async function fetchEstimatedTime(reciter, surah, startAyah, endAyah) {
// إنشاء مفتاح للـ cache
const cacheKey = `${reciter}-${surah}-${startAyah}-${endAyah}`;
// لو موجود في الـ cache
if (durationCache[cacheKey]) {
return durationCache[cacheKey];
}
try {
const res = await fetch('/api/estimate-duration', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ reciter, surah, startAyah, endAyah })
});
const data = await res.json();
if (data.ok) {
// حفظ في الـ cache
durationCache[cacheKey] = data;
return data;
}
} catch (err) {
console.error('Duration fetch error:', err);
}
return null;
}
function updateEstimatedTime() {
const reciter = document.getElementById('reciterSelect')?.value;
const surah = parseInt(document.getElementById('surahSelect')?.value) || 1;
const start = parseInt(document.getElementById('startAyah')?.value) || 1;
const end = parseInt(document.getElementById('endAyah')?.value) || 1;
const el = document.getElementById('estimatedTime');
if (el) {
el.textContent = '⏳ جاري الحساب...';
}
fetchEstimatedTime(reciter, surah, start, end).then(data => {
if (data && data.formatted) {
if (el) {
el.textContent = data.formatted;
}
} else {
if (el) {
el.textContent = '--:--';
}
}
});
}
function updateBatchEstimatedTime() {
const reciter = document.getElementById('batchReciterSelect')?.value;
const surah = parseInt(document.getElementById('batchSurahSelect')?.value) || 1;
const start = parseInt(document.getElementById('batchStartAyah')?.value) || 1;
const end = parseInt(document.getElementById('batchEndAyah')?.value) || 1;
const el = document.getElementById('batchEstimatedTime');
if (el) {
el.textContent = '⏳ جاري الحساب...';
}
fetchEstimatedTime(reciter, surah, start, end).then(data => {
if (data && data.formatted) {
if (el) {
el.textContent = data.formatted;
}
} else {
if (el) {
el.textContent = '--:--';
}
}
});
}
async function updateBatchTotalTime() {
// حساب الوقت الكلي لكل الفيديوهات في القائمة
const totalTimeDisplay = document.getElementById('batchTotalTimeDisplay');
const totalTimeEl = document.getElementById('batchTotalTime');
if (batchItems.length === 0) {
if (totalTimeDisplay) totalTimeDisplay.style.display = 'none';
return;
}
if (totalTimeDisplay) totalTimeDisplay.style.display = 'block';
if (totalTimeEl) totalTimeEl.textContent = '⏳ جاري الحساب...';
let totalMs = 0;
for (const item of batchItems) {
const data = await fetchEstimatedTime(item.reciter, item.surah, item.startAyah, item.endAyah);
if (data && data.durationMs) {
totalMs += data.durationMs;
}
}
const totalSeconds = totalMs / 1000;
if (totalTimeEl) {
if (totalSeconds > 0) {
totalTimeEl.textContent = formatDurationLocal(totalSeconds);
} else {
totalTimeEl.textContent = '--:--';
}
}
}
function formatDurationLocal(seconds) {
if (seconds < 60) {
return `${Math.round(seconds)} ثانية`;
} else if (seconds < 3600) {
const mins = Math.floor(seconds / 60);
const secs = Math.round(seconds % 60);
if (secs > 0) {
return `${mins} د ${secs} ث`;
}
return `${mins} دقيقة`;
} else {
const hours = Math.floor(seconds / 3600);
const mins = Math.floor((seconds % 3600) / 60);
if (mins > 0) {
return `${hours} س ${mins} د`;
}
return `${hours} ساعة`;
}
}
function randomize() {
if (reciterSelect.options.length > 0) {
reciterSelect.selectedIndex = Math.floor(Math.random() * reciterSelect.options.length);
}
if (surahSelect.options.length > 0) {
surahSelect.selectedIndex = Math.floor(Math.random() * surahSelect.options.length);
const max = VERSE_COUNTS[parseInt(surahSelect.value)] || 286;
const start = Math.floor(Math.random() * Math.max(1, max - 5)) + 1;
startAyah.value = start;
endAyah.value = Math.min(start + 4, max);
}
updateEstimatedTime();
}
function toggleAdvanced() {
const options = document.getElementById('advancedOptions');
options.classList.toggle('show');
}
function toggleSearchInput() {
const input = document.getElementById('searchInputGroup');
input.style.display = useSearch.checked ? 'block' : 'none';
}
// ═══════════════════════════════════════
// 🎬 Video Generation
// ═══════════════════════════════════════
async function startGeneration() {
const style = getStyleSettings();
const payload = {
sessionId: SESSION_ID,
reciter: reciterSelect.value,
surah: parseInt(surahSelect.value),
startAyah: parseInt(startAyah.value),
endAyah: parseInt(endAyah.value),
fps: parseInt(fpsSelect.value),
quality: qualitySelect.value,
dynamicBg: dynamicBg.checked,
useGlow: useGlow.checked,
useVignette: useVignette.checked,
bgQuery: useSearch.checked ? bgQuery.value : '',
pexelsKey: localStorage.getItem('user_pexels_key') || '',
reverbLevel: document.getElementById('reverbLevel').value || 'none', // 🎛️ Reverb effect
style: style
};
try {
const res = await fetch('/api/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const data = await res.json();
if (data.ok) {
currentJobId = data.jobId;
showProgress();
startPolling(data.jobId);
} else {
showToast('فشل بدء المعالجة', 'error');
}
} catch (err) {
console.error('Generation error:', err);
showToast('حدث خطأ في الاتصال', 'error');
}
}
function showProgress() {
document.getElementById('videoForm').style.display = 'none';
document.getElementById('progressSection').classList.add('active');
document.getElementById('resultSection').classList.remove('active');
}
function startPolling(jobId) {
if (pollInterval) clearInterval(pollInterval);
pollInterval = setInterval(async () => {
try {
const res = await fetch(`/api/progress?jobId=${jobId}`);
const data = await res.json();
updateProgress(data);
if (data.status === 'complete') {
clearInterval(pollInterval);
showResult(data);
} else if (data.status === 'error' || data.status === 'cancelled') {
clearInterval(pollInterval);
showToast(data.error || 'حدث خطأ', 'error');
resetForm();
}
} catch (err) {
console.error('Poll error:', err);
}
}, 1500);
}
function updateProgress(data) {
const percent = data.percent || 0;
const circle = document.getElementById('progressCircle');
const circumference = 2 * Math.PI * 65;
circle.style.strokeDashoffset = circumference - (percent / 100) * circumference;
document.getElementById('progressPercent').textContent = `${percent}%`;
document.getElementById('progressStatus').textContent = data.status || 'جاري المعالجة...';
if (data.eta && data.eta !== '--:--') {
document.getElementById('progressEta').style.display = 'block';
document.getElementById('etaTime').textContent = data.eta;
}
}
function showResult(data) {
document.getElementById('progressSection').classList.remove('active');
document.getElementById('resultSection').classList.add('active');
const videoUrl = data.download_url || `/api/download?jobId=${currentJobId}`;
document.getElementById('videoPlayer').src = videoUrl;
document.getElementById('downloadLink').href = videoUrl;
loadHistory();
showToast('تم إنشاء الفيديو بنجاح!', 'success');
}
function resetForm() {
document.getElementById('videoForm').style.display = 'block';
document.getElementById('progressSection').classList.remove('active');
document.getElementById('resultSection').classList.remove('active');
document.getElementById('progressCircle').style.strokeDashoffset = 408;
document.getElementById('progressPercent').textContent = '0%';
currentJobId = null;
}
async function stopGeneration() {
if (!currentJobId) return;
if (confirm('هل تريد إيقاف المعالجة؟')) {
clearInterval(pollInterval);
await fetch('/api/cancel', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ jobId: currentJobId })
});
resetForm();
showToast('تم الإيقاف', 'error');
}
}
// ═══════════════════════════════════════
// 📜 History
// ═══════════════════════════════════════
async function loadHistory() {
try {
const res = await fetch(`/api/history?sessionId=${encodeURIComponent(SESSION_ID)}`);
const data = await res.json();
const list = document.getElementById('historyList');
const empty = document.getElementById('historyEmpty');
if (data.ok && data.history && data.history.length > 0) {
empty.style.display = 'none';
list.innerHTML = data.history.map(item => createHistoryItem(item)).join('');
} else {
empty.style.display = 'block';
list.innerHTML = '';
}
} catch (err) {
console.error('History load error:', err);
}
}
function createHistoryItem(item) {
const date = new Date(item.createdAt * 1000);
const dateStr = date.toLocaleDateString('ar-EG') + ' ' + date.toLocaleTimeString('ar-EG', { hour: '2-digit', minute: '2-digit' });
const now = Date.now() / 1000;
const remaining = Math.max(0, 3600 - (now - item.createdAt));
const mins = Math.floor(remaining / 60);
let timerHtml = '';
if (item.status === 'complete' && remaining > 0) {
const urgent = mins < 10;
timerHtml = `<span class="history-timer ${urgent ? 'urgent' : ''}">⏰ ${mins} د</span>`;
}
const statusIcon = item.status === 'complete' ? '✅' : item.status === 'error' ? '❌' : '⏳';
// تجهيز بيانات الوصف
const surahName = item.surahName || 'سورة';
const reciterName = item.reciter || 'قارئ';
const ayahRange = item.startAyah && item.endAyah ? `(${item.startAyah}-${item.endAyah})` : '';
let actionsHtml = '';
if (item.downloadUrl && remaining > 0) {
actionsHtml = `
<button onclick="previewVideo('${item.downloadUrl}')">▶️ عرض</button>
<a href="${item.downloadUrl}" download>⬇️ تحميل</a>
<button onclick="openYoutubeDialog('${item.jobId}', '${item.title?.replace(/'/g, "\\'") || 'فيديو قرآني'}', '${reciterName.replace(/'/g, "\\'")}', '${surahName}')" style="background: #ff0000; color: white; border: none;">📺 يوتيوب</button>
<button class="btn-delete" onclick="deleteHistoryItem(${item.id})">🗑️</button>
`;
} else {
actionsHtml = `
<span style="color: var(--text-muted); font-size: 0.85rem;">غير متاح</span>
<button class="btn-delete" onclick="deleteHistoryItem(${item.id})">🗑️</button>
`;
}
return `
<div class="history-item" data-id="${item.id}">
<div class="history-item-header">
<span class="history-item-title">${item.title}</span>
<span class="history-item-status">${statusIcon}</span>
</div>
<div class="history-item-meta">
${timerHtml}
🎤 ${item.reciter || 'غير محدد'} • 📺 ${item.quality}p • 🕒 ${dateStr}
</div>
<div class="history-item-actions">
${actionsHtml}
</div>
</div>
`;
}
// ═══════════════════════════════════════
// 📺 YouTube Integration
// ═══════════════════════════════════════
let youtubeAuthWindow = null;
let currentYoutubeJob = null;
async function checkYoutubeStatus() {
try {
const res = await fetch(`/api/youtube/status?sessionId=${encodeURIComponent(SESSION_ID)}`);
return await res.json();
} catch {
return { configured: false, authorized: false };
}
}
async function openYoutubeDialog(jobId, defaultTitle, reciterName, surahName) {
currentYoutubeJob = jobId;
// إعداد الوصف الافتراضي
const defaultDescription = `قرآن كريم بصوت القارئ ${reciterName || 'قارئ'} من سورة ${surahName || 'سورة'}
#قران_كريم #قران #quran #fyp #قرآن_كريم_راحة_نفسية #قرآن_كريم`;
// إعداد الكلمات المفتاحية
const defaultTags = 'قرآن كريم, تلاوة, إسلام, Quran, قرآن, تلاوة قرآنية';
// التحقق من حالة YouTube
const status = await checkYoutubeStatus();
if (!status.configured) {
showToast('⚠️ ميزة يوتيوب غير مُعدة. تواصل مع المطور.', 'error');
return;
}
if (!status.authorized) {
// عرض الـ redirect URI للمستخدم
if (status.redirectUri) {
console.log('YouTube Redirect URI:', status.redirectUri);
}
// فتح نافذة المصادقة
const authRes = await fetch(`/api/youtube/auth-url?sessionId=${encodeURIComponent(SESSION_ID)}`);
const authData = await authRes.json();
if (authData.authUrl) {
showToast('سيتم فتح نافذة المصادقة...', 'success');
youtubeAuthWindow = window.open(authData.authUrl, 'youtube-auth', 'width=500,height=600');
// الاستماع لرسالة النجاح
window.addEventListener('message', function ytAuthHandler(e) {
if (e.data.type === 'youtube_auth_success') {
showToast('✅ تم ربط يوتيوب بنجاح!', 'success');
window.removeEventListener('message', ytAuthHandler);
// فتح الـ dialog مرة أخرى
setTimeout(() => openYoutubeDialog(jobId, defaultTitle), 500);
}
});
// التحقق من إغلاق النافذة (في حالة الخطأ)
const checkClosed = setInterval(() => {
if (youtubeAuthWindow && youtubeAuthWindow.closed) {
clearInterval(checkClosed);
// التحقق من الحالة مرة أخرى بعد إغلاق النافذة
setTimeout(async () => {
const newStatus = await checkYoutubeStatus();
if (!newStatus.authorized && newStatus.redirectUri) {
// عرض رسالة مساعدة للمستخدم
showToast('⚠️ قد تحتاج لإضافة الـ Redirect URI في Google Cloud Console', 'error', 8000);
console.log('أضف هذا URL في Google Cloud Console:', newStatus.redirectUri);
}
}, 1000);
}
}, 500);
}
return;
}
// عرض Dialog النشر
showYoutubeUploadDialog(jobId, defaultTitle, defaultDescription, defaultTags);
}
function showYoutubeUploadDialog(jobId, defaultTitle, defaultDescription, defaultTags) {
// إنشاء الـ dialog
// حساب أقرب وقت للجدولة (بعد ساعة من الآن)
const now = new Date();
now.setHours(now.getHours() + 1);
const minScheduleTime = now.toISOString().slice(0, 16);
const dialog = document.createElement('div');
dialog.id = 'youtubeDialog';
dialog.innerHTML = `
<div style="position: fixed; inset: 0; background: rgba(0,0,0,0.8); z-index: 9999; display: flex; align-items: center; justify-content: center;">
<div style="background: var(--bg-card); border-radius: 16px; padding: 24px; width: 90%; max-width: 450px; max-height: 90vh; overflow-y: auto;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
<h3 style="color: #ff0000;">📺 نشر على يوتيوب</h3>
<button onclick="closeYoutubeDialog()" style="background: none; border: none; color: var(--text-muted); font-size: 1.5rem; cursor: pointer;"></button>
</div>
<div class="form-group">
<label class="form-label">📌 العنوان</label>
<input type="text" id="ytTitle" class="form-input" value="${defaultTitle}" maxlength="100" placeholder="عنوان الفيديو">
</div>
<div class="form-group">
<label class="form-label">📝 الوصف</label>
<textarea id="ytDescription" class="form-input" style="height: 100px; resize: vertical;" placeholder="وصف الفيديو">${defaultDescription || ''}</textarea>
</div>
<div class="form-group">
<label class="form-label">🏷️ الكلمات المفتاحية (مفصولة بفاصلة)</label>
<input type="text" id="ytTags" class="form-input" value="${defaultTags || 'قرآن, تلاوة, إسلام, Quran'}" placeholder="قرآن, تلاوة, إسلام">
</div>
<div class="form-group">
<label class="form-label">🔒 الخصوصية</label>
<select id="ytPrivacy" class="form-select" onchange="toggleScheduleOption()">
<option value="unlisted">🔗 غير مدرج (للأصدقاء فقط)</option>
<option value="private">🔐 خاص</option>
<option value="public">🌍 عام</option>
<option value="scheduled">📅 مجدول (عام في وقت محدد)</option>
</select>
</div>
<div id="scheduleOptions" style="display: none; margin-top: 12px; padding: 16px; background: var(--bg-input); border-radius: 8px;">
<label class="form-label" style="margin-bottom: 8px;">📆 وقت النشر المجدول</label>
<input type="datetime-local" id="ytScheduleTime" class="form-input" min="${minScheduleTime}" style="width: 100%;">
<p style="font-size: 12px; color: var(--text-muted); margin-top: 8px;">
⚠️ يجب أن يكون الوقت على الأقل ساعة من الآن
</p>
</div>
<div style="margin-top: 20px; display: flex; gap: 10px;">
<button onclick="uploadToYoutube('${jobId}')" class="btn-primary" style="flex: 1;">
<span id="uploadBtnText">📤 نشر الآن</span>
</button>
<button onclick="closeYoutubeDialog()" class="btn-secondary" style="flex: 1;">
إلغاء
</button>
</div>
<div id="youtubeUploadProgress" style="display: none; margin-top: 16px; text-align: center;">
<div style="color: var(--accent);">⏳ جاري الرفع... يرجى الانتظار</div>
</div>
</div>
</div>
`;
document.body.appendChild(dialog);
}
function toggleScheduleOption() {
const privacy = document.getElementById('ytPrivacy')?.value;
const scheduleOptions = document.getElementById('scheduleOptions');
const uploadBtnText = document.getElementById('uploadBtnText');
if (privacy === 'scheduled') {
scheduleOptions.style.display = 'block';
uploadBtnText.textContent = '📅 جدولة النشر';
} else {
scheduleOptions.style.display = 'none';
uploadBtnText.textContent = '📤 نشر الآن';
}
}
function closeYoutubeDialog() {
const dialog = document.getElementById('youtubeDialog');
if (dialog) dialog.remove();
}
async function uploadToYoutube(jobId) {
const title = document.getElementById('ytTitle')?.value || 'فيديو قرآني';
const description = document.getElementById('ytDescription')?.value || '';
const tags = document.getElementById('ytTags')?.value.split(',').map(t => t.trim()).filter(t => t) || [];
let privacyStatus = document.getElementById('ytPrivacy')?.value || 'unlisted';
const scheduleTimeLocal = document.getElementById('ytScheduleTime')?.value || null;
// التحقق من وقت الجدولة
let scheduleTime = null;
if (privacyStatus === 'scheduled') {
if (!scheduleTimeLocal) {
showToast('⚠️ الرجاء اختيار وقت للجدولة', 'error');
return;
}
// تحويل الوقت المحلي لـ ISO string مع timezone
const scheduledDate = new Date(scheduleTimeLocal);
const now = new Date();
const diffMinutes = (scheduledDate - now) / (1000 * 60);
if (diffMinutes < 15) {
showToast('⚠️ يجب أن يكون وقت الجدولة بعد 15 دقيقة على الأقل', 'error');
return;
}
// تحويل لـ ISO string (يتضمن timezone)
scheduleTime = scheduledDate.toISOString();
console.log('[YouTube] Local time:', scheduleTimeLocal);
console.log('[YouTube] UTC time:', scheduleTime);
// للجدولة، نرفع الفيديو كـ scheduled
privacyStatus = 'scheduled';
}
const progressEl = document.getElementById('youtubeUploadProgress');
if (progressEl) progressEl.style.display = 'block';
try {
const res = await fetch('/api/youtube/upload', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
sessionId: SESSION_ID,
jobId: jobId,
title: title,
description: description,
tags: tags,
privacyStatus: privacyStatus,
scheduleTime: scheduleTime
})
});
const data = await res.json();
if (data.ok) {
closeYoutubeDialog();
if (privacyStatus === 'scheduled') {
showToast('✅ تم جدولة الفيديو بنجاح!', 'success');
} else {
showToast('✅ تم نشر الفيديو بنجاح!', 'success');
}
// عرض رابط الفيديو
if (data.videoUrl) {
setTimeout(() => {
const message = privacyStatus === 'scheduled'
? `تم جدولة الفيديو!\n\nسيُنشر في: ${new Date(scheduleTime).toLocaleString('ar-EG')}\n\nهل تريد فتح الفيديو؟\n${data.videoUrl}`
: `تم النشر بنجاح!\n\nهل تريد فتح الفيديو؟\n${data.videoUrl}`;
if (confirm(message)) {
window.open(data.videoUrl, '_blank');
}
}, 500);
}
} else {
if (data.needsAuth) {
showToast('⚠️ يجب ربط حساب يوتيوب أولاً', 'error');
closeYoutubeDialog();
// إعادة المصادقة
openYoutubeDialog(jobId, title);
} else {
showToast(`❌ ${data.error || 'فشل في الرفع'}`, 'error');
}
}
} catch (err) {
console.error('YouTube upload error:', err);
showToast('❌ فشل في الاتصال', 'error');
}
if (progressEl) progressEl.style.display = 'none';
}
async function deleteHistoryItem(id) {
if (!confirm('حذف هذا العنصر؟')) return;
try {
const res = await fetch(`/api/history/${id}`, { method: 'DELETE' });
const data = await res.json();
if (data.ok) {
document.querySelector(`.history-item[data-id="${id}"]`)?.remove();
if (document.querySelectorAll('.history-item').length === 0) {
document.getElementById('historyEmpty').style.display = 'block';
}
showToast('تم الحذف', 'success');
}
} catch (err) {
showToast('فشل الحذف', 'error');
}
}
async function clearAllHistory() {
if (!confirm('مسح كل السجل؟ لا يمكن التراجع.')) return;
try {
const res = await fetch('/api/history/clear', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sessionId: SESSION_ID })
});
const data = await res.json();
if (data.ok) {
document.getElementById('historyList').innerHTML = '';
document.getElementById('historyEmpty').style.display = 'block';
showToast('تم مسح السجل', 'success');
}
} catch (err) {
showToast('فشل مسح السجل', 'error');
}
}
async function previewVideo(url) {
try {
document.querySelector('[data-tab="main"]').click();
showProgress();
document.getElementById('progressStatus').textContent = 'جاري تحميل الفيديو...';
const res = await fetch(url);
const blob = await res.blob();
const blobUrl = URL.createObjectURL(blob);
document.getElementById('progressSection').classList.remove('active');
document.getElementById('resultSection').classList.add('active');
document.getElementById('videoPlayer').src = blobUrl;
document.getElementById('downloadLink').href = blobUrl;
} catch (err) {
showToast('فشل تحميل الفيديو', 'error');
resetForm();
}
}
// ═══════════════════════════════════════
// ⚙️ Settings
// ═══════════════════════════════════════
function getStyleSettings() {
return {
arColor: document.getElementById('arColor')?.value || '#ffffff',
arSize: document.getElementById('arSize')?.value || '1',
arOutC: document.getElementById('arOutlineColor')?.value || '#000000',
arOutW: document.getElementById('arOutlineWidth')?.value || '4',
enColor: document.getElementById('enColor')?.value || '#FFD700',
enSize: document.getElementById('enSize')?.value || '1',
enOutC: document.getElementById('enOutlineColor')?.value || '#000000',
enOutW: document.getElementById('enOutlineWidth')?.value || '3'
};
}
function loadSettings() {
const saved = JSON.parse(localStorage.getItem('quran_app_styles_v2')) || defaultSettings;
Object.entries(saved).forEach(([key, value]) => {
const el = document.getElementById(key);
if (el) el.value = value;
});
const pexelsKey = localStorage.getItem('user_pexels_key');
if (pexelsKey) {
document.getElementById('pexelsKey').value = pexelsKey;
}
updatePreview();
}
function updatePreview() {
const ar = document.getElementById('previewAr');
const en = document.getElementById('previewEn');
ar.style.color = document.getElementById('arColor').value;
ar.style.fontSize = `${1.5 * parseFloat(document.getElementById('arSize').value)}rem`;
const arOutline = parseInt(document.getElementById('arOutlineWidth').value) / 5;
ar.style.webkitTextStroke = arOutline > 0 ? `${arOutline}px ${document.getElementById('arOutlineColor').value}` : 'none';
en.style.color = document.getElementById('enColor').value;
en.style.fontSize = `${0.85 * parseFloat(document.getElementById('enSize').value)}rem`;
const enOutline = parseInt(document.getElementById('enOutlineWidth').value) / 5;
en.style.webkitTextStroke = enOutline > 0 ? `${enOutline}px ${document.getElementById('enOutlineColor').value}` : 'none';
}
function saveAllSettings() {
const settings = {};
['arColor', 'arSize', 'arOutlineColor', 'arOutlineWidth',
'enColor', 'enSize', 'enOutlineColor', 'enOutlineWidth'].forEach(key => {
settings[key] = document.getElementById(key).value;
});
localStorage.setItem('quran_app_styles_v2', JSON.stringify(settings));
const pexelsKey = document.getElementById('pexelsKey').value;
if (pexelsKey) {
localStorage.setItem('user_pexels_key', pexelsKey);
} else {
localStorage.removeItem('user_pexels_key');
}
showToast('تم حفظ الإعدادات', 'success');
}
function resetAllSettings() {
if (!confirm('استعادة الإعدادات الافتراضية؟')) return;
localStorage.removeItem('quran_app_styles_v2');
localStorage.removeItem('user_pexels_key');
document.getElementById('pexelsKey').value = '';
loadSettings();
showToast('تم استعادة الإعدادات', 'success');
}
// ═══════════════════════════════════════
// 🎨 Theme
// ═══════════════════════════════════════
function toggleTheme() {
document.body.classList.toggle('light-theme');
const isLight = document.body.classList.contains('light-theme');
document.getElementById('themeBtn').textContent = isLight ? '☀️' : '🌙';
localStorage.setItem('theme', isLight ? 'light' : 'dark');
}
if (localStorage.getItem('theme') === 'light') {
document.body.classList.add('light-theme');
document.getElementById('themeBtn').textContent = '☀️';
}
// ═══════════════════════════════════════
// 🔔 Toast Notifications
// ═══════════════════════════════════════
function showToast(message, type = 'success') {
const container = document.getElementById('toastContainer');
const toast = document.createElement('div');
toast.className = `toast ${type}`;
toast.textContent = message;
container.appendChild(toast);
setTimeout(() => toast.remove(), 3000);
}
// ═══════════════════════════════════════
// 🔄 Check Running Job
// ═══════════════════════════════════════
async function checkRunningJob() {
try {
const res = await fetch(`/api/my-jobs?status=processing&sessionId=${encodeURIComponent(SESSION_ID)}`);
const data = await res.json();
if (data.ok && data.jobs && data.jobs.length > 0) {
const job = data.jobs[0];
const action = confirm(
`🎬 فيديو قيد المعالجة!\n` +
`📊 التقدم: ${job.percent}%\n\n` +
`هل تريد متابعته؟`
);
if (action) {
currentJobId = job.id;
showProgress();
startPolling(job.id);
}
}
} catch (err) {
console.log('No running jobs');
}
}
// ═══════════════════════════════════════
// 📦 Batch System
// ═══════════════════════════════════════
let batchItems = [];
let currentBatchId = null;
let batchCancelled = false;
function randomizeBatch() {
const reciterSelect = document.getElementById('batchReciterSelect');
const surahSelect = document.getElementById('batchSurahSelect');
if (reciterSelect.options.length > 0) {
reciterSelect.selectedIndex = Math.floor(Math.random() * reciterSelect.options.length);
}
if (surahSelect.options.length > 0) {
surahSelect.selectedIndex = Math.floor(Math.random() * surahSelect.options.length);
const max = VERSE_COUNTS[parseInt(surahSelect.value)] || 286;
const start = Math.floor(Math.random() * Math.max(1, max - 5)) + 1;
document.getElementById('batchStartAyah').value = start;
document.getElementById('batchEndAyah').value = Math.min(start + 4, max);
}
updateBatchEstimatedTime();
}
function adjustBatchAyah(id, delta) {
const input = document.getElementById(id);
const surah = parseInt(document.getElementById('batchSurahSelect').value);
const max = VERSE_COUNTS[surah] || 286;
let val = parseInt(input.value) + delta;
if (val < 1) val = 1;
if (val > max) val = max;
input.value = val;
const start = parseInt(document.getElementById('batchStartAyah').value);
const end = parseInt(document.getElementById('batchEndAyah').value);
if (id === 'batchStartAyah' && start > end) {
document.getElementById('batchEndAyah').value = start;
}
if (id === 'batchEndAyah' && end < start) {
document.getElementById('batchStartAyah').value = end;
}
updateBatchEstimatedTime();
}
const MAX_BATCH_VIDEOS = 15; // الحد الأقصى لعدد الفيديوهات في الدفعة
function addToBatchList() {
const reciterSelect = document.getElementById('batchReciterSelect');
const surahSelect = document.getElementById('batchSurahSelect');
// التحقق من الحد الأقصى
if (batchItems.length >= MAX_BATCH_VIDEOS) {
showToast(`الحد الأقصى هو ${MAX_BATCH_VIDEOS} فيديو في الدفعة الواحدة`, 'error');
return;
}
const reciter = reciterSelect.value;
const reciterName = reciterSelect.options[reciterSelect.selectedIndex]?.text || 'غير محدد';
const surah = parseInt(surahSelect.value);
const surahName = surahSelect.options[surahSelect.selectedIndex]?.text.replace(/^\d+\.\s*/, '') || 'غير محدد';
const startAyah = parseInt(document.getElementById('batchStartAyah').value);
const endAyah = parseInt(document.getElementById('batchEndAyah').value);
const exists = batchItems.some(item =>
item.reciter === reciter && item.surah === surah &&
item.startAyah === startAyah && item.endAyah === endAyah
);
if (exists) {
showToast('هذا الفيديو موجود بالفعل في القائمة', 'error');
return;
}
const effects = {
dynamicBg: document.getElementById('batchDynamicBg').checked,
useGlow: document.getElementById('batchUseGlow').checked,
useVignette: document.getElementById('batchUseVignette').checked
};
batchItems.push({
reciter,
reciterName,
surah,
startAyah,
endAyah,
surahName,
effects
});
renderBatchList();
showToast(`تمت الإضافة: ${surahName} (${reciterName})`, 'success');
}
function removeFromBatchList(index) {
batchItems.splice(index, 1);
renderBatchList();
updateBatchTotalVideos();
}
function clearBatchList() {
batchItems = [];
renderBatchList();
updateBatchTotalVideos();
updateBatchTotalTime();
}
function updateBatchTotalVideos() {
document.getElementById('batchTotalVideos').textContent = batchItems.length;
updateBatchTotalTime();
}
function renderBatchList() {
const listEl = document.getElementById('batchList');
const countEl = document.getElementById('batchCount');
countEl.textContent = batchItems.length;
updateBatchTotalVideos();
updateBatchTotalTime();
// لو القائمة فاضية
if (batchItems.length === 0) {
listEl.innerHTML = `
<div class="history-empty" id="batchListEmpty">
<p style="font-size: 0.85rem;">لم تتم إضافة أي فيديوهات</p>
</div>
`;
return;
}
// عرض الفيديوهات
listEl.innerHTML = batchItems.map((item, index) => {
const effectsIcons = [];
if (item.effects.dynamicBg) effectsIcons.push('🖼️');
if (item.effects.useGlow) effectsIcons.push('✨');
if (item.effects.useVignette) effectsIcons.push('🎭');
// محاولة الحصول على الوقت من الـ cache
const cacheKey = `${item.reciter}-${item.surah}-${item.startAyah}-${item.endAyah}`;
const cachedData = durationCache[cacheKey];
const timeStr = cachedData ? cachedData.formatted : '⏳...';
return `
<div class="batch-item" data-index="${index}" style="
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 10px;
padding: 12px;
margin-bottom: 8px;
display: flex;
justify-content: space-between;
align-items: center;
">
<div style="flex: 1;">
<div style="display: flex; justify-content: space-between; align-items: center;">
<div style="font-weight: 600; font-size: 0.95rem;">
📖 ${item.surahName}
</div>
<div style="display: flex; align-items: center; gap: 8px;">
<span class="batch-item-time" data-index="${index}" style="font-size: 0.75rem; color: var(--accent);">⏱️ ${timeStr}</span>
<span style="font-size: 0.75rem; color: var(--text-muted);">#${index + 1}</span>
</div>
</div>
<div style="font-size: 0.8rem; color: var(--text-muted); margin-top: 4px;">
🎤 ${item.reciterName} • آية ${item.startAyah} ← ${item.endAyah}
</div>
<div style="font-size: 0.75rem; color: var(--accent); margin-top: 4px;">
${effectsIcons.join(' ')}
</div>
</div>
<button onclick="removeFromBatchList(${index})" style="
background: none;
border: none;
color: var(--danger);
font-size: 1.2rem;
cursor: pointer;
padding: 8px;
"></button>
</div>
`}).join('');
// تحديث أوقات الفيديوهات في الخلفية
updateBatchItemTimes();
}
async function updateBatchItemTimes() {
// تحديث وقت كل فيديو في القائمة
for (let i = 0; i < batchItems.length; i++) {
const item = batchItems[i];
const data = await fetchEstimatedTime(item.reciter, item.surah, item.startAyah, item.endAyah);
if (data && data.formatted) {
const timeEl = document.querySelector(`.batch-item-time[data-index="${i}"]`);
if (timeEl) {
timeEl.textContent = `⏱️ ${data.formatted}`;
}
}
}
}
async function startBatchGeneration() {
if (batchItems.length === 0) {
showToast('أضف فيديوهات للقائمة أولاً', 'error');
return;
}
batchCancelled = false;
const payload = {
sessionId: SESSION_ID,
items: batchItems.map(item => ({
surah: item.surah,
startAyah: item.startAyah,
endAyah: item.endAyah,
reciter: item.reciter,
dynamicBg: item.effects.dynamicBg,
useGlow: item.effects.useGlow,
useVignette: item.effects.useVignette
})),
quality: document.getElementById('qualitySelect')?.value || '720',
fps: parseInt(document.getElementById('fpsSelect')?.value || 20),
pexelsKey: localStorage.getItem('user_pexels_key') || '',
style: getStyleSettings()
};
try {
showToast('جاري إنشاء الدفعة...', 'success');
const res = await fetch('/api/batch/create', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (!res.ok) {
throw new Error(`HTTP error! status: ${res.status}`);
}
const data = await res.json();
if (data.ok) {
currentBatchId = data.batchId;
showToast(`تم إنشاء دفعة من ${data.totalJobs} فيديو`, 'success');
const statusSection = document.getElementById('batchStatusSection');
const totalCount = document.getElementById('batchTotalCount');
const completedCount = document.getElementById('batchCompletedCount');
if (statusSection) statusSection.style.display = 'block';
if (totalCount) totalCount.textContent = data.totalJobs;
if (completedCount) completedCount.textContent = 0;
startBatchPolling(data.batchId);
batchItems = [];
renderBatchList();
} else {
showToast(data.error || 'فشل إنشاء الدفعة', 'error');
}
} catch (err) {
console.error('Batch creation error:', err);
}
}
function cancelBatch() {
if (!currentBatchId) return;
batchCancelled = true;
fetch('/api/batch/cancel', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ batchId: currentBatchId })
});
showToast('تم إلغاء الدفعة', 'error');
}
let batchPollInterval = null;
function startBatchPolling(batchId) {
if (batchPollInterval) clearInterval(batchPollInterval);
batchPollInterval = setInterval(async () => {
if (batchCancelled) {
clearInterval(batchPollInterval);
return;
}
try {
const res = await fetch(`/api/batch/status?batchId=${batchId}`);
const data = await res.json();
if (data.ok) {
updateBatchProgress(data.batch);
if (data.batch.status === 'complete' || data.batch.status === 'cancelled') {
clearInterval(batchPollInterval);
if (data.batch.status === 'complete') {
showToast(`تم إنشاء ${data.batch.completedJobs} فيديو بنجاح!`, 'success');
loadHistory();
}
}
}
} catch (err) {
console.error('Batch polling error:', err);
}
}, 2000);
}
function updateBatchProgress(batch) {
const total = batch.totalJobs;
const completed = batch.completedJobs || 0;
const failed = batch.failedJobs || 0;
const percent = total > 0 ? Math.round(((completed + failed) / total) * 100) : 0;
const circle = document.getElementById('batchProgressCircle');
const circumference = 2 * Math.PI * 65;
if (circle) {
circle.style.strokeDashoffset = circumference - (percent / 100) * circumference;
}
document.getElementById('batchProgressPercent').textContent = `${percent}%`;
document.getElementById('batchCompletedCount').textContent = completed + failed;
const statusEl = document.getElementById('batchProgressStatus');
if (batch.status === 'running') {
statusEl.textContent = `جاري المعالجة... (${batch.currentJobIndex + 1}/${total})`;
} else if (batch.status === 'complete') {
statusEl.textContent = `✅ اكتمل!`;
} else if (batch.status === 'cancelled') {
statusEl.textContent = `⚠️ تم الإلغاء`;
} else if (batch.status === 'pending') {
statusEl.textContent = `⏳ في الانتظار...`;
}
}
function initBatchPage() {
const batchSurahSelect = document.getElementById('batchSurahSelect');
const mainSurahSelect = document.getElementById('surahSelect');
if (batchSurahSelect && mainSurahSelect) {
batchSurahSelect.innerHTML = mainSurahSelect.innerHTML;
batchSurahSelect.addEventListener('change', () => {
const max = VERSE_COUNTS[parseInt(batchSurahSelect.value)] || 286;
document.getElementById('batchStartAyah').value = 1;
document.getElementById('batchEndAyah').value = Math.min(5, max);
});
}
const batchReciterSelect = document.getElementById('batchReciterSelect');
const mainReciterSelect = document.getElementById('reciterSelect');
if (batchReciterSelect && mainReciterSelect) {
batchReciterSelect.innerHTML = mainReciterSelect.innerHTML;
}
}
document.querySelectorAll('.tab-btn').forEach(btn => {
btn.addEventListener('click', () => {
if (btn.dataset.tab === 'batch') {
initBatchPage();
}
});
});
async function checkActiveBatch() {
try {
const res = await fetch(`/api/batch/list?sessionId=${encodeURIComponent(SESSION_ID)}`);
const data = await res.json();
if (data.ok && data.batches) {
const activeBatch = data.batches.find(b =>
b.status === 'pending' || b.status === 'running'
);
if (activeBatch) {
currentBatchId = activeBatch.id;
document.getElementById('batchStatusSection').style.display = 'block';
document.getElementById('batchTotalCount').textContent = activeBatch.totalJobs;
startBatchPolling(activeBatch.id);
}
}
} catch (err) {
console.log('No active batches');
}
}
setTimeout(checkActiveBatch, 1000);
</script>
</body>
</html>