@vcgstudiosy-beep/dlof 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,6 +4,7 @@ exports.renderPlayerHtml = renderPlayerHtml;
4
4
  const theme_1 = require("../viewer/theme");
5
5
  const renderContent_1 = require("../viewer/renderContent");
6
6
  const loopUtils_1 = require("../loop/loopUtils");
7
+ const brand_1 = require("../viewer/brand");
7
8
  const DEFAULT_MAX_INLINE = 20 * 1024 * 1024;
8
9
  const MIME_BY_EXT = {
9
10
  mp4: "video/mp4",
@@ -28,10 +29,12 @@ function isAudio(mime) {
28
29
  return mime.startsWith("audio/");
29
30
  }
30
31
  /**
31
- * مشغّل dlof: يبني تطبيق HTML/JS تفاعلي واحد لسلسلة أو حلقة كاملة من ملفات .dlof.
32
- * يوفر: قائمة تصفح جانبية، أزرار التالي/السابق (تتبع loopLinks)، تشغيل الوسائط
33
- * المرجعية (mediaRef / mediaFolder) مع شريط تقدّم قابل للسحب، وحفظ آخر موضع
34
- * تصفح في التخزين المحلي للمتصفح بين الجلسات.
32
+ * مشغّل dlof: يبني تطبيق HTML/JS تفاعلي واحد لسلسلة أو حلقة كاملة من ملفات .dlof،
33
+ * بنفس الهوية البصرية لتطبيق DLoF الأصلي. يوفر: قائمة "الملفات" الجانبية (مطابقة
34
+ * لشاشة "ملفاتي" في التطبيق: بطاقات بأيقونة/لون حسب نوع المحتوى، شارة المجال،
35
+ * وبحث فوري)، أزرار التالي/السابق (تتبع loopLinks)، تشغيل الوسائط المرجعية
36
+ * (mediaRef / mediaFolder) مع شريط تقدّم قابل للسحب، وحفظ آخر موضع تصفح في
37
+ * التخزين المحلي للمتصفح بين الجلسات.
35
38
  */
36
39
  function renderPlayerHtml(series, options = {}) {
37
40
  const chain = (0, loopUtils_1.resolveLoopChain)(series.documents, series.rootFileName);
@@ -62,22 +65,29 @@ function renderPlayerHtml(series, options = {}) {
62
65
  mediaHtml = `<p class="tag">📁 مرجع وسائط خارجي: ${(0, theme_1.escapeHtml)(normalized)}</p>`;
63
66
  }
64
67
  }
68
+ const kindInfo = brand_1.CONTENT_KIND_INFO[document.content.kind] ?? brand_1.CONTENT_KIND_INFO.genericItem;
69
+ const navLabel = document.content.kind === "episodeItem" ? document.content.episodeTitle : document.metadata.title;
65
70
  return {
66
71
  fileName,
67
72
  title: document.metadata.title,
68
- navLabel: document.content.kind === "episodeItem"
69
- ? document.content.episodeTitle
70
- : document.metadata.title,
73
+ navLabel: navLabel || document.metadata.title || fileName,
74
+ domain: (0, brand_1.domainLabel)(document.metadata.domain),
75
+ kindIcon: kindInfo.icon,
76
+ kindColor: kindInfo.color,
71
77
  duration: document.content.kind === "episodeItem" ? document.content.duration ?? 0 : 0,
72
78
  cardHtml: (0, renderContent_1.renderDocumentCard)(document, theme.layout),
73
79
  mediaHtml,
74
80
  hasMedia: !!mediaHtml && mediaHtml.includes('data-role="media"'),
75
81
  };
76
82
  });
83
+ // ── قائمة "الملفات" الجانبية — بطاقات مطابقة لشاشة ملفاتي في التطبيق الأصلي ──
77
84
  const navListHtml = items
78
- .map((it, idx) => `<li data-index="${idx}" class="nav-item${idx === 0 ? " active" : ""}">
79
- <span class="nav-num">${idx + 1}</span>
80
- <span class="nav-title">${(0, theme_1.escapeHtml)(it.navLabel || it.title || it.fileName)}</span>
85
+ .map((it, idx) => `<li data-index="${idx}" data-search="${(0, theme_1.escapeHtml)((it.navLabel + " " + it.domain).toLowerCase())}" class="file-card nav-item${idx === 0 ? " active" : ""}" role="button" tabindex="0">
86
+ <div class="file-icon" style="background:${it.kindColor}22;color:${it.kindColor}">${it.kindIcon}</div>
87
+ <div class="file-info">
88
+ <div class="file-name">${(0, theme_1.escapeHtml)(it.navLabel)}</div>
89
+ <div class="file-meta"><span>${idx + 1} / ${items.length}</span><span>${(0, theme_1.escapeHtml)(it.domain)}</span></div>
90
+ </div>
81
91
  </li>`)
82
92
  .join("");
83
93
  const panelsHtml = items
@@ -93,32 +103,59 @@ function renderPlayerHtml(series, options = {}) {
93
103
  </section>`)
