stat2025 commited on
Commit
15427c1
·
verified ·
1 Parent(s): 592de21

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +138 -80
index.html CHANGED
@@ -2,7 +2,7 @@
2
  <html lang="ar" dir="rtl">
3
  <head>
4
  <meta charset="UTF-8" />
5
- <title>أداة دمج ملفات المجموعة | PDF واحد بسهولة</title>
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
  <link rel="stylesheet" href="style.css" />
8
  <script src="https://unpkg.com/pdf-lib@1.17.1/dist/pdf-lib.min.js"></script>
@@ -31,8 +31,8 @@
31
  <section class="hero">
32
  <h1>ادمج ملفاتك بسهولة في ملف PDF واحد منسّق.</h1>
33
  <p>
34
- اختر مجموعة من الصور أو ملفات PDF، وستقوم الأداة بإنشاء ملف PDF واحد مرتب حسب أسماء الملفات،
35
- ليسهل حفظه ومشاركته بين أعضاء المجموعة.
36
  </p>
37
  </section>
38
 
@@ -40,15 +40,15 @@
40
  <section class="steps">
41
  <div class="step">
42
  <span class="step-number">1</span>
43
- <span class="step-text">اختر ملفاتك (يمكن الإضافة على دفعات من الجوال أو الكمبيوتر).</span>
44
  </div>
45
  <div class="step">
46
  <span class="step-number">2</span>
47
- <span class="step-text">تأكد أن الملفات: صور فقط أو PDF فقط في نفس العملية.</span>
48
  </div>
49
  <div class="step">
50
  <span class="step-number">3</span>
51
- <span class="step-text">اضغط "دمج" لتحميل ملف PDF النهائي مباشرة.</span>
52
  </div>
53
  </section>
54
 
@@ -60,15 +60,16 @@
60
  <h2 class="card-title">اختيار الملفات</h2>
61
  <p class="hint">
62
  يدعم:
63
- <strong>دمج عدة ملفات PDF</strong> في ملف واحد،
 
64
  أو
65
- <strong>تحويل عدة صور (JPG / PNG)</strong> إلى ملف PDF واحد.
66
- لا تخلط بين الصور وملفات PDF في عملية واحدة لتفادي الخطأ.
67
  </p>
68
 
69
  <label class="file-picker">
70
  <span class="file-picker-icon">📂</span>
71
- <span class="file-picker-text">اضغط هنا لاختيار الملفات من جهازك</span>
72
  <input id="files" type="file" multiple accept=".pdf,image/*" />
73
  </label>
74
  </div>
@@ -107,15 +108,45 @@
107
  const fileListDiv = document.getElementById("fileList");
108
  const outputNameInput = document.getElementById("outputName");
109
 
110
- // مصفوفة للاحتفاظ بكل الملفات المختارة (من عدة مرات اختيار)
111
  let selectedFiles = [];
112
 
 
 
113
  function setStatus(msg, type = "") {
114
  statusDiv.textContent = msg;
115
  statusDiv.className = "status" + (type ? " " + type : "");
116
  if (!msg) statusDiv.className = "status";
117
  }
118
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
  function renderFileList(files) {
120
  if (!files.length) {
121
  fileListDiv.classList.add("hidden");
@@ -124,11 +155,25 @@
124
  }
125
 
126
  fileListDiv.classList.remove("hidden");
 
 
 
 
 
 
 
 
 
 
 
 
 
127
  fileListDiv.innerHTML = `
128
  <div class="file-list-header">
129
  <span>الملفات المختارة: ${files.length}</span>
130
- <span class="file-note">يمكنك حذف أي ملف قبل الدمج أو مسح الكل.</span>
131
  </div>
 
132
  <ul class="file-list-ul">
133
  ${files
134
  .map(
@@ -137,37 +182,56 @@
137
  <span class="index">${i + 1}</span>
138
  <span class="name" title="${f.name}">${f.name}</span>
139
  <span class="size">${(f.size / 1024).toFixed(1)} كيلوبايت</span>
140
- <button class="delete-btn" data-index="${i}" title="حذف هذا الملف">🗑️</button>
 
 
 
 
141
  </li>`
142
  )
143
  .join("")}
144
  </ul>
