id / web /web_storage /index.html
Esmaill1
Optimize caching and further reduce asset sizes for better UI responsiveness
08a45b9
Raw
History Blame Contribute Delete
70.5 kB
<!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">
<!-- Cropper.js -->
<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>
<!-- JSZip for Download All -->
<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');
/* ── Theme Variables ── */
: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; }
/* Indicators & Glow */
.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); }
/* Skeleton Animation */
@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 Notifications ── */
#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 ── */
.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 ── */
#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 ── */
.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 ── */
.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);
}
/* RTL: drawer slides from left */
[dir="rtl"] .mobile-drawer { left: 0; right: auto; transform: translateX(-100%); }
.mobile-drawer.active { transform: translateX(0) !important; }
/* ── Before/After Toggle ── */
.ba-toggle {
position: absolute; top: 12px; left: 12px; z-index: 10;
backdrop-filter: blur(8px);
}
/* ── Empty State Animation ── */
@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; }
/* ── Keyboard Shortcuts Modal ── */
#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; }
/* Horizontal queue slider */
.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">
<!-- Toast Container -->
<div id="toast-container"></div>
<!-- Zoom Modal (#14) -->
<div id="zoom-modal" onclick="closeZoom()">
<img id="zoom-img" src="" alt="Zoomed preview">
</div>
<!-- Keyboard Shortcuts Modal -->
<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">&larr; &rarr;</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 &amp; 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>
<!-- Mobile Drawer Overlay -->
<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 -->
<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>
<!-- Theme Toggle (#11) -->
<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>
<!-- Keyboard Shortcuts Hint -->
<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">
<!-- Mobile Queue Toggle Button (#10) -->
<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>
<!-- ── COLUMN 1: QUEUE (RIGHT SIDE) ── -->
<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>
<!-- Mobile Drop Zone (shown only on mobile when drawer is closed) -->
<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>
<!-- ── COLUMN 2: CENTER (inputs + preview) ── -->
<div class="lg:col-span-6 flex flex-col gap-6 order-2">
<!-- Data Input Card -->
<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>
<!-- Moved navigation here -->
<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>
<!-- Editor/Preview Area -->
<div class="studio-card rounded-2xl p-2 flex-grow relative min-h-[450px] flex items-center justify-center overflow-hidden" id="main-area">
<!-- Before/After Toggle (#3) -->
<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>
<!-- Zoom Hint (#14) -->
<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>
<!-- Cropper Mode -->
<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>
<!-- Preview Mode -->
<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()">
<!-- Improved Empty State (#13) -->
<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>
<!-- Batch Processing Overlay -->
<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>
<!-- ── COLUMN 3: OPTIONS (LEFT SIDE) ── -->
<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>
<!-- Processing Steps -->
<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>
<!-- Settings Panel (#5) - Always visible in side column? Or collapsible? Let's keep collapsible but cleaner -->
<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);">
<!-- Retouch Sensitivity -->
<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>
<!-- Tone Smoothing -->
<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>
<!-- ID Font Size -->
<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>
<!-- Name Font Size -->
<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>
<!-- Actions List -->
<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">
<!-- Re-process Single (#7) -->
<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>
// ══════════════════════════════════════════════════
// STATE
// ══════════════════════════════════════════════════
let imageData = [];
let currentIndex = -1;
let cropper = null;
let isAIReady = false;
let showingOriginal = false; // Before/After state
// ══════════════════════════════════════════════════
// UI ELEMENTS
// ══════════════════════════════════════════════════
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');
// ══════════════════════════════════════════════════
// THEME TOGGLE (#11)
// ══════════════════════════════════════════════════
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();
// ══════════════════════════════════════════════════
// TOAST NOTIFICATIONS
// ══════════════════════════════════════════════════
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);
}
// ══════════════════════════════════════════════════
// SETTINGS PANEL (#5)
// ══════════════════════════════════════════════════
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();
// ══════════════════════════════════════════════════
// MOBILE DRAWER (#10)
// ══════════════════════════════════════════════════
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();
}
// ══════════════════════════════════════════════════
// AI STATUS CHECK
// ══════════════════════════════════════════════════
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);
// ══════════════════════════════════════════════════
// UPLOAD & QUEUE
// ══════════════════════════════════════════════════
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>';
}
// ══════════════════════════════════════════════════
// QUEUE RENDER - WITH DELETE BUTTON
// ══════════════════════════════════════════════════
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;
// Auto-scroll to the active slide
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;
}
// ══════════════════════════════════════════════════
// SELECTION & NAVIGATION
// ══════════════════════════════════════════════════
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');
// Load new image
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);
}
// ══════════════════════════════════════════════════
// ZOOM
// ══════════════════════════════════════════════════
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'); }
// ══════════════════════════════════════════════════
// PROCESSING
// ══════════════════════════════════════════════════
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; }
// Show batch overlay with counter
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}`;
}
// Hide overlay
overlay.classList.replace('flex', 'hidden');
processAllBtn.disabled = false;
fileInput.disabled = false;
// Refresh preview to show result of currently selected image
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');
}
// ══════════════════════════════════════════════════
// CROP & DOWNLOAD
// ══════════════════════════════════════════════════
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();
}
// Global Delete function for header button (keep for keyboard/compatibility)
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>