94
104
  .join("");
95
105
  const playerCss = `
96
- .layout { display: flex; gap: 24px; align-items: flex-start; }
106
+ .layout { display: flex; gap: 22px; align-items: flex-start; }
97
107
  .sidebar {
98
- width: 220px; flex-shrink: 0; max-height: 80vh; overflow-y: auto;
99
- border-inline-start: 1px solid rgba(0,0,0,.08); padding-inline-start: 12px;
100
- }
101
- .nav-item {
102
- display: flex; align-items: center; gap: 8px; padding: 8px 6px;
103
- border-radius: 10px; cursor: pointer; font-size: .9rem; list-style: none;
104
- }
105
- .nav-item:hover { background: rgba(0,0,0,.05); }
106
- .nav-item.active { background: var(--primary); color: #fff; }
107
- .nav-num {
108
- display: inline-flex; align-items: center; justify-content: center;
109
- width: 22px; height: 22px; border-radius: 50%; background: rgba(0,0,0,.08);
110
- font-size: .75rem; flex-shrink: 0;
111
- }
112
- .nav-item.active .nav-num { background: rgba(255,255,255,.3); }
113
- ul#navList { padding: 0; margin: 0; }
108
+ width: 260px; flex-shrink: 0; max-height: 80vh; overflow-y: auto;
109
+ border-inline-start: 1px solid var(--border); padding-inline-start: 14px;
110
+ }
111
+ .files-search {
112
+ width: 100%; padding: 9px 12px; margin-bottom: 10px;
113
+ border-radius: 10px; border: 1px solid var(--border);
114
+ background: var(--surface); color: var(--text); font-family: var(--font); font-size: .85rem;
115
+ }
116
+ .nav-item { cursor: pointer; margin-bottom: 8px; }
117
+ .nav-item:hover { border-color: var(--primary); }
118
+ .nav-item.active { background: var(--primary); border-color: var(--primary); }
119
+ .nav-item.active .file-name, .nav-item.active .file-meta { color: #fff; }
120
+ .nav-item.active .file-icon { background: rgba(255,255,255,.25) !important; color: #fff !important; }
121
+ .nav-item[hidden] { display: none !important; }
122
+ ul#navList { padding: 0; margin: 0; list-style: none; }
114
123
  .main { flex: 1; min-width: 0; }
115
124
  .player-media { width: 100%; border-radius: 14px; background: #000; margin-bottom: 4px; }
116
125
  .progress-wrap { margin-bottom: 16px; }
117
126
  .seek { width: 100%; accent-color: var(--primary); }
118
- .time-row { display: flex; justify-content: space-between; font-size: .75rem; opacity: .7; }
127
+ .time-row { display: flex; justify-content: space-between; font-size: .75rem; color: var(--muted); }
128
+
129
+ /* ── شريط تقدّم الحلقات — يعرض الموضع الحالي ضمن كامل الحلقة ويُسحب للانتقال مباشرة ── */
130
+ .loop-progress { margin: 2px 0 22px; user-select: none; }
131
+ .loop-progress-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; }
132
+ .loop-progress-label { font-size: .78rem; color: var(--muted); font-weight: 600; transition: color .15s; }
133
+ .loop-progress-label.dragging { color: var(--secondary); }
134
+ .loop-progress-count {
135
+ font-size: .8rem; font-weight: 800; color: var(--secondary);
136
+ background: color-mix(in srgb, var(--secondary) 16%, transparent);
137
+ border-radius: 20px; padding: 2px 11px;
138
+ }
139
+ .loop-progress-track {
140
+ position: relative; height: 10px; border-radius: 999px;
141
+ background: var(--surface-variant); cursor: pointer; touch-action: none;
142
+ }
143
+ .loop-progress-fill {
144
+ position: absolute; inset-inline-start: 0; top: 0; bottom: 0; width: 0%;
145
+ border-radius: 999px;
146
+ background: linear-gradient(90deg, var(--primary), var(--secondary));
147
+ }
148
+ .loop-progress-thumb {
149
+ position: absolute; top: 50%; inset-inline-start: 0%;
150
+ width: 18px; height: 18px; border-radius: 50%;
151
+ background: #fff; border: 3px solid var(--primary);
152
+ box-shadow: 0 1px 5px rgba(0,0,0,.35);
153
+ transform: translate(-50%, -50%); transition: transform .15s;
154
+ }
155
+ .loop-progress-thumb.dragging { transform: translate(-50%, -50%) scale(1.35); border-color: var(--secondary); }
119
156
  @media (max-width: 720px) {
120
157
  .layout { flex-direction: column; }
121
- .sidebar { width: 100%; max-height: 220px; border-inline-start: none; border-bottom: 1px solid rgba(0,0,0,.08); padding-bottom: 8px; }
158
+ .sidebar { width: 100%; max-height: 260px; border-inline-start: none; border-bottom: 1px solid var(--border); padding-bottom: 10px; }
122
159
  }
123
160
  `;
124
161
  const script = `
@@ -129,6 +166,13 @@ function renderPlayerHtml(series, options = {}) {
129
166
  const panels = document.querySelectorAll(".panel");
130
167
  const prevBtn = document.getElementById("prevBtn");
131
168
  const nextBtn = document.getElementById("nextBtn");
169
+ const searchInput = document.getElementById("filesSearch");
170
+ const progressTrack = document.getElementById("loopProgressTrack");
171
+ const progressFill = document.getElementById("loopProgressFill");
172
+ const progressThumb = document.getElementById("loopProgressThumb");
173
+ const progressLabel = document.querySelector('[data-role="progress-label"]');
174
+ const progressCount = document.querySelector('[data-role="progress-count"]');
175
+ let progressDragging = false;
132
176
 
133
177
  function fmt(t) {
134
178
  if (!isFinite(t)) return "0:00";
@@ -136,6 +180,29 @@ function renderPlayerHtml(series, options = {}) {
136
180
  return m + ":" + String(s).padStart(2, "0");
137
181
  }
138
182
 
183
+ function setProgressVisual(frac) {
184
+ if (!progressTrack) return;
185
+ const pct = Math.round(frac * 1000) / 10 + "%";
186
+ progressFill.style.width = pct;
187
+ progressThumb.style.insetInlineStart = pct;
188
+ }
189
+
190
+ function syncProgressBar(index) {
191
+ if (!progressTrack) return;
192
+ const frac = total > 1 ? index / (total - 1) : 0;
193
+ setProgressVisual(frac);
194
+ progressCount.textContent = (index + 1) + "/" + total;
195
+ }
196
+
197
+ function fracFromClientX(clientX) {
198
+ const rect = progressTrack.getBoundingClientRect();
199
+ // اتجاه الصفحة RTL: بداية الحلقة (الحلقة 1) تقع أقصى اليمين
200
+ return Math.min(1, Math.max(0, (rect.right - clientX) / rect.width));
201
+ }
202
+ function indexFromFrac(frac) {
203
+ return Math.round(frac * (total - 1));
204
+ }
205
+
139
206
  function setupMedia(panel) {
140
207
  const media = panel.querySelector('[data-role="media"]');
141
208
  const seek = panel.querySelector('[data-role="seek"]');
@@ -177,6 +244,7 @@ function renderPlayerHtml(series, options = {}) {
177
244
  });
178
245
  prevBtn.disabled = index === 0;
179
246
  nextBtn.disabled = index === total - 1;
247
+ syncProgressBar(index);
180
248
  try {
181
249
  const data = JSON.parse(localStorage.getItem(STATE_KEY) || "{}");
182
250
  data.lastIndex = index;
@@ -188,6 +256,9 @@ function renderPlayerHtml(series, options = {}) {
188
256
  }
189
257
 
190
258
  navItems.forEach((el) => el.addEventListener("click", () => goTo(Number(el.dataset.index))));
259
+ navItems.forEach((el) => el.addEventListener("keydown", (e) => {
260
+ if (e.key === "Enter" || e.key === " ") { e.preventDefault(); goTo(Number(el.dataset.index)); }
261
+ }));
191
262
  prevBtn.addEventListener("click", () => goTo(current - 1));
192
263
  nextBtn.addEventListener("click", () => goTo(current + 1));
193
264
  document.addEventListener("keydown", (e) => {
@@ -195,15 +266,78 @@ function renderPlayerHtml(series, options = {}) {
195
266
  if (e.key === "ArrowLeft") goTo(current + 1); // RTL: يسار = التالي
196
267
  });
197
268
 
269
+ if (searchInput) {
270
+ searchInput.addEventListener("input", () => {
271
+ const q = searchInput.value.trim().toLowerCase();
272
+ navItems.forEach((el) => {
273
+ const matches = !q || (el.dataset.search || "").includes(q);
274
+ el.hidden = !matches;
275
+ });
276
+ });
277
+ }
278
+
279
+ if (progressTrack) {
280
+ const startDrag = (e) => {
281
+ progressDragging = true;
282
+ progressTrack.setPointerCapture(e.pointerId);
283
+ progressLabel.classList.add("dragging");
284
+ progressThumb.classList.add("dragging");
285
+ progressLabel.textContent = "اسحب للانتقال إلى حلقة أخرى";
286
+ const frac = fracFromClientX(e.clientX);
287
+ setProgressVisual(frac);
288
+ progressCount.textContent = (indexFromFrac(frac) + 1) + "/" + total;
289
+ };
290
+ const moveDrag = (e) => {
291
+ if (!progressDragging) return;
292
+ const frac = fracFromClientX(e.clientX);
293
+ setProgressVisual(frac);
294
+ progressCount.textContent = (indexFromFrac(frac) + 1) + "/" + total;
295
+ };
296
+ const endDrag = (e) => {
297
+ if (!progressDragging) return;
298
+ progressDragging = false;
299
+ progressLabel.classList.remove("dragging");
300
+ progressThumb.classList.remove("dragging");
301
+ progressLabel.textContent = "تقدّم الحلقات";
302
+ goTo(indexFromFrac(fracFromClientX(e.clientX)));
303
+ };
304
+ progressTrack.addEventListener("pointerdown", startDrag);
305
+ progressTrack.addEventListener("pointermove", moveDrag);
306
+ progressTrack.addEventListener("pointerup", endDrag);
307
+ progressTrack.addEventListener("pointercancel", endDrag);
308
+ }
309
+
198
310
  // استئناف آخر موضع تصفح محفوظ
199
311
  try {
200
312
  const data = JSON.parse(localStorage.getItem(STATE_KEY) || "{}");
201
313
  if (typeof data.lastIndex === "number") goTo(data.lastIndex);
202
- else setupMedia(panels[0]);
203
- } catch (e) { setupMedia(panels[0]); }
314
+ else { setupMedia(panels[0]); syncProgressBar(0); }
315
+ } catch (e) { setupMedia(panels[0]); syncProgressBar(0); }
204
316
  prevBtn.disabled = current === 0;
205
317
  nextBtn.disabled = current === total - 1;
206
318
  `;
319
+ const header = `<header class="dlof-header">
320
+ ${(0, theme_1.loopLogoSvg)(36, "var(--secondary)", "var(--primary)")}
321
+ <div>
322
+ <div class="logo-title">DLoF <span class="accent">Player</span></div>
323
+ <div class="logo-sub">${(0, theme_1.escapeHtml)(title)}</div>
324
+ </div>
325
+ <div style="margin-inline-start:auto">
326
+ <span class="badge">${items.length} ملف</span>
327
+ </div>
328
+ </header>`;
329
+ const loopProgressHtml = items.length > 1
330
+ ? `<div class="loop-progress">
331
+ <div class="loop-progress-head">
332
+ <span class="loop-progress-label" data-role="progress-label">تقدّم الحلقات</span>
333
+ <span class="loop-progress-count" data-role="progress-count">1/${items.length}</span>
334
+ </div>
335
+ <div class="loop-progress-track" id="loopProgressTrack">
336
+ <div class="loop-progress-fill" id="loopProgressFill"></div>
337
+ <div class="loop-progress-thumb" id="loopProgressThumb"></div>
338
+ </div>
339
+ </div>`
340
+ : "";
207
341
  return `<!DOCTYPE html>
208
342
  <html lang="${(0, theme_1.escapeHtml)(firstDoc.metadata.language ?? "ar")}" dir="rtl">
209
343
  <head>
@@ -214,10 +348,12 @@ function renderPlayerHtml(series, options = {}) {
214
348
  </head>
215
349
  <body>
216
350
  <div class="app">
217
- <div class="badge">DLoF Player</div>
218
- <h1 style="margin-top:6px">${(0, theme_1.escapeHtml)(title)}</h1>
351
+ ${header}
352
+ ${loopProgressHtml}
219
353
  <div class="layout">
220
354
  <aside class="sidebar">
355
+ <div class="files-section-title">📁 الملفات</div>
356
+ <input id="filesSearch" class="files-search" type="search" placeholder="بحث في الملفات…"/>
221
357
  <ul id="navList">${navListHtml}</ul>
222
358
  </aside>
223
359
  <main class="main">
@@ -0,0 +1,50 @@
1
+ /**
2
+ * الهوية البصرية الرسمية لـ DLoF — مطابقة لتطبيق Android الأصلي (ui/theme/Theme.kt)
3
+ * ولأداة web-signature (App.jsx): أخضر داكن "Copper" كلون أساسي، ذهبي/برتقالي
4
+ * "Moss" كلون ثانوي، وخلفية حبرية للوضع الليلي. تُستخدم هذه الألوان كافتراضيات
5
+ * لعارض HTML ومشغّل الحلقات في حزمة npm، بحيث يظهر الناتج بنفس هوية DLoF الأصلية.
6
+ */
7
+ export declare const BRAND_LIGHT: {
8
+ primary: string;
9
+ secondary: string;
10
+ background: string;
11
+ surface: string;
12
+ surfaceVariant: string;
13
+ text: string;
14
+ muted: string;
15
+ border: string;
16
+ };
17
+ export declare const BRAND_DARK: {
18
+ primary: string;
19
+ secondary: string;
20
+ background: string;
21
+ surface: string;
22
+ surfaceVariant: string;
23
+ text: string;
24
+ muted: string;
25
+ border: string;
26
+ };
27
+ /** شعار الحلقة (نقاط متقطعة + نواة) — نفس شعار أداة web-signature، بلون قابل للتخصيص */
28
+ export declare function loopLogoSvg(size?: number, color?: string, dim?: string): string;
29
+ /** تسميات المجالات بالعربية — مطابقة لـ model/DlofDocument.kt (enum Domain) */
30
+ export declare const DOMAIN_LABELS: Record<string, string>;
31
+ export declare function domainLabel(domain?: string): string;
32
+ /** تسميات وأيقونات وألوان أنواع المحتوى (content kind) — تُستخدم في قوائم الملفات */
33
+ export declare const CONTENT_KIND_INFO: Record<string, {
34
+ label: string;
35
+ icon: string;
36
+ color: string;
37
+ }>;
38
+ export interface FileTypeInfo {
39
+ icon: string;
40
+ color: string;
41
+ label: string;
42
+ }
43
+ /**
44
+ * توزيع أيقونة ولون على امتداد ملف معطى — مطابق لـ FileTypeIcons.kt في تطبيق
45
+ * Android الأصلي (نفس الألوان تقريباً)، لكن بأيقونات نصية (بلا اعتماد خارجي)
46
+ * بدل أيقونات Material كي تعمل الأداة بلا أي اعتماديات.
47
+ */
48
+ export declare function fileTypeInfo(fileName: string, mimeHint?: string): FileTypeInfo;
49
+ /** ينسّق حجم الملف بالبايت إلى نص مقروء بالعربية — مطابق لـ formatFileSize في FileTypeIcons.kt */
50
+ export declare function formatFileSize(bytes?: number): string;
@@ -0,0 +1,118 @@
1
+ "use strict";
2
+ /**
3
+ * الهوية البصرية الرسمية لـ DLoF — مطابقة لتطبيق Android الأصلي (ui/theme/Theme.kt)
4
+ * ولأداة web-signature (App.jsx): أخضر داكن "Copper" كلون أساسي، ذهبي/برتقالي
5
+ * "Moss" كلون ثانوي، وخلفية حبرية للوضع الليلي. تُستخدم هذه الألوان كافتراضيات
6
+ * لعارض HTML ومشغّل الحلقات في حزمة npm، بحيث يظهر الناتج بنفس هوية DLoF الأصلية.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.CONTENT_KIND_INFO = exports.DOMAIN_LABELS = exports.BRAND_DARK = exports.BRAND_LIGHT = void 0;
10
+ exports.loopLogoSvg = loopLogoSvg;
11
+ exports.domainLabel = domainLabel;
12
+ exports.fileTypeInfo = fileTypeInfo;
13
+ exports.formatFileSize = formatFileSize;
14
+ exports.BRAND_LIGHT = {
15
+ primary: "#1D7A3F", // DlofCopper
16
+ secondary: "#D2A020", // DlofMoss
17
+ background: "#EDEFF7", // DlofParchment
18
+ surface: "#F9FAFD",
19
+ surfaceVariant: "#E3E7F0",
20
+ text: "#0B0B14", // DlofInk
21
+ muted: "#5B5F6E",
22
+ border: "rgba(11,11,20,.10)",
23
+ };
24
+ exports.BRAND_DARK = {
25
+ primary: "#4CC47B",
26
+ secondary: "#E8C24A",
27
+ background: "#0A0A14", // DlofSurfaceDark
28
+ surface: "#15151F",
29
+ surfaceVariant: "#232336",
30
+ text: "#ECEEF6", // DlofInkLight
31
+ muted: "#9A9DB0",
32
+ border: "rgba(236,238,246,.10)",
33
+ };
34
+ /** شعار الحلقة (نقاط متقطعة + نواة) — نفس شعار أداة web-signature، بلون قابل للتخصيص */
35
+ function loopLogoSvg(size = 34, color = exports.BRAND_LIGHT.secondary, dim) {
36
+ const d = dim ?? color;
37
+ return `<svg width="${size}" height="${size}" viewBox="0 0 38 38" aria-hidden="true">
38
+ <circle cx="19" cy="19" r="16" fill="none" stroke="${color}" stroke-width="2.5" stroke-dasharray="6 3"/>
39
+ <circle cx="19" cy="19" r="8" fill="${d}" opacity="0.55"/>
40
+ <circle cx="19" cy="19" r="3.5" fill="${color}"/>
41
+ <path d="M19 7 L19 3 M19 35 L19 31 M7 19 L3 19 M35 19 L31 19" stroke="${color}" stroke-width="1.5" stroke-linecap="round"/>
42
+ </svg>`;
43
+ }
44
+ /** تسميات المجالات بالعربية — مطابقة لـ model/DlofDocument.kt (enum Domain) */
45
+ exports.DOMAIN_LABELS = {
46
+ education: "تعليم",
47
+ book: "كتاب",
48
+ infoApp: "تطبيق معلومات",
49
+ infoLoop: "حلقة معلومات",
50
+ recipe: "وصفة طعام",
51
+ journal: "يوميات",
52
+ series: "مسلسل / سلسلة",
53
+ comic: "قصة مصورة",
54
+ podcast: "بودكاست",
55
+ characters: "شخصيات",
56
+ custom: "مخصص",
57
+ };
58
+ function domainLabel(domain) {
59
+ if (!domain)
60
+ return exports.DOMAIN_LABELS.custom;
61
+ return exports.DOMAIN_LABELS[domain] ?? domain;
62
+ }
63
+ /** تسميات وأيقونات وألوان أنواع المحتوى (content kind) — تُستخدم في قوائم الملفات */
64
+ exports.CONTENT_KIND_INFO = {
65
+ bookChapter: { label: "فصل كتاب", icon: "📖", color: "#6A1B9A" },
66
+ qaItem: { label: "سؤال وجواب", icon: "❓", color: "#0277BD" },
67
+ termDefinition: { label: "تعريف مصطلح", icon: "🔤", color: "#2E7D32" },
68
+ infoExplain: { label: "شرح معلومة", icon: "💡", color: "#D2A020" },
69
+ episodeItem: { label: "حلقة", icon: "🎬", color: "#2D5C6B" },
70
+ genericItem: { label: "عنصر عام", icon: "📄", color: "#455A64" },
71
+ };
72
+ /**
73
+ * توزيع أيقونة ولون على امتداد ملف معطى — مطابق لـ FileTypeIcons.kt في تطبيق
74
+ * Android الأصلي (نفس الألوان تقريباً)، لكن بأيقونات نصية (بلا اعتماد خارجي)
75
+ * بدل أيقونات Material كي تعمل الأداة بلا أي اعتماديات.
76
+ */
77
+ function fileTypeInfo(fileName, mimeHint) {
78
+ const ext = (fileName.split(".").pop() ?? "").toLowerCase();
79
+ const mime = mimeHint ?? "";
80
+ if (ext === "dlof" || ext === "dloftemplate")
81
+ return { icon: "🔁", color: "#AA5B2E", label: "ملف DLoF" };
82
+ if (ext === "dlofcomic")
83
+ return { icon: "📚", color: "#4B5D3A", label: "قصة مصورة" };
84
+ if (ext === "dlofvideo")
85
+ return { icon: "📺", color: "#2D5C6B", label: "مسلسل فيديو" };
86
+ if (mime.startsWith("image/") || ["jpg", "jpeg", "png", "webp", "gif", "bmp", "heic", "svg"].includes(ext))
87
+ return { icon: "🖼️", color: "#2E7D32", label: "صورة" };
88
+ if (mime.startsWith("video/") || ["mp4", "mkv", "webm", "avi", "mov", "3gp"].includes(ext))
89
+ return { icon: "🎬", color: "#6A1B9A", label: "فيديو" };
90
+ if (mime.startsWith("audio/") || ["mp3", "wav", "ogg", "m4a", "flac", "aac"].includes(ext))
91
+ return { icon: "🎵", color: "#0277BD", label: "صوت" };
92
+ if (ext === "pdf")
93
+ return { icon: "📕", color: "#C62828", label: "PDF" };
94
+ if (["zip", "rar", "7z", "tar", "gz"].includes(ext))
95
+ return { icon: "🗜️", color: "#8D6E63", label: "أرشيف" };
96
+ if (["xls", "xlsx", "csv", "tsv"].includes(ext))
97
+ return { icon: "📊", color: "#2E7D32", label: "جدول بيانات" };
98
+ if (["json", "xml", "yaml", "yml", "kt", "java", "py", "js", "ts", "html", "css"].includes(ext))
99
+ return { icon: "🧩", color: "#455A64", label: "بيانات/كود" };
100
+ if (["txt", "md", "doc", "docx"].includes(ext))
101
+ return { icon: "📃", color: "#1976D2", label: "مستند" };
102
+ return { icon: "📁", color: "#757575", label: "ملف" };
103
+ }
104
+ /** ينسّق حجم الملف بالبايت إلى نص مقروء بالعربية — مطابق لـ formatFileSize في FileTypeIcons.kt */
105
+ function formatFileSize(bytes) {
106
+ if (bytes === undefined || bytes === null || Number.isNaN(bytes))
107
+ return "";
108
+ if (bytes < 1024)
109
+ return `${bytes} بايت`;
110
+ const kb = bytes / 1024;
111
+ if (kb < 1024)
112
+ return `${kb.toFixed(1)} ك.ب`;
113
+ const mb = kb / 1024;
114
+ if (mb < 1024)
115
+ return `${mb.toFixed(1)} م.ب`;
116
+ const gb = mb / 1024;
117
+ return `${gb.toFixed(2)} غ.ب`;
118
+ }
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.renderDocumentCard = renderDocumentCard;
4
4
  const theme_1 = require("./theme");
5
+ const brand_1 = require("./brand");
5
6
  function attachmentSrc(a) {
6
7
  if (a.data)
7
8
  return `data:${a.mimeType};base64,${a.data}`;
@@ -9,46 +10,63 @@ function attachmentSrc(a) {
9
10
  return a.uri;
10
11
  return undefined;
11
12
  }
13
+ /** يعرض مرفقاً واحداً كبطاقة ملف (أيقونة + اسم + حجم)، مطابقة لشكل بطاقة الملف
14
+ * في شاشة "ملفاتي" بتطبيق DLoF الأصلي، مع معاينة مضمّنة أسفلها للصور والفيديو. */
12
15
  function renderAttachment(a) {
13
16
  const src = attachmentSrc(a);
17
+ const info = (0, brand_1.fileTypeInfo)(a.fileName, a.mimeType);
18
+ const size = (0, brand_1.formatFileSize)(a.sizeBytes);
19
+ const card = `<div class="file-card">
20
+ <div class="file-icon" style="background:${info.color}22;color:${info.color}">${info.icon}</div>
21
+ <div class="file-info">
22
+ <div class="file-name">${(0, theme_1.escapeHtml)(a.fileName)}</div>
23
+ <div class="file-meta"><span>${(0, theme_1.escapeHtml)(info.label)}</span>${size ? `<span>${size}</span>` : ""}</div>
24
+ </div>
25
+ </div>`;
14
26
  if (!src)
15
- return "";
16
- const caption = a.caption ? `<div class="tag">${(0, theme_1.escapeHtml)(a.caption)}</div>` : "";
27
+ return card;
28
+ let preview = "";
17
29
  if (a.kind === "image") {
18
- return `<div class="attachment"><img src="${src}" alt="${(0, theme_1.escapeHtml)(a.fileName)}" loading="lazy"/>${caption}</div>`;
30
+ preview = `<div class="attachment-preview"><img src="${src}" alt="${(0, theme_1.escapeHtml)(a.fileName)}" loading="lazy"/></div>`;
19
31
  }
20
- if (a.kind === "video") {
21
- return `<div class="attachment"><video controls src="${src}"></video>${caption}</div>`;
32
+ else if (a.kind === "video") {
33
+ preview = `<div class="attachment-preview"><video controls src="${src}"></video></div>`;
22
34
  }
23
- return `<div class="attachment"><a href="${src}" download="${(0, theme_1.escapeHtml)(a.fileName)}">📎 ${(0, theme_1.escapeHtml)(a.fileName)}</a>${caption}</div>`;
35
+ const caption = a.caption ? `<div class="attachment-caption">${(0, theme_1.escapeHtml)(a.caption)}</div>` : "";
36
+ return `${card}${preview}${caption}`;
24
37
  }
25
38
  function contentHtml(doc) {
26
39
  const c = doc.content;
40
+ const kindInfo = brand_1.CONTENT_KIND_INFO[c.kind] ?? brand_1.CONTENT_KIND_INFO.genericItem;
41
+ const kindBadge = `<span class="badge outline">${kindInfo.icon} ${(0, theme_1.escapeHtml)(kindInfo.label)}</span>`;
27
42
  switch (c.kind) {
28
43
  case "bookChapter":
29
44
  return `
30
- ${c.chapterNumber !== undefined ? `<div class="tag">الفصل ${c.chapterNumber}</div>` : ""}
45
+ <div style="margin-bottom:8px">${kindBadge}${c.chapterNumber !== undefined ? `<span class="badge ghost">الفصل ${c.chapterNumber}</span>` : ""}</div>
31
46
  <h1>${(0, theme_1.escapeHtml)(c.chapterTitle)}</h1>
32
47
  ${c.summary ? `<p class="tag">${(0, theme_1.escapeHtml)(c.summary)}</p>` : ""}
33
48
  <div class="body-text">${(0, theme_1.escapeHtml)(c.text)}</div>`;
34
49
  case "qaItem":
35
50
  return `
51
+ <div style="margin-bottom:8px">${kindBadge}${c.difficulty ? `<span class="badge ghost">${(0, theme_1.escapeHtml)(c.difficulty)}</span>` : ""}</div>
36
52
  <h2>❓ ${(0, theme_1.escapeHtml)(c.question)}</h2>
37
53
  <div class="body-text">${(0, theme_1.escapeHtml)(c.answer)}</div>
38
- ${c.explanation ? `<p class="tag">${(0, theme_1.escapeHtml)(c.explanation)}</p>` : ""}
39
- ${c.difficulty ? `<span class="badge">${(0, theme_1.escapeHtml)(c.difficulty)}</span>` : ""}`;
54
+ ${c.explanation ? `<p class="tag">${(0, theme_1.escapeHtml)(c.explanation)}</p>` : ""}`;
40
55
  case "termDefinition":