145
  `;
146
 
147
- // أزرار الحذف لكل ملف
 
 
 
 
 
 
 
 
148
  fileListDiv.querySelectorAll(".delete-btn").forEach((btn) => {
149
  btn.addEventListener("click", (e) => {
150
  const index = parseInt(e.currentTarget.dataset.index, 10);
151
  if (!isNaN(index)) {
152
  selectedFiles.splice(index, 1);
153
  renderFileList(selectedFiles);
154
- setStatus("");
155
  }
156
  });
157
  });
158
- }
159
 
160
- function detectMode(files) {
161
- if (!files.length) return null;
162
- const allImages = files.every((f) => f.type.startsWith("image/"));
163
- const allPDFs = files.every(
164
- (f) =>
165
- f.type === "application/pdf" ||
166
- f.name.toLowerCase().endsWith(".pdf")
167
- );
168
- if (allImages) return "images";
169
- if (allPDFs) return "pdfs";
170
- return null;
 
 
 
 
 
 
 
 
171
  }
172
 
173
  function downloadPdf(bytes, filename) {
@@ -182,25 +246,26 @@
182
  URL.revokeObjectURL(url);
183
  }
184
 
185
- // إضافة ملفات على دفعات (مهم للجوال)
186
  filesInput.addEventListener("change", () => {
187
- const newFiles = Array.from(filesInput.files || []);
188
- if (!newFiles.length) return;
 
 
 
189
 
190
- // دمج بدون تكرار (اسم + حجم + وقت تعديل)
191
  const map = new Map();
192
  [...selectedFiles, ...newFiles].forEach((f) => {
193
  const key = `${f.name}|${f.size}|${f.lastModified}`;
194
  if (!map.has(key)) map.set(key, f);
195
  });
196
 
197
- selectedFiles = Array.from(map.values()).sort((a, b) =>
198
- a.name.localeCompare(b.name, undefined, { numeric: true })
199
- );
200
-
201
  renderFileList(selectedFiles);
202
  setStatus("");
203
- filesInput.value = ""; // للس��اح باختيار دفعات جديدة
 
 
204
  });
205
 
206
  // زر مسح جميع الملفات
@@ -212,28 +277,28 @@
212
  outputNameInput.value = "";
213
  });
214
 
215
- // تنفيذ الدمج
216
  mergeBtn.addEventListener("click", async () => {
217
- let files = [...selectedFiles];
218
 
219
  if (!files.length) {
220
  setStatus("الرجاء اختيار الملفات أولاً.", "error");
221
  return;
222
  }
223
 
224
- const mode = detectMode(files);
225
- if (!mode) {
226
- setStatus(
227
- "تنبيه: يجب أن تكون جميع الملفات صورًا فقط أو جميعها PDF فقط في العملية الواحدة.",
228
- "error"
229
- );
 
 
 
 
230
  return;
231
  }
232
 
233
- // ترتيب حسب الاسم
234
- files.sort((a, b) =>
235
- a.name.localeCompare(b.name, undefined, { numeric: true })
236
- );
237
  renderFileList(files);
238
 
239
  try {
@@ -241,13 +306,22 @@
241
  mergeBtn.disabled = true;
242
  mergeBtn.classList.add("disabled");
243
 
244
- if (mode === "images") {
245
- const pdfDoc = await PDFLib.PDFDocument.create();
246
 
247
- for (const file of files) {
248
- const bytes = await file.arrayBuffer();
249
- const lower = file.name.toLowerCase();
250
 
 
 
 
 
 
 
 
 
 
 
 
251
  let image;
252
  if (
253
  file.type === "image/jpeg" ||
@@ -274,41 +348,25 @@
274
 
275
  page.drawImage(image, { x, y, width: drawWidth, height: drawHeight });
276
  }
277
-
278
- const pdfBytes = await pdfDoc.save();
279
- const outName =
280
- (outputNameInput.value || "merged-images.pdf").trim() ||
281
- "merged-images.pdf";
282
- downloadPdf(pdfBytes, outName);
283
- setStatus("تم إنشاء ملف PDF من الصور بنجاح.", "ok");
284
  }
285
 
286
- if (mode === "pdfs") {
287
- const pdfDoc = await PDFLib.PDFDocument.create();
288
 
289
- for (const file of files) {
290
- const bytes = await file.arrayBuffer();
291
- const donorPdf = await PDFLib.PDFDocument.load(bytes);
292
- const pages = await pdfDoc.copyPages(
293
- donorPdf,
294
- donorPdf.getPageIndices()
295
- );
296
- pages.forEach((p) => pdfDoc.addPage(p));
297
- }
298
 
299
- const pdfBytes = await pdfDoc.save();
300
- const outName =
301
- (outputNameInput.value || "merged-pdfs.pdf").trim() ||
302
- "merged-pdfs.pdf";
303
- downloadPdf(pdfBytes, outName);
304
- setStatus("تم دمج ملفات PDF بنجاح.", "ok");
305
- }
306
 
307
- // تنظيف بعد الدمج
308
  selectedFiles = [];
309
  renderFileList([]);
310
  filesInput.value = "";
311
- // لا نمسح اسم الملف؛ يمكن استخدامه مرة أخرى لو أحب
312
  } catch (err) {
313
  console.error(err);
314
  setStatus("حدث خطأ أثناء المعالجة. تأكد من الملفات وحاول مرة أخرى.", "error");
 
2
  <html lang="ar" dir="rtl">
3
  <head>
4
  <meta charset="UTF-8" />
5
+ <title>إدمج ملفاتك بكل سهولة</title>
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
  <link rel="stylesheet" href="style.css" />
8
  <script src="https://unpkg.com/pdf-lib@1.17.1/dist/pdf-lib.min.js"></script>
 
31
  <section class="hero">
32
  <h1>ادمج ملفاتك بسهولة في ملف PDF واحد منسّق.</h1>
33
  <p>
34
+ اختر صورًا، ملفات PDF، أو مزيجًا من الاثنين، وستقوم الأداة بإنشاء ملف PDF واحد
35
+ وفق الترتيب الظاهر في القائمة، مناسب لمشاركات وتقارير المجموعة.
36
  </p>
37
  </section>
38
 
 
40
  <section class="steps">
41
  <div class="step">
42
  <span class="step-number">1</span>
43
+ <span class="step-text">اختر الملفات (يمكن الإضافة على دفعات من الجوال أو الكمبيوتر).</span>
44
  </div>
45
  <div class="step">
46
  <span class="step-number">2</span>
47
+ <span class="step-text">رتّب الملفات، احذف ما لا تحتاجه.</span>
48
  </div>
49
  <div class="step">
50
  <span class="step-number">3</span>
51
+ <span class="step-text">اضغط دمج لتحميل ملف PDF النهائي مباشرة.</span>
52
  </div>
53
  </section>
54
 
 
60
  <h2 class="card-title">اختيار الملفات</h2>
61
  <p class="hint">
62
  يدعم:
63
+ <strong>ملفات PDF</strong>،
64
+ <strong>صور JPG / PNG</strong>،
65
  أو
66
+ <strong>دمج الاثنين معًا</strong> في ملف PDF واحد.
67
+ يتم الدمج بالترتيب في القائمة أدناه.
68
  </p>
69
 
70
  <label class="file-picker">
71
  <span class="file-picker-icon">📂</span>
72
+ <span class="file-picker-text">اضغط هنا لاختيار الملفات من جهازك (يمكن التكرار)</span>
73
  <input id="files" type="file" multiple accept=".pdf,image/*" />
74
  </label>
75
  </div>
 
108
  const fileListDiv = document.getElementById("fileList");
109
  const outputNameInput = document.getElementById("outputName");
110
 
111
+ // نحتفظ بالملفات المختارة (من عدة دفعات)
112
  let selectedFiles = [];
113
 
114
+ const MAX_RECOMMENDED_FILES = 200; // للتنبيه فقط
115
+
116
  function setStatus(msg, type = "") {
117
  statusDiv.textContent = msg;
118
  statusDiv.className = "status" + (type ? " " + type : "");
119
  if (!msg) statusDiv.className = "status";
120
  }
121
 
122
+ function isImage(file) {
123
+ const name = file.name.toLowerCase();
124
+ return (
125
+ file.type.startsWith("image/") ||
126
+ name.endsWith(".jpg") ||
127
+ name.endsWith(".jpeg") ||
128
+ name.endsWith(".png")
129
+ );
130
+ }
131
+
132
+ function isPDF(file) {
133
+ const name = file.name.toLowerCase();
134
+ return (
135
+ file.type === "application/pdf" ||
136
+ name.endsWith(".pdf")
137
+ );
138
+ }
139
+
140
+ function getFilesInfo(files) {
141
+ let hasImages = false;
142
+ let hasPDFs = false;
143
+ files.forEach((f) => {
144
+ if (isImage(f)) hasImages = true;
145
+ else if (isPDF(f)) hasPDFs = true;
146
+ });
147
+ return { hasImages, hasPDFs };
148
+ }
149
+
150
  function renderFileList(files) {
151
  if (!files.length) {
152
  fileListDiv.classList.add("hidden");
 
155
  }
156
 
157
  fileListDiv.classList.remove("hidden");
158
+
159
+ const { hasImages, hasPDFs } = getFilesInfo(files);
160
+ let modeText = "";
161
+ if (hasImages && hasPDFs) {
162
+ modeText = "الوضع الحالي: دمج صور + ملفات PDF في ملف واحد حسب الترتيب.";
163
+ } else if (hasPDFs) {
164
+ modeText = "الوضع الحالي: دمج ملفات PDF في ملف واحد.";
165
+ } else if (hasImages) {
166
+ modeText = "الوضع الحالي: تحويل صور إلى ملف PDF واحد.";
167
+ } else {
168
+ modeText = "لا توجد ملفات مدعومة في القائمة.";
169
+ }
170
+
171
  fileListDiv.innerHTML = `
