Spaces:
Running
Running
Upload 12 files
Browse files- admin (1).html +518 -0
- apple-touch-icon (1).png +0 -0
- config (1).js +28 -0
- dl-sw (1).js +74 -0
- icon-192 (1).png +0 -0
- icon-maskable-512 (1).png +0 -0
- index.html +0 -0
- manifest.webmanifest.txt +25 -0
- og-image (1).jpg +0 -0
- reset (1).html +151 -0
- sw (1).js +125 -0
admin (1).html
ADDED
|
@@ -0,0 +1,518 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="sw">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="utf-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
|
| 6 |
+
<meta name="robots" content="noindex,nofollow">
|
| 7 |
+
<meta name="theme-color" content="#0e1116">
|
| 8 |
+
<title>JAYNES MAX β Admin</title>
|
| 9 |
+
<style>
|
| 10 |
+
:root{--bg:#0e1116;--s1:#151a22;--s2:#1c222c;--ln:#262d38;--tx:#f2f4f7;--mu:#98a2b3;--rd:#e50914;--lv:#c7b8ff;--ok:#4ade80;--wn:#fbbf24;--er:#ff7b72}
|
| 11 |
+
*{box-sizing:border-box;margin:0;padding:0}
|
| 12 |
+
html{scroll-padding-top:90px}
|
| 13 |
+
body{background:var(--bg);color:var(--tx);font:15px/1.45 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;min-height:100vh;padding:env(safe-area-inset-top) 0 env(safe-area-inset-bottom)}
|
| 14 |
+
button,input,select,textarea{font:inherit;color:inherit}
|
| 15 |
+
:focus-visible{outline:2px solid var(--lv);outline-offset:2px}
|
| 16 |
+
[hidden]{display:none!important}
|
| 17 |
+
.brand{font-weight:800;font-size:18px;letter-spacing:.2px}.brand b{color:var(--rd)}
|
| 18 |
+
/* gate */
|
| 19 |
+
#gate{min-height:100vh;display:grid;place-items:center;padding:20px}
|
| 20 |
+
.gbox{width:100%;max-width:380px;background:var(--s1);border:1px solid var(--ln);border-radius:18px;padding:28px 24px}
|
| 21 |
+
.mark{width:52px;height:52px;border-radius:14px;background:var(--rd);display:grid;place-items:center;margin-bottom:18px}
|
| 22 |
+
.gbox h1{font-size:22px;margin-bottom:4px}.gbox p{color:var(--mu);font-size:13.5px;margin-bottom:18px}
|
| 23 |
+
/* shell */
|
| 24 |
+
header{position:sticky;top:0;z-index:20;background:rgba(14,17,22,.92);backdrop-filter:blur(10px);border-bottom:1px solid var(--ln);padding:12px 16px;display:flex;align-items:center;justify-content:space-between;gap:10px}
|
| 25 |
+
.hr{display:flex;gap:8px;align-items:center}
|
| 26 |
+
.lang{display:flex;border:1px solid var(--ln);border-radius:10px;overflow:hidden}
|
| 27 |
+
.lang button{background:none;border:0;padding:6px 11px;font-weight:700;font-size:12.5px;color:var(--mu);cursor:pointer}
|
| 28 |
+
.lang button.on{background:var(--rd);color:#fff}
|
| 29 |
+
.shell{max-width:1120px;margin:0 auto;padding:14px 16px 60px;display:grid;gap:16px}
|
| 30 |
+
nav{display:flex;gap:6px;overflow-x:auto;scrollbar-width:none;padding-bottom:2px}
|
| 31 |
+
nav button{white-space:nowrap;background:none;border:1px solid transparent;border-radius:10px;padding:9px 14px;color:var(--mu);font-weight:600;cursor:pointer;text-align:left}
|
| 32 |
+
nav button.on{background:var(--s2);color:var(--tx);border-color:var(--ln);box-shadow:inset 3px 0 0 var(--rd)}
|
| 33 |
+
@media(min-width:900px){.shell{grid-template-columns:200px 1fr;align-items:start}nav{flex-direction:column;position:sticky;top:76px}}
|
| 34 |
+
h2{font-size:18px;margin-bottom:12px}
|
| 35 |
+
.card{background:var(--s1);border:1px solid var(--ln);border-radius:14px;padding:16px;margin-bottom:14px}
|
| 36 |
+
.card h3{font-size:14.5px;margin-bottom:10px}
|
| 37 |
+
.g2{display:grid;grid-template-columns:1fr 1fr;gap:10px 12px}.full{grid-column:1/-1}
|
| 38 |
+
@media(max-width:560px){.g2{grid-template-columns:1fr}}
|
| 39 |
+
.f{margin-bottom:10px}.f label{display:block;font-size:12.5px;color:var(--mu);margin-bottom:5px}
|
| 40 |
+
input[type=text],input[type=password],input[type=url],input[type=number],textarea,select{width:100%;background:var(--bg);border:1px solid var(--ln);border-radius:9px;padding:10px 12px;outline:none}
|
| 41 |
+
input:focus,textarea:focus,select:focus{border-color:var(--lv)}
|
| 42 |
+
textarea{resize:vertical;min-height:70px}
|
| 43 |
+
.chk{display:flex;gap:9px;align-items:center;font-size:14px;margin:4px 0 12px}.chk input{width:18px;height:18px;accent-color:var(--rd)}
|
| 44 |
+
.hint{font-size:12px;color:var(--mu);margin-top:4px}
|
| 45 |
+
.btn{display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:10px 16px;border-radius:9px;border:1px solid var(--ln);background:var(--s2);font-weight:700;font-size:14px;cursor:pointer}
|
| 46 |
+
.btn.p{background:var(--rd);border-color:var(--rd);color:#fff}.btn.d{color:var(--er);background:#2a1618;border-color:#4a2125}.btn.s{padding:6px 10px;font-size:12.5px}
|
| 47 |
+
.btn:disabled{opacity:.5;cursor:default}.row{display:flex;gap:8px;flex-wrap:wrap;align-items:center}
|
| 48 |
+
.pill{display:inline-block;font-size:11px;font-weight:700;padding:2px 9px;border-radius:99px;background:var(--s2);color:var(--mu);border:1px solid var(--ln)}
|
| 49 |
+
.pill.ok{color:var(--ok);border-color:#1f5a37}.pill.wn{color:var(--wn);border-color:#6b5316}.pill.er{color:var(--er);border-color:#5c2a27}
|
| 50 |
+
.stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px;margin-bottom:14px}
|
| 51 |
+
.stat{background:var(--s1);border:1px solid var(--ln);border-radius:12px;padding:14px}.stat b{font-size:26px;display:block;line-height:1.1}.stat span{font-size:12.5px;color:var(--mu)}
|
| 52 |
+
.stat.hot b{color:var(--rd)}
|
| 53 |
+
.tw{overflow-x:auto}table{width:100%;border-collapse:collapse;font-size:13.5px}th{text-align:left;color:var(--mu);font-weight:600;font-size:12.5px;padding:8px;border-bottom:1px solid var(--ln);white-space:nowrap}td{padding:9px 8px;border-bottom:1px solid var(--ln);vertical-align:middle}
|
| 54 |
+
.item{display:flex;gap:12px;background:var(--s2);border:1px solid var(--ln);border-radius:12px;padding:10px;margin-bottom:10px}
|
| 55 |
+
.item img{width:56px;height:78px;object-fit:cover;border-radius:7px;background:#000;flex:none}.item .in{flex:1;min-width:0}.item .tt{font-weight:700;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
| 56 |
+
.mu{color:var(--mu);font-size:13px}.er{color:var(--er)}.ok{color:var(--ok)}.empty{color:var(--mu);text-align:center;padding:26px 10px}
|
| 57 |
+
.tabs{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-bottom:6px}.tabs button{padding:10px 6px;border:1px solid var(--ln);background:var(--bg);border-radius:10px;font-weight:600;cursor:pointer}.tabs button.on{background:var(--rd);border-color:var(--rd);color:#fff}
|
| 58 |
+
.prev{background:var(--s2);border-radius:14px;padding:12px;margin-top:14px}.prev .r{display:flex;gap:12px;align-items:center}.prev i{flex:none;width:40px;height:40px;border-radius:10px;background:#000;color:var(--rd);display:grid;place-items:center;font:800 14px system-ui;font-style:normal}.prev b{display:block;font-size:14px}.prev img{display:block;width:100%;max-height:170px;object-fit:cover;border-radius:10px;margin-top:10px}
|
| 59 |
+
.toast{position:fixed;left:50%;bottom:calc(18px + env(safe-area-inset-bottom));transform:translateX(-50%);background:var(--s2);border:1px solid var(--ln);padding:10px 16px;border-radius:10px;font-size:13.5px;z-index:99;opacity:0;pointer-events:none;transition:opacity .2s;max-width:90vw}.toast.on{opacity:1}
|
| 60 |
+
.pgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(104px,1fr));gap:8px;max-height:430px;overflow-y:auto}
|
| 61 |
+
.pcard{background:var(--bg);border:1px solid var(--ln);border-radius:10px;padding:0 0 8px;text-align:left;cursor:pointer;overflow:hidden;display:flex;flex-direction:column;gap:2px;color:inherit}
|
| 62 |
+
.pcard img,.pimg{width:100%;aspect-ratio:2/3;object-fit:cover;background:#000;display:block}
|
| 63 |
+
.pcard b{font-size:12.5px;padding:4px 8px 0;line-height:1.25;overflow:hidden;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}
|
| 64 |
+
.pcard small{padding:0 8px;color:var(--mu);font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
| 65 |
+
.pcard:active{border-color:var(--rd)}
|
| 66 |
+
@media(prefers-reduced-motion:reduce){.toast{transition:none}}
|
| 67 |
+
</style>
|
| 68 |
+
</head>
|
| 69 |
+
<body>
|
| 70 |
+
|
| 71 |
+
<div id="app">
|
| 72 |
+
<header>
|
| 73 |
+
<div class="brand">JAYNES <b>MAX</b> <span class="mu" style="font-weight:600;font-size:14px">Admin</span></div>
|
| 74 |
+
<div class="hr">
|
| 75 |
+
<div class="lang"><button data-l="sw">SW</button><button data-l="en">EN</button></div>
|
| 76 |
+
</div>
|
| 77 |
+
</header>
|
| 78 |
+
|
| 79 |
+
<div class="shell">
|
| 80 |
+
<nav id="nav">
|
| 81 |
+
<button data-p="ov" data-i="nOv"></button><button data-p="mv" data-i="nMv"></button><button data-p="us" data-i="nUs"></button>
|
| 82 |
+
<button data-p="pu" data-i="nPu"></button><button data-p="an" data-i="nAn"></button><button data-p="hm" data-i="nHm"></button>
|
| 83 |
+
</nav>
|
| 84 |
+
<main>
|
| 85 |
+
|
| 86 |
+
<!-- OVERVIEW -->
|
| 87 |
+
<section id="p-ov"><h2 data-i="nOv"></h2><div id="ovArea" class="mu" data-i="loading"></div></section>
|
| 88 |
+
|
| 89 |
+
<!-- MOVIES -->
|
| 90 |
+
<section id="p-mv" hidden>
|
| 91 |
+
<h2 data-i="nMv"></h2>
|
| 92 |
+
<div class="row" style="margin-bottom:12px">
|
| 93 |
+
<input type="text" id="mvSearch" data-ph="search" style="flex:1;min-width:180px">
|
| 94 |
+
<button class="btn" id="mvReload" data-i="refresh"></button>
|
| 95 |
+
<button class="btn p" id="mvNew" data-i="mvNew"></button>
|
| 96 |
+
</div>
|
| 97 |
+
<div class="card" id="mvForm" hidden>
|
| 98 |
+
<h3 id="mvFormT"></h3>
|
| 99 |
+
<div class="g2">
|
| 100 |
+
<div class="f full"><label data-i="mTitle"></label><input type="text" id="m_title"></div>
|
| 101 |
+
<div class="f"><label data-i="mType"></label><select id="m_type"><option value="movie" data-i="tMovie"></option><option value="series" data-i="tSeries"></option></select></div>
|
| 102 |
+
<div class="f"><label>DJ</label><input type="text" id="m_dj"></div>
|
| 103 |
+
<div class="f"><label data-i="mStatus"></label><select id="m_status"><option value="draft">Draft</option><option value="public">Public</option></select></div>
|
| 104 |
+
<div class="f"><label>Active</label><select id="m_active"><option value="true" data-i="yes"></option><option value="false" data-i="no"></option></select></div>
|
| 105 |
+
<div class="f full"><label>Poster URL</label><input type="url" id="m_poster" placeholder="https://"></div>
|
| 106 |
+
<div class="f full"><label>Thumbnail URL</label><input type="url" id="m_thumb" placeholder="https://"></div>
|
| 107 |
+
</div>
|
| 108 |
+
<div class="f" id="movieUrlField"><label data-i="mUrls"></label><div id="epList"></div><button class="btn s" data-a="addPart" type="button" data-i="mAddPart"></button>
|
| 109 |
+
<details style="margin-top:10px"><summary class="mu" style="cursor:pointer" data-i="mBulk"></summary><textarea id="bulkUrls" rows="5" data-ph="mBulkPh" style="margin-top:8px"></textarea><button class="btn s" style="margin-top:8px" data-a="bulk" type="button" data-i="mSplit"></button></details>
|
| 110 |
+
</div>
|
| 111 |
+
<div class="f" id="seasonsField" hidden><label data-i="mSeasons"></label><div id="seasonsList"></div><button class="btn s" data-a="addSeason" type="button" data-i="mAddSeason"></button></div>
|
| 112 |
+
<div class="row"><button class="btn p" id="mvSave"></button><button class="btn" id="mvCancel" data-i="cancel"></button></div>
|
| 113 |
+
</div>
|
| 114 |
+
<div id="mvList" class="mu"></div>
|
| 115 |
+
</section>
|
| 116 |
+
|
| 117 |
+
<!-- USERS -->
|
| 118 |
+
<section id="p-us" hidden>
|
| 119 |
+
<h2 data-i="nUs"></h2>
|
| 120 |
+
<div class="row" style="margin-bottom:12px"><input type="text" id="usSearch" data-ph="searchEmail" style="flex:1;min-width:180px"><button class="btn" id="usReload" data-i="refresh"></button></div>
|
| 121 |
+
<div id="usArea" class="mu"></div>
|
| 122 |
+
</section>
|
| 123 |
+
|
| 124 |
+
<!-- PUSH -->
|
| 125 |
+
<section id="p-pu" hidden>
|
| 126 |
+
<h2 data-i="nPu"></h2>
|
| 127 |
+
<div class="card">
|
| 128 |
+
<div class="tabs" id="kinds"><button data-k="live" class="on" data-i="kLive"></button><button data-k="vod" data-i="kVod"></button><button data-k="gen" data-i="kGen"></button></div>
|
| 129 |
+
<div class="f" style="margin-top:12px"><label id="puNameL"></label><input type="text" id="puName" maxlength="80"></div>
|
| 130 |
+
<div class="f"><label data-i="msg"></label><textarea id="puMsg" maxlength="240"></textarea></div>
|
| 131 |
+
<div class="f"><label data-i="img"></label><input type="url" id="puImg" placeholder="https://.../picha.jpg"><div class="hint" data-i="imgH"></div></div>
|
| 132 |
+
<div class="f"><label data-i="target"></label><select id="puTarget"></select><input type="text" id="puTid" style="margin-top:8px" hidden placeholder="ID"></div>
|
| 133 |
+
<label class="chk"><input type="checkbox" id="puSave" checked><span><span data-i="saveIn"></span><br><small class="mu" data-i="saveInH"></small></span></label>
|
| 134 |
+
<button class="btn p" style="width:100%" id="puSend" data-i="send"></button>
|
| 135 |
+
<div id="puOut" class="hint" style="margin-top:10px;min-height:1.2em"></div>
|
| 136 |
+
</div>
|
| 137 |
+
<div class="prev"><div class="r"><i>JM</i><div><b id="pvT"></b><span class="mu" id="pvM"></span></div></div><img id="pvI" alt="" hidden><div class="hint" id="pvL" style="word-break:break-all"></div></div>
|
| 138 |
+
</section>
|
| 139 |
+
|
| 140 |
+
<!-- ANNOUNCEMENTS -->
|
| 141 |
+
<section id="p-an" hidden>
|
| 142 |
+
<h2 data-i="nAn"></h2>
|
| 143 |
+
<div class="card">
|
| 144 |
+
<div class="g2">
|
| 145 |
+
<div class="f"><label data-i="aKind"></label><select id="anKind"><option value="notification" data-i="aNotif"></option><option value="update" data-i="aUpd"></option></select></div>
|
| 146 |
+
<div class="f" id="anVerF" hidden><label data-i="aVer"></label><input type="text" id="anVer" placeholder="1.1"></div>
|
| 147 |
+
</div>
|
| 148 |
+
<div class="f"><label data-i="aTitle"></label><input type="text" id="anTitle"></div>
|
| 149 |
+
<div class="f"><label data-i="msg"></label><textarea id="anMsg"></textarea></div>
|
| 150 |
+
<div class="f"><label data-i="aLink"></label><input type="url" id="anUrl" placeholder="https://"></div>
|
| 151 |
+
<label class="chk"><input type="checkbox" id="anActive" checked><span data-i="aActive"></span></label>
|
| 152 |
+
<button class="btn p" id="anPost" data-i="aPost"></button>
|
| 153 |
+
</div>
|
| 154 |
+
<div id="anList" class="mu"></div>
|
| 155 |
+
</section>
|
| 156 |
+
|
| 157 |
+
<!-- HOME -->
|
| 158 |
+
<section id="p-hm" hidden><h2 data-i="nHm"></h2><div id="hmArea" class="mu"></div></section>
|
| 159 |
+
|
| 160 |
+
</main>
|
| 161 |
+
</div>
|
| 162 |
+
</div>
|
| 163 |
+
<div class="toast" id="toast"></div>
|
| 164 |
+
|
| 165 |
+
<script>
|
| 166 |
+
/* βββββββββ Config βββββββββ */
|
| 167 |
+
const USERS_FN={url:'https://ybtklpzhacaazulntzyh.supabase.co/functions/v1/admin-users',anon:'sb_publishable_kTp9GWPaSozKbigB61iq3Q_DVjChLyP'};
|
| 168 |
+
const HOME_FN={url:'https://ooqvsqmsnwjqvuzbhczx.supabase.co/functions/v1/home-config',anon:'sb_publishable_-B4goS_nTpi2UvK8bFyCiA_CV5UH4Ic'};
|
| 169 |
+
const CONTENT_FN={url:'https://ooqvsqmsnwjqvuzbhczx.supabase.co/functions/v1/admin-content',anon:'sb_publishable_-B4goS_nTpi2UvK8bFyCiA_CV5UH4Ic'};
|
| 170 |
+
const PUSH_URL='https://ooqvsqmsnwjqvuzbhczx.supabase.co/functions/v1/send-push';
|
| 171 |
+
const WEBHOOK_SECRET='89cbf19e2ae535da463bcb554e7980ed64b01d6ad5cf96da';
|
| 172 |
+
|
| 173 |
+
/* βββββββββ i18n (ongeza lugha mpya hapa) βββββββββ */
|
| 174 |
+
const T={
|
| 175 |
+
sw:{gSub:'Ingia kusimamia app, watumiaji na maudhui.',gPw:'Password',gBtn:'Ingia',gBad:'Password si sahihi.',gHttps:'Fungua ukurasa huu kupitia https.',logout:'Toka',loading:'Inapakia...',refresh:'Onyesha upya',save:'Hifadhi',cancel:'Ghairi',yes:'Ndiyo',no:'Hapana',del:'Futa',none:'Hakuna kitu bado.',err:'Hitilafu: ',saved:'Imehifadhiwa.',deleted:'Imefutwa.',
|
| 176 |
+
nOv:'Muhtasari',nMv:'Filamu',nUs:'Watumiaji',nPu:'Push',nAn:'Matangazo',nHm:'Home',nSt:'Mipangilio',search:'Tafuta kwa jina au DJ...',searchEmail:'Tafuta kwa email...',
|
| 177 |
+
oUsers:'Jumla ya watumiaji',oNew:'Wapya wiki hii',oActive:'Waliingia wiki hii',oWl:'Vitu vya Watchlist',oPin:'PIN ya Wazazi imewashwa',oBan:'Wamezuiwa',oMv:'Filamu',oAn:'Matangazo active',oNote:'Takwimu za watumiaji zinatoka kwenye data ya wingu (user_profiles); wasio na akaunti hawaonekani.',
|
| 178 |
+
mType:'Aina',tMovie:'Movie (sehemu moja)',tSeries:'Series (episode nyingi)',mUrls:'URL za movie (unaweza kuweka sehemu nyingi)',mAddPart:'+ Ongeza sehemu',mBulk:'Bandika URL nyingi kwa pamoja',mBulkPh:'URL moja kila mstari (mstari 1 = Sehemu 1, mstari 2 = Sehemu 2...)',mSplit:'Gawanya kuwa sehemu',mSeasons:'Seasons na episodes',mAddSeason:'+ Ongeza season',delSeason:'Futa season',addEp:'+ Ongeza episode',needUrl:'Weka URL angalau moja.',partsSet:'Sehemu {n} zimewekwa.',epsToParts:'Episodes {n} zimekuwa sehemu {n}.',partsB:'Sehemu',mvNew:'Filamu mpya',mvAdd:'Ongeza filamu / series',mvEdit:'Hariri: ',mvSave:'Hifadhi filamu',mvUpd:'Sasisha filamu',mTitle:'Jina (title)',mYear:'Mwaka',mCat:'Kategoria',cNormal:'Kawaida',cAdult:'Wakubwa',mPrice:'Bei (TZS)',mStatus:'Hali',mDesc:'Maelezo',mCast:'Waigizaji (tenganisha kwa koma)',mEps:'Episode / Video URLs',mAddEp:'+ Ongeza episode',edit:'Hariri',needTitle:'Jaza jina la filamu.',connect:'Function URL ya filamu haipatikani. Weka config.js kwenye folda moja na admin.html.',confirmMv:'Futa filamu hii? Haiwezi kurudishwa.',noMv:'Hakuna filamu zinazolingana.',
|
| 179 |
+
email:'Email',joined:'Amejiunga',lastIn:'Mwisho kuingia',wl:'Watchlist',locked:'Locked',pin:'PIN Wazazi',state:'Hali',act:'Vitendo',banned:'Amezuiwa',normal:'Kawaida',ban:'Zuia',unban:'Fungua',noUs:'Hakuna watumiaji.',confirmBan:'Kumzuia mtumiaji huyu?',confirmUser:'KUFUTA akaunti ya {x}? Hatua hii haiwezi kurudishwa.',
|
| 180 |
+
kLive:'πΊ Live TV',kVod:'π¬ VOD',kGen:'π Jumla',nLive:'Jina la channel',nVod:'Jina la filamu / kipindi',nGen:'Kichwa',msg:'Ujumbe',img:'Picha (URL)',imgH:'Hiari. https, uwiano 2:1 (mf. 1024Γ512) unaonekana vizuri.',target:'Ikibonyezwa, fungua app kwenye',saveIn:'Hifadhi pia ndani ya app',saveInH:'Inaonekana kwenye kengele ya notifications.',send:'Tuma sasa',sending:'Inatuma...',fill:'Jaza jina na ujumbe.',confirmP:'Tuma kwa watumiaji wote?',okSave:'Imehifadhiwa na push imetumwa kwa wote.',okPush:'Push imetumwa: ',badSecret:'Neno la siri si sahihi.',liveT:'πΊ Live TV: {n}',liveM:'Inaonyeshwa sasa. Bonyeza kutazama!',vodT:'π¬ Mpya: {n}',vodM:'Sasa inapatikana kwenye JAYNES MAX. Tazama sasa!',pvT:'Kichwa',pvM:'Ujumbe utaonekana hapa',needId:'Weka ID ya ukurasa unaotaka kufungua.',badImg:'Picha lazima ianze na https://',
|
| 181 |
+
tg:{home:'π Nyumbani',live:'πΊ Live TV (orodha)',movie:'π¬ Filamu (orodha)',translated:'π£ Zilizotafsiriwa',sports:'β½ Michezo',title:'π¬ Filamu/kipindi maalum (ID)',chan:'πΊ Channel maalum (ID)',match:'β½ Mechi maalum (ID)'},
|
| 182 |
+
aKind:'Aina',aNotif:'Arifa ya kawaida',aUpd:'Sasisho la app',aVer:'Toleo jipya',aTitle:'Kichwa',aLink:'Kiungo (hiari)',aActive:'Ionekane sasa',aPost:'Tuma tangazo',aOn:'Washa',aOff:'Zima',aActiveP:'Active',aOffP:'Imezimwa',aUpdP:'Update v',aNotifP:'Arifa',aNoTitle:'(hakuna kichwa)',needAnn:'Weka kichwa au ujumbe.',confirmAnn:'Futa tangazo hili?',
|
| 183 |
+
hOrder:'Mpangilio wa sehemu (juu β chini)',hBanner:'Bango la juu',hShow:'Onyesha',hText:'Maandishi',hLink:'Kiungo (hiari)',hMatches:'β½ Mechi Live',hTv:'πΊ Live TV',hMovies:'π¬ Filamu',hLeagues:'Ligi zinazoruhusiwa (moja kwa mstari)',hMax:'Idadi ya ligi kuonyesha',hTitle:'Kichwa cha sehemu',hChan:'Chaneli (moja kwa mstari, kwa mpangilio)',hTrend:'Onyesha "Zinazotamba"',hRails:'Idadi ya rails za filamu (0β10)',hPick:'Chagua kutoka kwenye maudhui',pTr:'Zilizotafsiriwa',pMv:'Movie',pSe:'Series',pAn:'Animation',pLv:'Live TV',pMt:'Mechi',slAdded:'Imeongezwa kwenye slider.',slDup:'Tayari ipo kwenye slider.',noImg:'Hii haina picha. Tumia "+ Ongeza slide" na weka URL ya picha.',hSlider:'πΌ Slider (picha zinazotelezesha)',hTrans:'π£ Zilizotafsiriwa',hInterval:'Sekunde kati ya slide',hSlImg:'Picha ya slide (URL ya https)',hSlTitle:'Kichwa (hiari)',hSlLink:'Kiungo (hiari): https://... au ./#/title/ID',hAddSlide:'+ Ongeza slide',hSlHint:'Slide zisizo na picha hazihifadhiwi.',hTMax:'Idadi ya vipengele kuonyesha',hSave:'Hifadhi Home',hReset:'Rudisha chaguo-msingi',hSaved:'Home imehifadhiwa.',confirmReset:'Rudisha Home kuwa chaguo-msingi?',
|
| 184 |
+
sAdmin:'Admin API',sTok:'Admin token (hiari)',sTokH:'Ukiacha wazi, inatumika thamani ya msingi (jaynes-max-tv).',sMovie:'Filamu (Supabase)',sHint:'Zinahifadhiwa kwenye kivinjari hiki tu. URL na key ya filamu zinasomwa kiotomatiki kutoka config.js; hapa unabadilisha tu ukihitaji.',sClear:'Futa taarifa hizi'},
|
| 185 |
+
en:{gSub:'Sign in to manage the app, users and content.',gPw:'Password',gBtn:'Sign in',gBad:'Wrong password.',gHttps:'Open this page over https.',logout:'Sign out',loading:'Loading...',refresh:'Refresh',save:'Save',cancel:'Cancel',yes:'Yes',no:'No',del:'Delete',none:'Nothing here yet.',err:'Error: ',saved:'Saved.',deleted:'Deleted.',
|
| 186 |
+
nOv:'Overview',nMv:'Movies',nUs:'Users',nPu:'Push',nAn:'Announcements',nHm:'Home',nSt:'Settings',search:'Search by title or DJ...',searchEmail:'Search by email...',
|
| 187 |
+
oUsers:'Total users',oNew:'New this week',oActive:'Signed in this week',oWl:'Watchlist items',oPin:'Parental PIN on',oBan:'Banned',oMv:'Movies',oAn:'Active announcements',oNote:'User stats come from cloud profiles (user_profiles); people without an account are not counted.',
|
| 188 |
+
mType:'Type',tMovie:'Movie (single part)',tSeries:'Series (many episodes)',mUrls:'Movie URLs (you can add several parts)',mAddPart:'+ Add part',mBulk:'Paste many URLs at once',mBulkPh:'One URL per line (line 1 = Part 1, line 2 = Part 2...)',mSplit:'Split into parts',mSeasons:'Seasons and episodes',mAddSeason:'+ Add season',delSeason:'Delete season',addEp:'+ Add episode',needUrl:'Enter at least one URL.',partsSet:'{n} parts set.',epsToParts:'{n} episodes became {n} parts.',partsB:'parts',mvNew:'New movie',mvAdd:'Add movie / series',mvEdit:'Edit: ',mvSave:'Save movie',mvUpd:'Update movie',mTitle:'Title',mYear:'Year',mCat:'Category',cNormal:'Regular',cAdult:'Adults',mPrice:'Price (TZS)',mStatus:'Status',mDesc:'Description',mCast:'Cast (comma separated)',mEps:'Episode / video URLs',mAddEp:'+ Add episode',edit:'Edit',needTitle:'Enter the movie title.',connect:'Movies Function URL not found. Put config.js in the same folder as admin.html.',confirmMv:'Delete this movie? This cannot be undone.',noMv:'No matching movies.',
|
| 189 |
+
email:'Email',joined:'Joined',lastIn:'Last sign-in',wl:'Watchlist',locked:'Locked',pin:'Parental PIN',state:'Status',act:'Actions',banned:'Banned',normal:'Active',ban:'Ban',unban:'Unban',noUs:'No users.',confirmBan:'Ban this user?',confirmUser:'DELETE the account of {x}? This cannot be undone.',
|
| 190 |
+
kLive:'πΊ Live TV',kVod:'π¬ VOD',kGen:'π General',nLive:'Channel name',nVod:'Movie / show title',nGen:'Title',msg:'Message',img:'Image (URL)',imgH:'Optional. Use https; a 2:1 ratio (e.g. 1024Γ512) looks best.',target:'When tapped, open the app on',saveIn:'Also save inside the app',saveInH:'Shows in the in-app notification bell.',send:'Send now',sending:'Sending...',fill:'Fill in the name and message.',confirmP:'Send to all users?',okSave:'Saved and push sent to all subscribers.',okPush:'Push sent: ',badSecret:'Invalid secret.',liveT:'πΊ Live TV: {n}',liveM:'Live now. Tap to watch!',vodT:'π¬ New: {n}',vodM:'Now available on JAYNES MAX. Watch now!',pvT:'Title',pvM:'Message will appear here',needId:'Enter the ID of the page to open.',badImg:'Image must start with https://',
|
| 191 |
+
tg:{home:'π Home',live:'πΊ Live TV (list)',movie:'π¬ Movies (list)',translated:'π£ Translated',sports:'β½ Sports',title:'π¬ Specific movie/show (ID)',chan:'πΊ Specific channel (ID)',match:'β½ Specific match (ID)'},
|
| 192 |
+
aKind:'Type',aNotif:'Regular notice',aUpd:'App update',aVer:'New version',aTitle:'Title',aLink:'Link (optional)',aActive:'Visible now',aPost:'Publish announcement',aOn:'Turn on',aOff:'Turn off',aActiveP:'Active',aOffP:'Off',aUpdP:'Update v',aNotifP:'Notice',aNoTitle:'(no title)',needAnn:'Enter a title or message.',confirmAnn:'Delete this announcement?',
|
| 193 |
+
hOrder:'Section order (top β bottom)',hBanner:'Top banner',hShow:'Show',hText:'Text',hLink:'Link (optional)',hMatches:'β½ Live matches',hTv:'πΊ Live TV',hMovies:'π¬ Movies',hLeagues:'Allowed leagues (one per line)',hMax:'Max leagues to show',hTitle:'Section title',hChan:'Channels (one per line, in display order)',hTrend:'Show "Trending"',hRails:'Number of movie rails (0β10)',hPick:'Pick from content',pTr:'Translated',pMv:'Movies',pSe:'Series',pAn:'Animation',pLv:'Live TV',pMt:'Matches',slAdded:'Added to slider.',slDup:'Already in the slider.',noImg:'This has no image. Use "+ Add slide" and enter an image URL.',hSlider:'πΌ Slider (swipeable banners)',hTrans:'π£ Translated',hInterval:'Seconds between slides',hSlImg:'Slide image (https URL)',hSlTitle:'Title (optional)',hSlLink:'Link (optional): https://... or ./#/title/ID',hAddSlide:'+ Add slide',hSlHint:'Slides without an image are not saved.',hTMax:'Number of items to show',hSave:'Save Home',hReset:'Restore defaults',hSaved:'Home saved.',confirmReset:'Restore Home to defaults?',
|
| 194 |
+
sAdmin:'Admin API',sTok:'Admin token (optional)',sTokH:'If empty, the default value (jaynes-max-tv) is used.',sMovie:'Movies (Supabase)',sHint:'Stored in this browser only. The movies URL and key are read automatically from config.js; change them here only if needed.',sClear:'Clear these settings'}
|
| 195 |
+
};
|
| 196 |
+
let lang=(()=>{try{return localStorage.getItem('jm_lang')}catch(e){}})()||((navigator.language||'').startsWith('sw')?'sw':'en');
|
| 197 |
+
const t=(k,r)=>{let s=(T[lang]&&T[lang][k]);if(s==null)s=T.en[k];if(s==null)return k;if(r)for(const x in r)s=s.replace('{'+x+'}',r[x]);return s};
|
| 198 |
+
const $=id=>document.getElementById(id);
|
| 199 |
+
const esc=s=>String(s==null?'':s).replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
| 200 |
+
const sget=k=>{try{return localStorage.getItem(k)||''}catch(e){return''}};
|
| 201 |
+
const sset=(k,v)=>{try{v?localStorage.setItem(k,v):localStorage.removeItem(k)}catch(e){}};
|
| 202 |
+
const fdate=i=>{if(!i)return'β';const d=new Date(i),l=lang==='sw'?'sw-TZ':'en-GB';return d.toLocaleDateString(l)+' '+d.toLocaleTimeString(l,{hour:'2-digit',minute:'2-digit'})};
|
| 203 |
+
let toastT;const toast=m=>{const e=$('toast');e.textContent=m;e.classList.add('on');clearTimeout(toastT);toastT=setTimeout(()=>e.classList.remove('on'),3000)};
|
| 204 |
+
const fail=e=>toast(t('err')+(e&&e.message||e));
|
| 205 |
+
|
| 206 |
+
/* ββ Mipangilio yote iko hapa (hakuna sehemu ya Settings) ββ */
|
| 207 |
+
const PW='jaynes-max-tv'; // admin token (users, matangazo, home)
|
| 208 |
+
// Muunganisho wa filamu (jaynes-max-tv-zilizotafsiriwa)
|
| 209 |
+
const MOVIE_FN='https://kqqdsumvvbuvyoneanku.supabase.co/functions/v1/jaynes-max-tv-zilizotafsiriwa';
|
| 210 |
+
const MOVIE_ANON='sb_publishable_0XQ9PQ0CRJnC0A_Tz18oGg_fSONkxB3';
|
| 211 |
+
const MOVIE_KEY='jaynes'; // x-admin-key
|
| 212 |
+
const CONTENT_URL='https://ooqvsqmsnwjqvuzbhczx.supabase.co'; // backend ya app (filamu, live TV, mechi)
|
| 213 |
+
const CONTENT_KEY=HOME_FN.anon;
|
| 214 |
+
|
| 215 |
+
/* βββββββββ Language & nav βββββββββ */
|
| 216 |
+
function applyLang(){
|
| 217 |
+
document.documentElement.lang=lang;
|
| 218 |
+
document.querySelectorAll('[data-i]').forEach(e=>e.textContent=t(e.dataset.i));
|
| 219 |
+
document.querySelectorAll('[data-ph]').forEach(e=>e.placeholder=t(e.dataset.ph));
|
| 220 |
+
document.querySelectorAll('.lang button').forEach(b=>b.classList.toggle('on',b.dataset.l===lang));
|
| 221 |
+
[...$('puTarget').options].forEach(o=>o.textContent=t('tg')[o.value]);
|
| 222 |
+
mvFormTitle();pushRender();
|
| 223 |
+
if(PW){renderOv();renderUsers();renderMovies();renderAnn();renderHome()}
|
| 224 |
+
}
|
| 225 |
+
document.querySelectorAll('.lang button').forEach(b=>b.onclick=()=>{lang=b.dataset.l;sset('jm_lang',lang);applyLang()});
|
| 226 |
+
function show(p){document.querySelectorAll('nav button').forEach(b=>b.classList.toggle('on',b.dataset.p===p));document.querySelectorAll('main section').forEach(s=>s.hidden=s.id!=='p-'+p)}
|
| 227 |
+
document.querySelectorAll('nav button').forEach(b=>b.onclick=()=>show(b.dataset.p));
|
| 228 |
+
|
| 229 |
+
/* βββββββββ API βββββββββ */
|
| 230 |
+
const token=()=>PW;
|
| 231 |
+
async function callFn(fn,action,args={}){
|
| 232 |
+
const r=await fetch(fn.url,{method:'POST',headers:{'Content-Type':'application/json',apikey:fn.anon,Authorization:'Bearer '+fn.anon,'x-admin-token':token()},body:JSON.stringify({action,args})});
|
| 233 |
+
const d=await r.json().catch(()=>({}));if(!r.ok)throw new Error(d.error||('HTTP '+r.status));return d;
|
| 234 |
+
}
|
| 235 |
+
const mh=x=>Object.assign({'x-admin-key':MOVIE_KEY,Authorization:'Bearer '+MOVIE_ANON,apikey:MOVIE_ANON},x||{});
|
| 236 |
+
const emsg=e=>typeof e==='string'?e:(e.message||JSON.stringify(e));
|
| 237 |
+
async function mreq(method,body){const r=await fetch(MOVIE_FN,{method,headers:mh(body?{'Content-Type':'application/json'}:{}),body:body?JSON.stringify(body):undefined});return{r,d:await r.json().catch(()=>({}))}}
|
| 238 |
+
|
| 239 |
+
/* βββββββββ Overview βββββββββ */
|
| 240 |
+
let users=[],movies=[],anns=[];
|
| 241 |
+
function renderOv(){
|
| 242 |
+
const now=Date.now(),wk=now-6048e5,isB=u=>u.banned_until&&new Date(u.banned_until)>new Date();
|
| 243 |
+
const c=[[users.length,'oUsers',1],[users.filter(u=>new Date(u.created_at)>wk).length,'oNew'],[users.filter(u=>u.last_sign_in_at&&new Date(u.last_sign_in_at)>wk).length,'oActive'],
|
| 244 |
+
[users.reduce((s,u)=>s+(u.profile?u.profile.watchlist_count:0),0),'oWl'],[users.filter(u=>u.profile&&u.profile.parental_enabled).length,'oPin'],[users.filter(isB).length,'oBan'],
|
| 245 |
+
[movies.length,'oMv'],[anns.filter(a=>a.active).length,'oAn']];
|
| 246 |
+
$('ovArea').className='';
|
| 247 |
+
$('ovArea').innerHTML=`<div class="stats">${c.map(x=>`<div class="stat${x[2]?' hot':''}"><b>${x[0]}</b><span>${t(x[1])}</span></div>`).join('')}</div><div class="hint">${t('oNote')}</div>`;
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
/* βββββββββ Users βββββββββ */
|
| 251 |
+
const banned=u=>u.banned_until&&new Date(u.banned_until)>new Date();
|
| 252 |
+
async function loadUsers(){
|
| 253 |
+
$('usArea').textContent=t('loading');
|
| 254 |
+
try{users=(await callFn(USERS_FN,'listUsers',{perPage:500})).users||[]}catch(e){$('usArea').innerHTML=`<div class="er">${esc(t('err')+e.message)}</div>`;return}
|
| 255 |
+
renderUsers();renderOv();
|
| 256 |
+
}
|
| 257 |
+
function renderUsers(){
|
| 258 |
+
const q=$('usSearch').value.trim().toLowerCase(),l=users.filter(u=>!q||(u.email||'').toLowerCase().includes(q)),a=$('usArea');
|
| 259 |
+
if(!l.length){a.className='empty';a.textContent=t('noUs');return}
|
| 260 |
+
a.className='tw';
|
| 261 |
+
a.innerHTML=`<table><thead><tr><th>${t('email')}</th><th>${t('joined')}</th><th>${t('lastIn')}</th><th>${t('wl')}</th><th>${t('locked')}</th><th>${t('pin')}</th><th>${t('state')}</th><th>${t('act')}</th></tr></thead><tbody>${l.map(u=>`<tr>
|
| 262 |
+
<td>${esc(u.email)}</td><td>${fdate(u.created_at)}</td><td>${fdate(u.last_sign_in_at)}</td>
|
| 263 |
+
<td>${u.profile?u.profile.watchlist_count:'β'}</td><td>${u.profile?u.profile.locked_content_count:'β'}</td>
|
| 264 |
+
<td>${u.profile&&u.profile.parental_enabled?`<span class="pill ok">${t('yes')}</span>`:`<span class="pill">${t('no')}</span>`}</td>
|
| 265 |
+
<td>${banned(u)?`<span class="pill er">${t('banned')}</span>`:`<span class="pill ok">${t('normal')}</span>`}</td>
|
| 266 |
+
<td><div class="row">${banned(u)?`<button class="btn s" data-a="unban" data-id="${esc(u.id)}">${t('unban')}</button>`:`<button class="btn s" data-a="ban" data-id="${esc(u.id)}">${t('ban')}</button>`}<button class="btn s d" data-a="delUser" data-id="${esc(u.id)}">${t('del')}</button></div></td></tr>`).join('')}</tbody></table>`;
|
| 267 |
+
}
|
| 268 |
+
$('usReload').onclick=loadUsers;$('usSearch').oninput=renderUsers;
|
| 269 |
+
|
| 270 |
+
/* βββββββββ Movies βββββββββ */
|
| 271 |
+
let editId=null;
|
| 272 |
+
const isSer=v=>Object.values(v||{}).some(x=>x&&typeof x==='object');
|
| 273 |
+
const countEps=v=>{let n=0;Object.values(v||{}).forEach(x=>n+=x&&typeof x==='object'?Object.keys(x).length:1);return n};
|
| 274 |
+
function mvFormTitle(){$('mvFormT').textContent=editId?t('mvEdit')+($('m_title').value||''):t('mvAdd');$('mvSave').textContent=editId?t('mvUpd'):t('mvSave')}
|
| 275 |
+
function addRow(w,k,u,ph){const r=document.createElement('div');r.className='row ep';r.style.marginBottom='6px';r.innerHTML=`<input type="text" class="ek" placeholder="${esc(ph)}" value="${esc(k)}" style="flex:1;min-width:80px;max-width:120px"><input type="url" class="eu" placeholder="https://" value="${esc(u)}" style="flex:3;min-width:140px"><button class="btn s" data-a="rowDel" type="button">β</button>`;w.appendChild(r)}
|
| 276 |
+
function setParts(v){const w=$('epList');w.innerHTML='';const k=Object.keys(v||{});if(!k.length){addRow(w,'Sehemu 1','','Sehemu 1');return}k.forEach((x,i)=>addRow(w,x==='main'?'Sehemu 1':x,v[x],'Sehemu '+(i+1)))}
|
| 277 |
+
function addSeason(name,eps){
|
| 278 |
+
const w=$('seasonsList'),n=w.children.length+1,b=document.createElement('div');b.className='card season';b.style.background='var(--s2)';
|
| 279 |
+
b.innerHTML=`<div class="row" style="margin-bottom:8px"><input type="text" class="sn" value="${esc(name||('Season '+n))}" style="flex:1"><button class="btn s d" data-a="delSeason" type="button">${t('delSeason')}</button></div><div class="seps"></div><button class="btn s" data-a="addEpS" type="button">${t('addEp')}</button>`;
|
| 280 |
+
w.appendChild(b);const sw=b.querySelector('.seps'),l=eps&&Object.keys(eps).length?eps:{'Episode 1':''};Object.keys(l).forEach(k=>addRow(sw,k,l[k],'Episode'));
|
| 281 |
+
}
|
| 282 |
+
function applyType(v){$('m_type').value=v;$('movieUrlField').hidden=v==='series';$('seasonsField').hidden=v!=='series'}
|
| 283 |
+
$('m_type').onchange=e=>{
|
| 284 |
+
const ser=e.target.value==='series';
|
| 285 |
+
if(ser){const eps={};let n=0;document.querySelectorAll('#epList .ep').forEach(r=>{const v=r.querySelector('.eu').value.trim();if(v){n++;eps['Episode '+n]=v}});$('seasonsList').innerHTML='';addSeason('Season 1',n?eps:undefined)}
|
| 286 |
+
else{const p={};let n=0;document.querySelectorAll('#seasonsList .seps .ep').forEach(r=>{const v=r.querySelector('.eu').value.trim();if(v){n++;p['Sehemu '+n]=v}});setParts(p);if(n>1)toast(t('epsToParts',{n}))}
|
| 287 |
+
applyType(e.target.value);
|
| 288 |
+
};
|
| 289 |
+
function bulk(){const u=$('bulkUrls').value.split(/\r?\n/).map(x=>x.trim()).filter(Boolean);if(!u.length){toast(t('needUrl'));return}const o={};u.forEach((x,i)=>o['Sehemu '+(i+1)]=x);setParts(o);$('bulkUrls').value='';toast(t('partsSet',{n:u.length}))}
|
| 290 |
+
function collect(){
|
| 291 |
+
if($('m_type').value!=='series'){
|
| 292 |
+
const p=[];document.querySelectorAll('#epList .ep').forEach((r,i)=>{const v=r.querySelector('.eu').value.trim();if(!v)return;p.push([(r.querySelector('.ek').value||('Sehemu '+(i+1))).trim(),v])});
|
| 293 |
+
if(!p.length)return{};if(p.length===1)return{main:p[0][1]};
|
| 294 |
+
const o={};p.forEach(([k,v])=>{let key=k,c=2;while(o[key])key=k+' ('+(c++)+')';o[key]=v});return o;
|
| 295 |
+
}
|
| 296 |
+
const o={};document.querySelectorAll('#seasonsList .season').forEach(b=>{const sn=b.querySelector('.sn').value.trim()||'Season',e={};
|
| 297 |
+
b.querySelectorAll('.seps .ep').forEach((r,i)=>{const k=(r.querySelector('.ek').value||('Episode '+(i+1))).trim(),v=r.querySelector('.eu').value.trim();if(v)e[k]=v});
|
| 298 |
+
if(Object.keys(e).length)o[sn]=e});return o;
|
| 299 |
+
}
|
| 300 |
+
function mvReset(open){
|
| 301 |
+
editId=null;['title','dj','poster','thumb'].forEach(f=>$('m_'+f).value='');$('m_status').value='draft';$('m_active').value='true';
|
| 302 |
+
applyType('movie');$('seasonsList').innerHTML='';$('bulkUrls').value='';setParts({});$('mvForm').hidden=!open;mvFormTitle();
|
| 303 |
+
}
|
| 304 |
+
async function loadMovies(){
|
| 305 |
+
$('mvList').className='mu';$('mvList').textContent=t('loading');
|
| 306 |
+
try{const{d}=await mreq('GET');if(d&&d.error)throw new Error(emsg(d.error));movies=Array.isArray(d)?d:(d.movies||[])}catch(e){$('mvList').innerHTML=`<div class="er">${esc(t('err')+e.message)}</div>`;return}
|
| 307 |
+
renderMovies();renderOv();
|
| 308 |
+
}
|
| 309 |
+
function renderMovies(){
|
| 310 |
+
const q=$('mvSearch').value.toLowerCase(),a=$('mvList'),l=movies.filter(m=>(m.title||'').toLowerCase().includes(q)||(m.dj||'').toLowerCase().includes(q));
|
| 311 |
+
if(!l.length){a.className='empty';a.textContent=t('noMv');return}
|
| 312 |
+
a.className='';
|
| 313 |
+
a.innerHTML=l.map(m=>{const vu=m.video_urls||{},ser=isSer(vu)||m.category==='series',parts=ser?0:Object.keys(vu).length;
|
| 314 |
+
return`<div class="item"><img src="${esc(m.thumb_url||m.poster_url||m.thumb||m.poster||'')}" alt="" onerror="this.style.display='none'"><div class="in"><div class="tt">${esc(m.title||'β')}</div>
|
| 315 |
+
<div style="margin:4px 0 8px;display:flex;gap:5px;flex-wrap:wrap"><span class="pill ${ser?'wn':''}">${ser?'Series':'Movie'}</span><span class="pill ${m.status==='public'?'ok':'wn'}">${esc(m.status||'draft')}</span><span class="pill">${esc(m.dj||'β')}</span>${ser?`<span class="pill">${m.episodes||0} EP</span>`:(parts>1?`<span class="pill">${parts} ${t('partsB')}</span>`:'')}</div>
|
| 316 |
+
<div class="row"><button class="btn s" data-a="mvEdit" data-id="${esc(m.id)}">${t('edit')}</button><button class="btn s d" data-a="mvDel" data-id="${esc(m.id)}">${t('del')}</button></div></div></div>`}).join('');
|
| 317 |
+
}
|
| 318 |
+
function mvFill(m){
|
| 319 |
+
mvReset(true);editId=m.id;const vids=m.video_urls||{},ser=isSer(vids),set=(i,v)=>$('m_'+i).value=v==null?'':v;
|
| 320 |
+
set('title',m.title);set('dj',m.dj);set('status',m.status||'draft');set('active',m.active===false?'false':'true');set('poster',m.poster_url||m.poster);set('thumb',m.thumb_url||m.thumb);
|
| 321 |
+
applyType(ser?'series':'movie');
|
| 322 |
+
if(ser){const n=Object.keys(vids);n.length?n.forEach(x=>addSeason(x,vids[x])):addSeason('Season 1')}else setParts(vids);
|
| 323 |
+
mvFormTitle();$('mvForm').scrollIntoView({behavior:'smooth',block:'start'});
|
| 324 |
+
}
|
| 325 |
+
$('mvNew').onclick=()=>mvReset(true);$('mvCancel').onclick=()=>mvReset(false);
|
| 326 |
+
$('mvReload').onclick=loadMovies;$('mvSearch').oninput=renderMovies;$('m_title').oninput=mvFormTitle;
|
| 327 |
+
$('mvSave').onclick=async()=>{
|
| 328 |
+
const title=$('m_title').value.trim();if(!title){toast(t('needTitle'));return}
|
| 329 |
+
const vu=collect(),g=i=>$('m_'+i).value.trim();
|
| 330 |
+
const p={title,dj:g('dj'),category:g('type')==='series'?'series':'kawaida',status:g('status'),active:g('active')==='true',poster_url:g('poster'),thumb_url:g('thumb'),video_urls:vu,episodes:countEps(vu)};
|
| 331 |
+
try{if(editId)p.id=editId;const{d}=await mreq(editId?'PATCH':'POST',p);if(d&&d.error)throw new Error(emsg(d.error));toast(t('saved'));mvReset(false);loadMovies()}catch(e){fail(e)}
|
| 332 |
+
};
|
| 333 |
+
|
| 334 |
+
/* βββββββββ Push βββββββββ */
|
| 335 |
+
let kind='live',msgTouched=false,tgTouched=false;
|
| 336 |
+
const tg=$('puTarget');tg.innerHTML=['home','live','movie','translated','sports','title','chan','match'].map(v=>`<option value="${v}"></option>`).join('');tg.value='live';
|
| 337 |
+
function deepLink(){const v=tg.value,id=encodeURIComponent($('puTid').value.trim());
|
| 338 |
+
const m={home:'./#/',live:'./#/live',movie:'./#/movie',translated:'./#/translated',sports:'./#/match'};
|
| 339 |
+
if(m[v])return m[v];if(v==='title')return id?'./#/title/'+id:'';if(v==='chan')return id?'./#/live/'+id:'';if(v==='match')return id?'./#/match/'+id:'';return'./'}
|
| 340 |
+
function pushTitle(){const n=$('puName').value.trim()||(kind==='live'?'Azam Sports 1':kind==='vod'?'Movie':'');return kind==='live'?t('liveT',{n}):kind==='vod'?t('vodT',{n}):$('puName').value.trim()}
|
| 341 |
+
const defMsg=()=>kind==='live'?t('liveM'):kind==='vod'?t('vodM'):'';
|
| 342 |
+
function pushRender(){
|
| 343 |
+
$('puNameL').textContent=t(kind==='live'?'nLive':kind==='vod'?'nVod':'nGen');
|
| 344 |
+
$('puTid').hidden=!/^(title|chan|match)$/.test(tg.value);
|
| 345 |
+
const im=$('puImg').value.trim(),ok=/^https:\/\//.test(im);$('pvI').hidden=!ok;if(ok)$('pvI').src=im;
|
| 346 |
+
$('pvL').textContent='β '+(deepLink()||'#/');
|
| 347 |
+
if(!msgTouched)$('puMsg').value=defMsg();
|
| 348 |
+
document.querySelectorAll('#kinds button').forEach(b=>b.classList.toggle('on',b.dataset.k===kind));
|
| 349 |
+
$('pvT').textContent=pushTitle()||t('pvT');$('pvM').textContent=$('puMsg').value||t('pvM');
|
| 350 |
+
}
|
| 351 |
+
document.querySelectorAll('#kinds button').forEach(b=>b.onclick=()=>{kind=b.dataset.k;msgTouched=false;if(!tgTouched)tg.value=kind==='live'?'live':kind==='vod'?'movie':'home';pushRender()});
|
| 352 |
+
tg.onchange=()=>{tgTouched=true;pushRender()};['puTid','puImg','puName'].forEach(i=>$(i).oninput=pushRender);$('puMsg').oninput=()=>{msgTouched=true;pushRender()};
|
| 353 |
+
$('puSend').onclick=async()=>{
|
| 354 |
+
const name=$('puName').value.trim(),message=$('puMsg').value.trim(),save=$('puSave').checked,o=$('puOut'),image_url=$('puImg').value.trim(),url=deepLink();
|
| 355 |
+
const bad=m=>{o.className='hint er';o.textContent=m};
|
| 356 |
+
if(!name||!message)return bad(t('fill'));
|
| 357 |
+
if(image_url&&!/^https:\/\//.test(image_url))return bad(t('badImg'));
|
| 358 |
+
if(!url)return bad(t('needId'));
|
| 359 |
+
if(!confirm(t('confirmP')))return;
|
| 360 |
+
const title=kind==='gen'?name:pushTitle();
|
| 361 |
+
$('puSend').disabled=true;o.className='hint';o.textContent=t('sending');
|
| 362 |
+
try{
|
| 363 |
+
const r=await fetch(PUSH_URL,{method:'POST',headers:{'Content-Type':'application/json','x-webhook-secret':WEBHOOK_SECRET},body:JSON.stringify(save?{save:true,title,message,image_url:image_url||null,url}:{record:{title,message,image_url:image_url||null,url}})});
|
| 364 |
+
const x=await r.text();if(!r.ok)throw new Error(r.status===401?t('badSecret'):x.slice(0,200));
|
| 365 |
+
o.className='hint ok';o.textContent=save?t('okSave'):t('okPush')+x;
|
| 366 |
+
$('puName').value='';$('puImg').value='';$('puTid').value='';msgTouched=false;pushRender();
|
| 367 |
+
}catch(e){bad(t('err')+e.message)}finally{$('puSend').disabled=false}
|
| 368 |
+
};
|
| 369 |
+
|
| 370 |
+
/* βββββββββ Announcements βββββββββ */
|
| 371 |
+
$('anKind').onchange=e=>$('anVerF').hidden=e.target.value!=='update';
|
| 372 |
+
$('anPost').onclick=async()=>{
|
| 373 |
+
const v=i=>$(i).value.trim(),a={kind:$('anKind').value,title:v('anTitle'),message:v('anMsg'),url:v('anUrl'),version:v('anVer'),active:$('anActive').checked};
|
| 374 |
+
if(!a.title&&!a.message){toast(t('needAnn'));return}
|
| 375 |
+
try{await callFn(CONTENT_FN,'createAnnouncement',a);toast(t('saved'));['anTitle','anMsg','anUrl','anVer'].forEach(i=>$(i).value='');loadAnn()}catch(e){fail(e)}
|
| 376 |
+
};
|
| 377 |
+
async function loadAnn(){
|
| 378 |
+
$('anList').textContent=t('loading');
|
| 379 |
+
try{anns=(await callFn(CONTENT_FN,'listAnnouncements')).rows||[]}catch(e){$('anList').innerHTML=`<div class="er">${esc(t('err')+e.message)}</div>`;return}
|
| 380 |
+
renderAnn();renderOv();
|
| 381 |
+
}
|
| 382 |
+
function renderAnn(){
|
| 383 |
+
const a=$('anList');if(!anns.length){a.className='empty';a.textContent=t('none');return}a.className='';
|
| 384 |
+
a.innerHTML=anns.map(r=>`<div class="card"><div class="row" style="justify-content:space-between;align-items:flex-start"><div><span class="pill ${r.kind==='update'?'wn':''}">${r.kind==='update'?t('aUpdP')+esc(r.version||''):t('aNotifP')}</span> <b>${esc(r.title||t('aNoTitle'))}</b></div><span class="pill ${r.active?'ok':''}">${r.active?t('aActiveP'):t('aOffP')}</span></div>
|
| 385 |
+
<div class="mu" style="margin-top:6px">${esc(r.message||'')}</div>${r.url?`<div class="hint">π ${esc(r.url)}</div>`:''}<div class="hint">${fdate(r.created_at)}</div>
|
| 386 |
+
<div class="row" style="margin-top:10px"><button class="btn s" data-a="annToggle" data-id="${esc(r.id)}" data-v="${r.active?0:1}">${r.active?t('aOff'):t('aOn')}</button><button class="btn s d" data-a="annDel" data-id="${esc(r.id)}">${t('del')}</button></div></div>`).join('');
|
| 387 |
+
}
|
| 388 |
+
|
| 389 |
+
/* βββββββββ Home βββββββββ */
|
| 390 |
+
let hc=null;
|
| 391 |
+
async function loadHome(){try{hc=normHome((await callFn(HOME_FN,'get')).config);renderHome()}catch(e){$('hmArea').innerHTML=`<div class="er">${esc(t('err')+e.message)}</div>`}}
|
| 392 |
+
const HN=k=>t({slider:'hSlider',matches:'hMatches',tv:'hTv',translated:'hTrans',movies:'hMovies'}[k]);
|
| 393 |
+
function normHome(c){
|
| 394 |
+
c=c||{};c.slider=c.slider||{enabled:false,interval:5,slides:[]};c.slider.slides=c.slider.slides||[];
|
| 395 |
+
c.translated=c.translated||{enabled:true,title:'π£ Zilizotafsiriwa',max:12};
|
| 396 |
+
const o=Array.isArray(c.order)?c.order.slice():['matches','tv','movies'];
|
| 397 |
+
if(!o.includes('slider'))o.unshift('slider');
|
| 398 |
+
if(!o.includes('translated')){const i=o.indexOf('movies');i<0?o.push('translated'):o.splice(i,0,'translated')}
|
| 399 |
+
c.order=o;return c;
|
| 400 |
+
}
|
| 401 |
+
function renderHome(){
|
| 402 |
+
if(!hc)return;const c=hc,a=$('hmArea');a.className='';
|
| 403 |
+
const ck=(id,v,l)=>`<label class="chk"><input type="checkbox" id="${id}" ${v?'checked':''}><span>${l}</span></label>`;
|
| 404 |
+
a.innerHTML=`<div class="card"><h3>${t('hOrder')}</h3>${c.order.map((k,i)=>`<div class="row" style="margin-bottom:6px"><span style="flex:1">${i+1}. ${HN(k)}</span><button class="btn s" data-a="hm" data-id="${i}" data-d="-1">β</button><button class="btn s" data-a="hm" data-id="${i}" data-d="1">β</button></div>`).join('')}</div>
|
| 405 |
+
<div class="card"><h3>${HN('slider')}</h3>${ck('hSlEn',c.slider.enabled,t('hShow'))}<div class="f"><label>${t('hInterval')}</label><input type="number" id="hSlInt" min="2" max="30" value="${c.slider.interval||5}"></div>
|
| 406 |
+
<div id="slRows">${c.slider.slides.map((s,i)=>`<div class="item" style="flex-direction:column;gap:8px"><input type="url" class="sl-i" placeholder="${t('hSlImg')}" value="${esc(s.image)}"><input type="text" class="sl-t" placeholder="${t('hSlTitle')}" value="${esc(s.title)}"><input type="text" class="sl-u" placeholder="${t('hSlLink')}" value="${esc(s.url)}"><div class="row"><button class="btn s" data-a="slUp" data-id="${i}" type="button">β</button><button class="btn s" data-a="slDn" data-id="${i}" type="button">β</button><button class="btn s d" data-a="slDel" data-id="${i}" type="button">${t('del')}</button></div></div>`).join('')}</div>
|
| 407 |
+
<div class="row"><button class="btn s p" data-a="pickToggle" type="button">${t('hPick')}</button><button class="btn s" data-a="slAdd" type="button">${t('hAddSlide')}</button></div><div id="pickBox"></div><div class="hint" style="margin-top:6px">${t('hSlHint')}</div></div>
|
| 408 |
+
<div class="card"><h3>${HN('translated')}</h3>${ck('hZEn',c.translated.enabled,t('hShow'))}<div class="f"><label>${t('hTitle')}</label><input type="text" id="hZTitle" value="${esc(c.translated.title)}"></div><div class="f"><label>${t('hTMax')}</label><input type="number" id="hZMax" min="1" max="50" value="${c.translated.max||12}"></div></div>
|
| 409 |
+
<div class="card"><h3>${t('hBanner')}</h3>${ck('hBanEn',c.banner.enabled,t('hShow'))}<div class="f"><label>${t('hText')}</label><input type="text" id="hBanText" value="${esc(c.banner.text)}"></div><div class="f"><label>${t('hLink')}</label><input type="url" id="hBanUrl" value="${esc(c.banner.url)}"></div></div>
|
| 410 |
+
<div class="card"><h3>${HN('matches')}</h3>${ck('hMEn',c.matches.enabled,t('hShow'))}<div class="f"><label>${t('hLeagues')}</label><textarea id="hMLeagues" rows="6">${esc(c.matches.leagues.join('\n'))}</textarea></div><div class="f"><label>${t('hMax')}</label><input type="number" id="hMMax" value="${c.matches.max_rails}"></div></div>
|
| 411 |
+
<div class="card"><h3>${HN('tv')}</h3>${ck('hTEn',c.tv.enabled,t('hShow'))}<div class="f"><label>${t('hTitle')}</label><input type="text" id="hTTitle" value="${esc(c.tv.title)}"></div><div class="f"><label>${t('hChan')}</label><textarea id="hTChan" rows="8">${esc(c.tv.channels.join('\n'))}</textarea></div></div>
|
| 412 |
+
<div class="card"><h3>${HN('movies')}</h3>${ck('hVEn',c.movies.enabled,t('hShow'))}${ck('hVTr',c.movies.trending,t('hTrend'))}<div class="f"><label>${t('hRails')}</label><input type="number" id="hVRails" min="0" max="10" value="${c.movies.rails}"></div></div>
|
| 413 |
+
<div class="row"><button class="btn p" data-a="hmSave">${t('hSave')}</button><button class="btn d" data-a="hmReset">${t('hReset')}</button></div>`;
|
| 414 |
+
renderPicker();
|
| 415 |
+
}
|
| 416 |
+
/* ββ Picker: chagua maudhui yaingie kwenye slider ββ */
|
| 417 |
+
const PICK_SRC=[['tr','pTr'],['mv','pMv'],['se','pSe'],['an','pAn'],['lv','pLv'],['mt','pMt']];
|
| 418 |
+
let pick={open:false,src:'tr',q:'',list:[]};const PC={};
|
| 419 |
+
async function pickMain(){
|
| 420 |
+
if(PC._main)return PC._main;
|
| 421 |
+
const call=(op,args)=>fetch(CONTENT_URL+'/functions/v1/jaynes-max-tv-api-',{method:'POST',headers:{'Content-Type':'application/json',apikey:CONTENT_KEY},body:JSON.stringify({op,args})}).then(r=>r.json()).catch(()=>({}));
|
| 422 |
+
const [home,tabs]=await Promise.all([call('movies.home'),call('movies.tabs')]);
|
| 423 |
+
let raw=[];const td=tabs&&tabs.data;
|
| 424 |
+
if(Array.isArray(td))raw=td;else if(td&&Array.isArray(td.subTabs))raw=td.subTabs;else if(td&&Array.isArray(td.bottomTabs))td.bottomTabs.forEach(b=>(b.subTabs||[]).forEach(x=>raw.push(x)));
|
| 425 |
+
const tr=await Promise.all(raw.slice(0,12).map(x=>call('movies.tab',{tabId:x.tabId||x.id})));
|
| 426 |
+
const out=new Map();
|
| 427 |
+
const add=el=>{const o=el&&el.subject?el.subject:el;if(!o||typeof o!=='object')return;const id=o.id||o.subjectId;if(!id||out.has(String(id)))return;
|
| 428 |
+
const genre=o.genre||(Array.isArray(o.genres)?o.genres.join(', '):'');
|
| 429 |
+
out.set(String(id),{id,title:o.title||o.name||'',img:(o.cover&&o.cover.url)||o.poster||o.thumbnailUrl||o.logo||'',sub:[genre,o.year].filter(Boolean).join(' Β· '),ser:o.subjectType===2||/series/i.test(String(o.type||'')),anim:/animation/i.test(genre),url:'./#/title/'+id})};
|
| 430 |
+
const walk=d=>{if(!d)return;(d.heroes||[]).forEach(add);if(d.hero)add(d.hero);(d.trending||[]).forEach(add);(d.rails||d.items||[]).forEach(sec=>{if(sec&&sec.type==='BANNER')return;((sec&&(sec.items||sec.subjects))||[]).forEach(add)})};
|
| 431 |
+
walk(home.data);tr.forEach(r=>walk(r.data));
|
| 432 |
+
return PC._main=[...out.values()];
|
| 433 |
+
}
|
| 434 |
+
async function pickLoad(src){
|
| 435 |
+
if(PC[src])return PC[src];let items=[];
|
| 436 |
+
if(src==='tr'){
|
| 437 |
+
let L=movies;if(!L.length){const{d}=await mreq('GET');L=Array.isArray(d)?d:(d.movies||[])}
|
| 438 |
+
items=L.filter(m=>(m.status||'public')==='public'&&m.active!==false).map(m=>({id:m.id,title:m.title,img:m.thumb_url||m.poster_url||m.thumb||m.poster||'',sub:(m.dj?m.dj+' Β· ':'')+((isSer(m.video_urls)||m.category==='series')?'Series':'Movie'),url:'./#/translated/'+m.id}));
|
| 439 |
+
}else if(src==='mv'||src==='se'||src==='an'){
|
| 440 |
+
const all=await pickMain();items=all.filter(x=>src==='se'?x.ser:src==='an'?x.anim:!x.ser);
|
| 441 |
+
}else if(src==='lv'){
|
| 442 |
+
const h={Authorization:'Bearer '+CONTENT_KEY,apikey:CONTENT_KEY};
|
| 443 |
+
const [ch,po,px]=await Promise.all([
|
| 444 |
+
fetch(CONTENT_URL+'/functions/v1/jaynes-max-tv/api/content/channels',{headers:h}).then(r=>r.json()).catch(()=>({})),
|
| 445 |
+
fetch(CONTENT_URL+'/rest/v1/channel_posters?select=channel_id,poster_url',{headers:h}).then(r=>r.json()).catch(()=>[]),
|
| 446 |
+
fetch(CONTENT_URL+'/functions/v1/pixtvmax-channels',{method:'POST',headers:Object.assign({'Content-Type':'application/json'},h),body:'{}'}).then(r=>r.json()).catch(()=>({}))]);
|
| 447 |
+
const pm={};(Array.isArray(po)?po:[]).forEach(x=>{if(x.poster_url)pm[String(x.channel_id)]=x.poster_url});
|
| 448 |
+
(ch.channels||[]).forEach(c=>{if(c.id!=null)items.push({id:c.id,title:c.name||('Channel '+c.id),img:pm[String(c.id)]||'',sub:'Live TV',url:'./#/live/'+encodeURIComponent(c.id)})});
|
| 449 |
+
(px.channels||[]).forEach(c=>{if(c.name&&c.url)items.push({id:'px:'+c.name,title:c.name,img:c.logo||'',sub:c.category||'Live TV',url:'./#/live/'+encodeURIComponent('px:'+c.name)})});
|
| 450 |
+
}else{
|
| 451 |
+
const d=await fetch(CONTENT_URL+'/functions/v1/fawa-live/matches',{headers:{Authorization:'Bearer '+CONTENT_KEY}}).then(r=>r.json());
|
| 452 |
+
(d.matches||[]).forEach(m=>items.push({id:m.slug,title:m.title,img:m.logo||'',sub:m.info||'',url:'./#/match/'+encodeURIComponent(m.slug)}));
|
| 453 |
+
}
|
| 454 |
+
if(!items.length)throw new Error(t('none'));
|
| 455 |
+
return PC[src]=items;
|
| 456 |
+
}
|
| 457 |
+
function renderPicker(){
|
| 458 |
+
const b=$('pickBox');if(!b)return;if(!pick.open){b.innerHTML='';return}
|
| 459 |
+
b.innerHTML=`<div class="card" style="background:var(--s2);margin:10px 0 0"><div class="row" style="margin-bottom:10px">${PICK_SRC.map(([k,l])=>`<button type="button" class="btn s ${pick.src===k?'p':''}" data-a="pickSrc" data-id="${k}">${t(l)}</button>`).join('')}</div><input type="text" id="pickQ" placeholder="${t('search')}" value="${esc(pick.q)}"><div id="pickGrid" class="mu" style="margin-top:10px"></div></div>`;
|
| 460 |
+
$('pickQ').oninput=e=>{pick.q=e.target.value;fillGrid()};fillGrid();
|
| 461 |
+
}
|
| 462 |
+
async function fillGrid(){
|
| 463 |
+
const g=$('pickGrid');if(!g)return;const src=pick.src;if(!PC[src]){g.className='mu';g.textContent=t('loading')}
|
| 464 |
+
let items;try{items=await pickLoad(src)}catch(e){if(src===pick.src&&$('pickGrid')){$('pickGrid').className='er';$('pickGrid').textContent=t('err')+e.message}return}
|
| 465 |
+
const gg=$('pickGrid');if(!gg||src!==pick.src)return;
|
| 466 |
+
const q=pick.q.trim().toLowerCase();pick.list=items.filter(x=>!q||(x.title||'').toLowerCase().includes(q));
|
| 467 |
+
if(!pick.list.length){gg.className='empty';gg.textContent=t('none');return}
|
| 468 |
+
gg.className='pgrid';
|
| 469 |
+
gg.innerHTML=pick.list.slice(0,80).map((x,i)=>`<button type="button" class="pcard" data-a="pickAdd" data-id="${i}">${x.img?`<img src="${esc(x.img)}" alt="" loading="lazy" referrerpolicy="no-referrer" onerror="this.style.visibility='hidden'">`:'<span class="pimg"></span>'}<b>${esc(x.title)}</b><small>${esc(x.sub||'')}</small></button>`).join('');
|
| 470 |
+
}
|
| 471 |
+
const lines=id=>$(id).value.split('\n').map(x=>x.trim()).filter(Boolean);
|
| 472 |
+
function readHome(){
|
| 473 |
+
if(!$('hBanEn'))return;const g=$,c=hc;
|
| 474 |
+
c.slider={enabled:g('hSlEn').checked,interval:Math.min(30,Math.max(2,parseInt(g('hSlInt').value,10)||5)),slides:[...document.querySelectorAll('#slRows .item')].map(r=>({image:r.querySelector('.sl-i').value.trim(),title:r.querySelector('.sl-t').value.trim(),url:r.querySelector('.sl-u').value.trim()})).filter(x=>x.image)};
|
| 475 |
+
c.translated={enabled:g('hZEn').checked,title:g('hZTitle').value.trim(),max:Math.min(50,Math.max(1,parseInt(g('hZMax').value,10)||12))};
|
| 476 |
+
c.banner={enabled:g('hBanEn').checked,text:g('hBanText').value.trim(),url:g('hBanUrl').value.trim()};
|
| 477 |
+
c.matches={enabled:g('hMEn').checked,leagues:lines('hMLeagues'),max_rails:parseInt(g('hMMax').value,10)||6};
|
| 478 |
+
c.tv={enabled:g('hTEn').checked,title:g('hTTitle').value.trim(),channels:lines('hTChan')};
|
| 479 |
+
c.movies={enabled:g('hVEn').checked,trending:g('hVTr').checked,rails:Math.min(10,Math.max(0,parseInt(g('hVRails').value,10)||0))};
|
| 480 |
+
}
|
| 481 |
+
|
| 482 |
+
/* βββββββββ Actions (event delegation) βββββββββ */
|
| 483 |
+
const run=async(fn,ok)=>{try{await fn();if(ok)toast(ok)}catch(e){fail(e)}};
|
| 484 |
+
const A={
|
| 485 |
+
ban:id=>confirm(t('confirmBan'))&&run(async()=>{await callFn(USERS_FN,'banUser',{id});loadUsers()},t('banned')),
|
| 486 |
+
unban:id=>run(async()=>{await callFn(USERS_FN,'unbanUser',{id});loadUsers()},t('unban')),
|
| 487 |
+
delUser:id=>{const u=users.find(x=>x.id===id);confirm(t('confirmUser',{x:u?u.email:''}))&&run(async()=>{await callFn(USERS_FN,'deleteUser',{id});loadUsers()},t('deleted'))},
|
| 488 |
+
mvEdit:id=>{const m=movies.find(x=>String(x.id)===id);if(m)mvFill(m)},
|
| 489 |
+
mvDel:id=>confirm(t('confirmMv'))&&run(async()=>{const{d}=await mreq('DELETE',{id});if(d&&d.error)throw new Error(emsg(d.error));loadMovies()},t('deleted')),
|
| 490 |
+
rowDel:(id,b)=>{const p=b.parentElement,w=p.parentElement;p.remove();if(w.id==='epList'&&!w.children.length)setParts({})},
|
| 491 |
+
addPart:()=>{const w=$('epList'),n=w.children.length+1;addRow(w,'Sehemu '+n,'','Sehemu '+n)},
|
| 492 |
+
bulk:()=>bulk(),addSeason:()=>addSeason(),
|
| 493 |
+
delSeason:(id,b)=>b.closest('.season').remove(),
|
| 494 |
+
addEpS:(id,b)=>{const w=b.previousElementSibling;addRow(w,'Episode '+(w.children.length+1),'','Episode')},
|
| 495 |
+
annToggle:(id,b)=>run(async()=>{await callFn(CONTENT_FN,'updateAnnouncement',{id,active:b.dataset.v==='1'});loadAnn()}),
|
| 496 |
+
annDel:id=>confirm(t('confirmAnn'))&&run(async()=>{await callFn(CONTENT_FN,'deleteAnnouncement',{id});loadAnn()},t('deleted')),
|
| 497 |
+
hm:(id,b)=>{readHome();const o=hc.order,i=+id,j=i+ +b.dataset.d;if(j<0||j>=o.length)return;[o[i],o[j]]=[o[j],o[i]];renderHome()},
|
| 498 |
+
pickToggle:()=>{pick.open=!pick.open;renderPicker()},
|
| 499 |
+
pickSrc:id=>{pick.src=id;pick.q='';renderPicker()},
|
| 500 |
+
pickAdd:id=>{const x=pick.list[+id];if(!x)return;
|
| 501 |
+
if(!/^https?:\/\//.test(x.img||''))return toast(t('noImg'));
|
| 502 |
+
readHome();if(hc.slider.slides.some(s=>s.url===x.url))return toast(t('slDup'));
|
| 503 |
+
hc.slider.slides.push({image:x.img,title:x.title,url:x.url});hc.slider.enabled=true;renderHome();toast(t('slAdded'))},
|
| 504 |
+
slUp:id=>{readHome();const a=hc.slider.slides,i=+id;if(i>0){[a[i-1],a[i]]=[a[i],a[i-1]];renderHome()}},
|
| 505 |
+
slDn:id=>{readHome();const a=hc.slider.slides,i=+id;if(i<a.length-1){[a[i+1],a[i]]=[a[i],a[i+1]];renderHome()}},
|
| 506 |
+
slAdd:()=>{readHome();hc.slider.slides.push({image:'',title:'',url:''});renderHome();},
|
| 507 |
+
slDel:id=>{readHome();hc.slider.slides.splice(+id,1);renderHome()},
|
| 508 |
+
hmSave:()=>run(async()=>{readHome();hc=normHome((await callFn(HOME_FN,'save',{config:hc})).config);renderHome()},t('hSaved')),
|
| 509 |
+
hmReset:()=>confirm(t('confirmReset'))&&run(async()=>{hc=normHome((await callFn(HOME_FN,'reset')).config);renderHome()},t('saved'))
|
| 510 |
+
};
|
| 511 |
+
document.addEventListener('click',e=>{const b=e.target.closest('[data-a]');if(b&&A[b.dataset.a])A[b.dataset.a](b.dataset.id,b)});
|
| 512 |
+
|
| 513 |
+
/* βββββββββ Init βββββββββ */
|
| 514 |
+
function init(){mvReset(false);loadUsers();loadAnn();loadHome();loadMovies()}
|
| 515 |
+
applyLang();show('ov');init();
|
| 516 |
+
</script>
|
| 517 |
+
</body>
|
| 518 |
+
</html>
|
apple-touch-icon (1).png
ADDED
|
|
config (1).js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 2 |
+
// JAYNES MAX β CONFIG (mahali pamoja pa URL na funguo)
|
| 3 |
+
// Badilisha thamani hapa tu; index.html haina funguo tena.
|
| 4 |
+
// TAHADHARI: faili hili linapakuliwa na kila mtumiaji (ni JS ya wazi).
|
| 5 |
+
// Weka HAPA funguo za "sb_publishable_β¦" (anon) PEKE YAKE.
|
| 6 |
+
// KAMWE usiweke "service_role" / "sb_secret_β¦" hapa.
|
| 7 |
+
// ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 8 |
+
window.JM_CONFIG = Object.freeze({
|
| 9 |
+
// Mradi wa maudhui (edge functions: search, live TV, fawa, trailer, mux, home-config, announcements)
|
| 10 |
+
CONTENT_URL: 'https://ooqvsqmsnwjqvuzbhczx.supabase.co',
|
| 11 |
+
CONTENT_KEY: 'sb_publishable_-B4goS_nTpi2UvK8bFyCiA_CV5UH4Ic',
|
| 12 |
+
|
| 13 |
+
// Mradi wa akaunti na profile (Auth + user_profiles)
|
| 14 |
+
PROFILE_URL: 'https://ybtklpzhacaazulntzyh.supabase.co',
|
| 15 |
+
PROFILE_KEY: 'sb_publishable_kTp9GWPaSozKbigB61iq3Q_DVjChLyP',
|
| 16 |
+
|
| 17 |
+
// Mradi wa Movies Zilizotafsiriwa (DJ movies/series β tab ya "Zilizotafsiriwa")
|
| 18 |
+
// KUMBUKA: jina la function lililo-deploy ni "movie-zilizotafsriwa" (bila "i" baada ya "tafs")
|
| 19 |
+
TRANSLATED_URL: 'https://kqqdsumvvbuvyoneanku.supabase.co',
|
| 20 |
+
TRANSLATED_KEY: 'sb_publishable_0XQ9PQ0CRJnC0A_Tz18oGg_fSONkxB3',
|
| 21 |
+
TRANSLATED_FN: 'movie-zilizotafsriwa',
|
| 22 |
+
|
| 23 |
+
// Web Push (VAPID) β funguo YA UMMA tu. Private key iwe Supabase secrets pekee.
|
| 24 |
+
VAPID_PUBLIC_KEY: 'BCgKqA0rqp_xKw90XP0kGwNuTm5nPA1BAOaqiDM3be-lDoeBW7hKRhnEi7sJ8VcCQ5TdJ-6rZ0WVGBGKV7bwfFw',
|
| 25 |
+
|
| 26 |
+
// Matangazo (Adsterra). Zima yote kwa kubadilisha kuwa false β hakuna haja ya kugusa index.html.
|
| 27 |
+
ADS_ENABLED: true,
|
| 28 |
+
});
|
dl-sw (1).js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// dl-sw.js β downloads za background (Background Fetch) kwa JAYNES MAX
|
| 2 |
+
// Weka mstari huu JUU kabisa kwenye sw.js yako: importScripts('dl-sw.js');
|
| 3 |
+
const JM_OFFLINE_DIR = 'jm-offline'; // lazima iwe sawa na OFFLINE_DIR kwenye index.html
|
| 4 |
+
|
| 5 |
+
async function jmDir(){ const root = await navigator.storage.getDirectory(); return root.getDirectoryHandle(JM_OFFLINE_DIR, { create: true }); }
|
| 6 |
+
async function jmReadMeta(dir, oid){
|
| 7 |
+
try { const fh = await dir.getFileHandle(oid + '.json'); return JSON.parse(await (await fh.getFile()).text()); } catch (_) { return null; }
|
| 8 |
+
}
|
| 9 |
+
async function jmWriteMeta(dir, oid, obj){
|
| 10 |
+
const fh = await dir.getFileHandle(oid + '.json', { create: true });
|
| 11 |
+
const w = await fh.createWritable(); await w.write(JSON.stringify(obj)); await w.close();
|
| 12 |
+
}
|
| 13 |
+
async function jmBroadcast(msg){
|
| 14 |
+
const all = await self.clients.matchAll({ type: 'window', includeUncontrolled: true });
|
| 15 |
+
all.forEach(c => c.postMessage(msg));
|
| 16 |
+
}
|
| 17 |
+
async function jmOpenDownloads(){
|
| 18 |
+
const all = await self.clients.matchAll({ type: 'window', includeUncontrolled: true });
|
| 19 |
+
for (const c of all) {
|
| 20 |
+
if ('focus' in c) { try { await c.focus(); } catch (_) {} c.postMessage({ type: 'jm-open-downloads' }); return; }
|
| 21 |
+
}
|
| 22 |
+
await self.clients.openWindow('./?nav=downloads');
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
// Ukurasa unauliza "je dl-sw.js ipo?"
|
| 26 |
+
self.addEventListener('message', (e) => {
|
| 27 |
+
if (e.data && e.data.type === 'jm-ping' && e.source) e.source.postMessage({ type: 'jm-pong' });
|
| 28 |
+
});
|
| 29 |
+
|
| 30 |
+
// Download imekamilika (hata app ikiwa imefungwa): hamisha faili kwenda OPFS
|
| 31 |
+
self.addEventListener('backgroundfetchsuccess', (event) => {
|
| 32 |
+
const bgf = event.registration;
|
| 33 |
+
event.waitUntil((async () => {
|
| 34 |
+
const oid = bgf.id.replace(/^jm-/, '');
|
| 35 |
+
const dir = await jmDir();
|
| 36 |
+
const meta = (await jmReadMeta(dir, oid)) || { id: oid, title: 'Video' };
|
| 37 |
+
let ok = false;
|
| 38 |
+
try {
|
| 39 |
+
const records = await bgf.matchAll();
|
| 40 |
+
const res = await records[0].responseReady;
|
| 41 |
+
const ct = (res.headers.get('content-type') || '').toLowerCase();
|
| 42 |
+
if (!res.ok || !res.body || /mpegurl|text\/|html|json/.test(ct)) throw new Error('not_direct_video');
|
| 43 |
+
const fh = await dir.getFileHandle(oid + '.bin', { create: true });
|
| 44 |
+
const w = await fh.createWritable();
|
| 45 |
+
await res.body.pipeTo(w); // stream β haitumii RAM
|
| 46 |
+
const size = (await fh.getFile()).size;
|
| 47 |
+
await jmWriteMeta(dir, oid, { ...meta, state: 'done', size });
|
| 48 |
+
ok = true;
|
| 49 |
+
} catch (e) {
|
| 50 |
+
try { await dir.removeEntry(oid + '.bin'); } catch (_) {}
|
| 51 |
+
await jmWriteMeta(dir, oid, { ...meta, state: 'failed', reason: String((e && e.message) || e) });
|
| 52 |
+
}
|
| 53 |
+
try { event.updateUI({ title: (meta.title || 'Video') + (ok ? ' β imehifadhiwa β' : ' β imeshindwa') }); } catch (_) {}
|
| 54 |
+
await jmBroadcast({ type: 'jm-bg-update', bgId: bgf.id });
|
| 55 |
+
})());
|
| 56 |
+
});
|
| 57 |
+
|
| 58 |
+
async function jmMarkFailed(bgf, reason){
|
| 59 |
+
const oid = bgf.id.replace(/^jm-/, '');
|
| 60 |
+
const dir = await jmDir();
|
| 61 |
+
const meta = (await jmReadMeta(dir, oid)) || { id: oid };
|
| 62 |
+
await jmWriteMeta(dir, oid, { ...meta, state: 'failed', reason });
|
| 63 |
+
await jmBroadcast({ type: 'jm-bg-update', bgId: bgf.id });
|
| 64 |
+
}
|
| 65 |
+
self.addEventListener('backgroundfetchfail', (e) => { e.waitUntil(jmMarkFailed(e.registration, e.registration.failureReason || 'failed')); });
|
| 66 |
+
self.addEventListener('backgroundfetchabort', (e) => { e.waitUntil(jmMarkFailed(e.registration, 'aborted')); });
|
| 67 |
+
self.addEventListener('backgroundfetchclick', (e) => { e.waitUntil(jmOpenDownloads()); });
|
| 68 |
+
|
| 69 |
+
// Bonyeza notification ya "xx%" / "Imekamilika" (njia ya ndani ya ukurasa) β fungua Downloads
|
| 70 |
+
self.addEventListener('notificationclick', (e) => {
|
| 71 |
+
if (!/^jm-dl-/.test((e.notification && e.notification.tag) || '')) return;
|
| 72 |
+
e.notification.close();
|
| 73 |
+
e.waitUntil(jmOpenDownloads());
|
| 74 |
+
});
|
icon-192 (1).png
ADDED
|
|
icon-maskable-512 (1).png
ADDED
|
|
index.html
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
manifest.webmanifest.txt
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "JAYNES MAX β Filamu, Series na TV Live",
|
| 3 |
+
"short_name": "JAYNES MAX",
|
| 4 |
+
"description": "Tazama filamu, series na chaneli za TV live kwa Kiswahili.",
|
| 5 |
+
"lang": "sw",
|
| 6 |
+
"dir": "ltr",
|
| 7 |
+
"start_url": "./",
|
| 8 |
+
"scope": "./",
|
| 9 |
+
"id": "./",
|
| 10 |
+
"display": "standalone",
|
| 11 |
+
"orientation": "portrait",
|
| 12 |
+
"background_color": "#0e1116",
|
| 13 |
+
"theme_color": "#0e1116",
|
| 14 |
+
"categories": ["entertainment", "video"],
|
| 15 |
+
"icons": [
|
| 16 |
+
{ "src": "icon-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any" },
|
| 17 |
+
{ "src": "icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any" },
|
| 18 |
+
{ "src": "icon-maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" }
|
| 19 |
+
],
|
| 20 |
+
"shortcuts": [
|
| 21 |
+
{ "name": "Live TV", "url": "./#/live", "icons": [{ "src": "icon-192.png", "sizes": "192x192" }] },
|
| 22 |
+
{ "name": "Michezo", "url": "./#/match", "icons": [{ "src": "icon-192.png", "sizes": "192x192" }] },
|
| 23 |
+
{ "name": "Orodha Yangu", "url": "./#/watchlist", "icons": [{ "src": "icon-192.png", "sizes": "192x192" }] }
|
| 24 |
+
]
|
| 25 |
+
}
|
og-image (1).jpg
ADDED
|
reset (1).html
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="sw">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
| 6 |
+
<meta name="theme-color" content="#0e1116">
|
| 7 |
+
<meta name="robots" content="noindex">
|
| 8 |
+
<title>Weka Nenosiri Jipya β JAYNES MAX</title>
|
| 9 |
+
<style>
|
| 10 |
+
:root{--bg:#0e1116;--card:#161a21;--text:#f5f6f8;--muted:#9aa3af;--accent:#e50914;--accent2:#c7b8ff;--border:#242a33;}
|
| 11 |
+
*{box-sizing:border-box;margin:0;padding:0;}
|
| 12 |
+
html,body{height:100%;}
|
| 13 |
+
body{background:var(--bg);color:var(--text);font-family:-apple-system,Roboto,Segoe UI,Helvetica,Arial,sans-serif;
|
| 14 |
+
display:flex;align-items:center;justify-content:center;padding:20px;}
|
| 15 |
+
.card{width:100%;max-width:400px;background:var(--card);border:1px solid var(--border);border-radius:18px;padding:26px 22px;}
|
| 16 |
+
.brand{font-weight:800;font-size:18px;text-align:center;margin-bottom:6px;}
|
| 17 |
+
.brand span{color:var(--accent);}
|
| 18 |
+
h1{font-size:19px;text-align:center;margin-bottom:6px;}
|
| 19 |
+
.sub{font-size:13px;color:var(--muted);text-align:center;margin-bottom:20px;line-height:1.45;}
|
| 20 |
+
label{display:block;font-size:12px;color:var(--muted);margin:0 0 6px 2px;}
|
| 21 |
+
.field{position:relative;margin-bottom:14px;}
|
| 22 |
+
input{width:100%;padding:13px 44px 13px 14px;border-radius:12px;border:1px solid var(--border);
|
| 23 |
+
background:var(--bg);color:var(--text);font-size:15px;outline:none;}
|
| 24 |
+
input:focus{border-color:var(--accent2);}
|
| 25 |
+
.eye{position:absolute;right:6px;bottom:6px;width:36px;height:36px;border:none;background:none;color:var(--muted);font-size:17px;cursor:pointer;}
|
| 26 |
+
.track{height:4px;border-radius:999px;background:rgba(255,255,255,.08);overflow:hidden;margin:-6px 0 4px;}
|
| 27 |
+
.fill{height:100%;width:0;border-radius:999px;background:#ff5252;transition:width .25s,background .25s;}
|
| 28 |
+
.strength{font-size:11px;color:var(--muted);text-align:right;min-height:1.3em;margin-bottom:12px;}
|
| 29 |
+
.btn{width:100%;padding:13px;border:none;border-radius:999px;background:var(--accent2);color:#1b1230;
|
| 30 |
+
font-weight:700;font-size:15px;cursor:pointer;display:block;text-align:center;text-decoration:none;}
|
| 31 |
+
.btn:disabled{opacity:.55;cursor:default;}
|
| 32 |
+
.msg{font-size:13px;text-align:center;margin-top:14px;min-height:1.3em;line-height:1.4;}
|
| 33 |
+
.err{color:#ff8a80;} .ok{color:#5ee6a0;}
|
| 34 |
+
.big{font-size:44px;text-align:center;margin-bottom:10px;}
|
| 35 |
+
.hide{display:none;}
|
| 36 |
+
</style>
|
| 37 |
+
</head>
|
| 38 |
+
<body>
|
| 39 |
+
<div class="card">
|
| 40 |
+
<div class="brand">JAYNES <span>MAX</span></div>
|
| 41 |
+
|
| 42 |
+
<!-- Hali: kiungo kibaya / kimeisha muda -->
|
| 43 |
+
<div id="bad" class="hide">
|
| 44 |
+
<div class="big">β οΈ</div>
|
| 45 |
+
<h1>Kiungo hakifanyi kazi</h1>
|
| 46 |
+
<p class="sub" id="badText">Kiungo hiki si sahihi au kimeisha muda. Omba kingine kupitia "Umesahau nenosiri?" kwenye app.</p>
|
| 47 |
+
<a class="btn" href="index.html">Rudi kwenye app</a>
|
| 48 |
+
</div>
|
| 49 |
+
|
| 50 |
+
<!-- Fomu -->
|
| 51 |
+
<div id="form" class="hide">
|
| 52 |
+
<h1>Weka nenosiri jipya</h1>
|
| 53 |
+
<p class="sub">Andika nenosiri jipya la akaunti yako.</p>
|
| 54 |
+
<div class="field">
|
| 55 |
+
<label for="p1">Nenosiri jipya</label>
|
| 56 |
+
<input id="p1" type="password" autocomplete="new-password" placeholder="Angalau herufi 8">
|
| 57 |
+
<button class="eye" type="button" data-for="p1" aria-label="Onyesha nenosiri">π</button>
|
| 58 |
+
</div>
|
| 59 |
+
<div class="track"><div class="fill" id="fill"></div></div>
|
| 60 |
+
<div class="strength" id="strength"></div>
|
| 61 |
+
<div class="field">
|
| 62 |
+
<label for="p2">Rudia nenosiri</label>
|
| 63 |
+
<input id="p2" type="password" autocomplete="new-password" placeholder="Rudia nenosiri">
|
| 64 |
+
<button class="eye" type="button" data-for="p2" aria-label="Onyesha nenosiri">π</button>
|
| 65 |
+
</div>
|
| 66 |
+
<button class="btn" id="save">Badilisha nenosiri</button>
|
| 67 |
+
<div class="msg" id="msg"></div>
|
| 68 |
+
</div>
|
| 69 |
+
|
| 70 |
+
<!-- Imefanikiwa -->
|
| 71 |
+
<div id="done" class="hide">
|
| 72 |
+
<div class="big">β
</div>
|
| 73 |
+
<h1>Nenosiri limebadilishwa</h1>
|
| 74 |
+
<p class="sub">Sasa unaweza kuingia kwa nenosiri lako jipya.</p>
|
| 75 |
+
<a class="btn" href="index.html">Ingia kwenye JAYNES MAX</a>
|
| 76 |
+
</div>
|
| 77 |
+
</div>
|
| 78 |
+
|
| 79 |
+
<script src="config.js"></script>
|
| 80 |
+
<script>
|
| 81 |
+
(function(){
|
| 82 |
+
const $ = id => document.getElementById(id);
|
| 83 |
+
const AUTH = JM_CONFIG.PROFILE_URL + '/auth/v1';
|
| 84 |
+
const KEY = JM_CONFIG.PROFILE_KEY;
|
| 85 |
+
const show = id => { ['bad','form','done'].forEach(x => $(x).classList.toggle('hide', x !== id)); };
|
| 86 |
+
|
| 87 |
+
// Supabase huweka token kwenye hash ya kiungo: #access_token=...&type=recovery
|
| 88 |
+
const params = new URLSearchParams(location.hash.replace(/^#/, ''));
|
| 89 |
+
const token = params.get('access_token');
|
| 90 |
+
const type = params.get('type');
|
| 91 |
+
const errCode = params.get('error_code');
|
| 92 |
+
try { history.replaceState(null, '', location.pathname); } catch (_) {} // ficha token kwenye URL
|
| 93 |
+
|
| 94 |
+
if (errCode === 'otp_expired') {
|
| 95 |
+
$('badText').textContent = 'Kiungo kimeisha muda au kimeshatumika. Omba kingine kupitia "Umesahau nenosiri?" kwenye app.';
|
| 96 |
+
show('bad');
|
| 97 |
+
} else if (!token || type !== 'recovery') {
|
| 98 |
+
show('bad');
|
| 99 |
+
} else {
|
| 100 |
+
show('form');
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
document.querySelectorAll('.eye').forEach(b => b.onclick = () => {
|
| 104 |
+
const i = $(b.dataset.for); i.type = i.type === 'password' ? 'text' : 'password';
|
| 105 |
+
});
|
| 106 |
+
|
| 107 |
+
$('p1').addEventListener('input', () => {
|
| 108 |
+
const v = $('p1').value;
|
| 109 |
+
let s = 0;
|
| 110 |
+
if (v.length >= 8) s++;
|
| 111 |
+
if (v.length >= 12) s++;
|
| 112 |
+
if (/[a-z]/.test(v) && /[A-Z]/.test(v)) s++;
|
| 113 |
+
if (/\d/.test(v)) s++;
|
| 114 |
+
if (/[^A-Za-z0-9]/.test(v)) s++;
|
| 115 |
+
if (!v) s = 0;
|
| 116 |
+
const lv = [
|
| 117 |
+
['0%', '#ff5252', ''], ['25%', '#ff5252', 'Dhaifu'], ['45%', '#ffb74d', 'Wastani'],
|
| 118 |
+
['70%', '#ffd54f', 'Nzuri'], ['90%', '#9ccc65', 'Imara'], ['100%', '#5ee6a0', 'Imara sana']
|
| 119 |
+
][s];
|
| 120 |
+
$('fill').style.width = lv[0]; $('fill').style.background = lv[1]; $('strength').textContent = lv[2];
|
| 121 |
+
});
|
| 122 |
+
|
| 123 |
+
const msg = (t, cls) => { $('msg').textContent = t; $('msg').className = 'msg ' + (cls || ''); };
|
| 124 |
+
|
| 125 |
+
$('save').onclick = async () => {
|
| 126 |
+
const a = $('p1').value, b = $('p2').value;
|
| 127 |
+
if (a.length < 8) return msg('Nenosiri liwe angalau herufi 8.', 'err');
|
| 128 |
+
if (a !== b) return msg('Manenosiri hayalingani.', 'err');
|
| 129 |
+
$('save').disabled = true; msg('Subiriβ¦');
|
| 130 |
+
try {
|
| 131 |
+
const res = await fetch(AUTH + '/user', {
|
| 132 |
+
method: 'PUT',
|
| 133 |
+
headers: { 'Content-Type': 'application/json', 'apikey': KEY, 'Authorization': 'Bearer ' + token },
|
| 134 |
+
body: JSON.stringify({ password: a })
|
| 135 |
+
});
|
| 136 |
+
const d = await res.json().catch(() => ({}));
|
| 137 |
+
if (!res.ok) throw new Error(d.msg || d.error_description || d.error_code || 'Imeshindikana');
|
| 138 |
+
show('done');
|
| 139 |
+
} catch (e) {
|
| 140 |
+
const m = String(e.message || '');
|
| 141 |
+
msg(/same|different/i.test(m) ? 'Nenosiri jipya lazima lisiwe sawa na la zamani.'
|
| 142 |
+
: /expired|invalid|jwt/i.test(m) ? 'Kiungo kimeisha muda. Omba kingine kwenye app.'
|
| 143 |
+
: 'Imeshindikana: ' + m, 'err');
|
| 144 |
+
$('save').disabled = false;
|
| 145 |
+
}
|
| 146 |
+
};
|
| 147 |
+
[$('p1'), $('p2')].forEach(i => i.addEventListener('keydown', e => { if (e.key === 'Enter') $('save').click(); }));
|
| 148 |
+
})();
|
| 149 |
+
</script>
|
| 150 |
+
</body>
|
| 151 |
+
</html>
|
sw (1).js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// JAYNES MAX β Service Worker
|
| 2 |
+
// Hifadhi "app shell" + maktaba za Shaka/hls ili app ifunguke hata bila mtandao.
|
| 3 |
+
// API, video na streams HAZIHIFADHIWI (zinahitaji mtandao). Movie zilizopakuliwa ziko OPFS ndani ya app.
|
| 4 |
+
const VERSION = 'jm-v9';
|
| 5 |
+
const RUNTIME = 'jm-cdn-v1';
|
| 6 |
+
const SHELL = ['./', './index.html', './manifest.webmanifest', './config.js', './icon-192.png', './icon-512.png', './icon-maskable-512.png', './apple-touch-icon.png'];
|
| 7 |
+
const CDN_HOST = 'cdnjs.cloudflare.com';
|
| 8 |
+
const CDN = [
|
| 9 |
+
'https://cdnjs.cloudflare.com/ajax/libs/hls.js/1.5.13/hls.min.js',
|
| 10 |
+
'https://cdnjs.cloudflare.com/ajax/libs/shaka-player/4.10.0/shaka-player.compiled.js',
|
| 11 |
+
];
|
| 12 |
+
|
| 13 |
+
self.addEventListener('install', (e) => {
|
| 14 |
+
e.waitUntil((async () => {
|
| 15 |
+
// Kila faili peke yake: faili moja likikosekana, install HAIFEI (addAll ingefeli yote)
|
| 16 |
+
const shell = await caches.open(VERSION);
|
| 17 |
+
await Promise.all(SHELL.map((u) => shell.add(u).catch(() => {})));
|
| 18 |
+
const rt = await caches.open(RUNTIME);
|
| 19 |
+
await Promise.all(CDN.map((u) => fetch(u, { mode: 'no-cors' }).then((r) => rt.put(u, r)).catch(() => {})));
|
| 20 |
+
await self.skipWaiting();
|
| 21 |
+
})());
|
| 22 |
+
});
|
| 23 |
+
|
| 24 |
+
self.addEventListener('activate', (e) => {
|
| 25 |
+
e.waitUntil(
|
| 26 |
+
caches.keys().then((keys) => Promise.all(keys.filter((k) => k !== VERSION && k !== RUNTIME).map((k) => caches.delete(k))))
|
| 27 |
+
.then(() => self.clients.claim())
|
| 28 |
+
);
|
| 29 |
+
});
|
| 30 |
+
|
| 31 |
+
function shellFallback() {
|
| 32 |
+
return caches.match('./index.html', { ignoreSearch: true })
|
| 33 |
+
.then((r) => r || caches.match('./', { ignoreSearch: true }));
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
self.addEventListener('fetch', (e) => {
|
| 37 |
+
const req = e.request;
|
| 38 |
+
if (req.method !== 'GET') return;
|
| 39 |
+
const url = new URL(req.url);
|
| 40 |
+
|
| 41 |
+
// Maktaba za CDN (hls/shaka): cache-first ili zipatikane offline na zipakie haraka
|
| 42 |
+
if (url.hostname === CDN_HOST) {
|
| 43 |
+
e.respondWith(
|
| 44 |
+
caches.match(req).then((hit) => hit || fetch(req).then((res) => {
|
| 45 |
+
if (res && (res.ok || res.type === 'opaque')) { const copy = res.clone(); caches.open(RUNTIME).then((c) => c.put(req, copy)); }
|
| 46 |
+
return res;
|
| 47 |
+
}))
|
| 48 |
+
);
|
| 49 |
+
return;
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
// Usiguse chochote kingine nje ya asili yetu (Supabase API, video, picha za CDN n.k.)
|
| 53 |
+
if (url.origin !== self.location.origin) return;
|
| 54 |
+
// Range requests (video/sauti) β acha mtandao ushughulikie
|
| 55 |
+
if (req.headers.has('range')) return;
|
| 56 |
+
|
| 57 |
+
// Ukurasa mkuu: network-first, lakini mtandao ukichelewa (>3.5s) au ukikosekana β cache.
|
| 58 |
+
// Hii inazuia app "kuning'inia" ukiwa na mtandao dhaifu / bila bando.
|
| 59 |
+
if (req.mode === 'navigate') {
|
| 60 |
+
e.respondWith(new Promise((resolve) => {
|
| 61 |
+
let settled = false;
|
| 62 |
+
const settle = (r) => { if (!settled && r) { settled = true; resolve(r); } };
|
| 63 |
+
const timer = setTimeout(() => { shellFallback().then(settle); }, 3500);
|
| 64 |
+
fetch(req).then((res) => {
|
| 65 |
+
clearTimeout(timer);
|
| 66 |
+
if (res && res.ok) { const copy = res.clone(); caches.open(VERSION).then((c) => c.put('./index.html', copy)); }
|
| 67 |
+
settle(res);
|
| 68 |
+
}).catch(() => {
|
| 69 |
+
clearTimeout(timer);
|
| 70 |
+
shellFallback().then((r) => { if (!settled) { settled = true; resolve(r || Response.error()); } });
|
| 71 |
+
});
|
| 72 |
+
}));
|
| 73 |
+
return;
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
// config.js: network-first (funguo/URL zikibadilishwa zifike mara moja), cache ikiwa offline
|
| 77 |
+
if (url.pathname.endsWith('/config.js')) {
|
| 78 |
+
e.respondWith(
|
| 79 |
+
fetch(req).then((res) => { if (res.ok) { const copy = res.clone(); caches.open(VERSION).then((c) => c.put(req, copy)); } return res; })
|
| 80 |
+
.catch(() => caches.match(req, { ignoreSearch: true }))
|
| 81 |
+
);
|
| 82 |
+
return;
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
// Faili tuli (icons, manifest): cache-first
|
| 86 |
+
e.respondWith(
|
| 87 |
+
caches.match(req).then((hit) => hit || fetch(req).then((res) => {
|
| 88 |
+
if (res.ok) { const copy = res.clone(); caches.open(VERSION).then((c) => c.put(req, copy)); }
|
| 89 |
+
return res;
|
| 90 |
+
}))
|
| 91 |
+
);
|
| 92 |
+
});
|
| 93 |
+
|
| 94 |
+
// ββ Web Push (inatoka Supabase Edge Function 'send-push') ββ
|
| 95 |
+
self.addEventListener('push', (e) => {
|
| 96 |
+
let d = {};
|
| 97 |
+
try { d = e.data ? e.data.json() : {}; } catch (_) { d = { body: e.data && e.data.text() }; }
|
| 98 |
+
const opts = {
|
| 99 |
+
body: d.body || '',
|
| 100 |
+
icon: './icon-192.png',
|
| 101 |
+
badge: './icon-192.png',
|
| 102 |
+
tag: d.id ? 'jm-ann-' + d.id : undefined,
|
| 103 |
+
data: { url: d.url || './' },
|
| 104 |
+
};
|
| 105 |
+
if (d.image) opts.image = d.image; // picha kubwa (Android/Chrome)
|
| 106 |
+
e.waitUntil(self.registration.showNotification(d.title || 'JAYNES MAX', opts));
|
| 107 |
+
});
|
| 108 |
+
|
| 109 |
+
// Kubonyeza notification: fungua app moja kwa moja kwenye ukurasa ulioainishwa (#/title/ID, #/live, ...)
|
| 110 |
+
self.addEventListener('notificationclick', (e) => {
|
| 111 |
+
e.notification.close();
|
| 112 |
+
const raw = (e.notification.data && e.notification.data.url) || './';
|
| 113 |
+
const target = new URL(raw, self.registration.scope).href;
|
| 114 |
+
e.waitUntil((async () => {
|
| 115 |
+
const all = await self.clients.matchAll({ type: 'window', includeUncontrolled: true });
|
| 116 |
+
for (const c of all) {
|
| 117 |
+
if (c.url.startsWith(self.registration.scope)) {
|
| 118 |
+
try { c.postMessage({ type: 'jm-open', url: target }); } catch (_) {}
|
| 119 |
+
if ('focus' in c) await c.focus();
|
| 120 |
+
return;
|
| 121 |
+
}
|
| 122 |
+
}
|
| 123 |
+
await self.clients.openWindow(target);
|
| 124 |
+
})());
|
| 125 |
+
});
|