jaynesmax commited on
Commit
39a738c
Β·
verified Β·
1 Parent(s): a512ec9

Upload index.html

Browse files
Files changed (1) hide show
  1. index.html +116 -96
index.html CHANGED
@@ -122,12 +122,6 @@
122
  .jtv-b{margin-left:auto; background:rgba(255,255,255,.18); color:#fff; border:0; border-radius:8px; padding:5px 10px; font:inherit; font-size:12px;}
123
  .jtv-epg{margin-top:6px; max-height:38vh; overflow:auto; border-top:1px solid rgba(255,255,255,.2); padding-top:6px;}
124
  .jtv-epg div{padding:3px 0;} .jtv-epg span{display:inline-block; min-width:48px; opacity:.7;}
125
- .jtv-logo{position:absolute; right:12px; bottom:56px; width:64px; max-height:40px; object-fit:contain; opacity:.75; z-index:4; pointer-events:none; filter:drop-shadow(0 1px 3px rgba(0,0,0,.6));}
126
- .jtv-ad{position:absolute; left:12px; bottom:56px; z-index:6; background:#ffc53d; color:#1b1230; font-weight:900; font-size:11px; letter-spacing:1px; padding:3px 9px; border-radius:6px; display:none;}
127
- .jtv-ad.on{display:block;}
128
- .jtv-ad i{font-style:normal; font-weight:700; opacity:.8; margin-left:6px;}
129
- .jtv-epg .now{color:#ffc53d; font-weight:700;}
130
- .jtv-epg .k{font-size:10px; opacity:.6; margin-left:6px;}
131
  .rank-num{position:absolute; left:6px; bottom:0; font-size:58px; font-weight:900; line-height:1; letter-spacing:-3px; color:transparent;
132
  -webkit-text-stroke:2px rgba(255,255,255,.92); text-shadow:0 4px 14px rgba(0,0,0,.65); pointer-events:none; z-index:2;}
133
  .poster-art:has(.rank-num)::after{content:''; position:absolute; inset:auto 0 0 0; height:46%; background:linear-gradient(transparent,rgba(0,0,0,.7)); pointer-events:none;}
@@ -1221,6 +1215,13 @@
1221
  /* Nafasi ya kusogeza (scroll) ili kipengele chenye focus kisifichwe na topbar / nav */
1222
  .poster,.cw-card,.tab-chip,.settings-row,.dq-item,.tr-ep,.cast-item{scroll-margin:110px 3vw 110px 3vw;}
1223
  </style>
 
 
 
 
 
 
 
1224
  </head>
1225
  <body>
1226
  <div id="gate">
@@ -1491,7 +1492,17 @@ const PIXTVMAX_KEY = SUPABASE_ANON_KEY;
1491
  const TRAILER_BASE = JM_CONFIG.CONTENT_URL + '/functions/v1/movie-trailer';
1492
  const TRAILER_KEY = SUPABASE_ANON_KEY;
1493
 
1494
- // Chanzo kimoja cha Zilizotafsiriwa: movie-translated-all (inaunganisha old + n2 + absalom)
 
 
 
 
 
 
 
 
 
 
1495
  const TRANSLATED_ALL = JM_CONFIG.CONTENT_URL + '/functions/v1/movie-translated-all';
1496
  const TR_MODEL_URL = 'https://huggingface.co/jaynesmaxtv/translated-recommender/resolve/main/model.json';
1497
  let trModelP = null;
@@ -1500,14 +1511,6 @@ function loadTrModel() {
1500
  return trModelP;
1501
  }
1502
  // Rails za mlalo: Trending Now (Top 10, kwa model au mpya zaidi) -> New Releases -> kila DJ (DJ Mack kwanza) -> Series -> More to Watch
1503
- const TR_DJ_NOISE = /\b(movies?|filamu|zote|zilizotafsiriwa|zilizotafsiri|imetafsiriwa|iliyotafsiriwa|tafsiriwa|kiswahili|swahili|kwa|mpya|kali|translated|new|all|series|full)\b/gi;
1504
-
1505
- function normDj(raw) { // jina safi la DJ (bila maneno ya ziada na bila tofauti za herufi kubwa/ndogo)
1506
- const s = String(raw || '').replace(/\(.*?\)|\[.*?\]/g, ' ').replace(TR_DJ_NOISE, ' ').replace(/[|_\-\u2013\u2014:,.]+/g, ' ').replace(/\s+/g, ' ').trim();
1507
- if (!s || /^dj$/i.test(s)) return '';
1508
- return s.split(' ').map(w => /^dj$/i.test(w) ? 'DJ' : w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join(' ');
1509
- }
1510
-
1511
  function buildTranslatedSections(items, model) {
1512
  const pop = (model && model.popularity) || {}, sections = [], covered = new Set();
1513
  const hot = items.filter(it => pop[it.id] > 0).sort((a, b) => pop[b.id] - pop[a.id]);
@@ -1559,41 +1562,13 @@ async function rpc(op, args = {}) {
1559
  }
1560
 
1561
  // ── Share: kiungo cha moja kwa moja kwenye video (#/watch/ID) ──
1562
- // ── Kushiriki na picha: bango (poster) au nembo ya chaneli inaambatanishwa ──
1563
- async function shareImageFile(src){
1564
- if (!src) return null;
1565
- try {
1566
- const r = await fetch(src, { mode: 'cors' });
1567
- if (!r.ok) return null;
1568
- const b = await r.blob();
1569
- if (!b.type || !b.type.startsWith('image/')) return null;
1570
- const ext = (b.type.split('/')[1] || 'jpg').replace('jpeg', 'jpg').replace('svg+xml', 'svg');
1571
- return new File([b], 'jaynesmax-' + Date.now() + '.' + ext, { type: b.type });
1572
- } catch (_) { return null; }
1573
- }
1574
-
1575
- async function shareWithPoster(d, url, text){
1576
- const poster = d.poster || d.logo || null;
1577
- if (navigator.share) {
1578
- const file = await shareImageFile(poster);
1579
- const withFile = { title: text, text, url };
1580
- if (file && navigator.canShare && navigator.canShare({ files: [file] })) withFile.files = [file];
1581
- try { await navigator.share(withFile); return; }
1582
- catch (e) { if (e && e.name === 'AbortError') return; }
1583
- // Kama kushiriki na picha kumeshindikana, jaribu kushiriki kiungo na maelezo tu
1584
- try { await navigator.share({ title: text, text, url }); } catch (_) {}
1585
- return;
1586
- }
1587
- const clip = text + '\n' + url + (poster ? '\nPicha: ' + poster : '');
1588
- try { await navigator.clipboard.writeText(clip); showToast(t('link_copied')); }
1589
- catch (_) { prompt('Nakili kiungo:', clip); }
1590
- }
1591
-
1592
  async function shareTitle(d, se, ep){
1593
  const base = location.origin + location.pathname.replace(/index\.html$/i, '');
1594
  const url = base + '#/watch/' + d.id + (se != null && ep != null ? '?se=' + se + '&ep=' + ep : '');
1595
  const text = d.title + ' β€” JAYNES MAX';
1596
- return shareWithPoster(d, url, text);
 
 
1597
  }
1598
 
1599
  // ── Watchlist (localStorage) ──
@@ -2910,6 +2885,13 @@ const I18N_EXTRA10 = {
2910
  fr: { jtv_next: 'Ensuite', jtv_schedule: 'Programme', jtv_down: 'JaynesTV est indisponible', tr_trending: 'Tendances actuelles', tr_new: 'NouveautΓ©s', tr_more: 'Γ€ dΓ©couvrir' }
2911
  };
2912
  Object.keys(I18N_EXTRA10).forEach(l => { I18N[l] = Object.assign(I18N[l] || {}, I18N_EXTRA10[l]); });
 
 
 
 
 
 
 
2913
  function T(k, v) { return t(k, v); }
2914
  // Maandishi yaliyoandikwa kwenye HTML kupitia data-i18n / data-i18n-ph / data-i18n-title
2915
  function applyI18nAttrs() {
@@ -3630,71 +3612,40 @@ function jtvNow(s, nowMs) { // kipindi cha sasa + vinavyofuata, kwa saa: kila mt
3630
  }
3631
  async function openJaynesTV() {
3632
  openPlayerShell('JaynesTV');
3633
- let s = await jtvSchedule(true);
3634
  if (!s) return statusMsg('<span style="color:#ffb4ab">' + esc(T('jtv_down')) + '</span>');
3635
  stage.style.position = 'relative';
3636
- stage.innerHTML = '<video id="vid" controls autoplay playsinline></video><div class="jtv-bar" id="jtvBar"></div>' +
3637
- '<div class="jtv-ad" id="jtvAd">TANGAZO<i></i></div>' + (s.logo ? '<img class="jtv-logo" id="jtvLogo" alt="" src="' + esc(s.logo) + '">' : '');
3638
- const v = document.getElementById('vid'), bar = document.getElementById('jtvBar'), adEl = document.getElementById('jtvAd');
3639
- const logoEl = document.getElementById('jtvLogo'); if (logoEl) logoEl.onerror = () => logoEl.remove();
3640
  attachPlayerEnhancements(v); ensureSound(v);
3641
- const bad = new Set(); let cur = null, epg = false, hideT = null, last = null, liveOn = false, hls = null;
3642
  const hhmm = ms => new Date(ms).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
3643
- const kindOf = it => (it && it.kind) || 'program';
3644
  const show = (ms) => { bar.classList.remove('hide'); clearTimeout(hideT); if (ms) hideT = setTimeout(() => { if (!epg) bar.classList.add('hide'); }, ms); };
3645
- const nextProgram = n => (n.upcoming.find(u => kindOf(u.item) === 'program') || n.upcoming[0]);
3646
- const stopHls = () => { if (hls) { try { hls.destroy(); } catch (_) {} hls = null; } };
3647
- const playSrc = (url, off) => { // MP4 au HLS (.m3u8) β€” live ya studio ni HLS
3648
- stopHls();
3649
- const isHls = /\.m3u8(\?|$)/i.test(url);
3650
- if (isHls && !v.canPlayType('application/vnd.apple.mpegurl') && window.Hls && Hls.isSupported()) {
3651
- hls = new Hls(); hls.loadSource(url); hls.attachMedia(v);
3652
- } else v.src = isHls ? url : url + '#t=' + Math.floor(off || 0);
3653
- v.addEventListener('loadedmetadata', () => { if (!isHls && Math.abs(v.currentTime - off) > 3) v.currentTime = off; const p = v.play(); if (p && p.catch) p.catch(() => {}); }, { once: true });
3654
- };
3655
  const draw = (n, it) => {
3656
- if (liveOn) {
3657
- bar.innerHTML = '<div class="jtv-row"><span class="jtv-live">● LIVE</span><b>' + esc(s.live.title || 'Matangazo ya moja kwa moja') + '</b>' +
3658
- '<span class="jtv-m">Studio</span></div>';
3659
- adEl.classList.remove('on'); return;
3660
- }
3661
- const k = kindOf(it), nx = nextProgram(n);
3662
- adEl.classList.toggle('on', k === 'ad');
3663
- if (k === 'ad') adEl.querySelector('i').textContent = Math.max(0, Math.ceil(n.left)) + 's';
3664
- const nowTitle = k === 'program' ? it.title : (nx.item.title); // wakati wa tangazo/kitambulisho: "inaanza punde"
3665
- bar.innerHTML = '<div class="jtv-row"><span class="jtv-live">● LIVE</span><b>' + (k === 'program' ? '' : 'Punde: ') + esc(nowTitle) + '</b>' +
3666
- (k === 'program' && it.dj ? '<span class="jtv-m">' + esc(it.dj) + '</span>' : '') +
3667
  '<button class="jtv-b" data-j="epg">' + esc(T('jtv_schedule')) + '</button></div>' +
3668
- (k === 'program' ? '<div class="jtv-m">Sasa hivi Β· ' + T('jtv_next') + ': ' + esc(nx.item.title) + ' Β· ' + hhmm(nx.at) + '</div>' : '') +
3669
- (epg ? '<div class="jtv-epg">' +
3670
- (k === 'program' ? '<div class="now"><span>' + hhmm(Date.now() - (it.dur - n.left) * 1000) + '</span> ' + esc(it.title) + '<span class="k">SASA</span></div>' : '') +
3671
- n.upcoming.filter(u => kindOf(u.item) === 'program').map(u => '<div><span>' + hhmm(u.at) + '</span> ' + esc(u.item.title) + '</div>').join('') + '</div>' : '');
3672
  };
3673
  const sync = () => {
3674
- const liveUrl = s.live && s.live.active && s.live.url;
3675
- if (liveUrl) { // studio iko hewani: hamia live yenyewe
3676
- if (!liveOn || cur !== liveUrl) { liveOn = true; cur = liveUrl; playSrc(liveUrl, 0); show(8000); }
3677
- draw(null, null); return;
3678
- }
3679
- if (liveOn) { liveOn = false; cur = null; } // live imeisha: rudi kwenye ratiba
3680
  const n = jtvNow(s, Date.now()), isBad = bad.has(n.idx);
3681
  const it = isBad ? n.upcoming[0].item : n.item, off = isBad ? 0 : n.offset; // video mbovu: cheza inayofuata kutoka mwanzo
3682
  if (cur !== it.url) {
3683
- cur = it.url; playSrc(it.url, off);
3684
- if (kindOf(it) === 'program') show(8000); else bar.classList.add('hide');
 
3685
  } else if (!isBad && v.readyState > 1 && Math.abs(v.currentTime - off) > 4) v.currentTime = off; // rudi kwenye 'live'
3686
  last = n; draw(n, it);
3687
  };
3688
- const refresh = async () => { const ns = await jtvSchedule(true); if (ns) { s = ns; sync(); } }; // kuangalia live/ratiba mpya
3689
- bar.onclick = (e) => { if (e.target.closest('[data-j="epg"]') && last) { epg = !epg; draw(last, bad.has(last.idx) ? last.upcoming[0].item : last.item); show(epg ? 0 : 6000); } };
3690
  v.addEventListener('ended', sync);
3691
  v.addEventListener('play', sync);
3692
- v.addEventListener('seeked', () => { if (!liveOn && last && Math.abs(v.currentTime - last.offset) > 6) sync(); });
3693
- v.addEventListener('error', () => { if (liveOn) { s = Object.assign({}, s, { live: { active: false } }); cur = null; sync(); } else if (last) { bad.add(last.idx); sync(); } });
3694
  v.addEventListener('click', () => show(6000));
3695
- const timer = setInterval(sync, 15000), tick = setInterval(() => { if (last && !liveOn && adEl.classList.contains('on')) draw(jtvNow(s, Date.now()), jtvNow(s, Date.now()).item); }, 1000);
3696
- const liveTimer = setInterval(refresh, 60000);
3697
- window.__jtvStop = () => { clearInterval(timer); clearInterval(tick); clearInterval(liveTimer); clearTimeout(hideT); stopHls(); };
3698
  sync();
3699
  }
3700
 
@@ -3762,11 +3713,12 @@ async function loadLive() {
3762
  }
3763
 
3764
  // ── ZILIZOTAFSIRIWA (movies/series za DJ β€” mradi tofauti) ──
3765
- async function fetchTranslatedList() { // movie-translated-all: orodha moja iliyounganishwa
3766
  const card = (m, idPrefix) => ({
3767
  id: idPrefix + m.id, kind: 'translated', title: m.title,
3768
  poster: m.thumb_url || m.poster_url || null, imgFallbackOn: true,
3769
  subtitle: normDj(m.dj) || (m.category === 'series' ? 'Series' : 'Movie'), isSeries: m.category === 'series',
 
3770
  });
3771
  const r = await fetch(TRANSLATED_ALL, { headers: { apikey: SUPABASE_ANON_KEY } });
3772
  if (!r.ok) throw new Error(T('err_generic') + ' ' + r.status);
@@ -3775,12 +3727,78 @@ async function fetchTranslatedList() { // movie-translated-all: orodha moja iliy
3775
  return (d.movies || []).map(m => card(m, ''));
3776
  }
3777
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3778
  async function loadTranslated() {
3779
  heroWrap.innerHTML = ''; tabsEl.style.display = 'none';
3780
  renderSkeletonRails(2);
3781
  try {
3782
  const [items, model] = await Promise.all([fetchTranslatedList(), loadTrModel()]);
3783
- renderRails(buildTranslatedSections(items, model), false);
 
3784
  } catch (e) {
3785
  railsEl.innerHTML = '';
3786
  railsEl.appendChild(stateBox(t('err_failed', { e: esc(e.message || t('err_generic')) }), true, loadTranslated));
@@ -4104,7 +4122,9 @@ async function shareTranslatedTitle(d) {
4104
  const base = location.origin + location.pathname.replace(/index\.html$/i, '');
4105
  const shareUrl = base + '#/translated/' + d.id;
4106
  const text = d.title + ' β€” JAYNES MAX (' + T('nav_translated') + ')';
4107
- return shareWithPoster(d, shareUrl, text);
 
 
4108
  }
4109
 
4110
  // ── MICHEZO (fawa-live) ──
 
122
  .jtv-b{margin-left:auto; background:rgba(255,255,255,.18); color:#fff; border:0; border-radius:8px; padding:5px 10px; font:inherit; font-size:12px;}
123
  .jtv-epg{margin-top:6px; max-height:38vh; overflow:auto; border-top:1px solid rgba(255,255,255,.2); padding-top:6px;}
124
  .jtv-epg div{padding:3px 0;} .jtv-epg span{display:inline-block; min-width:48px; opacity:.7;}
 
 
 
 
 
 
125
  .rank-num{position:absolute; left:6px; bottom:0; font-size:58px; font-weight:900; line-height:1; letter-spacing:-3px; color:transparent;
126
  -webkit-text-stroke:2px rgba(255,255,255,.92); text-shadow:0 4px 14px rgba(0,0,0,.65); pointer-events:none; z-index:2;}
127
  .poster-art:has(.rank-num)::after{content:''; position:absolute; inset:auto 0 0 0; height:46%; background:linear-gradient(transparent,rgba(0,0,0,.7)); pointer-events:none;}
 
1215
  /* Nafasi ya kusogeza (scroll) ili kipengele chenye focus kisifichwe na topbar / nav */
1216
  .poster,.cw-card,.tab-chip,.settings-row,.dq-item,.tr-ep,.cast-item{scroll-margin:110px 3vw 110px 3vw;}
1217
  </style>
1218
+ <style>
1219
+ .tr-filter{margin:0 0 10px;}
1220
+ .tr-filter .tabs{padding:10px 16px 4px;}
1221
+ .tr-filter .tr-dj-row{padding-top:4px;}
1222
+ .tr-filter .tr-dj-row .tab-chip{font-size:12px; padding:6px 12px;}
1223
+ .tr-filter-note{padding:4px 16px 6px; font-size:12px; color:var(--muted);}
1224
+ </style>
1225
  </head>
1226
  <body>
1227
  <div id="gate">
 
1492
  const TRAILER_BASE = JM_CONFIG.CONTENT_URL + '/functions/v1/movie-trailer';
1493
  const TRAILER_KEY = SUPABASE_ANON_KEY;
1494
 
1495
+ // Zilizotafsiriwa: chanzo kimoja (movie-translated-all) β€” n2-, ab-, abm- ids
1496
+ const TR_DJ_NOISE = /\b(movies?|filamu|zote|zilizotafsiriwa|zilizotafsiri|imetafsiriwa|iliyotafsiriwa|tafsiriwa|kiswahili|swahili|kwa|mpya|kali|translated|new|all|series|full|single|singo|action|cartoon|katuni|season|seasons|(?:19|20)\d{2})\b/gi;
1497
+ function normDj(raw) { // jina safi la DJ (bila maneno ya ziada na bila tofauti za herufi kubwa/ndogo)
1498
+ const s = String(raw || '').replace(/\(.*?\)|\[.*?\]/g, ' ').replace(TR_DJ_NOISE, ' ').replace(/[|_\-\u2013\u2014:,.]+/g, ' ').replace(/\s+/g, ' ').trim();
1499
+ if (!s || /^dj$/i.test(s)) return '';
1500
+ const name = s.split(' ').map(w => /^dj$/i.test(w) ? 'DJ' : w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join(' ');
1501
+ return TR_DJ_ALIAS[name.toLowerCase()] || name;
1502
+ }
1503
+ // Majina tofauti ya DJ mmoja (andika kwa herufi ndogo upande wa kushoto)
1504
+ const TR_DJ_ALIAS = { 'dj blaki': 'DJ Black', 'blaki': 'DJ Black', 'black': 'DJ Black' };
1505
+ // Model iliyofunzwa (train.py -> HF Model). Ikikosekana, app inafanya kazi kama kawaida (Trending = mpya zaidi).
1506
  const TRANSLATED_ALL = JM_CONFIG.CONTENT_URL + '/functions/v1/movie-translated-all';
1507
  const TR_MODEL_URL = 'https://huggingface.co/jaynesmaxtv/translated-recommender/resolve/main/model.json';
1508
  let trModelP = null;
 
1511
  return trModelP;
1512
  }
1513
  // Rails za mlalo: Trending Now (Top 10, kwa model au mpya zaidi) -> New Releases -> kila DJ (DJ Mack kwanza) -> Series -> More to Watch
 
 
 
 
 
 
 
 
1514
  function buildTranslatedSections(items, model) {
1515
  const pop = (model && model.popularity) || {}, sections = [], covered = new Set();
1516
  const hot = items.filter(it => pop[it.id] > 0).sort((a, b) => pop[b.id] - pop[a.id]);
 
1562
  }
1563
 
1564
  // ── Share: kiungo cha moja kwa moja kwenye video (#/watch/ID) ──
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1565
  async function shareTitle(d, se, ep){
1566
  const base = location.origin + location.pathname.replace(/index\.html$/i, '');
1567
  const url = base + '#/watch/' + d.id + (se != null && ep != null ? '?se=' + se + '&ep=' + ep : '');
1568
  const text = d.title + ' β€” JAYNES MAX';
1569
+ if (navigator.share) { try { await navigator.share({ title: text, text, url }); } catch (_) {} return; }
1570
+ try { await navigator.clipboard.writeText(url); showToast(t('link_copied')); }
1571
+ catch (_) { prompt('Nakili kiungo:', url); }
1572
  }
1573
 
1574
  // ── Watchlist (localStorage) ──
 
2885
  fr: { jtv_next: 'Ensuite', jtv_schedule: 'Programme', jtv_down: 'JaynesTV est indisponible', tr_trending: 'Tendances actuelles', tr_new: 'NouveautΓ©s', tr_more: 'Γ€ dΓ©couvrir' }
2886
  };
2887
  Object.keys(I18N_EXTRA10).forEach(l => { I18N[l] = Object.assign(I18N[l] || {}, I18N_EXTRA10[l]); });
2888
+ const I18N_EXTRA_TRF = {
2889
+ sw: { trf_all: 'Zote', trf_new: 'Mpya', trf_trend_movie: 'Movie Zinazovuma', trf_trend_series: 'Series Zinazovuma', trf_movie: 'Movie', trf_series: 'Series', trf_dj: 'Kwa DJ', trf_no_trend: 'Bado hakuna takwimu za kutosha za zinazovuma, tunaonyesha mpya zaidi kwanza.' },
2890
+ en: { trf_all: 'All', trf_new: 'New Releases', trf_trend_movie: 'Trending Movies', trf_trend_series: 'Trending Series', trf_movie: 'Movies', trf_series: 'Series', trf_dj: 'By DJ', trf_no_trend: 'Not enough viewing data yet, showing newest first.' },
2891
+ ar: { trf_all: 'Ψ§Ω„ΩƒΩ„', trf_new: 'Ψ§Ω„Ψ¬Ψ―ΩŠΨ―', trf_trend_movie: 'أفلام Ψ±Ψ§Ψ¦Ψ¬Ψ©', trf_trend_series: 'Ω…Ψ³Ω„Ψ³Ω„Ψ§Ψͺ Ψ±Ψ§Ψ¦Ψ¬Ψ©', trf_movie: 'أفلام', trf_series: 'Ω…Ψ³Ω„Ψ³Ω„Ψ§Ψͺ', trf_dj: 'Ψ­Ψ³Ψ¨ DJ', trf_no_trend: 'Ω„Ψ§ Ψͺوجد Ψ¨ΩŠΨ§Ω†Ψ§Ψͺ Ω…Ψ΄Ψ§Ω‡Ψ―Ψ© ΩƒΨ§ΩΩŠΨ© بعد، Ω†ΨΉΨ±ΨΆ Ψ§Ω„Ψ£Ψ­Ψ―Ψ« Ψ£ΩˆΩ„Ψ§Ω‹.' },
2892
+ fr: { trf_all: 'Tout', trf_new: 'NouveautΓ©s', trf_trend_movie: 'Films tendance', trf_trend_series: 'SΓ©ries tendance', trf_movie: 'Films', trf_series: 'SΓ©ries', trf_dj: 'Par DJ', trf_no_trend: "Pas encore assez de donnΓ©es, les plus rΓ©cents d'abord." }
2893
+ };
2894
+ Object.keys(I18N_EXTRA_TRF).forEach(l => { I18N[l] = Object.assign(I18N[l] || {}, I18N_EXTRA_TRF[l]); });
2895
  function T(k, v) { return t(k, v); }
2896
  // Maandishi yaliyoandikwa kwenye HTML kupitia data-i18n / data-i18n-ph / data-i18n-title
2897
  function applyI18nAttrs() {
 
3612
  }
3613
  async function openJaynesTV() {
3614
  openPlayerShell('JaynesTV');
3615
+ const s = await jtvSchedule(true);
3616
  if (!s) return statusMsg('<span style="color:#ffb4ab">' + esc(T('jtv_down')) + '</span>');
3617
  stage.style.position = 'relative';
3618
+ stage.innerHTML = '<video id="vid" controls autoplay playsinline></video><div class="jtv-bar" id="jtvBar"></div>';
3619
+ const v = document.getElementById('vid'), bar = document.getElementById('jtvBar');
 
 
3620
  attachPlayerEnhancements(v); ensureSound(v);
3621
+ const bad = new Set(); let cur = null, epg = false, hideT = null, last = null;
3622
  const hhmm = ms => new Date(ms).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
 
3623
  const show = (ms) => { bar.classList.remove('hide'); clearTimeout(hideT); if (ms) hideT = setTimeout(() => { if (!epg) bar.classList.add('hide'); }, ms); };
 
 
 
 
 
 
 
 
 
 
3624
  const draw = (n, it) => {
3625
+ const nx = n.upcoming[0];
3626
+ bar.innerHTML = '<div class="jtv-row"><span class="jtv-live">● LIVE</span><b>' + esc(it.title) + '</b>' + (it.dj ? '<span class="jtv-m">' + esc(it.dj) + '</span>' : '') +
 
 
 
 
 
 
 
 
 
3627
  '<button class="jtv-b" data-j="epg">' + esc(T('jtv_schedule')) + '</button></div>' +
3628
+ '<div class="jtv-m">' + esc(T('jtv_next')) + ': ' + esc(nx.item.title) + ' Β· ' + hhmm(nx.at) + '</div>' +
3629
+ (epg ? '<div class="jtv-epg">' + n.upcoming.map(u => '<div><span>' + hhmm(u.at) + '</span> ' + esc(u.item.title) + '</div>').join('') + '</div>' : '');
 
 
3630
  };
3631
  const sync = () => {
 
 
 
 
 
 
3632
  const n = jtvNow(s, Date.now()), isBad = bad.has(n.idx);
3633
  const it = isBad ? n.upcoming[0].item : n.item, off = isBad ? 0 : n.offset; // video mbovu: cheza inayofuata kutoka mwanzo
3634
  if (cur !== it.url) {
3635
+ cur = it.url; v.src = it.url + '#t=' + Math.floor(off);
3636
+ v.addEventListener('loadedmetadata', () => { if (Math.abs(v.currentTime - off) > 3) v.currentTime = off; const p = v.play(); if (p && p.catch) p.catch(() => {}); }, { once: true });
3637
+ show(8000);
3638
  } else if (!isBad && v.readyState > 1 && Math.abs(v.currentTime - off) > 4) v.currentTime = off; // rudi kwenye 'live'
3639
  last = n; draw(n, it);
3640
  };
3641
+ bar.onclick = (e) => { if (e.target.closest('[data-j="epg"]')) { epg = !epg; draw(last, last.item); show(epg ? 0 : 6000); } };
 
3642
  v.addEventListener('ended', sync);
3643
  v.addEventListener('play', sync);
3644
+ v.addEventListener('seeked', () => { if (last && Math.abs(v.currentTime - last.offset) > 6) sync(); });
3645
+ v.addEventListener('error', () => { if (last) { bad.add(last.idx); sync(); } });
3646
  v.addEventListener('click', () => show(6000));
3647
+ const timer = setInterval(sync, 15000);
3648
+ window.__jtvStop = () => { clearInterval(timer); clearTimeout(hideT); };
 
3649
  sync();
3650
  }
3651
 
 
3713
  }
3714
 
3715
  // ── ZILIZOTAFSIRIWA (movies/series za DJ β€” mradi tofauti) ──
3716
+ async function fetchTranslatedList() {
3717
  const card = (m, idPrefix) => ({
3718
  id: idPrefix + m.id, kind: 'translated', title: m.title,
3719
  poster: m.thumb_url || m.poster_url || null, imgFallbackOn: true,
3720
  subtitle: normDj(m.dj) || (m.category === 'series' ? 'Series' : 'Movie'), isSeries: m.category === 'series',
3721
+ added: m.created_at || m.updated_at || '',
3722
  });
3723
  const r = await fetch(TRANSLATED_ALL, { headers: { apikey: SUPABASE_ANON_KEY } });
3724
  if (!r.ok) throw new Error(T('err_generic') + ' ' + r.status);
 
3727
  return (d.movies || []).map(m => card(m, ''));
3728
  }
3729
 
3730
+ // ── KUPANGA ZILIZOTAFSIRIWA: Zote / Mpya / Movie Zinazovuma / Series Zinazovuma / Movie / Series / Kwa DJ ──
3731
+ let trCache = null; // { items, model } -- kubadilisha kichujio hakupakui upya
3732
+ let trFilter = 'all', trDj = '';
3733
+ try { trFilter = sessionStorage.getItem('jm_tr_filter') || 'all'; trDj = sessionStorage.getItem('jm_tr_dj') || ''; } catch (_) {}
3734
+ const TR_FILTERS = ['all', 'new', 'trend_movie', 'trend_series', 'movie', 'series', 'dj'];
3735
+ function trDjGroups(items) { // DJ Mack kwanza, kisha DJ mwenye kazi nyingi zaidi
3736
+ const g = {};
3737
+ items.forEach(it => {
3738
+ const dj = String(it.subtitle || '').trim();
3739
+ if (!dj || dj === 'Series' || dj === 'Movie') return;
3740
+ (g[dj.toLowerCase()] = g[dj.toLowerCase()] || { name: dj, list: [] }).list.push(it);
3741
+ });
3742
+ return Object.values(g).sort((a, b) => (/mack/i.test(b.name) - /mack/i.test(a.name)) || b.list.length - a.list.length || a.name.localeCompare(b.name));
3743
+ }
3744
+ function trByNewest(list) { // mpya -> zamani kwa tarehe ya kuongezwa; zisizo na tarehe zinabaki kwa mpangilio wa seva
3745
+ return list.map((it, i) => [it, i])
3746
+ .sort((a, b) => (Date.parse(b[0].added) || 0) - (Date.parse(a[0].added) || 0) || a[1] - b[1]).map(x => x[0]);
3747
+ }
3748
+ function trTrending(list, model) { // kwa uchezaji halisi (model); zikipungua, zinaongezwa mpya zaidi bila namba
3749
+ const pop = (model && model.popularity) || {};
3750
+ const hot = list.filter(it => pop[it.id] > 0).sort((a, b) => pop[b.id] - pop[a.id]);
3751
+ const ranked = hot.slice(0, 30).map((it, i) => ({ ...it, rank: i < 10 ? i + 1 : undefined }));
3752
+ if (ranked.length < 10) {
3753
+ const have = new Set(ranked.map(it => it.id));
3754
+ trByNewest(list).forEach(it => { if (ranked.length < 30 && !have.has(it.id)) ranked.push(it); });
3755
+ }
3756
+ return { items: ranked, fallback: hot.length === 0 };
3757
+ }
3758
+ function renderTranslatedView() {
3759
+ if (!trCache) return;
3760
+ const { items, model } = trCache;
3761
+ const movies = items.filter(it => !it.isSeries), series = items.filter(it => it.isSeries);
3762
+ const groups = trDjGroups(items);
3763
+ if (!TR_FILTERS.includes(trFilter)) trFilter = 'all';
3764
+ if (trFilter === 'dj' && !groups.some(g => g.name === trDj)) trDj = groups[0] ? groups[0].name : '';
3765
+ let sections, grid = true, note = '';
3766
+ if (trFilter === 'new') sections = [{ title: t('tr_new'), items: trByNewest(items) }];
3767
+ else if (trFilter === 'trend_movie' || trFilter === 'trend_series') {
3768
+ const r = trTrending(trFilter === 'trend_movie' ? movies : series, model);
3769
+ if (r.fallback) note = t('trf_no_trend');
3770
+ sections = [{ title: t('trf_' + trFilter), items: r.items }];
3771
+ }
3772
+ else if (trFilter === 'movie') sections = [{ title: t('trf_movie'), items: trByNewest(movies) }];
3773
+ else if (trFilter === 'series') sections = [{ title: t('trf_series'), items: trByNewest(series) }];
3774
+ else if (trFilter === 'dj') {
3775
+ const g = groups.find(x => x.name === trDj);
3776
+ sections = g ? [{ title: g.name, items: trByNewest(g.list).map(it => ({ ...it, subtitle: it.isSeries ? 'Series' : '' })) }] : [];
3777
+ }
3778
+ else { sections = buildTranslatedSections(items, model); grid = false; }
3779
+ renderRails(sections, grid);
3780
+ const bar = document.createElement('div');
3781
+ bar.className = 'tr-filter';
3782
+ bar.innerHTML = `<div class="tabs">${TR_FILTERS.map(f => `<div class="tab-chip${f === trFilter ? ' active' : ''}" data-trf="${f}">${esc(t('trf_' + f))}</div>`).join('')}</div>` +
3783
+ (trFilter === 'dj' && groups.length ? `<div class="tabs tr-dj-row">${groups.map(g => `<div class="tab-chip${g.name === trDj ? ' active' : ''}" data-trdj="${esc(g.name)}">${esc(g.name)} Β· ${g.list.length}</div>`).join('')}</div>` : '') +
3784
+ (note ? `<div class="tr-filter-note">${esc(note)}</div>` : '');
3785
+ bar.onclick = (e) => {
3786
+ const c = e.target.closest('.tab-chip'); if (!c) return;
3787
+ if (c.dataset.trf) trFilter = c.dataset.trf; else if (c.dataset.trdj != null) trDj = c.dataset.trdj; else return;
3788
+ try { sessionStorage.setItem('jm_tr_filter', trFilter); sessionStorage.setItem('jm_tr_dj', trDj); } catch (_) {}
3789
+ renderTranslatedView();
3790
+ };
3791
+ railsEl.insertBefore(bar, railsEl.firstChild);
3792
+ bar.querySelectorAll('.tab-chip.active').forEach(c => { const row = c.parentElement; row.scrollLeft = c.offsetLeft - (row.clientWidth - c.offsetWidth) / 2; });
3793
+ }
3794
+
3795
  async function loadTranslated() {
3796
  heroWrap.innerHTML = ''; tabsEl.style.display = 'none';
3797
  renderSkeletonRails(2);
3798
  try {
3799
  const [items, model] = await Promise.all([fetchTranslatedList(), loadTrModel()]);
3800
+ trCache = { items, model };
3801
+ renderTranslatedView();
3802
  } catch (e) {
3803
  railsEl.innerHTML = '';
3804
  railsEl.appendChild(stateBox(t('err_failed', { e: esc(e.message || t('err_generic')) }), true, loadTranslated));
 
4122
  const base = location.origin + location.pathname.replace(/index\.html$/i, '');
4123
  const shareUrl = base + '#/translated/' + d.id;
4124
  const text = d.title + ' β€” JAYNES MAX (' + T('nav_translated') + ')';
4125
+ if (navigator.share) { try { await navigator.share({ title: text, text, url: shareUrl }); } catch (_) {} return; }
4126
+ try { await navigator.clipboard.writeText(shareUrl); showToast(t('link_copied')); }
4127
+ catch (_) { prompt('Nakili kiungo:', shareUrl); }
4128
  }
4129
 
4130
  // ── MICHEZO (fawa-live) ──