172
  <div class="file-list-header">
173
  <span>الملفات المختارة: ${files.length}</span>
174
+ <span class="file-note">يمكنك إعادة ترتيب الملفات أو حذف أي ملف قبل الدمج.</span>
175
  </div>
176
+ <div class="mode-label">${modeText}</div>
177
  <ul class="file-list-ul">
178
  ${files
179
  .map(
 
182
  <span class="index">${i + 1}</span>
183
  <span class="name" title="${f.name}">${f.name}</span>
184
  <span class="size">${(f.size / 1024).toFixed(1)} كيلوبايت</span>
185
+ <div class="row-actions">
186
+ <button class="move-btn" data-index="${i}" data-dir="up" title="نقل لأعلى">▲</button>
187
+ <button class="move-btn" data-index="${i}" data-dir="down" title="نقل لأسفل">▼</button>
188
+ <button class="delete-btn" data-index="${i}" title="حذف هذا الملف">🗑️</button>
189
+ </div>
190
  </li>`
191
  )
192
  .join("")}
193
  </ul>
194
  `;
195
 
196
+ // تحذير إذا العدد كبير
197
+ if (files.length > MAX_RECOMMENDED_FILES) {
198
+ setStatus(
199
+ "تنبيه: عدد الملفات كبير، قد تستغرق عملية الدمج وقتًا أطول على بعض الأجهزة.",
200
+ "warning"
201
+ );
202
+ }
203
+
204
+ // ربط أزرار الحذف
205
  fileListDiv.querySelectorAll(".delete-btn").forEach((btn) => {
206
  btn.addEventListener("click", (e) => {
207
  const index = parseInt(e.currentTarget.dataset.index, 10);
208
  if (!isNaN(index)) {
209
  selectedFiles.splice(index, 1);
210
  renderFileList(selectedFiles);
211
+ if (!selectedFiles.length) setStatus("");
212
  }
213
  });
214
  });
 
215
 
216
+ // ربط أزرار إعادة الترتيب
217
+ fileListDiv.querySelectorAll(".move-btn").forEach((btn) => {
218
+ btn.addEventListener("click", (e) => {
219
+ const index = parseInt(e.currentTarget.dataset.index, 10);
220
+ const dir = e.currentTarget.dataset.dir;
221
+ if (isNaN(index)) return;
222
+
223
+ if (dir === "up" && index > 0) {
224
+ const tmp = selectedFiles[index - 1];
225
+ selectedFiles[index - 1] = selectedFiles[index];
226
+ selectedFiles[index] = tmp;
227
+ } else if (dir === "down" && index < selectedFiles.length - 1) {
228
+ const tmp = selectedFiles[index + 1];
229
+ selectedFiles[index + 1] = selectedFiles[index];
230
+ selectedFiles[index] = tmp;
231
+ }
232
+ renderFileList(selectedFiles);
233
+ });
234
+ });
235
  }
236
 
237
  function downloadPdf(bytes, filename) {
 
246
  URL.revokeObjectURL(url);
247
  }
248
 
249
+ // إضافة ملفات على دفعات
250
  filesInput.addEventListener("change", () => {
251
+ const newFilesRaw = Array.from(filesInput.files || []);
252
+ if (!newFilesRaw.length) return;
253
+
254
+ // نسمح فقط بالصور و PDF، نتجاهل غيرها
255
+ const newFiles = newFilesRaw.filter((f) => isImage(f) || isPDF(f));
256
 
 
257
  const map = new Map();
258
  [...selectedFiles, ...newFiles].forEach((f) => {
259
  const key = `${f.name}|${f.size}|${f.lastModified}`;
260
  if (!map.has(key)) map.set(key, f);
261
  });
262
 
263
+ selectedFiles = Array.from(map.values());
 
 
 
264
  renderFileList(selectedFiles);
265
  setStatus("");
266
+
267
+ // إفراغ input للسماح بالاختيار مرة أخرى
268
+ filesInput.value = "";
269
  });
270
 
271
  // زر مسح جميع الملفات
 
277
  outputNameInput.value = "";
278
  });
279
 
280
+ // عملية الدمج
281
  mergeBtn.addEventListener("click", async () => {
282
+ const files = [...selectedFiles];
283
 
284
  if (!files.length) {
285
  setStatus("الرجاء اختيار الملفات أولاً.", "error");
286
  return;
287
  }
288
 
289
+ // تأكيد أن كل الملفات مدعومة
290
+ const unsupported = files.filter((f) => !isImage(f) && !isPDF(f));
291
+ if (unsupported.length) {
292
+ setStatus("يوجد ملفات غير مدعومة. يرجى حذفها من القائمة.", "error");
293
+ return;
294
+ }
295
+
296
+ const { hasImages, hasPDFs } = getFilesInfo(files);
297
+ if (!hasImages && !hasPDFs) {
298
+ setStatus("لا توجد ملفات مدعومة للدمج.", "error");
299
  return;
300
  }
301
 
 
 
 
 
302
  renderFileList(files);
303
 
304
  try {
 
306
  mergeBtn.disabled = true;
307
  mergeBtn.classList.add("disabled");
308
 
309
+ const pdfDoc = await PDFLib.PDFDocument.create();
 
310
 
311
+ for (const file of files) {
312
+ const bytes = await file.arrayBuffer();
 
313
 
314
+ if (isPDF(file)) {
315
+ // نسخ صفحات PDF كما هي
316
+ const donorPdf = await PDFLib.PDFDocument.load(bytes);
317
+ const pages = await pdfDoc.copyPages(
318
+ donorPdf,
319
+ donorPdf.getPageIndices()
320
+ );
321
+ pages.forEach((p) => pdfDoc.addPage(p));
322
+ } else if (isImage(file)) {
323
+ // إضافة الصورة في صفحة A4
324
+ const lower = file.name.toLowerCase();
325
  let image;
326
  if (
327
  file.type === "image/jpeg" ||
 
348
 
349
  page.drawImage(image, { x, y, width: drawWidth, height: drawHeight });
350
  }
 
 
 
 
 
 
 
351
  }
352
 
353
+ const pdfBytes = await pdfDoc.save();
 
354
 
355
+ let defaultName = "merged.pdf";
356
+ if (hasImages && !hasPDFs) defaultName = "merged-images.pdf";
357
+ else if (!hasImages && hasPDFs) defaultName = "merged-pdfs.pdf";
358
+ else if (hasImages && hasPDFs) defaultName = "merged-mixed.pdf";
 
 
 
 
 
359
 
360
+ const outName =
361
+ (outputNameInput.value || defaultName).trim() || defaultName;
362
+
363
+ downloadPdf(pdfBytes, outName);
364
+ setStatus("تم إنشاء ملف PDF النهائي بنجاح.", "ok");
 
 
365
 
366
+ // لا نفرغ الاسم، لكن نفرغ الملفات
367
  selectedFiles = [];
368
  renderFileList([]);
369
  filesInput.value = "";
 
370
  } catch (err) {
371
  console.error(err);
372
  setStatus("حدث خطأ أثناء المعالجة. تأكد من الملفات وحاول مرة أخرى.", "error");