Update index.html
Browse files- index.html +752 -19
index.html
CHANGED
|
@@ -1,19 +1,752 @@
|
|
| 1 |
-
<!
|
| 2 |
-
<html>
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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=>({"&":"&","<":"<",">":">",'"':"""}[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>
|