Fulwah commited on
Commit
deeda5a
·
verified ·
1 Parent(s): 25899a5

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +752 -19
index.html CHANGED
@@ -1,19 +1,752 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="ar" dir="rtl">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>الورّاق نسخة مفتوحة</title>
7
+ <link rel="manifest" href="data:application/json,{%22name%22:%22الورّاق%22,%22short_name%22:%22الورّاق%22,%22display%22:%22standalone%22,%22background_color%22:%22%230E1B2B%22,%22theme_color%22:%22%230E1B2B%22,%22start_url%22:%22.%22}">
8
+ <meta name="theme-color" content="#0E1B2B">
9
+ <link rel="preconnect" href="https://fonts.googleapis.com">
10
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
+ <link href="https://fonts.googleapis.com/css2?family=Amiri:ital,wght@0,400;0,700;1,400&family=IBM+Plex+Sans+Arabic:wght@300;400;500;600&display=swap" rel="stylesheet">
12
+ <style>
13
+ :root{
14
+ --lapis:#16283F; --lapis-deep:#0E1B2B; --slab:#1C3149;
15
+ --gold:#C9A227; --gold-soft:#E3C566;
16
+ --page:#F2ECDD; --page-dim:#DCD3BE; --ink:#241F17;
17
+ --rule:rgba(201,162,39,.28);
18
+ --sans:"IBM Plex Sans Arabic",system-ui,sans-serif;
19
+ --serif:"Amiri",Georgia,serif;
20
+ }
21
+ *{box-sizing:border-box}
22
+ html,body{margin:0;padding:0}
23
+ body{background:var(--lapis-deep);color:var(--page);font-family:var(--sans);font-weight:300;min-height:100vh}
24
+ button,input,textarea,select{font-family:inherit;font-size:inherit}
25
+ :focus-visible{outline:2px solid var(--gold-soft);outline-offset:2px}
26
+ .masthead{display:flex;align-items:baseline;gap:.9rem;flex-wrap:wrap;padding:1.1rem 1.4rem .9rem;border-bottom:1px solid var(--rule);background:linear-gradient(180deg,var(--lapis),var(--lapis-deep))}
27
+ .brand{font-family:var(--serif);font-size:1.9rem;font-weight:700;color:var(--gold-soft)}
28
+ .brand span{color:var(--page);opacity:.55;font-size:.9rem;font-family:var(--sans);font-weight:300;margin-inline-start:.6rem}
29
+ .masthead .spacer{flex:1}
30
+ .shell{display:grid;grid-template-columns:290px 1fr;min-height:calc(100vh - 68px)}
31
+ @media(max-width:860px){.shell{grid-template-columns:1fr}.stack{order:2}}
32
+ .stack{border-inline-start:1px solid var(--rule);background:var(--lapis);padding:1.1rem;display:flex;flex-direction:column;gap:.7rem}
33
+ .stack h2,.pane h2{font-size:.72rem;letter-spacing:.18em;font-weight:500;text-transform:uppercase;color:var(--gold);margin:0 0 .2rem}
34
+ .leaf{position:relative;display:flex;align-items:center;gap:.6rem;padding:.6rem .7rem;background:var(--slab);border:1px solid transparent;border-radius:2px;cursor:pointer;width:100%;color:inherit;text-align:start;transition:background .15s,border-color .15s}
35
+ .leaf::before{content:"";position:absolute;inset-inline-end:0;top:0;bottom:0;width:4px;background:linear-gradient(90deg,var(--page-dim),var(--page));opacity:.5}
36
+ .leaf:hover{background:#22405F}
37
+ .leaf.on{border-color:var(--gold);background:#22405F}
38
+ .leaf.on::before{opacity:1}
39
+ .leaf .n{font-family:var(--serif);color:var(--gold-soft);min-width:1.4rem}
40
+ .leaf .t{flex:1;font-size:.87rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
41
+ .leaf .dot{width:7px;height:7px;border-radius:50%;background:#6b7c90}
42
+ .leaf .dot.done{background:var(--gold)}
43
+ .leaf .dot.busy{background:var(--gold-soft);animation:pulse 1s infinite}
44
+ @keyframes pulse{50%{opacity:.25}}
45
+ @media(prefers-reduced-motion:reduce){*{animation:none!important;transition:none!important}}
46
+ .btn{border:1px solid var(--rule);background:transparent;color:var(--page);padding:.5rem .9rem;border-radius:2px;cursor:pointer;font-size:.85rem;transition:background .15s,border-color .15s}
47
+ .btn:hover{background:rgba(201,162,39,.12);border-color:var(--gold)}
48
+ .btn.solid{background:var(--gold);color:#1B1405;border-color:var(--gold);font-weight:500}
49
+ .btn.solid:hover{background:var(--gold-soft)}
50
+ .btn:disabled{opacity:.4;cursor:not-allowed}
51
+ .row{display:flex;gap:.5rem;flex-wrap:wrap;align-items:center}
52
+ input[type=text],input[type=password],select,textarea{background:var(--lapis-deep);color:var(--page);border:1px solid var(--rule);border-radius:2px;padding:.5rem .7rem;width:100%}
53
+ select{width:auto}
54
+ textarea{min-height:200px;line-height:1.9;resize:vertical}
55
+ label.fld{display:block;font-size:.75rem;color:var(--gold);letter-spacing:.08em;margin:.85rem 0 .3rem}
56
+ .pane{padding:1.2rem 1.4rem 3rem;max-width:900px}
57
+ .tabs{display:flex;gap:.2rem;border-bottom:1px solid var(--rule);margin-bottom:1.1rem;flex-wrap:wrap}
58
+ .tab{background:none;border:none;color:var(--page);opacity:.55;cursor:pointer;padding:.6rem .9rem;font-size:.9rem;border-bottom:2px solid transparent}
59
+ .tab.on{opacity:1;border-bottom-color:var(--gold);color:var(--gold-soft)}
60
+ .hint{font-size:.8rem;opacity:.55;line-height:1.75;margin:.5rem 0 0}
61
+ .status{font-size:.82rem;color:var(--gold-soft);min-height:1.3em;margin-top:.7rem}
62
+ .bar{height:2px;background:var(--slab);margin-top:.5rem;overflow:hidden}
63
+ .bar i{display:block;height:100%;background:var(--gold);width:0;transition:width .3s}
64
+ .sheet{background:var(--page);color:var(--ink);padding:2.4rem 2.2rem;border-radius:2px;font-family:var(--serif);font-size:1.16rem;line-height:2.15;box-shadow:0 18px 40px rgba(0,0,0,.35)}
65
+ @media(max-width:600px){.sheet{padding:1.4rem 1.1rem;font-size:1.06rem}}
66
+ .sheet h3{font-size:1.5rem;margin:0 0 1.2rem}
67
+ .sheet h3::after{content:"";display:block;width:60px;height:2px;background:var(--gold);margin-top:.7rem}
68
+ .sheet p{margin:0 0 1.05em;text-align:justify}
69
+ .sheet .orig{opacity:.5;font-size:.9rem;direction:ltr;text-align:left;border-top:1px dashed #b9ae96;margin-top:2rem;padding-top:1rem}
70
+ .thumbs{display:flex;gap:.5rem;flex-wrap:wrap;margin-top:.7rem}
71
+ .thumbs img{height:72px;border:1px solid var(--rule)}
72
+ .empty{opacity:.55;font-size:.88rem;line-height:1.9;padding:1rem 0}
73
+ .card{border:1px solid var(--rule);border-radius:2px;padding:.9rem 1rem;margin-bottom:.8rem;background:rgba(28,49,73,.5)}
74
+ .card h4{margin:0 0 .3rem;font-size:.95rem;font-weight:500;color:var(--gold-soft)}
75
+ .notice{font-size:.75rem;opacity:.45;line-height:1.7;border-top:1px solid var(--rule);margin-top:2.5rem;padding-top:.8rem}
76
+ #printArea{display:none}
77
+ @media print{
78
+ body{background:#fff;color:#000}
79
+ .masthead,.shell{display:none!important}
80
+ #printArea{display:block;font-family:var(--serif);color:#000;font-size:12.5pt;line-height:1.95}
81
+ #printArea .title-page{text-align:center;padding-top:26vh;break-after:page}
82
+ #printArea .title-page h1{font-size:30pt;margin:0 0 .6rem}
83
+ #printArea .title-page .by{font-size:13pt;color:#444}
84
+ #printArea .title-page .mark{margin-top:2rem;font-size:11pt;color:#7a6a2e;letter-spacing:.3em}
85
+ #printArea .toc{break-after:page}
86
+ #printArea .toc h2,#printArea .chap h2{font-size:17pt;margin:0 0 1rem;border-bottom:1.5pt solid #7a6a2e;padding-bottom:.4rem}
87
+ #printArea .chap{break-before:page}
88
+ #printArea p{margin:0 0 .85em;text-align:justify}
89
+ @page{margin:2cm 2.2cm}
90
+ }
91
+ </style>
92
+ </head>
93
+ <body>
94
+
95
+ <div class="masthead">
96
+ <div class="brand">الورّاق<span>نسخة مفتوحة — بلا اعتماد على مزوّد واحد</span></div>
97
+ <div class="spacer"></div>
98
+ <div class="row">
99
+ <select id="bookSel" style="min-width:150px"></select>
100
+ <button class="btn" id="newBook">رواية جديدة</button>
101
+ </div>
102
+ </div>
103
+
104
+ <div class="shell">
105
+ <aside class="stack">
106
+ <h2>الفصول</h2>
107
+ <div id="leaves" style="display:flex;flex-direction:column;gap:.3rem"></div>
108
+ <button class="btn" id="addChap">+ فصل جديد</button>
109
+ <div style="flex:1;min-height:1rem"></div>
110
+ <h2>الكتاب</h2>
111
+ <button class="btn solid" id="exportPdf">تصدير PDF</button>
112
+ <button class="btn" id="exportEpub">تصدير EPUB</button>
113
+ <button class="btn" id="exportJson">نسخة احتياطية</button>
114
+ <button class="btn" id="importJson">استعادة نسخة</button>
115
+ <input type="file" id="importFile" accept="application/json" hidden>
116
+ </aside>
117
+
118
+ <main class="pane">
119
+ <div class="tabs">
120
+ <button class="tab on" data-tab="text">لصق نص</button>
121
+ <button class="tab" data-tab="image">رفع صور</button>
122
+ <button class="tab" data-tab="read">القراءة</button>
123
+ <button class="tab" data-tab="engine">المحرّك</button>
124
+ <button class="tab" data-tab="meta">بيانات الرواية</button>
125
+ </div>
126
+
127
+ <section id="tab-text">
128
+ <label class="fld">عنوان الفصل</label>
129
+ <input type="text" id="chapTitle" placeholder="الفصل الأول">
130
+ <label class="fld">النص الأصلي</label>
131
+ <textarea id="srcText" placeholder="الصق هنا نص الفصل…"></textarea>
132
+ <div class="row" style="margin-top:.8rem">
133
+ <button class="btn solid" id="runText">ترجمة الفصل</button>
134
+ <span style="font-size:.8rem;opacity:.6" id="engLabel"></span>
135
+ </div>
136
+ <div class="status" id="st1"></div>
137
+ <div class="bar"><i id="pb1"></i></div>
138
+ </section>
139
+
140
+ <section id="tab-image" hidden>
141
+ <label class="fld">عنوان الفصل</label>
142
+ <input type="text" id="chapTitleImg" placeholder="الفصل الأول">
143
+ <label class="fld">صور الصفحات</label>
144
+ <input type="file" id="files" accept="image/*" multiple>
145
+ <div class="thumbs" id="thumbs"></div>
146
+ <div class="row" style="margin-top:.8rem">
147
+ <button class="btn solid" id="runImg">قراءة الصور وترجمتها</button>
148
+ </div>
149
+ <div class="status" id="st2"></div>
150
+ <div class="bar"><i id="pb2"></i></div>
151
+ <p class="hint">تُقرأ الصور محليًا بمحرّك Tesseract، ثم يُترجَم النص المستخرج بالمحرّك المختار. جودة القراءة تعتمد على وضوح الصورة — الصور المستقيمة عالية التباين تعطي أفضل نتيجة.</p>
152
+ </section>
153
+
154
+ <section id="tab-read" hidden>
155
+ <div class="row" style="margin-bottom:.9rem">
156
+ <button class="btn" id="prevCh">السابق</button>
157
+ <button class="btn" id="nextCh">التالي</button>
158
+ <label style="font-size:.82rem;opacity:.7"><input type="checkbox" id="showOrig"> إظهار الأصل</label>
159
+ <button class="btn" id="editCh">تحرير الترجمة</button>
160
+ <button class="btn" id="delCh">حذف الفصل</button>
161
+ </div>
162
+ <div class="sheet" id="sheet"></div>
163
+ </section>
164
+
165
+ <section id="tab-engine" hidden>
166
+ <div class="card">
167
+ <h4>محرّك الترجمة</h4>
168
+ <div class="row" style="margin-top:.5rem">
169
+ <select id="engine">
170
+ <option value="local">محلي في المتصفح — Transformers.js + NLLB-200</option>
171
+ <option value="openai">خادم متوافق مع OpenAI — Ollama / Groq / OpenRouter / Space خاص</option>
172
+ <option value="libre">LibreTranslate — خادم مفتوح</option>
173
+ </select>
174
+ </div>
175
+ </div>
176
+
177
+ <div class="card" id="cfg-local">
178
+ <h4>الوضع المحلي</h4>
179
+ <p class="hint">يُنزَّل النموذج مرة واحدة (≈ 300 ميغابايت بصيغة q8) ويُخزَّن في المتصفح، ثم يعمل بلا إنترنت. مناسب للحاسب؛ على الجوال أبطأ بوضوح.</p>
180
+ <label class="fld">النموذج</label>
181
+ <select id="localModel">
182
+ <option value="Xenova/nllb-200-distilled-600M">NLLB-200 distilled 600M — لغات كثيرة</option>
183
+ <option value="Xenova/opus-mt-en-ar">opus-mt-en-ar — إنجليزي فقط، أخف وأسرع</option>
184
+ </select>
185
+ <label class="fld">لغة المصدر</label>
186
+ <select id="srcLang">
187
+ <option value="eng_Latn">إنجليزي</option>
188
+ <option value="fra_Latn">فرنسي</option>
189
+ <option value="spa_Latn">إسباني</option>
190
+ <option value="deu_Latn">ألماني</option>
191
+ <option value="rus_Cyrl">روسي</option>
192
+ <option value="tur_Latn">تركي</option>
193
+ <option value="jpn_Jpan">ياباني</option>
194
+ <option value="kor_Hang">كوري</option>
195
+ <option value="zho_Hans">صيني مبسّط</option>
196
+ <option value="ind_Latn">إندونيسي</option>
197
+ </select>
198
+ <div class="row" style="margin-top:.9rem">
199
+ <button class="btn" id="preload">تنزيل النموذج الآن</button>
200
+ <span class="status" id="stLoad" style="margin:0"></span>
201
+ </div>
202
+ <div class="bar"><i id="pbLoad"></i></div>
203
+ </div>
204
+
205
+ <div class="card" id="cfg-openai" hidden>
206
+ <h4>خادم متوافق مع OpenAI</h4>
207
+ <p class="hint">Ollama محليًا: <code dir="ltr">http://localhost:11434/v1</code> — ‏Groq: <code dir="ltr">https://api.groq.com/openai/v1</code> — ‏OpenRouter: <code dir="ltr">https://openrouter.ai/api/v1</code></p>
208
+ <label class="fld">عنوان الخادم</label>
209
+ <input type="text" id="oaBase" dir="ltr" placeholder="http://localhost:11434/v1">
210
+ <label class="fld">اسم النموذج</label>
211
+ <input type="text" id="oaModel" dir="ltr" placeholder="qwen2.5:7b-instruct">
212
+ <label class="fld">المفتاح (اتركيه فارغًا مع Ollama)</label>
213
+ <input type="password" id="oaKey" dir="ltr">
214
+ <p class="hint">المفتاح يُحفظ في متصفحك فقط ولا يُرسل لغير الخادم الذي تحدّدينه.</p>
215
+ <div class="row" style="margin-top:.8rem"><button class="btn" id="testConn">اختبار الاتصال</button></div>
216
+ <pre id="testOut" dir="ltr" style="white-space:pre-wrap;font-size:.78rem;opacity:.85;margin:.7rem 0 0"></pre>
217
+ </div>
218
+
219
+ <div class="card" id="cfg-libre" hidden>
220
+ <h4>LibreTranslate</h4>
221
+ <label class="fld">عنوان الخادم</label>
222
+ <input type="text" id="ltBase" dir="ltr" placeholder="https://libretranslate.com">
223
+ <label class="fld">المفتاح (إن لزم)</label>
224
+ <input type="password" id="ltKey" dir="ltr">
225
+ <label class="fld">رمز لغة المصدر</label>
226
+ <input type="text" id="ltSrc" dir="ltr" value="en">
227
+ </div>
228
+
229
+ <div class="card">
230
+ <h4>أسلوب الترجمة</h4>
231
+ <select id="style">
232
+ <option value="literary">أدبي سلس</option>
233
+ <option value="faithful">أمين قريب من الأصل</option>
234
+ <option value="simple">مبسّط</option>
235
+ </select>
236
+ <p class="hint">يُطبَّق في وضع الخادم المتوافق مع OpenAI فقط؛ النماذج المحلية لا تقبل توجيهًا أسلوبيًا.</p>
237
+ </div>
238
+ <div class="card">
239
+ <h4>فحص التشغيل</h4>
240
+ <p class="hint">يفحص بيئة المتصفح ويعرض النتيجة نصًا — انسخيه لي لو بقيت المشكلة.</p>
241
+ <div class="row" style="margin-top:.6rem"><button class="btn" id="diag">شغّلي الفحص</button></div>
242
+ <pre id="diagOut" dir="ltr" style="white-space:pre-wrap;font-size:.78rem;opacity:.8;margin:.7rem 0 0"></pre>
243
+ </div>
244
+
245
+ <div class="row"><button class="btn solid" id="saveEngine">حفظ الإعدادات</button><span class="status" id="stEng" style="margin:0"></span></div>
246
+ </section>
247
+
248
+ <section id="tab-meta" hidden>
249
+ <label class="fld">اسم الرواية</label>
250
+ <input type="text" id="mTitle">
251
+ <label class="fld">المؤلف</label>
252
+ <input type="text" id="mAuthor">
253
+ <label class="fld">مسرد الأسماء (اسم = الترجمة، سطر لكل مدخل)</label>
254
+ <textarea id="mGloss" style="min-height:130px" placeholder="Elias = إلياس"></textarea>
255
+ <p class="hint">يُطبَّق المسرد على كل الفصول: في وضع الخادم يُرسل ضمن التعليمات، وفي الوضع المحلي يُستبدل في النص بعد الترجمة.</p>
256
+ <div class="row" style="margin-top:.9rem">
257
+ <button class="btn solid" id="saveMeta">حفظ</button>
258
+ <button class="btn" id="delBook">حذف الرواية</button>
259
+ </div>
260
+ <p class="notice">استعملي الورّاق مع نصوص تملكين حق استخدامها: الأعمال في الملك العام، أو كتاباتك، أو ما رخّصه ناشره. الترجمة الناتجة للاستعمال الشخصي.</p>
261
+ </section>
262
+ </main>
263
+ </div>
264
+
265
+ <div id="printArea"></div>
266
+
267
+ <script type="module">
268
+ const $ = s => document.querySelector(s);
269
+ const uid = () => Date.now().toString(36) + Math.random().toString(36).slice(2,6);
270
+ const esc = s => (s||"").replace(/[&<>"]/g,c=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;"}[c]));
271
+ /* ═══ إظهار الأخطاء بدل ابتلاعها ═══ */
272
+ function fail(msg){
273
+ let b = document.getElementById("errBar");
274
+ if(!b){
275
+ b = document.createElement("div"); b.id = "errBar";
276
+ b.style.cssText = "background:#5a2020;color:#F2ECDD;padding:.7rem 1.4rem;font-size:.82rem;line-height:1.8;direction:ltr;text-align:left;white-space:pre-wrap";
277
+ document.body.insertBefore(b, document.body.firstChild);
278
+ }
279
+ b.textContent = String(msg);
280
+ }
281
+ window.addEventListener("error", e => fail("خطأ: " + (e.message || e.error)));
282
+ window.addEventListener("unhandledrejection", e => fail("خطأ: " + (e.reason?.message || e.reason)));
283
+ /* ═══ تحميل مكتبة عند الحاجة، مع مصدر بديل ═══ */
284
+ const loaded = {};
285
+ function loadLib(name, urls){
286
+ if(loaded[name]) return loaded[name];
287
+ loaded[name] = new Promise((res, rej) => {
288
+ const attempt = i => {
289
+ if(i >= urls.length) return rej(new Error("تعذّر تحميل مكتبة " + name + " — تحقّقي من الاتصال أو من مانع الإعلانات."));
290
+ const s = document.createElement("script");
291
+ s.src = urls[i]; s.onload = () => res(); s.onerror = () => attempt(i+1);
292
+ document.head.appendChild(s);
293
+ };
294
+ attempt(0);
295
+ });
296
+ return loaded[name];
297
+ }
298
+ /* ═══ تخزين على IndexedDB مباشرة ═══ */
299
+ const store = (() => {
300
+ let dbp = null;
301
+ const open = () => dbp || (dbp = (async () => {
302
+ const first = await new Promise((res, rej) => {
303
+ const r = indexedDB.open("alwarraq_store");
304
+ r.onupgradeneeded = () => { if(!r.result.objectStoreNames.contains("kv")) r.result.createObjectStore("kv"); };
305
+ r.onsuccess = () => res(r.result);
306
+ r.onerror = () => rej(r.error);
307
+ });
308
+ if(first.objectStoreNames.contains("kv")) return first;
309
+ const next = first.version + 1;
310
+ first.close();
311
+ return new Promise((res, rej) => {
312
+ const r = indexedDB.open("alwarraq_store", next);
313
+ r.onupgradeneeded = () => { if(!r.result.objectStoreNames.contains("kv")) r.result.createObjectStore("kv"); };
314
+ r.onsuccess = () => res(r.result);
315
+ r.onerror = () => rej(r.error);
316
+ });
317
+ })());
318
+ const run = async (mode, fn) => {
319
+ const db = await open();
320
+ return new Promise((res, rej) => {
321
+ const t = db.transaction("kv", mode);
322
+ const req = fn(t.objectStore("kv"));
323
+ t.oncomplete = () => res(req ? req.result : undefined);
324
+ t.onerror = () => rej(t.error);
325
+ t.onabort = () => rej(t.error);
326
+ });
327
+ };
328
+ const ls = {
329
+ get: k => { try{ return JSON.parse(localStorage.getItem("aw:"+k)); }catch(e){ return null; } },
330
+ set: (k,v) => localStorage.setItem("aw:"+k, JSON.stringify(v)),
331
+ del: k => localStorage.removeItem("aw:"+k)
332
+ };
333
+ const usable = typeof indexedDB !== "undefined";
334
+ return {
335
+ get: async k => usable ? (await run("readonly", s => s.get(k))) ?? null : ls.get(k),
336
+ set: async (k,v) => usable ? run("readwrite", s => s.put(v,k)) : ls.set(k,v),
337
+ del: async k => usable ? run("readwrite", s => s.delete(k)) : ls.del(k)
338
+ };
339
+ })();
340
+ let lib = [], book = null, curCh = 0, pending = [];
341
+ let cfg = { engine:"local", localModel:"Xenova/nllb-200-distilled-600M", srcLang:"eng_Latn",
342
+ oaBase:"", oaModel:"", oaKey:"", ltBase:"https://libretranslate.com", ltKey:"", ltSrc:"en", style:"literary" };
343
+ /* ═══ المحرّكات ═══ */
344
+ let translator = null, translatorKey = "";
345
+ async function getLocal(onProg){
346
+ const key = cfg.localModel;
347
+ if(translator && translatorKey === key) return translator;
348
+ let mod;
349
+ try{
350
+ mod = await import("https://cdn.jsdelivr.net/npm/@xenova/transformers@2.17.2");
351
+ }catch(e){
352
+ mod = await import("https://unpkg.com/@xenova/transformers@2.17.2");
353
+ }
354
+ const { pipeline, env } = mod;
355
+ env.allowLocalModels = false;
356
+ translator = await pipeline("translation", key, { quantized: true, progress_callback: onProg });
357
+ translatorKey = key;
358
+ return translator;
359
+ }
360
+ /* NLLB يعمل أفضل على مستوى الجملة */
361
+ function sentences(text){
362
+ return text.split(/\n\s*\n/).map(p =>
363
+ (p.match(/[^.!?。?!]+[.!?。?!]*\s*/g) || [p]).map(s=>s.trim()).filter(Boolean)
364
+ );
365
+ }
366
+ async function translateLocal(text, prog, note){
367
+ const t = await getLocal(p => {
368
+ if(p.status === "progress" && note){
369
+ const mb = n => (n/1048576).toFixed(1);
370
+ const size = p.total ? ` (${mb(p.loaded||0)} / ${mb(p.total)} ميغابايت)` : "";
371
+ note(`تنزيل النموذج — ${p.file||""} ${Math.round(p.progress||0)}%${size}`);
372
+ }
373
+ else if(p.status === "download" && note) note("بدء تنزيل " + (p.file||""));
374
+ else if(p.status === "ready" && note) note("النموذج جاهز، تبدأ الترجمة…");
375
+ });
376
+ const paras = sentences(text);
377
+ const total = paras.reduce((a,p)=>a+p.length,0);
378
+ let done = 0; const out = [];
379
+ const isNllb = cfg.localModel.includes("nllb");
380
+ for(const para of paras){
381
+ const parts = [];
382
+ for(const s of para){
383
+ const r = isNllb
384
+ ? await t(s, { src_lang: cfg.srcLang, tgt_lang: "arb_Arab" })
385
+ : await t(s);
386
+ parts.push((r[0].translation_text || "").trim());
387
+ done++; prog(done/total);
388
+ }
389
+ out.push(parts.join(" "));
390
+ }
391
+ return applyGloss(out.join("\n\n"));
392
+ }
393
+ const STYLES = {
394
+ literary:"ترجمة أدبية سلسة تحافظ على الصورة والإيقاع بعربية فصحى معاصرة.",
395
+ faithful:"ترجمة أمينة قريبة من الأصل، بلا حذف ولا إضافة.",
396
+ simple:"ترجمة بعربية مبسّطة واضحة."
397
+ };
398
+ function cleanReply(raw){
399
+ let s = raw || "";
400
+ s = s.replace(/<think>[\s\S]*?<\/think>/gi, "");
401
+ s = s.replace(/<reasoning>[\s\S]*?<\/reasoning>/gi, "");
402
+ if(/<think>/i.test(s)){
403
+ const i = s.toLowerCase().lastIndexOf("</think>");
404
+ s = i >= 0 ? s.slice(i + 8) : "";
405
+ }
406
+ s = s.replace(/^\s*```[a-z]*\s*\n?/i, "").replace(/\n?```\s*$/,"");
407
+ s = s.trim();
408
+ if(!s) throw new Error("النموذج أعاد تفكيره الداخلي بلا ترجمة نهائية. اختاري نموذجًا بلا وضع تفكير.");
409
+ return s;
410
+ }
411
+ async function callOpenAI(messages){
412
+ const h = { "Content-Type":"application/json" };
413
+ if(cfg.oaKey) h.Authorization = "Bearer " + cfg.oaKey;
414
+ const mkBody = quiet => JSON.stringify(Object.assign({
415
+ model: cfg.oaModel, messages, temperature: 0.6, max_completion_tokens: 4000
416
+ }, quiet ? { reasoning_format: "hidden", reasoning_effort: "none" } : {}));
417
+ let quiet = true;
418
+ for(let attempt=0; attempt<5; attempt++){
419
+ const res = await fetch(cfg.oaBase.replace(/\/$/,"") + "/chat/completions",
420
+ { method:"POST", headers:h, body: mkBody(quiet) });
421
+ if(res.ok){
422
+ const d = await res.json();
423
+ const m = d.choices?.[0]?.message || {};
424
+ return cleanReply(m.content);
425
+ }
426
+ let detail = "";
427
+ try{ const j = await res.json(); detail = j.error?.message || JSON.stringify(j); }
428
+ catch(e){ detail = await res.text().catch(()=>""); }
429
+ if(quiet && res.status === 400 && /reasoning|unrecognized|unsupported|invalid/i.test(detail)){
430
+ quiet = false;
431
+ continue;
432
+ }
433
+ if(res.status === 429 && attempt < 4){
434
+ const wait = Number(res.headers.get("retry-after")) || (8 * (attempt+1));
435
+ if(typeof window.__note === "function")
436
+ window.__note(`تجاوز حد الاستخدام — إعادة المحاولة بعد ${wait} ثانية.`);
437
+ await new Promise(r => setTimeout(r, wait*1000));
438
+ continue;
439
+ }
440
+ throw new Error(`الخادم رفض الطلب (${res.status}): ${detail}`);
441
+ }
442
+ throw new Error("تعذّر إتمام الطلب بعد عدة محاولات.");
443
+ }
444
+ async function translateOpenAI(text){
445
+ const g = (book.gloss||"").trim();
446
+ const sys = ["أنت مترجم أدبي إلى العربية.", STYLES[cfg.style],
447
+ "حافظ على تقسيم الفقرات والحوار.", g?("التزم بهذا المسرد:\n"+g):"",
448
+ "أ��د الترجمة فقط دون تعليق."].filter(Boolean).join("\n");
449
+ return callOpenAI([{role:"system",content:sys},{role:"user",content:text}]);
450
+ }
451
+ async function translateLibre(text){
452
+ const res = await fetch(cfg.ltBase.replace(/\/$/,"") + "/translate", {
453
+ method:"POST", headers:{"Content-Type":"application/json"},
454
+ body: JSON.stringify({ q:text, source:cfg.ltSrc||"auto", target:"ar", format:"text", api_key:cfg.ltKey||undefined })
455
+ });
456
+ if(!res.ok) throw new Error("خادم LibreTranslate لم يستجب ("+res.status+").");
457
+ const d = await res.json();
458
+ return applyGloss(d.translatedText || "");
459
+ }
460
+ function applyGloss(txt){
461
+ const lines = (book.gloss||"").split("\n").map(l=>l.split("=")).filter(p=>p.length===2);
462
+ for(const [a,b] of lines){
463
+ const from = a.trim(), to = b.trim();
464
+ if(from) txt = txt.split(from).join(to);
465
+ }
466
+ return txt;
467
+ }
468
+ function chunk(text, size=1400){
469
+ const paras = text.split(/\n\s*\n/); const out=[]; let cur="";
470
+ for(const p of paras){
471
+ if(cur && (cur.length+p.length)>size){ out.push(cur); cur=p; }
472
+ else cur = cur ? cur+"\n\n"+p : p;
473
+ }
474
+ if(cur.trim()) out.push(cur);
475
+ return out;
476
+ }
477
+ async function translate(text, prog, note){
478
+ window.__note = note || null;
479
+ if(cfg.engine === "local") return translateLocal(text, prog, note);
480
+ const pieces = chunk(text); const out=[];
481
+ for(let i=0;i<pieces.length;i++){
482
+ out.push(cfg.engine==="openai" ? await translateOpenAI(pieces[i]) : await translateLibre(pieces[i]));
483
+ prog((i+1)/pieces.length);
484
+ }
485
+ return out.join("\n\n");
486
+ }
487
+ /* ═══ المكتبة ═══ */
488
+ async function loadAll(){
489
+ cfg = Object.assign(cfg, await store.get("cfg") || {});
490
+ fillEngine();
491
+ lib = await store.get("library:index") || [];
492
+ if(!lib.length){
493
+ const b = { id:uid(), title:"روايتي الأولى", author:"", gloss:"", chapters:[] };
494
+ lib = [{id:b.id,title:b.title}];
495
+ await store.set("library:index",lib); await store.set("book:"+b.id,b);
496
+ }
497
+ await openBook(lib[0].id); renderSel();
498
+ }
499
+ function renderSel(){
500
+ $("#bookSel").innerHTML = lib.map(b=>`<option value="${b.id}" ${book&&b.id===book.id?"selected":""}>${esc(b.title)}</option>`).join("");
501
+ }
502
+ async function openBook(id){ book = await store.get("book:"+id); curCh=0; renderLeaves(); fillMeta(); renderSheet(); }
503
+ async function saveBook(){
504
+ await store.set("book:"+book.id, book);
505
+ const e = lib.find(b=>b.id===book.id); if(e) e.title = book.title;
506
+ await store.set("library:index", lib); renderSel();
507
+ }
508
+ function renderLeaves(){
509
+ const box = $("#leaves");
510
+ if(!book.chapters.length){ box.innerHTML = '<p class="empty">لا فصول بعد.</p>'; return; }
511
+ box.innerHTML = book.chapters.map((c,i)=>`<button class="leaf ${i===curCh?"on":""}" data-i="${i}">
512
+ <span class="n">${i+1}</span><span class="t">${esc(c.title||"بلا عنوان")}</span>
513
+ <span class="dot ${c.busy?"busy":(c.ar?"done":"")}"></span></button>`).join("");
514
+ box.querySelectorAll(".leaf").forEach(b=>b.onclick=()=>{curCh=+b.dataset.i;renderLeaves();renderSheet();tab("read");});
515
+ }
516
+ function renderSheet(){
517
+ const c = book.chapters[curCh];
518
+ if(!c){ $("#sheet").innerHTML='<p style="opacity:.5">لا يوجد فصل معروض.</p>'; return; }
519
+ const body = (c.ar||"").split(/\n\s*\n/).map(p=>`<p>${esc(p)}</p>`).join("");
520
+ const orig = $("#showOrig").checked && c.src ? `<div class="orig">${esc(c.src).replace(/\n/g,"<br>")}</div>` : "";
521
+ $("#sheet").innerHTML = `<h3>${esc(c.title||"")}</h3>${body||'<p style="opacity:.5">بلا ترجمة بعد.</p>'}${orig}`;
522
+ }
523
+ function fillMeta(){ $("#mTitle").value=book.title||""; $("#mAuthor").value=book.author||""; $("#mGloss").value=book.gloss||""; }
524
+ function fillEngine(){
525
+ $("#engine").value=cfg.engine; $("#localModel").value=cfg.localModel; $("#srcLang").value=cfg.srcLang;
526
+ $("#oaBase").value=cfg.oaBase; $("#oaModel").value=cfg.oaModel; $("#oaKey").value=cfg.oaKey;
527
+ $("#ltBase").value=cfg.ltBase; $("#ltKey").value=cfg.ltKey; $("#ltSrc").value=cfg.ltSrc;
528
+ $("#style").value=cfg.style; showCfg();
529
+ }
530
+ function showCfg(){
531
+ $("#cfg-local").hidden = $("#engine").value!=="local";
532
+ $("#cfg-openai").hidden = $("#engine").value!=="openai";
533
+ $("#cfg-libre").hidden = $("#engine").value!=="libre";
534
+ const names={local:"محرّك محلي",openai:"خادم خاص",libre:"LibreTranslate"};
535
+ $("#engLabel").textContent = "المحرّك: " + names[cfg.engine];
536
+ }
537
+ $("#engine").onchange = showCfg;
538
+ $("#saveEngine").onclick = async () => {
539
+ cfg = { engine:$("#engine").value, localModel:$("#localModel").value, srcLang:$("#srcLang").value,
540
+ oaBase:$("#oaBase").value.trim(), oaModel:$("#oaModel").value.trim(), oaKey:$("#oaKey").value.trim(),
541
+ ltBase:$("#ltBase").value.trim(), ltKey:$("#ltKey").value.trim(), ltSrc:$("#ltSrc").value.trim(), style:$("#style").value };
542
+ translator=null; await store.set("cfg",cfg); showCfg();
543
+ $("#stEng").textContent="حُفظت الإعدادات."; setTimeout(()=>$("#stEng").textContent="",1500);
544
+ };
545
+ $("#preload").onclick = async () => {
546
+ $("#preload").disabled=true;
547
+ try{
548
+ await getLocal(p=>{
549
+ if(p.status==="progress"){ $("#pbLoad").style.width=Math.round(p.progress)+"%"; $("#stLoad").textContent=`تنزيل ${p.file||""} — ${Math.round(p.progress)}%`; }
550
+ else if(p.status==="ready") $("#stLoad").textContent="النموذج جاهز.";
551
+ });
552
+ $("#pbLoad").style.width="100%"; $("#stLoad").textContent="النموذج جاهز ومخزَّن في المتصفح.";
553
+ }catch(e){ $("#stLoad").textContent="تعذّر التنزيل: "+e.message; }
554
+ $("#preload").disabled=false;
555
+ };
556
+ /* ═══ التبويبات ═══ */
557
+ function tab(n){
558
+ document.querySelectorAll(".tab").forEach(t=>t.classList.toggle("on",t.dataset.tab===n));
559
+ ["text","image","read","engine","meta"].forEach(x=>$("#tab-"+x).hidden = x!==n);
560
+ }
561
+ document.querySelectorAll(".tab").forEach(t=>t.onclick=()=>tab(t.dataset.tab));
562
+ /* ═══ ترجمة نص ═══ */
563
+ $("#runText").onclick = async () => {
564
+ const src = $("#srcText").value.trim();
565
+ if(!src){ $("#st1").textContent="الصق نص الفصل أولًا."; return; }
566
+ const ch = { id:uid(), title:$("#chapTitle").value.trim()||("الفصل "+(book.chapters.length+1)), src, ar:"", busy:true };
567
+ book.chapters.push(ch); curCh=book.chapters.length-1; renderLeaves();
568
+ $("#runText").disabled=true; $("#st1").textContent="جارٍ التحضير…";
569
+ try{
570
+ ch.ar = await translate(src,
571
+ p => { $("#pb1").style.width=Math.round(p*100)+"%"; $("#st1").textContent=`تُترجم… ${Math.round(p*100)}%`; },
572
+ msg => { $("#st1").textContent = msg; });
573
+ ch.busy=false; $("#st1").textContent="تمت ترجمة الفصل.";
574
+ $("#srcText").value=""; $("#chapTitle").value="";
575
+ await saveBook(); renderLeaves(); renderSheet(); tab("read");
576
+ }catch(e){ ch.busy=false; $("#st1").textContent=e.message; await saveBook(); renderLeaves(); }
577
+ $("#runText").disabled=false;
578
+ };
579
+ /* ═══ صور ═══ */
580
+ $("#files").onchange = e => {
581
+ pending=[...e.target.files];
582
+ $("#thumbs").innerHTML = pending.map(f=>`<img src="${URL.createObjectURL(f)}" alt="">`).join("");
583
+ };
584
+ const OCR_LANG = { eng_Latn:"eng", fra_Latn:"fra", spa_Latn:"spa", deu_Latn:"deu", rus_Cyrl:"rus",
585
+ tur_Latn:"tur", jpn_Jpan:"jpn", kor_Hang:"kor", zho_Hans:"chi_sim", ind_Latn:"ind" };
586
+ $("#runImg").onclick = async () => {
587
+ if(!pending.length){ $("#st2").textContent="اختاري صور الصفحات أولًا."; return; }
588
+ const ch = { id:uid(), title:$("#chapTitleImg").value.trim()||("الفصل "+(book.chapters.length+1)), src:"", ar:"", busy:true };
589
+ book.chapters.push(ch); curCh=book.chapters.length-1; renderLeaves();
590
+ $("#runImg").disabled=true;
591
+ try{
592
+ $("#st2").textContent="تحميل محرّك القراءة…";
593
+ await loadLib("tesseract", [
594
+ "https://cdn.jsdelivr.net/npm/tesseract.js@5.1.0/dist/tesseract.min.js",
595
+ "https://unpkg.com/tesseract.js@5.1.0/dist/tesseract.min.js"
596
+ ]);
597
+ const worker = await Tesseract.createWorker(OCR_LANG[cfg.srcLang] || "eng");
598
+ let raw = "";
599
+ for(let i=0;i<pending.length;i++){
600
+ $("#st2").textContent=`قراءة الصفحة ${i+1} من ${pending.length}…`;
601
+ $("#pb2").style.width=Math.round(i/pending.length*50)+"%";
602
+ const { data } = await worker.recognize(pending[i]);
603
+ raw += (raw?"\n\n":"") + data.text.trim();
604
+ }
605
+ await worker.terminate();
606
+ ch.src = raw; renderSheet();
607
+ $("#st2").textContent="جارٍ الترجمة…";
608
+ ch.ar = await translate(raw, p => { $("#pb2").style.width=(50+Math.round(p*50))+"%"; });
609
+ ch.busy=false; $("#st2").textContent="تمت ترجمة الصفحات.";
610
+ pending=[]; $("#files").value=""; $("#thumbs").innerHTML=""; $("#chapTitleImg").value="";
611
+ await saveBook(); renderLeaves(); renderSheet(); tab("read");
612
+ }catch(e){ ch.busy=false; $("#st2").textContent=e.message; await saveBook(); renderLeaves(); }
613
+ $("#runImg").disabled=false;
614
+ };
615
+ /* ═══ أدوات ═══ */
616
+ $("#prevCh").onclick=()=>{if(curCh>0){curCh--;renderLeaves();renderSheet();}};
617
+ $("#nextCh").onclick=()=>{if(curCh<book.chapters.length-1){curCh++;renderLeaves();renderSheet();}};
618
+ $("#showOrig").onchange=renderSheet;
619
+ $("#editCh").onclick=()=>{
620
+ const c=book.chapters[curCh]; if(!c) return;
621
+ $("#sheet").innerHTML=`<textarea id="ed" style="width:100%;min-height:60vh;background:#fff;color:#241F17;border:1px solid #c9bfa6;font-family:var(--serif);font-size:1.1rem;line-height:2">${esc(c.ar)}</textarea>
622
+ <div class="row" style="margin-top:.8rem"><button class="btn solid" id="okEd">حفظ التعديل</button></div>`;
623
+ $("#okEd").onclick=async()=>{ c.ar=$("#ed").value; await saveBook(); renderSheet(); };
624
+ };
625
+ $("#delCh").onclick=async()=>{
626
+ if(!book.chapters[curCh]||!confirm("حذف هذا الفصل نهائيًا؟"))return;
627
+ book.chapters.splice(curCh,1); curCh=Math.max(0,curCh-1);
628
+ await saveBook(); renderLeaves(); renderSheet();
629
+ };
630
+ $("#addChap").onclick=()=>tab("text");
631
+ $("#newBook").onclick=async()=>{
632
+ const t=prompt("اسم الرواية:"); if(!t)return;
633
+ const b={id:uid(),title:t,author:"",gloss:"",chapters:[]};
634
+ lib.push({id:b.id,title:t}); await store.set("library:index",lib); await store.set("book:"+b.id,b);
635
+ book=b; curCh=0; renderSel(); renderLeaves(); fillMeta(); renderSheet(); tab("meta");
636
+ };
637
+ $("#bookSel").onchange=e=>openBook(e.target.value);
638
+ $("#saveMeta").onclick=async()=>{
639
+ book.title=$("#mTitle").value.trim()||book.title; book.author=$("#mAuthor").value.trim(); book.gloss=$("#mGloss").value;
640
+ await saveBook(); $("#saveMeta").textContent="حُفظ"; setTimeout(()=>$("#saveMeta").textContent="حفظ",1200);
641
+ };
642
+ $("#delBook").onclick=async()=>{
643
+ if(!confirm("حذف الرواية بكل فصولها؟"))return;
644
+ await store.del("book:"+book.id); lib=lib.filter(b=>b.id!==book.id);
645
+ await store.set("library:index",lib);
646
+ if(lib.length) await openBook(lib[0].id); else location.reload();
647
+ renderSel();
648
+ };
649
+ /* ═══ التصدير ═══ */
650
+ function buildPrint(){
651
+ const toc = book.chapters.map((c,i)=>`<li>${esc(c.title||"فصل "+(i+1))}</li>`).join("");
652
+ const chaps = book.chapters.map(c=>`<div class="chap"><h2>${esc(c.title||"")}</h2>${
653
+ (c.ar||"").split(/\n\s*\n/).map(p=>`<p>${esc(p)}</p>`).join("")}</div>`).join("");
654
+ $("#printArea").innerHTML = `<div class="title-page"><h1>${esc(book.title)}</h1>
655
+ ${book.author?`<div class="by">${esc(book.author)}</div>`:""}<div class="mark">ترجمة عربية</div></div>
656
+ <div class="toc"><h2>المحتويات</h2><ol>${toc}</ol></div>${chaps}`;
657
+ }
658
+ $("#exportPdf").onclick=()=>{
659
+ if(!book.chapters.length){alert("لا فصول للتصدير.");return;}
660
+ buildPrint(); setTimeout(()=>window.print(),120);
661
+ };
662
+ function dl(blob,name){ const a=document.createElement("a"); a.href=URL.createObjectURL(blob); a.download=name; a.click(); }
663
+ $("#exportEpub").onclick=async()=>{
664
+ if(!book.chapters.length){alert("لا فصول للتصدير.");return;}
665
+ await loadLib("jszip", [
666
+ "https://cdn.jsdelivr.net/npm/jszip@3.10.1/dist/jszip.min.js",
667
+ "https://unpkg.com/jszip@3.10.1/dist/jszip.min.js"
668
+ ]);
669
+ const zip=new JSZip();
670
+ zip.file("mimetype","application/epub+zip",{compression:"STORE"});
671
+ zip.folder("META-INF").file("container.xml",
672
+ `<?xml version="1.0"?><container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container"><rootfiles><rootfile full-path="OEBPS/content.opf" media-type="application/oebps-package+xml"/></rootfiles></container>`);
673
+ const o=zip.folder("OEBPS");
674
+ o.file("style.css",`body{font-family:serif;direction:rtl;text-align:justify;line-height:1.9;margin:1em}h2{border-bottom:2px solid #7a6a2e;padding-bottom:.3em}p{margin:0 0 1em;text-indent:1.2em}`);
675
+ book.chapters.forEach((c,i)=>{
676
+ o.file(`ch${i+1}.xhtml`,
677
+ `<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html><html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ar" dir="rtl"><head><meta charset="utf-8"/><title>${esc(c.title)}</title><link rel="stylesheet" href="style.css"/></head><body><h2>${esc(c.title)}</h2>${
678
+ (c.ar||"").split(/\n\s*\n/).map(p=>`<p>${esc(p)}</p>`).join("")}</body></html>`);
679
+ });
680
+ const items=book.chapters.map((c,i)=>`<item id="c${i+1}" href="ch${i+1}.xhtml" media-type="application/xhtml+xml"/>`).join("");
681
+ const refs=book.chapters.map((c,i)=>`<itemref idref="c${i+1}"/>`).join("");
682
+ const nav=book.chapters.map((c,i)=>`<li><a href="ch${i+1}.xhtml">${esc(c.title)}</a></li>`).join("");
683
+ o.file("nav.xhtml",`<?xml version="1.0" encoding="utf-8"?><!DOCTYPE html><html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops" xml:lang="ar" dir="rtl"><head><meta charset="utf-8"/><title>الفهرس</title></head><body><nav epub:type="toc"><h1>المحتويات</h1><ol>${nav}</ol></nav></body></html>`);
684
+ o.file("content.opf",`<?xml version="1.0" encoding="utf-8"?><package xmlns="http://www.idpf.org/2007/opf" version="3.0" unique-identifier="bid"><metadata xmlns:dc="http://purl.org/dc/elements/1.1/"><dc:identifier id="bid">urn:uuid:${book.id}</dc:identifier><dc:title>${esc(book.title)}</dc:title><dc:language>ar</dc:language><dc:creator>${esc(book.author||"")}</dc:creator><meta property="dcterms:modified">${new Date().toISOString().split(".")[0]}Z</meta></metadata><manifest><item id="nav" href="nav.xhtml" media-type="application/xhtml+xml" properties="nav"/><item id="css" href="style.css" media-type="text/css"/>${items}</manifest><spine>${refs}</spine></package>`);
685
+ dl(await zip.generateAsync({type:"blob",mimeType:"application/epub+zip"}), (book.title||"رواية")+".epub");
686
+ };
687
+ $("#exportJson").onclick=()=>dl(new Blob([JSON.stringify(book,null,2)],{type:"application/json"}),(book.title||"رواية")+".json");
688
+ $("#importJson").onclick=()=>$("#importFile").click();
689
+ $("#importFile").onchange=async e=>{
690
+ const f=e.target.files[0]; if(!f)return;
691
+ const b=JSON.parse(await f.text()); b.id=uid();
692
+ lib.push({id:b.id,title:b.title}); await store.set("library:index",lib); await store.set("book:"+b.id,b);
693
+ book=b; curCh=0; renderSel(); renderLeaves(); fillMeta(); renderSheet();
694
+ };
695
+ $("#testConn").onclick = async () => {
696
+ const base = $("#oaBase").value.trim().replace(/\/$/,"");
697
+ const model = $("#oaModel").value.trim();
698
+ const key = $("#oaKey").value.trim();
699
+ $("#testOut").textContent = "…";
700
+ const h = { "Content-Type":"application/json" };
701
+ if(key) h.Authorization = "Bearer " + key;
702
+ try{
703
+ const res = await fetch(base + "/chat/completions", { method:"POST", headers:h,
704
+ body: JSON.stringify({ model, messages:[{role:"user",content:"Translate to Arabic: The cat sat by the window."}] }) });
705
+ const raw = await res.text();
706
+ if(res.ok){
707
+ let reply = raw;
708
+ try{
709
+ const content = JSON.parse(raw).choices?.[0]?.message?.content || raw;
710
+ const thinky = /<think>/i.test(content);
711
+ let clean = content;
712
+ try{ clean = cleanReply(content); }catch(e){ clean = "(بلا ترجمة نهائية)"; }
713
+ reply = (thinky ? "تنبيه: هذا نموذج تفكير — يكتب مسوّدته قبل الجواب.\n\n" : "") + clean;
714
+ }catch(e){}
715
+ $("#testOut").textContent = "HTTP " + res.status + " — نجح\n\n" + reply;
716
+ } else {
717
+ $("#testOut").textContent = "HTTP " + res.status + "\n\n" + raw;
718
+ }
719
+ }catch(e){ $("#testOut").textContent = "فشل الاتصال: " + e.message; }
720
+ };
721
+ $("#diag").onclick = async () => {
722
+ const out = [];
723
+ out.push("protocol: " + location.protocol);
724
+ out.push("browser: " + navigator.userAgent);
725
+ out.push("indexedDB: " + (typeof indexedDB !== "undefined"));
726
+ out.push("secureContext: " + window.isSecureContext);
727
+ out.push("WebAssembly: " + (typeof WebAssembly !== "undefined"));
728
+ out.push("WebGPU: " + ("gpu" in navigator));
729
+ try{ await store.set("__test", 1); await store.del("__test"); out.push("storage write: ok"); }
730
+ catch(e){ out.push("storage write: FAIL — " + e.message); }
731
+ out.push("books stored: " + lib.length);
732
+ try{
733
+ const r = await fetch("https://cdn.jsdelivr.net/npm/jszip@3.10.1/package.json", {method:"GET"});
734
+ out.push("jsdelivr reachable: " + r.ok);
735
+ }catch(e){ out.push("jsdelivr reachable: FAIL — " + e.message); }
736
+ try{
737
+ const r = await fetch("https://huggingface.co/Xenova/nllb-200-distilled-600M/resolve/main/config.json");
738
+ out.push("huggingface reachable: " + r.ok);
739
+ }catch(e){ out.push("huggingface reachable: FAIL — " + e.message); }
740
+ out.push("engine: " + cfg.engine + " / " + cfg.localModel);
741
+ $("#diagOut").textContent = out.join("\n");
742
+ };
743
+ loadAll().catch(e => fail("فشل بدء التطبيق: " + e.message));
744
+ if(location.protocol === "file:"){
745
+ const w = document.createElement("div");
746
+ w.style.cssText = "background:#5a2020;color:#F2ECDD;padding:.7rem 1.4rem;font-size:.85rem;line-height:1.8;border-bottom:1px solid var(--rule)";
747
+ w.innerHTML = 'الملف مفتوح مباشرة من القرص، والمحرّك المحلي لن يعمل في هذا الوضع. شغّلي خادمًا بسيطًا في مجلد الملف: <code dir="ltr">python3 -m http.server 8000</code> ثم افتحي <code dir="ltr">http://localhost:8000</code>';
748
+ document.body.insertBefore(w, document.body.firstChild);
749
+ }
750
+ </script>
751
+ </body>
752
+ </html>