| <!DOCTYPE html> |
| <html lang="ar" dir="rtl" data-theme="dark"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>ستوديو الهلال — نظام معالجة الصور</title> |
| <script src="https://cdn.tailwindcss.com"></script> |
| <script> |
| tailwind.config = { |
| darkMode: ['selector', '[data-theme="dark"]'], |
| } |
| </script> |
| <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet"> |
| |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.13/cropper.min.css"> |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.13/cropper.min.js"></script> |
| |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script> |
| <style> |
| @import url('https://fonts.googleapis.com/css2?family=Cairo:wght@300;400;600;700&display=swap'); |
| |
| |
| :root { |
| --bg-body: #f1f5f9; |
| --bg-card: #ffffff; |
| --bg-card-alt: #f8fafc; |
| --bg-input: #f1f5f9; |
| --border-card: #e2e8f0; |
| --border-input: #cbd5e1; |
| --text-primary: #0f172a; |
| --text-secondary: #475569; |
| --text-muted: #94a3b8; |
| --scrollbar-track: #f1f5f9; |
| --scrollbar-thumb: #cbd5e1; |
| --overlay-bg: rgba(255,255,255,0.92); |
| } |
| [data-theme="dark"] { |
| --bg-body: #0f172a; |
| --bg-card: #1e293b; |
| --bg-card-alt: #0f172a; |
| --bg-input: #0f172a; |
| --border-card: #334155; |
| --border-input: #334155; |
| --text-primary: #f8fafc; |
| --text-secondary: #94a3b8; |
| --text-muted: #475569; |
| --scrollbar-track: #0f172a; |
| --scrollbar-thumb: #334155; |
| --overlay-bg: rgba(15,23,42,0.92); |
| } |
| |
| body { |
| font-family: 'Cairo', sans-serif; |
| background-color: var(--bg-body); |
| color: var(--text-primary); |
| transition: background-color 0.3s, color 0.3s; |
| } |
| .studio-card { |
| background: var(--bg-card); |
| border: 1px solid var(--border-card); |
| transition: background-color 0.3s, border-color 0.3s; |
| } |
| .studio-input { |
| background: var(--bg-input); |
| border-color: var(--border-input); |
| color: var(--text-primary); |
| } |
| .text-theme-primary { color: var(--text-primary); } |
| .text-theme-secondary { color: var(--text-secondary); } |
| .text-theme-muted { color: var(--text-muted); } |
| |
| .gradient-text { background: linear-gradient(to left, #fbbf24, #f59e0b); -webkit-background-clip: text; -webkit-text-fill-color: transparent; } |
| .cropper-view-box, .cropper-face { border-radius: 4px; outline: 2px solid #fbbf24; } |
| ::-webkit-scrollbar { width: 6px; } |
| ::-webkit-scrollbar-track { background: var(--scrollbar-track); } |
| ::-webkit-scrollbar-thumb { background: var(--scrollbar-thumb); border-radius: 10px; } |
| |
| |
| .badge { font-size: 8px; padding: 1px 4px; border-radius: 3px; font-weight: 800; text-transform: uppercase; } |
| .badge-rmbg { background: #10b981; color: #064e3b; } |
| .badge-retouch { background: #3b82f6; color: #1e3a8a; } |
| .badge-crop { background: #f59e0b; color: #78350f; } |
| .badge-color { background: #a855f7; color: #4c1d95; } |
| .badge-quality { font-size: 7px; padding: 1px 3px; border-radius: 2px; font-weight: 700; } |
| .badge-hd { background: #10b981; color: white; } |
| .badge-lowres { background: #ef4444; color: white; } |
| .badge-mid { background: #f59e0b; color: white; } |
| |
| .drop-glow { border-color: #fbbf24 !important; box-shadow: 0 0 20px rgba(251, 191, 36, 0.3); } |
| |
| |
| @keyframes shimmer { |
| 0% { background-position: -468px 0 } |
| 100% { background-position: 468px 0 } |
| } |
| .skeleton { |
| background: var(--bg-card); |
| background-image: linear-gradient(to right, var(--bg-card) 0%, var(--border-card) 20%, var(--bg-card) 40%, var(--bg-card) 100%); |
| background-repeat: no-repeat; |
| background-size: 800px 104px; |
| animation: shimmer 1.5s infinite linear; |
| } |
| |
| .preview-transition { transition: opacity 0.3s ease-in-out, transform 0.3s ease-in-out; } |
| .processing-spin { animation: spin 2s linear infinite; } |
| @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } |
| |
| |
| #toast-container { |
| position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%); |
| z-index: 9999; display: flex; flex-direction: column-reverse; gap: 8px; align-items: center; |
| pointer-events: none; |
| } |
| .toast { |
| pointer-events: auto; |
| padding: 12px 24px; border-radius: 12px; font-size: 14px; font-weight: 600; |
| display: flex; align-items: center; gap: 10px; |
| box-shadow: 0 8px 30px rgba(0,0,0,0.2); |
| animation: toastIn 0.35s ease-out; |
| max-width: 420px; |
| } |
| .toast-success { background: #065f46; color: #d1fae5; } |
| .toast-error { background: #7f1d1d; color: #fecaca; } |
| .toast-info { background: #1e3a5f; color: #bfdbfe; } |
| .toast-warning { background: #78350f; color: #fef3c7; } |
| @keyframes toastIn { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } } |
| @keyframes toastOut { from { opacity: 1; transform: translateY(0); } to { opacity: 0; transform: translateY(20px); } } |
| |
| |
| .upload-progress-bar { |
| height: 4px; border-radius: 2px; background: var(--border-card); overflow: hidden; margin-top: 8px; |
| } |
| .upload-progress-fill { |
| height: 100%; background: linear-gradient(90deg, #fbbf24, #f59e0b); |
| border-radius: 2px; transition: width 0.15s linear; width: 0%; |
| } |
| |
| |
| #zoom-modal { |
| position: fixed; inset: 0; z-index: 100; display: none; |
| background: rgba(0,0,0,0.9); cursor: zoom-out; |
| align-items: center; justify-content: center; |
| } |
| #zoom-modal.active { display: flex; } |
| #zoom-modal img { |
| max-width: 95vw; max-height: 95vh; object-fit: contain; |
| transform-origin: center; transition: transform 0.2s; |
| } |
| |
| |
| .settings-panel { max-height: 0; overflow: hidden; transition: max-height 0.35s ease-out, opacity 0.25s; opacity: 0; } |
| .settings-panel.open { max-height: 500px; opacity: 1; } |
| |
| |
| .mobile-drawer-overlay { |
| position: fixed; inset: 0; background: rgba(0,0,0,0.5); z-index: 80; display: none; |
| } |
| .mobile-drawer-overlay.active { display: block; } |
| .mobile-drawer { |
| position: fixed; top: 0; bottom: 0; width: 85%; max-width: 380px; |
| z-index: 81; transition: transform 0.3s ease; |
| overflow-y: auto; padding: 20px; |
| background: var(--bg-card); |
| } |
| |
| [dir="rtl"] .mobile-drawer { left: 0; right: auto; transform: translateX(-100%); } |
| .mobile-drawer.active { transform: translateX(0) !important; } |
| |
| |
| .ba-toggle { |
| position: absolute; top: 12px; left: 12px; z-index: 10; |
| backdrop-filter: blur(8px); |
| } |
| |
| |
| @keyframes float { 0%,100% { transform: translateY(0); } 50% { transform: translateY(-10px); } } |
| .float-anim { animation: float 3s ease-in-out infinite; } |
| @keyframes pointDown { 0%,100% { transform: translateY(0); } 50% { transform: translateY(8px); } } |
| .point-anim { animation: pointDown 1.5s ease-in-out infinite; } |
| |
| |
| #shortcuts-modal { |
| position: fixed; inset: 0; z-index: 90; display: none; |
| background: rgba(0,0,0,0.6); align-items: center; justify-content: center; |
| backdrop-filter: blur(4px); |
| } |
| #shortcuts-modal.active { display: flex; } |
| |
| |
| .queue-slider { |
| display: flex; flex-direction: row; gap: 10px; |
| flex-wrap: wrap; |
| max-height: 340px; |
| overflow-y: auto; overflow-x: hidden; |
| padding: 8px 4px; scroll-behavior: smooth; |
| scrollbar-width: thin; |
| } |
| .queue-slider::-webkit-scrollbar { width: 5px; } |
| .queue-slider::-webkit-scrollbar-track { background: var(--scrollbar-track); border-radius: 10px; } |
| .queue-slider::-webkit-scrollbar-thumb { background: var(--scrollbar-thumb); border-radius: 10px; } |
| .queue-slider::-webkit-scrollbar-thumb:hover { background: #fbbf24; } |
| |
| .queue-slide { |
| flex: 0 0 auto; width: 72px; cursor: pointer; |
| position: relative; border-radius: 10px; |
| transition: transform 0.15s, box-shadow 0.15s; |
| } |
| .queue-slide:hover { transform: translateY(-2px); } |
| .queue-slide.active { |
| box-shadow: 0 0 0 2px #fbbf24; |
| transform: translateY(-2px); |
| } |
| .queue-slide img { |
| width: 72px; height: 72px; object-fit: cover; |
| border-radius: 10px; display: block; |
| border: 1px solid var(--border-card); |
| } |
| .queue-slide .slide-status { |
| position: absolute; top: -4px; right: -4px; |
| width: 16px; height: 16px; border-radius: 50%; |
| display: flex; align-items: center; justify-content: center; |
| font-size: 9px; z-index: 5; |
| background: var(--bg-card); box-shadow: 0 1px 3px rgba(0,0,0,0.3); |
| } |
| .queue-slide .slide-delete { |
| position: absolute; top: -4px; left: -4px; |
| width: 18px; height: 18px; border-radius: 50%; |
| background: #ef4444; color: white; |
| display: flex; align-items: center; justify-content: center; |
| font-size: 8px; z-index: 5; |
| opacity: 0; transition: opacity 0.15s; |
| border: none; cursor: pointer; |
| } |
| .queue-slide:hover .slide-delete { opacity: 1; } |
| .queue-slide .slide-name { |
| font-size: 9px; text-align: center; margin-top: 4px; |
| overflow: hidden; text-overflow: ellipsis; white-space: nowrap; |
| color: var(--text-secondary); font-weight: 600; |
| max-width: 72px; |
| } |
| </style> |
| </head> |
| <body class="min-h-screen p-4 md:p-10"> |
|
|
| |
| <div id="toast-container"></div> |
|
|
| |
| <div id="zoom-modal" onclick="closeZoom()"> |
| <img id="zoom-img" src="" alt="Zoomed preview"> |
| </div> |
|
|
| |
| <div id="shortcuts-modal" onclick="this.classList.remove('active')"> |
| <div class="studio-card rounded-2xl p-6 max-w-sm w-full mx-4 shadow-2xl" onclick="event.stopPropagation()"> |
| <h3 class="font-bold text-lg mb-4 text-right gradient-text">اختصارات لوحة المفاتيح</h3> |
| <div class="space-y-3 text-sm" dir="ltr"> |
| <div class="flex justify-between"><kbd class="bg-slate-700 text-white px-2 py-1 rounded text-xs">← →</kbd><span class="text-theme-secondary">Navigate images</span></div> |
| <div class="flex justify-between"><kbd class="bg-slate-700 text-white px-2 py-1 rounded text-xs">Delete</kbd><span class="text-theme-secondary">Delete selected</span></div> |
| <div class="flex justify-between"><kbd class="bg-slate-700 text-white px-2 py-1 rounded text-xs">Enter</kbd><span class="text-theme-secondary">Save & Next</span></div> |
| <div class="flex justify-between"><kbd class="bg-slate-700 text-white px-2 py-1 rounded text-xs">Ctrl+S</kbd><span class="text-theme-secondary">Process current</span></div> |
| <div class="flex justify-between"><kbd class="bg-slate-700 text-white px-2 py-1 rounded text-xs">Escape</kbd><span class="text-theme-secondary">Close modals</span></div> |
| </div> |
| <button onclick="document.getElementById('shortcuts-modal').classList.remove('active')" class="mt-5 w-full py-2 bg-yellow-500 text-slate-900 rounded-lg font-bold">حسناً</button> |
| </div> |
| </div> |
|
|
| |
| <div class="mobile-drawer-overlay lg:hidden" id="drawer-overlay" onclick="toggleDrawer(false)"></div> |
| <div class="mobile-drawer lg:hidden studio-card" id="mobile-drawer"> |
| <div class="flex justify-between items-center mb-4"> |
| <h3 class="font-bold text-sm text-theme-secondary">قائمة المعالجة</h3> |
| <button onclick="toggleDrawer(false)" class="w-8 h-8 rounded-full bg-slate-700 flex items-center justify-center"><i class="fa-solid fa-xmark text-white text-sm"></i></button> |
| </div> |
| <div id="mobile-image-list" class="flex flex-col gap-3 overflow-y-auto"></div> |
| </div> |
|
|
| |
| <header class="max-w-[1400px] mx-auto mb-8 flex flex-col md:flex-row justify-between items-center gap-4"> |
| <div> |
| <h1 class="text-4xl font-bold gradient-text text-right">ستوديو الهلال</h1> |
| <p class="text-theme-secondary text-right">سير عمل متكامل</p> |
| </div> |
| <div class="flex flex-wrap justify-center md:justify-end gap-2"> |
| <a href="https://esmailx50-job.hf.space" target="_blank" class="flex items-center gap-2 px-3 py-2 rounded-full bg-indigo-900/30 border border-indigo-500/50 text-sm text-indigo-300 hover:bg-indigo-500 hover:text-white transition-all font-bold"> |
| <i class="fa-solid fa-wand-magic-sparkles"></i> |
| <span class="hidden sm:inline">أداة تحسين الجودة</span> |
| </a> |
| <a href="https://esmailx50-rmbg2.hf.space" target="_blank" class="flex items-center gap-2 px-3 py-2 rounded-full bg-rose-900/30 border border-rose-500/50 text-sm text-rose-300 hover:bg-rose-500 hover:text-white transition-all font-bold"> |
| <i class="fa-solid fa-eraser"></i> |
| <span class="hidden sm:inline">أداة إزالة الخلفية</span> |
| </a> |
|
|
| |
| <button id="theme-toggle" onclick="toggleTheme()" title="تبديل السمة" class="w-10 h-10 rounded-full bg-slate-800 border border-slate-700 flex items-center justify-center hover:bg-slate-700 transition-all"> |
| <i class="fa-solid fa-sun text-yellow-400 text-sm" id="theme-icon"></i> |
| </button> |
|
|
| |
| <button onclick="document.getElementById('shortcuts-modal').classList.add('active')" title="اختصارات لوحة المفاتيح" class="w-10 h-10 rounded-full bg-slate-800 border border-slate-700 flex items-center justify-center hover:bg-slate-700 transition-all"> |
| <i class="fa-regular fa-keyboard text-slate-400 text-sm"></i> |
| </button> |
|
|
| <div id="ai-status" class="flex items-center gap-2 px-4 py-2 rounded-full bg-slate-800 border border-slate-700 text-sm text-slate-400"> |
| <div class="w-3 h-3 rounded-full bg-yellow-500 animate-pulse"></div> |
| </div> |
| </div> |
| </header> |
|
|
| <main class="max-w-[1400px] mx-auto grid grid-cols-1 lg:grid-cols-12 gap-6 lg:gap-6"> |
|
|
| |
| <div class="lg:hidden flex gap-3"> |
| <button onclick="toggleDrawer(true)" class="flex-1 studio-card rounded-xl p-3 flex items-center justify-center gap-3 active:scale-95 transition-transform"> |
| <i class="fa-solid fa-list text-yellow-500"></i> |
| <span class="font-bold text-sm text-theme-secondary">قائمة الصور</span> |
| <span id="mobile-queue-count" class="text-xs bg-yellow-500 text-slate-900 px-2 py-0.5 rounded-full font-bold">0</span> |
| </button> |
| </div> |
|
|
| |
| <div class="hidden lg:flex lg:col-span-3 flex-col gap-4 order-1"> |
| <div class="studio-card rounded-2xl p-5 text-center border-dashed border-2 hover:border-yellow-500 transition-all cursor-pointer group" style="border-color: var(--border-card);" id="drop-zone"> |
| <input type="file" id="file-input" multiple class="hidden" accept="image/*"> |
| <div class="float-anim"> |
| <i class="fa-solid fa-cloud-arrow-up text-4xl mb-2 text-slate-400 group-hover:text-yellow-500 transition-colors"></i> |
| </div> |
| <h3 class="font-semibold text-sm text-theme-primary">ارفع صورة</h3> |
| <p class="text-theme-muted text-[10px] mt-1">أو اسحب وأفلت هنا</p> |
| <div id="upload-progress-wrapper" class="hidden"> |
| <div class="upload-progress-bar"> |
| <div id="upload-progress-fill" class="upload-progress-fill"></div> |
| </div> |
| <p id="upload-progress-text" class="text-xs text-theme-muted mt-1">جاري الرفع...</p> |
| </div> |
| </div> |
|
|
| <div class="studio-card rounded-2xl p-4 flex flex-col"> |
| <div class="flex justify-between items-center mb-2 px-1"> |
| <h3 class="font-bold uppercase text-[10px] tracking-widest text-theme-muted">قائمة المعالجة</h3> |
| <span id="queue-count" class="text-[10px] bg-slate-700 text-white px-2 py-0.5 rounded">٠ صور</span> |
| </div> |
| <div id="image-list" class="queue-slider"> |
| <div class="text-center w-full py-6" id="empty-queue-placeholder"> |
| <p class="text-theme-muted text-xs font-medium">القائمة فارغة</p> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="lg:hidden"> |
| <div class="studio-card rounded-2xl p-4 text-center border-dashed border-2 hover:border-yellow-500 transition-all cursor-pointer group" style="border-color: var(--border-card);" id="drop-zone-mobile"> |
| <div class="float-anim inline-block"> |
| <i class="fa-solid fa-cloud-arrow-up text-3xl text-slate-400 group-hover:text-yellow-500 transition-colors"></i> |
| </div> |
| <p class="text-sm text-theme-muted mt-2">اضغط هنا لرفع الصور</p> |
| <div id="upload-progress-wrapper-mobile" class="hidden"> |
| <div class="upload-progress-bar"> |
| <div id="upload-progress-fill-mobile" class="upload-progress-fill"></div> |
| </div> |
| <p id="upload-progress-text-mobile" class="text-xs text-theme-muted mt-1">جاري الرفع...</p> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="lg:col-span-6 flex flex-col gap-6 order-2"> |
|
|
| |
| <div class="studio-card rounded-2xl p-4 shadow-lg"> |
| <div class="flex items-center justify-between mb-4 flex-wrap gap-2"> |
| <div class="flex items-center gap-3"> |
| <h3 class="font-bold uppercase text-xs tracking-widest text-theme-muted">بيانات الطالب</h3> |
| <span id="current-filename" class="text-xs font-bold text-theme-secondary truncate max-w-[140px]" style="border-right: 1px solid var(--border-card); padding-right: 12px;"></span> |
| </div> |
| |
| <div class="flex items-center gap-2" dir="ltr"> |
| <button onclick="navigate(-1)" class="w-8 h-8 rounded-full bg-slate-800 hover:bg-slate-700 border border-slate-700 flex items-center justify-center transition-all"> |
| <i class="fa-solid fa-chevron-left text-slate-400 text-xs"></i> |
| </button> |
| <span id="nav-counter" class="text-xs font-mono font-bold text-yellow-500 mx-1">0/0</span> |
| <button onclick="navigate(1)" class="w-8 h-8 rounded-full bg-slate-800 hover:bg-slate-700 border border-slate-700 flex items-center justify-center transition-all"> |
| <i class="fa-solid fa-chevron-right text-slate-400 text-xs"></i> |
| </button> |
| </div> |
| </div> |
|
|
| <div class="grid grid-cols-1 md:grid-cols-2 gap-4"> |
| <div> |
| <label class="block text-xs font-bold text-theme-muted uppercase mb-2 tracking-widest text-right">اسم الطالب</label> |
| <input type="text" id="student-name" dir="rtl" class="w-full studio-input border rounded-lg px-4 py-2.5 focus:ring-2 focus:ring-yellow-500 outline-none transition-all text-right" placeholder="الاسم هنا..."> |
| </div> |
| <div> |
| <label class="block text-xs font-bold text-theme-muted uppercase mb-2 tracking-widest text-right">الرقم القومي</label> |
| <input type="text" id="student-id" class="w-full studio-input border rounded-lg px-4 py-2.5 focus:ring-2 focus:ring-yellow-500 outline-none transition-all text-right" placeholder="٣٠٠٠..."> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="studio-card rounded-2xl p-2 flex-grow relative min-h-[450px] flex items-center justify-center overflow-hidden" id="main-area"> |
|
|
| |
| <div class="ba-toggle hidden" id="ba-toggle-wrapper"> |
| <button onclick="toggleBeforeAfter()" id="ba-toggle-btn" class="px-3 py-1.5 rounded-lg text-xs font-bold flex items-center gap-2 transition-all" style="background: rgba(0,0,0,0.6); color: white; border: 1px solid rgba(255,255,255,0.15);"> |
| <i class="fa-solid fa-eye"></i> |
| <span id="ba-toggle-label">الأصلية</span> |
| </button> |
| </div> |
|
|
| |
| <button onclick="openZoom()" id="zoom-hint" class="hidden absolute bottom-3 left-3 z-10 px-3 py-1.5 rounded-lg text-xs font-bold flex items-center gap-2 transition-all hover:scale-105" style="background: rgba(0,0,0,0.6); color: white; border: 1px solid rgba(255,255,255,0.15);"> |
| <i class="fa-solid fa-magnifying-glass-plus"></i> |
| تكبير |
| </button> |
|
|
| |
| <div id="cropper-container" class="hidden w-full h-full p-4 flex flex-col gap-4"> |
| <div class="flex-1 overflow-hidden rounded-lg bg-black"> |
| <img id="cropper-img" src=""> |
| </div> |
| <div class="flex justify-end gap-4"> |
| <button onclick="cancelCrop()" class="px-6 py-2 bg-slate-700 rounded-lg font-bold text-white">إلغاء</button> |
| <button onclick="applyCrop()" class="px-6 py-2 bg-yellow-500 text-slate-900 rounded-lg font-bold">حفظ وإعادة المعالجة</button> |
| </div> |
| </div> |
|
|
| |
| <div id="preview-container" class="w-full h-full flex items-center justify-center"> |
| <div id="preview-skeleton" class="hidden w-[80%] h-[80%] skeleton rounded-lg shadow-2xl opacity-50"></div> |
| <img id="main-preview" src="" class="max-w-full max-h-[500px] hidden rounded shadow-2xl preview-transition opacity-0 cursor-zoom-in" onclick="openZoom()"> |
| |
| <div id="preview-placeholder" class="text-center px-4" style="color: var(--text-muted);"> |
| <div class="float-anim inline-block mb-4"> |
| <svg width="100" height="100" viewBox="0 0 100 100" fill="none" class="mx-auto opacity-25"> |
| <rect x="12" y="18" width="76" height="64" rx="10" stroke="currentColor" stroke-width="2.5"/> |
| <circle cx="35" cy="40" r="8" stroke="currentColor" stroke-width="2.5"/> |
| <path d="M12 70 L35 50 L52 62 L65 45 L88 65" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/> |
| <path d="M62 22 L62 14 M62 14 L56 20 M62 14 L68 20" stroke="currentColor" stroke-width="2" stroke-linecap="round"/> |
| </svg> |
| </div> |
| <p class="font-semibold text-sm">اختر صورة من القائمة</p> |
| </div> |
| </div> |
|
|
| |
| <div id="batch-overlay" class="absolute inset-0 hidden items-center justify-center flex-col gap-5 z-50 rounded-2xl" style="background: rgba(0,0,0,0.7); backdrop-filter: blur(6px);"> |
| <div class="w-14 h-14 border-4 border-yellow-500 border-t-transparent rounded-full animate-spin"></div> |
| <p id="batch-counter" class="text-5xl font-extrabold text-yellow-400 font-mono tracking-wider" dir="ltr">0/0</p> |
| <p class="text-sm text-slate-300 font-semibold">جاري المعالجة...</p> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="lg:col-span-3 flex flex-col gap-6 order-3"> |
| <div class="studio-card rounded-2xl p-5 shadow-xl sticky top-6"> |
| <label class="block text-[10px] font-bold text-theme-muted uppercase mb-4 tracking-widest text-right">الخيارات والإعدادات</label> |
| |
| |
| <div class="space-y-3 mb-6" dir="rtl"> |
| <label class="flex items-center gap-3 cursor-pointer group p-2 rounded-lg hover:bg-slate-500/5 transition-colors"> |
| <input type="checkbox" id="step-retouch" checked class="w-5 h-5 rounded border-slate-700 bg-slate-800 text-yellow-500 focus:ring-yellow-500"> |
| <span class="text-sm font-medium text-theme-secondary group-hover:text-yellow-500 transition-colors">تجميل البشرة</span> |
| </label> |
| <label class="flex items-center gap-3 cursor-pointer group p-2 rounded-lg hover:bg-slate-500/5 transition-colors"> |
| <input type="checkbox" id="step-color" checked class="w-5 h-5 rounded border-slate-700 bg-slate-800 text-yellow-500 focus:ring-yellow-500"> |
| <span class="text-sm font-medium text-theme-secondary group-hover:text-yellow-500 transition-colors">تحسين الألوان</span> |
| </label> |
| <label class="flex items-center gap-3 cursor-pointer group p-2 rounded-lg hover:bg-slate-500/5 transition-colors"> |
| <input type="checkbox" id="step-rmbg" checked class="w-5 h-5 rounded border-slate-700 bg-slate-800 text-yellow-500 focus:ring-yellow-500"> |
| <span class="text-sm font-medium text-theme-secondary group-hover:text-yellow-500 transition-colors">إزالة الخلفية</span> |
| </label> |
| <label class="flex items-center gap-3 cursor-pointer group p-2 rounded-lg hover:bg-slate-500/5 transition-colors"> |
| <input type="checkbox" id="step-crop" checked class="w-5 h-5 rounded border-slate-700 bg-slate-800 text-yellow-500 focus:ring-yellow-500"> |
| <span class="text-sm font-medium text-theme-secondary group-hover:text-yellow-500 transition-colors">قص تلقائي</span> |
| </label> |
|
|
| <div class="border-t my-3" style="border-color: var(--border-card);"></div> |
|
|
| <label class="flex items-center gap-3 cursor-pointer group p-2 rounded-lg hover:bg-slate-500/5 transition-colors"> |
| <input type="checkbox" id="add-studio" checked class="w-5 h-5 rounded border-slate-700 bg-slate-800 text-emerald-500 focus:ring-emerald-500"> |
| <span class="text-sm font-medium text-theme-secondary group-hover:text-emerald-500 transition-colors">اسم الاستوديو</span> |
| </label> |
| <label class="flex items-center gap-3 cursor-pointer group p-2 rounded-lg hover:bg-slate-500/5 transition-colors"> |
| <input type="checkbox" id="add-logo" checked class="w-5 h-5 rounded border-slate-700 bg-slate-800 text-emerald-500 focus:ring-emerald-500"> |
| <span class="text-sm font-medium text-theme-secondary group-hover:text-emerald-500 transition-colors">شعار الاستوديو</span> |
| </label> |
| <label class="flex items-center gap-3 cursor-pointer group p-2 rounded-lg hover:bg-slate-500/5 transition-colors"> |
| <input type="checkbox" id="add-date" checked class="w-5 h-5 rounded border-slate-700 bg-slate-800 text-emerald-500 focus:ring-emerald-500"> |
| <span class="text-sm font-medium text-theme-secondary group-hover:text-emerald-500 transition-colors">إضافة التاريخ</span> |
| </label> |
| </div> |
|
|
| |
| <div class="mb-6"> |
| <button onclick="toggleSettings()" class="w-full justify-between flex items-center gap-2 text-xs text-theme-muted hover:text-yellow-500 transition-colors font-bold p-2 bg-slate-500/5 rounded-lg"> |
| <div class="flex items-center gap-2"> |
| <i class="fa-solid fa-sliders text-xs"></i> |
| <span>إعدادات متقدمة</span> |
| </div> |
| <i class="fa-solid fa-chevron-down text-[10px] transition-transform" id="settings-chevron"></i> |
| </button> |
| <div id="settings-panel" class="settings-panel mt-2"> |
| <div class="p-3 rounded-lg border space-y-4 text-right" style="background: var(--bg-card-alt); border-color: var(--border-card);"> |
| |
| <div> |
| <div class="flex justify-between items-center mb-1"> |
| <label class="text-[10px] font-bold text-theme-muted">حساسية التجميل</label> |
| <span id="sensitivity-val" class="text-[10px] font-mono text-yellow-500">3.0</span> |
| </div> |
| <input type="range" id="setting-sensitivity" min="1" max="5" step="0.5" value="3.0" class="w-full accent-yellow-500 h-1.5" oninput="document.getElementById('sensitivity-val').textContent=this.value"> |
| </div> |
| |
| <div> |
| <div class="flex justify-between items-center mb-1"> |
| <label class="text-[10px] font-bold text-theme-muted">نعومة البشرة</label> |
| <span id="smoothing-val" class="text-[10px] font-mono text-yellow-500">0.6</span> |
| </div> |
| <input type="range" id="setting-smoothing" min="0" max="1" step="0.1" value="0.6" class="w-full accent-yellow-500 h-1.5" oninput="document.getElementById('smoothing-val').textContent=parseFloat(this.value).toFixed(1)"> |
| </div> |
| |
| <div> |
| <div class="flex justify-between items-center mb-1"> |
| <label class="text-[10px] font-bold text-theme-muted">حجم خط الرقم</label> |
| <span id="fontsize-val" class="text-[10px] font-mono text-yellow-500">63</span> |
| </div> |
| <input type="range" id="setting-fontsize" min="30" max="120" step="1" value="63" class="w-full accent-yellow-500 h-1.5" oninput="document.getElementById('fontsize-val').textContent=this.value"> |
| </div> |
| |
| <div> |
| <div class="flex justify-between items-center mb-1"> |
| <label class="text-[10px] font-bold text-theme-muted">حجم خط الاسم</label> |
| <span id="namefontsize-val" class="text-[10px] font-mono text-yellow-500">43</span> |
| </div> |
| <input type="range" id="setting-namefontsize" min="20" max="80" step="1" value="43" class="w-full accent-yellow-500 h-1.5" oninput="document.getElementById('namefontsize-val').textContent=this.value"> |
| </div> |
| <button onclick="saveSettings()" class="w-full py-2 bg-yellow-500 hover:bg-yellow-600 text-slate-900 rounded-lg font-bold text-xs transition-all flex items-center justify-center gap-2"> |
| <i class="fa-solid fa-floppy-disk"></i> حفظ |
| </button> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="flex flex-col gap-2"> |
| <button id="process-all-btn" disabled class="w-full bg-yellow-500 hover:bg-yellow-600 disabled:opacity-50 disabled:cursor-not-allowed text-slate-900 font-bold py-3 rounded-lg transition-all flex items-center justify-center gap-2 text-sm shadow-lg shadow-yellow-500/20"> |
| <i class="fa-solid fa-play"></i> بدء معالجة الكل |
| </button> |
|
|
| <div class="grid grid-cols-2 gap-2 mt-2"> |
| |
| <button id="reprocess-btn" disabled onclick="reprocessCurrent()" title="إعادة معالجة هذه الصورة" class="bg-orange-600 hover:bg-orange-700 disabled:opacity-50 disabled:cursor-not-allowed text-white font-bold py-2 rounded-lg transition-all flex items-center justify-center gap-2 text-xs"> |
| <i class="fa-solid fa-rotate"></i> إعادة |
| </button> |
|
|
| <button id="edit-crop-btn" disabled class="bg-slate-700 hover:bg-slate-600 text-white font-bold py-2 rounded-lg transition-all flex items-center justify-center gap-2 text-xs"> |
| <i class="fa-solid fa-crop-simple"></i> قص |
| </button> |
| </div> |
|
|
| <div class="grid grid-cols-2 gap-2 mt-1"> |
| <button id="save-btn" disabled title="تحميل" class="bg-emerald-600 hover:bg-emerald-700 disabled:opacity-50 text-white font-bold py-2 rounded-lg transition-all"> |
| <i class="fa-solid fa-download"></i> |
| </button> |
| <button id="download-all-btn" disabled title="ضغط الكل" class="bg-blue-600 hover:bg-blue-700 disabled:opacity-50 text-white font-bold py-2 rounded-lg transition-all flex items-center justify-center gap-1"> |
| <i class="fa-solid fa-file-zipper"></i> |
| </button> |
| </div> |
| |
| <button id="clear-all-btn" title="حذف الكل" class="mt-4 w-full bg-transparent border border-rose-600/30 hover:bg-rose-600 hover:text-white text-rose-500 font-bold py-2 rounded-lg transition-all text-xs"> |
| <i class="fa-solid fa-trash-can ml-1"></i> حذف جميع الصور |
| </button> |
| </div> |
|
|
| </div> |
| </div> |
|
|
| </main> |
|
|
| <script> |
| |
| |
| |
| let imageData = []; |
| let currentIndex = -1; |
| let cropper = null; |
| let isAIReady = false; |
| let showingOriginal = false; |
| |
| |
| |
| |
| const fileInput = document.getElementById('file-input'); |
| const imageList = document.getElementById('image-list'); |
| const aiStatus = document.getElementById('ai-status'); |
| const processAllBtn = document.getElementById('process-all-btn'); |
| const downloadAllBtn = document.getElementById('download-all-btn'); |
| const clearAllBtn = document.getElementById('clear-all-btn'); |
| const editBtn = document.getElementById('edit-crop-btn'); |
| const saveBtn = document.getElementById('save-btn'); |
| const reprocessBtn = document.getElementById('reprocess-btn'); |
| const mainPreview = document.getElementById('main-preview'); |
| const previewPlaceholder = document.getElementById('preview-placeholder'); |
| const previewContainer = document.getElementById('preview-container'); |
| const cropperContainer = document.getElementById('cropper-container'); |
| const cropperImg = document.getElementById('cropper-img'); |
| const baToggleWrapper = document.getElementById('ba-toggle-wrapper'); |
| const zoomHint = document.getElementById('zoom-hint'); |
| |
| |
| |
| |
| function initTheme() { |
| const saved = localStorage.getItem('studio-theme') || 'dark'; |
| document.documentElement.setAttribute('data-theme', saved); |
| updateThemeIcon(saved); |
| } |
| function toggleTheme() { |
| const current = document.documentElement.getAttribute('data-theme'); |
| const next = current === 'dark' ? 'light' : 'dark'; |
| document.documentElement.setAttribute('data-theme', next); |
| localStorage.setItem('studio-theme', next); |
| updateThemeIcon(next); |
| } |
| function updateThemeIcon(theme) { |
| const icon = document.getElementById('theme-icon'); |
| if (theme === 'dark') { |
| icon.className = 'fa-solid fa-sun text-yellow-400 text-sm'; |
| } else { |
| icon.className = 'fa-solid fa-moon text-slate-600 text-sm'; |
| } |
| } |
| initTheme(); |
| |
| |
| |
| |
| function showToast(message, type = 'info', duration = 4000) { |
| const container = document.getElementById('toast-container'); |
| const toast = document.createElement('div'); |
| const icons = { success: 'fa-circle-check', error: 'fa-circle-exclamation', info: 'fa-circle-info', warning: 'fa-triangle-exclamation' }; |
| toast.className = `toast toast-${type}`; |
| toast.innerHTML = `<i class="fa-solid ${icons[type] || icons.info}"></i><span>${message}</span>`; |
| container.appendChild(toast); |
| setTimeout(() => { |
| toast.style.animation = 'toastOut 0.3s ease-in forwards'; |
| setTimeout(() => toast.remove(), 300); |
| }, duration); |
| } |
| |
| |
| |
| |
| function toggleSettings() { |
| const panel = document.getElementById('settings-panel'); |
| const chevron = document.getElementById('settings-chevron'); |
| panel.classList.toggle('open'); |
| chevron.style.transform = panel.classList.contains('open') ? 'rotate(180deg)' : ''; |
| } |
| |
| async function loadSettingsFromServer() { |
| try { |
| const res = await fetch('/settings'); |
| const data = await res.json(); |
| if (data.retouch) { |
| if (data.retouch.sensitivity !== undefined) { |
| document.getElementById('setting-sensitivity').value = data.retouch.sensitivity; |
| document.getElementById('sensitivity-val').textContent = data.retouch.sensitivity; |
| } |
| if (data.retouch.tone_smoothing !== undefined) { |
| document.getElementById('setting-smoothing').value = data.retouch.tone_smoothing; |
| document.getElementById('smoothing-val').textContent = parseFloat(data.retouch.tone_smoothing).toFixed(1); |
| } |
| } |
| if (data.overlays) { |
| if (data.overlays.id_font_size !== undefined) { |
| document.getElementById('setting-fontsize').value = data.overlays.id_font_size; |
| document.getElementById('fontsize-val').textContent = data.overlays.id_font_size; |
| } |
| if (data.overlays.name_font_size !== undefined) { |
| document.getElementById('setting-namefontsize').value = data.overlays.name_font_size; |
| document.getElementById('namefontsize-val').textContent = data.overlays.name_font_size; |
| } |
| } |
| } catch (e) { console.warn('Could not load settings', e); } |
| } |
| |
| async function saveSettings() { |
| const payload = { |
| retouch: { |
| sensitivity: parseFloat(document.getElementById('setting-sensitivity').value), |
| tone_smoothing: parseFloat(document.getElementById('setting-smoothing').value) |
| }, |
| overlays: { |
| id_font_size: parseInt(document.getElementById('setting-fontsize').value), |
| name_font_size: parseInt(document.getElementById('setting-namefontsize').value) |
| } |
| }; |
| try { |
| const res = await fetch('/settings', { |
| method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) |
| }); |
| const result = await res.json(); |
| if (result.status === 'success') { |
| showToast('تم حفظ الإعدادات بنجاح', 'success'); |
| } else { |
| showToast('فشل حفظ الإعدادات', 'error'); |
| } |
| } catch (e) { |
| showToast('خطأ في الاتصال بالسيرفر', 'error'); |
| } |
| } |
| loadSettingsFromServer(); |
| |
| |
| |
| |
| function toggleDrawer(open) { |
| document.getElementById('drawer-overlay').classList.toggle('active', open); |
| document.getElementById('mobile-drawer').classList.toggle('active', open); |
| } |
| const dropZoneMobile = document.getElementById('drop-zone-mobile'); |
| if (dropZoneMobile) { |
| dropZoneMobile.onclick = () => fileInput.click(); |
| } |
| |
| |
| |
| |
| async function checkStatus() { |
| try { |
| const res = await fetch('/status'); |
| const data = await res.json(); |
| if (data.ai_ready) { |
| isAIReady = true; |
| aiStatus.innerHTML = '<div class="w-3 h-3 rounded-full bg-emerald-500"></div>'; |
| aiStatus.className = "flex items-center gap-2 px-4 py-2 rounded-full bg-emerald-900/20 border border-emerald-800/50 text-sm text-emerald-400 font-bold"; |
| } |
| } catch (e) {} |
| } |
| setInterval(checkStatus, 2000); |
| |
| |
| |
| |
| const dropZone = document.getElementById('drop-zone'); |
| dropZone.onclick = () => fileInput.click(); |
| |
| ['dragenter', 'dragover'].forEach(name => { |
| dropZone.addEventListener(name, (e) => { e.preventDefault(); dropZone.classList.add('drop-glow'); }, false); |
| }); |
| ['dragleave', 'drop'].forEach(name => { |
| dropZone.addEventListener(name, (e) => { e.preventDefault(); dropZone.classList.remove('drop-glow'); }, false); |
| }); |
| dropZone.addEventListener('drop', (e) => { handleFiles(e.dataTransfer.files); }, false); |
| fileInput.onchange = (e) => handleFiles(e.target.files); |
| |
| function uploadFileWithProgress(file) { |
| return new Promise((resolve, reject) => { |
| const xhr = new XMLHttpRequest(); |
| const formData = new FormData(); |
| formData.append('file', file); |
| const wrappers = [document.getElementById('upload-progress-wrapper'), document.getElementById('upload-progress-wrapper-mobile')]; |
| const fills = [document.getElementById('upload-progress-fill'), document.getElementById('upload-progress-fill-mobile')]; |
| const texts = [document.getElementById('upload-progress-text'), document.getElementById('upload-progress-text-mobile')]; |
| |
| wrappers.forEach(w => { if (w) w.classList.remove('hidden'); }); |
| xhr.upload.onprogress = (e) => { |
| if (e.lengthComputable) { |
| const pct = Math.round((e.loaded / e.total) * 100); |
| fills.forEach(f => { if (f) f.style.width = pct + '%'; }); |
| texts.forEach(t => { if (t) t.textContent = `جاري الرفع... ${pct}%`; }); |
| } |
| }; |
| xhr.onload = () => { |
| wrappers.forEach(w => { if (w) w.classList.add('hidden'); }); |
| fills.forEach(f => { if (f) f.style.width = '0%'; }); |
| if (xhr.status >= 200 && xhr.status < 300) resolve(JSON.parse(xhr.responseText)); |
| else reject(new Error(`Upload failed: ${xhr.status}`)); |
| }; |
| xhr.onerror = () => { |
| wrappers.forEach(w => { if (w) w.classList.add('hidden'); }); |
| reject(new Error('Network error')); |
| }; |
| xhr.open('POST', '/upload'); |
| xhr.send(formData); |
| }); |
| } |
| |
| async function handleFiles(files) { |
| for (let file of files) { |
| try { |
| const data = await uploadFileWithProgress(file); |
| imageData.push({ ...data, name: "", id_num: "", result_url: null, preview_url: null, version: 1, custom_crop: null, steps: null, status: 'waiting' }); |
| renderQueue(); |
| if (currentIndex === -1) selectImage(imageData.length - 1); |
| } catch (e) { |
| showToast(`فشل رفع ${file.name}`, 'error'); |
| } |
| } |
| fileInput.value = ""; |
| } |
| |
| function getQualityBadge(width, height) { |
| const megapixels = (width * height) / 1000000; |
| const minDim = Math.min(width, height); |
| if (minDim >= 1500 || megapixels >= 3) return '<span class="badge-quality badge-hd">HD</span>'; |
| if (minDim >= 800 || megapixels >= 1) return '<span class="badge-quality badge-mid">OK</span>'; |
| return '<span class="badge-quality badge-lowres" title="جودة منخفضة">⚠ LOW</span>'; |
| } |
| |
| |
| |
| |
| function renderQueue() { |
| const count = imageData.length; |
| document.getElementById('queue-count').innerText = `${count} صور`; |
| document.getElementById('nav-counter').innerText = count > 0 ? `${currentIndex + 1}/${count}` : "0/0"; |
| const mobileCount = document.getElementById('mobile-queue-count'); |
| if (mobileCount) mobileCount.innerText = count; |
| |
| if (count === 0) { |
| const emptyHTML = ` |
| <div class="text-center py-12"> |
| <div class="float-anim inline-block mb-4"> |
| <i class="fa-solid fa-layer-group text-3xl opacity-30"></i> |
| </div> |
| <p class="text-theme-muted text-sm font-medium">القائمة فارغة</p> |
| </div>`; |
| imageList.innerHTML = emptyHTML; |
| const mobileList = document.getElementById('mobile-image-list'); |
| if (mobileList) mobileList.innerHTML = `<div class="text-center py-10 text-theme-muted italic text-sm">القائمة فارغة</div>`; |
| processAllBtn.disabled = true; |
| downloadAllBtn.disabled = true; |
| editBtn.disabled = true; |
| saveBtn.disabled = true; |
| reprocessBtn.disabled = true; |
| return; |
| } |
| |
| const html = imageData.map((img, idx) => { |
| let statusIcon = ''; |
| if (img.status === 'waiting') statusIcon = '<i class="fa-regular fa-clock text-slate-500"></i>'; |
| else if (img.status === 'processing') statusIcon = '<i class="fa-solid fa-gear processing-spin text-yellow-500"></i>'; |
| else if (img.status === 'done') statusIcon = '<i class="fa-solid fa-circle-check text-emerald-500"></i>'; |
| else if (img.status === 'error') statusIcon = '<i class="fa-solid fa-circle-exclamation text-rose-500"></i>'; |
| |
| return ` |
| <div onclick="selectImage(${idx})" class="queue-slide ${currentIndex === idx ? 'active' : ''}"> |
| <img src="${img.preview_url ? img.preview_url + '?v=' + img.version : img.thumb_url}" alt=""> |
| <div class="slide-status">${statusIcon}</div> |
| <button onclick="deleteImage(event, ${idx})" class="slide-delete" title="حذف"><i class="fa-solid fa-xmark"></i></button> |
| <div class="slide-name">${img.filename}</div> |
| </div> |
| `}).join(''); |
| |
| imageList.innerHTML = html; |
| const mobileList = document.getElementById('mobile-image-list'); |
| if (mobileList) mobileList.innerHTML = html; |
| |
| |
| const activeSlide = imageList.querySelector('.queue-slide.active'); |
| if (activeSlide) activeSlide.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' }); |
| |
| processAllBtn.disabled = count === 0; |
| const processedCount = imageData.filter(d => d.result_url).length; |
| downloadAllBtn.disabled = processedCount === 0; |
| } |
| |
| |
| |
| |
| function navigate(direction) { |
| if (imageData.length === 0) return; |
| let nextIndex = currentIndex + direction; |
| if (nextIndex >= 0 && nextIndex < imageData.length) selectImage(nextIndex); |
| } |
| |
| function selectImage(idx) { |
| saveCurrentFields(); |
| currentIndex = idx; |
| showingOriginal = false; |
| const data = imageData[idx]; |
| |
| document.getElementById('student-name').value = data.name; |
| document.getElementById('student-id').value = data.id_num; |
| document.getElementById('current-filename').innerText = data.filename; |
| |
| baToggleWrapper.classList.toggle('hidden', !data.result_url); |
| document.getElementById('ba-toggle-label').textContent = 'الأصلية'; |
| zoomHint.classList.toggle('hidden', !data.result_url && !data.thumb_url); |
| reprocessBtn.disabled = !data.result_url; |
| |
| const previewSkeleton = document.getElementById('preview-skeleton'); |
| const url = data.preview_url ? data.preview_url + '?v=' + data.version : |
| data.result_url ? data.result_url + '?v=' + data.version : |
| data.thumb_url; |
| |
| if (!data.result_url) { |
| previewSkeleton.classList.remove('hidden'); |
| } else { |
| previewSkeleton.classList.add('hidden'); |
| } |
| previewPlaceholder.classList.add('hidden'); |
| |
| |
| const tempImg = new Image(); |
| tempImg.onload = () => { |
| mainPreview.src = tempImg.src; |
| mainPreview.classList.remove('hidden'); |
| mainPreview.classList.remove('opacity-0'); |
| mainPreview.classList.add('opacity-100'); |
| previewSkeleton.classList.add('hidden'); |
| }; |
| tempImg.onerror = () => { |
| console.error('Failed to load preview:', url); |
| previewSkeleton.classList.add('hidden'); |
| }; |
| tempImg.src = url; |
| |
| editBtn.disabled = false; |
| saveBtn.disabled = !data.result_url; |
| renderQueue(); |
| toggleDrawer(false); |
| } |
| |
| function saveCurrentFields() { |
| if (currentIndex === -1) return; |
| imageData[currentIndex].name = document.getElementById('student-name').value; |
| imageData[currentIndex].id_num = document.getElementById('student-id').value; |
| } |
| |
| function toggleBeforeAfter() { |
| if (currentIndex === -1) return; |
| const data = imageData[currentIndex]; |
| if (!data.result_url) return; |
| showingOriginal = !showingOriginal; |
| const label = document.getElementById('ba-toggle-label'); |
| const t = new Date().getTime(); |
| mainPreview.classList.add('opacity-0'); |
| setTimeout(() => { |
| if (showingOriginal) { |
| mainPreview.src = data.thumb_url; |
| label.textContent = 'النتيجة'; |
| } else { |
| mainPreview.src = (data.preview_url || data.result_url) + '?v=' + data.version; |
| label.textContent = 'الأصلية'; |
| } |
| mainPreview.onload = () => { |
| mainPreview.classList.remove('opacity-0'); |
| mainPreview.classList.add('opacity-100'); |
| }; |
| }, 150); |
| } |
| |
| |
| |
| |
| function openZoom() { |
| if (!mainPreview.src) return; |
| const modal = document.getElementById('zoom-modal'); |
| const zoomImg = document.getElementById('zoom-img'); |
| zoomImg.src = mainPreview.src; |
| zoomImg.style.transform = 'scale(1)'; |
| modal.classList.add('active'); |
| modal.onwheel = (e) => { |
| e.preventDefault(); |
| const current = parseFloat(zoomImg.style.transform.replace('scale(', '').replace(')', '')) || 1; |
| const delta = e.deltaY > 0 ? -0.15 : 0.15; |
| const next = Math.max(0.5, Math.min(5, current + delta)); |
| zoomImg.style.transform = `scale(${next})`; |
| }; |
| } |
| function closeZoom() { document.getElementById('zoom-modal').classList.remove('active'); } |
| |
| |
| |
| |
| processAllBtn.onclick = async () => { |
| if (!isAIReady) return showToast("انتظر تحميل المحرك...", 'warning'); |
| saveCurrentFields(); |
| processAllBtn.disabled = true; |
| fileInput.disabled = true; |
| |
| const toProcess = imageData.filter(d => !d.result_url); |
| const total = toProcess.length; |
| if (total === 0) { processAllBtn.disabled = false; fileInput.disabled = false; return; } |
| |
| |
| const overlay = document.getElementById('batch-overlay'); |
| const counter = document.getElementById('batch-counter'); |
| overlay.classList.replace('hidden', 'flex'); |
| let done = 0; |
| counter.textContent = `0/${total}`; |
| |
| for (let i = 0; i < imageData.length; i++) { |
| if (imageData[i].result_url) continue; |
| await runPipelineForIndex(i); |
| done++; |
| counter.textContent = `${done}/${total}`; |
| } |
| |
| |
| overlay.classList.replace('flex', 'hidden'); |
| processAllBtn.disabled = false; |
| fileInput.disabled = false; |
| |
| |
| if (currentIndex !== -1) selectImage(currentIndex); |
| showToast(`تمت معالجة ${done} صورة`, 'success'); |
| }; |
| |
| async function runPipelineForIndex(idx, isCustom = false) { |
| const data = imageData[idx]; |
| data.status = 'processing'; |
| renderQueue(); |
| const formData = new FormData(); |
| formData.append('name', data.name); |
| formData.append('id_number', data.id_num); |
| formData.append('do_rmbg', document.getElementById('step-rmbg').checked); |
| formData.append('do_color', document.getElementById('step-color').checked); |
| formData.append('do_retouch', document.getElementById('step-retouch').checked); |
| formData.append('do_crop', document.getElementById('step-crop').checked); |
| formData.append('add_studio_name', document.getElementById('add-studio').checked); |
| formData.append('add_logo', document.getElementById('add-logo').checked); |
| formData.append('add_date', document.getElementById('add-date').checked); |
| if (isCustom && data.custom_crop) { |
| formData.append('x1', data.custom_crop.x1); |
| formData.append('y1', data.custom_crop.y1); |
| formData.append('x2', data.custom_crop.x2); |
| formData.append('y2', data.custom_crop.y2); |
| } |
| try { |
| const res = await fetch(`/process/${data.id}`, { method: 'POST', body: formData }); |
| const result = await res.json(); |
| if (result.error) throw new Error(result.error); |
| imageData[idx].result_url = result.result_url; |
| imageData[idx].preview_url = result.preview_url; |
| imageData[idx].version++; |
| imageData[idx].status = 'done'; |
| imageData[idx].steps = { |
| rmbg: document.getElementById('step-rmbg').checked, |
| color: document.getElementById('step-color').checked, |
| retouch: document.getElementById('step-retouch').checked, |
| crop: document.getElementById('step-crop').checked || !!data.custom_crop |
| }; |
| } catch (e) { |
| console.error("Processing", e); |
| imageData[idx].status = 'error'; |
| showToast(`خطأ في ${data.filename}`, 'error'); |
| } |
| renderQueue(); |
| } |
| |
| async function reprocessCurrent() { |
| if (currentIndex === -1 || !isAIReady) return; |
| saveCurrentFields(); |
| const data = imageData[currentIndex]; |
| data.result_url = null; |
| data.status = 'waiting'; |
| data.steps = null; |
| renderQueue(); |
| await runPipelineForIndex(currentIndex, !!data.custom_crop); |
| selectImage(currentIndex); |
| showToast('تمت إعادة المعالجة', 'success'); |
| } |
| |
| |
| |
| |
| editBtn.onclick = () => { |
| const data = imageData[currentIndex]; |
| cropperImg.src = data.thumb_url; |
| previewContainer.classList.add('hidden'); |
| baToggleWrapper.classList.add('hidden'); |
| zoomHint.classList.add('hidden'); |
| cropperContainer.classList.remove('hidden'); |
| if (cropper) cropper.destroy(); |
| cropper = new Cropper(cropperImg, { aspectRatio: 5/7, viewMode: 1, autoCropArea: 0.8 }); |
| }; |
| function cancelCrop() { |
| cropperContainer.classList.add('hidden'); |
| previewContainer.classList.remove('hidden'); |
| if (currentIndex >= 0 && imageData[currentIndex].result_url) baToggleWrapper.classList.remove('hidden'); |
| zoomHint.classList.remove('hidden'); |
| } |
| async function applyCrop() { |
| const cropData = cropper.getData(true); |
| const imgData = imageData[currentIndex]; |
| const displayImg = cropper.getImageData(); |
| const scaleX = imgData.width / displayImg.naturalWidth; |
| const scaleY = imgData.height / displayImg.naturalHeight; |
| imgData.custom_crop = { |
| x1: Math.max(0, Math.round(cropData.x * scaleX)), |
| y1: Math.max(0, Math.round(cropData.y * scaleY)), |
| x2: Math.min(imgData.width, Math.round((cropData.x + cropData.width) * scaleX)), |
| y2: Math.min(imgData.height, Math.round((cropData.y + cropData.height) * scaleY)) |
| }; |
| cancelCrop(); |
| imgData.result_url = null; |
| imgData.status = 'waiting'; |
| await runPipelineForIndex(currentIndex, true); |
| selectImage(currentIndex); |
| } |
| |
| downloadAllBtn.onclick = async () => { |
| const processed = imageData.filter(d => d.result_url); |
| if (processed.length === 0) return; |
| const overlay = document.getElementById('batch-overlay'); |
| const counter = document.getElementById('batch-counter'); |
| overlay.classList.replace('hidden', 'flex'); |
| counter.textContent = 'ZIP...'; |
| const zip = new JSZip(); |
| const folder = zip.folder("studio_layouts"); |
| for (let data of processed) { |
| const res = await fetch(data.result_url); |
| const blob = await res.blob(); |
| folder.file(`${PathStem(data.filename)}_layout.jpg`, blob); |
| } |
| const content = await zip.generateAsync({ type: "blob" }); |
| const link = document.createElement('a'); |
| link.href = URL.createObjectURL(content); |
| link.download = `Studio_Batch_${new Date().getTime()}.zip`; |
| link.click(); |
| overlay.classList.replace('flex', 'hidden'); |
| showToast(`تم تحميل ${processed.length} صورة`, 'success'); |
| }; |
| |
| clearAllBtn.onclick = async () => { |
| if (!confirm("هل تريد حذف جميع الصور؟")) return; |
| try { |
| const res = await fetch('/clear-all', { method: 'POST' }); |
| imageData = []; |
| currentIndex = -1; |
| fileInput.value = ""; |
| mainPreview.classList.add('hidden'); |
| previewPlaceholder.classList.remove('hidden'); |
| baToggleWrapper.classList.add('hidden'); |
| zoomHint.classList.add('hidden'); |
| document.getElementById('student-name').value = ""; |
| document.getElementById('student-id').value = ""; |
| document.getElementById('current-filename').innerText = ""; |
| renderQueue(); |
| showToast("تم الحذف", 'success'); |
| } catch (e) {} |
| }; |
| |
| function deleteImage(e, idx) { |
| e.stopPropagation(); |
| if(!confirm("حذف الصورة؟")) return; |
| imageData.splice(idx, 1); |
| if (imageData.length === 0) { |
| currentIndex = -1; |
| mainPreview.classList.add('hidden'); |
| previewPlaceholder.classList.remove('hidden'); |
| baToggleWrapper.classList.add('hidden'); |
| zoomHint.classList.add('hidden'); |
| document.getElementById('student-name').value = ""; |
| } else { |
| if (currentIndex === idx) { |
| currentIndex = Math.max(0, currentIndex - 1); |
| selectImage(currentIndex); |
| } else if (currentIndex > idx) { |
| currentIndex--; |
| } |
| } |
| renderQueue(); |
| } |
| |
| |
| function deleteSelected() { |
| if (currentIndex !== -1) deleteImage({stopPropagation:()=>{}}, currentIndex); |
| } |
| |
| saveBtn.onclick = () => { |
| const link = document.createElement('a'); |
| link.href = mainPreview.src; |
| link.download = `${PathStem(imageData[currentIndex].filename)}_layout.jpg`; |
| link.click(); |
| }; |
| |
| function PathStem(filename) { return filename.substring(0, filename.lastIndexOf('.')) || filename; } |
| |
| window.addEventListener('keydown', (e) => { |
| if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return; |
| if (e.key === 'Escape') { closeZoom(); document.getElementById('shortcuts-modal').classList.remove('active'); toggleDrawer(false); return; } |
| if (e.key === 'ArrowRight') navigate(1); |
| else if (e.key === 'ArrowLeft') navigate(-1); |
| else if (e.key === 'Delete') deleteSelected(); |
| else if (e.key === 'Enter') { saveCurrentFields(); if (currentIndex < imageData.length - 1) navigate(1); } |
| else if (e.key === 's' && (e.ctrlKey || e.metaKey)) { e.preventDefault(); saveCurrentFields(); runPipelineForIndex(currentIndex); } |
| }); |
| </script> |
| </body> |
| </html> |
|
|