diff --git a/src/web_ui/index.html b/src/web_ui/index.html
index cff51b3..b192a55 100644
--- a/src/web_ui/index.html
+++ b/src/web_ui/index.html
@@ -665,6 +665,7 @@
+
@@ -754,6 +755,103 @@ function exitRemote(){
loadLibrary();
setStatus('Back to your library');
}
+/* ── PeerTube browse (lazy two-level nav) ───────────────────────── */
+let ptState={id:null,channel:null,page:0,videos:[],done:false};
+function ptShow(on){
+ const g=document.getElementById('grid');
+ const v=document.getElementById('ptView');
+ if(g)g.style.display=on?'none':'';
+ if(v)v.style.display=on?'':'none';
+}
+async function openPeertube(id){
+ const r=remotes.find(x=>x.id===id);if(!r)return;
+ ptState={id,channel:null,page:0,videos:[],done:false};
+ remoteMode=null;document.body.classList.remove('remote-mode');
+ closeSidebar();renderSidebar();ptShow(true);
+ setStatus('Connecting to '+r.name+'…');
+ document.getElementById('ptView').innerHTML='Loading channels…
';
+ try{
+ const chs=await(await api('/api/remotes/'+id+'/channels')).json();
+ renderPtChannels(r,chs);
+ setStatus('Viewing '+r.name+' (read-only)');
+ }catch(e){document.getElementById('ptView').innerHTML='Error: '+esc(e.message)+'
';}
+}
+function exitPeertube(){
+ ptState={id:null,channel:null,page:0,videos:[],done:false};
+ ptShow(false);libraryEtag=null;renderSidebar();loadLibrary();
+ setStatus('Back to your library');
+}
+function renderPtChannels(r,chs){
+ let h=`← Back to my library
`;
+ h+=`🌐 ${esc(r.name)} — channels `;
+ if(!chs.length)h+='No channels.
';
+ // Handle/name are peer-controlled: keep them out of inline JS (an onclick with
+ // the handle interpolated is an XSS vector — a "'" in the handle breaks out).
+ // Stash the handle in a data-* attr (esc()'d) and wire the click in JS.
+ chs.forEach((c,i)=>{
+ const n=c.video_count!=null?` (${c.video_count})`:'';
+ h+=`${esc(c.display_name||c.handle)}${esc(n)}
`;
+ });
+ const box=document.getElementById('ptView');box.innerHTML=h;
+ box.querySelectorAll('.pt-chan').forEach(el=>el.onclick=()=>openPtChannel(el.dataset.handle));
+}
+async function openPtChannel(handle){
+ ptState.channel=handle;ptState.page=0;ptState.videos=[];ptState.done=false;
+ await loadPtPage(true);
+}
+async function loadPtPage(){
+ const id=ptState.id,handle=ptState.channel;
+ try{
+ const vids=await(await api('/api/remotes/'+id+'/channels/'+encodeURIComponent(handle)+'/videos?page='+ptState.page)).json();
+ ptState.videos.push(...vids);
+ if(vids.length<24)ptState.done=true;else ptState.page++;
+ renderPtVideos();
+ }catch(e){setStatus('Error: '+e.message);}
+}
+function renderPtVideos(){
+ let h=`← Back to channels
`;
+ h+=`${esc(ptState.channel)} `;
+ h+='';
+ ptState.videos.forEach((v,i)=>{
+ const dur=v.duration_secs?fmtDur(v.duration_secs):'';
+ const thumb=v.thumb_url?`
`:'';
+ h+=`
+ ${thumb}
+
${esc(v.title)}
+
${esc(dur)}
+
+ ▶ Play
+ ⬇ Archive
+
+
+
`;
+ });
+ h+='
';
+ if(!ptState.done)h+='Load more
';
+ const box=document.getElementById('ptView');box.innerHTML=h;
+ box.querySelectorAll('.pt-play').forEach(b=>b.onclick=()=>ptPlay(b.dataset.uuid,+b.dataset.i));
+ box.querySelectorAll('.pt-arch').forEach(b=>b.onclick=()=>ptArchive(b.dataset.uuid,b));
+}
+async function ptPlay(uuid,i){
+ const note=document.querySelector('.pt-note[data-i="'+i+'"]');
+ const btn=document.querySelector('.pt-play[data-i="'+i+'"]');
+ if(note)note.textContent='resolving…';
+ try{
+ const r=await api('/api/remotes/'+ptState.id+'/videos/'+encodeURIComponent(uuid)+'/media');
+ if(r.status===204){if(note)note.textContent='HLS-only — archive to watch';if(btn)btn.disabled=true;return;}
+ const j=await r.json();
+ if(note)note.textContent='';
+ const v=ptState.videos[i];
+ playVideo(uuid,{id:uuid,title:v.title,video_url:j.url,duration_secs:v.duration_secs,subtitles:[],has_chapters:false});
+ }catch(e){if(note)note.textContent='✗ '+e.message;}
+}
+async function ptArchive(uuid,btn){
+ btn.disabled=true;btn.textContent='⬇ Queued';
+ try{
+ await api('/api/remotes/'+ptState.id+'/archive',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({uuid})});
+ setStatus('Archiving — see Downloads');
+ }catch(e){btn.disabled=false;btn.textContent='⬇ Archive';setStatus('Archive failed: '+e.message);}
+}
async function loadLibrary(){
if(remoteMode!==null)return; // a remote is in view; don't clobber it with local polling
try{
@@ -794,7 +892,7 @@ function renderSidebar(){
if(remotes.length){
h+=``;
if(remoteMode!==null)h+=`← Back to my library
`;
- remotes.forEach(r=>{h+=`🌐 ${esc(r.name)}
`;});
+ remotes.forEach(r=>{const fn=r.kind==='peertube'?'openPeertube':'enterRemote';const act=(remoteMode===r.id||ptState.id===r.id)?' active':'';h+=`🌐 ${esc(r.name)}
`;});
}
h+=``;
if(contVids.length)h+=`▶ Continue (${contVids.length})
`;
@@ -1841,8 +1939,8 @@ function playerKey(e){
else return;
plActivity();
}
-function playVideo(id){
- const v=findVideo(id);if(!v||!v.video_url)return;
+function playVideo(id,vObj){
+ const v=vObj||findVideo(id);if(!v||!v.video_url)return;
currentPlayingId=id;
transcript={cues:[],loaded:false}; // reset for the new video
const bg=document.createElement('div');bg.className='modal-bg';