41
56
  return `
57
+ <div style="margin-bottom:8px">${kindBadge}</div>
42
58
  <h1>${(0, theme_1.escapeHtml)(c.term)}</h1>
43
59
  <div class="body-text">${(0, theme_1.escapeHtml)(c.definition)}</div>
44
60
  ${c.example ? `<p class="tag">مثال: ${(0, theme_1.escapeHtml)(c.example)}</p>` : ""}`;
45
61
  case "infoExplain":
46
62
  return `
63
+ <div style="margin-bottom:8px">${kindBadge}</div>
47
64
  <h1>${(0, theme_1.escapeHtml)(c.topic)}</h1>
48
65
  <div class="body-text">${(0, theme_1.escapeHtml)(c.explanation)}</div>
49
66
  ${c.source ? `<p class="tag">المصدر: ${(0, theme_1.escapeHtml)(c.source)}</p>` : ""}`;
50
67
  case "episodeItem":
51
68
  return `
69
+ <div style="margin-bottom:8px">${kindBadge}</div>
52
70
  <div class="tag">${c.seriesTitle ? (0, theme_1.escapeHtml)(c.seriesTitle) + " · " : ""}${c.seasonNumber !== undefined ? "الموسم " + c.seasonNumber + " · " : ""}${c.episodeNumber !== undefined ? "الحلقة " + c.episodeNumber : ""}</div>
53
71
  <h1>${(0, theme_1.escapeHtml)(c.episodeTitle)}</h1>
54
72
  ${c.synopsis ? `<p class="tag">${(0, theme_1.escapeHtml)(c.synopsis)}</p>` : ""}
@@ -56,8 +74,8 @@ function contentHtml(doc) {
56
74
  case "genericItem":
57
75
  default:
58
76
  return `
77
+ <div style="margin-bottom:8px">${kindBadge}${c.type ? `<span class="badge ghost">${(0, theme_1.escapeHtml)(c.type)}</span>` : ""}</div>
59
78
  <h1>${(0, theme_1.escapeHtml)(doc.metadata.title || "مستند")}</h1>
60
- ${c.type ? `<span class="badge">${(0, theme_1.escapeHtml)(c.type)}</span>` : ""}
61
79
  ${c.element ? `<p class="tag">${(0, theme_1.escapeHtml)(c.element)}</p>` : ""}
62
80
  ${c.body ? `<div class="body-text">${(0, theme_1.escapeHtml)(c.body)}</div>` : ""}`;
63
81
  }
@@ -65,13 +83,18 @@ function contentHtml(doc) {
65
83
  /** يبني كتلة HTML كاملة (بطاقة) لمستند DocumentLoop واحد مع مرفقاته */
66
84
  function renderDocumentCard(doc, layout) {
67
85
  const tags = (doc.metadata.tags ?? []).map((t) => `<span class="tag">#${(0, theme_1.escapeHtml)(t)}</span>`).join("");
68
- const meta = `<div class="meta-row">${(0, theme_1.escapeHtml)(doc.metadata.author ?? "")}${doc.metadata.author && doc.metadata.createdAt ? " · " : ""}${doc.metadata.createdAt ? (0, theme_1.escapeHtml)(doc.metadata.createdAt.slice(0, 10)) : ""}</div>`;
69
- const attachmentsHtml = (doc.attachments ?? []).map(renderAttachment).join("\n");
86
+ const meta = `<div class="meta-row">${doc.metadata.author ? `<span>👤 ${(0, theme_1.escapeHtml)(doc.metadata.author)}</span>` : ""}${doc.metadata.createdAt ? `<span>📅 ${(0, theme_1.escapeHtml)(doc.metadata.createdAt.slice(0, 10))}</span>` : ""}</div>`;
87
+ const attachments = doc.attachments ?? [];
88
+ const attachmentsHtml = attachments.length
89
+ ? `<div class="files-section-title">📎 المرفقات (${attachments.length})</div>${attachments
90
+ .map(renderAttachment)
91
+ .join("\n")}`
92
+ : "";
70
93
  return `
71
94
  <div class="card ${(0, theme_1.escapeHtml)(layout)}">
72
95
  ${meta}
73
96
  ${contentHtml(doc)}
74
97
  ${tags ? `<div style="margin-top:10px">${tags}</div>` : ""}
75
- ${attachmentsHtml}
76
- </div>`;
98
+ </div>
99
+ ${attachmentsHtml}`;
77
100
  }
@@ -5,7 +5,8 @@ export interface ViewerOptions {
5
5
  }
6
6
  /**
7
7
  * عارض dlofpkg / dlof: يبني صفحة HTML مستقلة واحدة (بلا اعتماديات خارجية)
8
- * تعرض بيانات meta.json الوصفية، محتوى المستند، مرفقاته، وتصميمه المخصّص (template).
9
- * الصفحة الناتجة قابلة للفتح مباشرة في أي متصفح دون خادم.
8
+ * بنفس الهوية البصرية لتطبيق DLoF الأصلي (الشعار، الألوان، بطاقات الملفات)،
9
+ * تعرض بيانات meta.json الوصفية، محتوى المستند، مرفقاته كبطاقات ملفات، وتصميمه
10
+ * المخصّص (template) إن وُجد. الصفحة الناتجة قابلة للفتح مباشرة في أي متصفح دون خادم.
10
11
  */
11
12
  export declare function renderViewerHtml(input: DlofPkg | DocumentLoop, options?: ViewerOptions): string;