@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.
- package/README.md +2 -1
- package/dist/browser.d.ts +11 -0
- package/dist/browser.js +55 -0
- package/dist/dlof.browser.js +189 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +10 -1
- package/dist/player/renderPlayerHtml.d.ts +6 -4
- package/dist/player/renderPlayerHtml.js +169 -33
- package/dist/viewer/brand.d.ts +50 -0
- package/dist/viewer/brand.js +118 -0
- package/dist/viewer/renderContent.js +37 -14
- package/dist/viewer/renderViewerHtml.d.ts +3 -2
- package/dist/viewer/renderViewerHtml.js +18 -5
- package/dist/viewer/theme.d.ts +11 -1
- package/dist/viewer/theme.js +105 -43
- package/dist/zip/zipReaderBrowser.d.ts +12 -0
- package/dist/zip/zipReaderBrowser.js +82 -0
- package/package.json +11 -5
|
@@ -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
|
-
*
|
|
33
|
-
*
|
|
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.
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
<
|
|
80
|
-
<
|
|
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:
|
|
106
|
+
.layout { display: flex; gap: 22px; align-items: flex-start; }
|
|
97
107
|
.sidebar {
|
|
98
|
-
width:
|
|
99
|
-
border-inline-start: 1px solid
|
|
100
|
-
}
|
|
101
|
-
.
|
|
102
|
-
|
|
103
|
-
border-radius: 10px;
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
.nav-item
|
|
107
|
-
.nav-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}
|
|
112
|
-
|
|
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;
|
|
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:
|
|
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
|
-
|
|
218
|
-
|
|
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
|
-
|
|
27
|
+
return card;
|
|
28
|
+
let preview = "";
|
|
17
29
|
if (a.kind === "image") {
|
|
18
|
-
|
|
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
|
-
|
|
32
|
+
else if (a.kind === "video") {
|
|
33
|
+
preview = `<div class="attachment-preview"><video controls src="${src}"></video></div>`;
|
|
22
34
|
}
|
|
23
|
-
|
|
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 ? `<
|
|
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
|
|
69
|
-
const
|
|
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
|
-
|
|
76
|
-
|
|
98
|
+
</div>
|
|
99
|
+
${attachmentsHtml}`;
|
|
77
100
|
}
|
|
@@ -5,7 +5,8 @@ export interface ViewerOptions {
|
|
|
5
5
|
}
|
|
6
6
|
/**
|
|
7
7
|
* عارض dlofpkg / dlof: يبني صفحة HTML مستقلة واحدة (بلا اعتماديات خارجية)
|
|
8
|
-
*
|
|
9
|
-
*
|
|
8
|
+
* بنفس الهوية البصرية لتطبيق DLoF الأصلي (الشعار، الألوان، بطاقات الملفات)،
|
|
9
|
+
* تعرض بيانات meta.json الوصفية، محتوى المستند، مرفقاته كبطاقات ملفات، وتصميمه
|
|
10
|
+
* المخصّص (template) إن وُجد. الصفحة الناتجة قابلة للفتح مباشرة في أي متصفح دون خادم.
|
|
10
11
|
*/
|
|
11
12
|
export declare function renderViewerHtml(input: DlofPkg | DocumentLoop, options?: ViewerOptions): string;
|