| <!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> |
| |
| |
| |
| :root { |
| |
| --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 { |
| max-width: 480px; |
| margin: 0 auto; |
| min-height: 100dvh; |
| display: flex; |
| flex-direction: column; |
| } |
| |
| |
| |
| |
| .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-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 { |
| 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-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-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; |
| } |
| |
| |
| |
| |
| .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-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 { |
| 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 { |
| 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-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-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-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 { |
| 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-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; |
| } |
| |
| |
| |
| |
| @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 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> |
|
|
| |
| |
| |
| <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> |
|
|
| |
| |
| |
| <main class="tab-content"> |
| |
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <div class="tab-panel" id="settings-panel"> |
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |