Problem:
- JPG files showed 'no preview'
- stream API calls missing user_id parameter
- probe API calls missing user_id parameter
Solution:
- Modified page.html stream calls:
/api/v2/files/{user_id}/{file_uuid}/stream
- Modified page.html probe calls:
/api/v2/files/{user_id}/{file_uuid}/probe
- Modified server.rs get_file_probe to accept user_id
Result:
- JPG/PNG images now show preview ✅
- Video files can be played ✅
- All file preview APIs use correct user database ✅
Files:
- src/page.html (3 API calls fixed)
- src/server.rs (get_file_probe)
1077 lines
58 KiB
HTML
1077 lines
58 KiB
HTML
<!DOCTYPE html>
|
||
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||
<meta http-equiv="Cache-Control" content="no-cache,no-store,must-revalidate">
|
||
<title>{__TITLE__}</title>
|
||
<style>
|
||
*{margin:0;padding:0;box-sizing:border-box}
|
||
body{font-family:-apple-system,system-ui,sans-serif;background:#0f172a;color:#e2e8f0;padding:24px 24px 80px;line-height:1.6;font-size:16px}
|
||
h1,h2,h3,h4{color:#60a5fa;margin:1em 0 .5em}
|
||
h1{font-size:1.8em;border-bottom:1px solid #334155;padding-bottom:.3em}
|
||
h2{font-size:1.4em} h3{font-size:1.15em}
|
||
p{margin:.6em 0} a{color:#60a5fa}
|
||
code{background:#1e293b;padding:2px 6px;border-radius:4px;font-size:.9em}
|
||
pre{background:#1e293b;padding:16px;border-radius:8px;overflow-x:auto;margin:.8em 0;font-size:.85em}
|
||
pre code{background:none;padding:0}
|
||
table{border-collapse:collapse;width:100%;margin:1em 0;font-size:.88em}
|
||
th,td{border:1px solid #334155;padding:8px 12px;text-align:left;vertical-align:top}
|
||
th{background:#1e293b;color:#94a3b8;font-weight:600;white-space:nowrap}
|
||
blockquote{border-left:4px solid #60a5fa;padding-left:16px;color:#94a3b8;margin:.8em 0}
|
||
ul,ol{padding-left:24px;margin:.6em 0}
|
||
img,video{max-width:100%;border-radius:8px} iframe{width:100%;height:98vh;border:none}
|
||
|
||
#mb-tree-panel{display:none;position:fixed;top:0;left:0;right:0;bottom:52px;background:#0f172a;z-index:9998;overflow-y:auto;padding:16px 24px}
|
||
#mb-tree-panel.active{display:block}
|
||
.mb-mode-bar{display:flex;gap:0;margin-bottom:16px;border-bottom:2px solid #1e293b;position:sticky;top:0;background:#0f172a;z-index:10}
|
||
.mb-mode-btn{background:none;border:none;color:#64748b;padding:10px 20px;cursor:pointer;font-size:14px;border-bottom:3px solid transparent;transition:all .2s;font-family:inherit}
|
||
.mb-mode-btn:hover{color:#94a3b8}
|
||
.mb-mode-btn.active{color:#60a5fa;border-bottom-color:#60a5fa}
|
||
.mb-mode-btn span{font-size:18px;margin-right:6px}
|
||
.mb-tree-node{padding:3px 0;cursor:default;border-radius:4px}
|
||
.mb-tree-node:hover{background:#1e293b}
|
||
.mb-tree-caret{display:inline-block;width:18px;cursor:pointer;color:#64748b;user-select:none}
|
||
.mb-tree-label{display:inline-flex;align-items:center;gap:6px}
|
||
.mb-tree-meta{color:#64748b;font-size:11px;margin-left:8px}
|
||
.mb-tree-file{cursor:pointer}
|
||
.mb-tree-file:hover{color:#60a5fa}
|
||
.mb-folder-actions{display:none;gap:3px;margin-left:8px}
|
||
.mb-tree-node:hover .mb-folder-actions{display:inline-flex}
|
||
body.mb-locked .mb-folder-actions{display:none!important}
|
||
body.mb-locked .mb-tree-node:hover .mb-folder-actions{display:none!important}
|
||
.mb-folder-btn{background:#334155;border:none;color:#94a3b8;padding:1px 6px;border-radius:3px;cursor:pointer;font-size:10px;font-family:inherit}
|
||
.mb-folder-btn:hover{background:#475569;color:#e2e8f0}
|
||
.mb-folder-btn.danger:hover{background:#7f1d1d;color:#fca5a5}
|
||
.mb-toast{position:fixed;bottom:60px;left:50%;transform:translateX(-50%);background:#064e3b;color:#4ade80;padding:6px 20px;border-radius:6px;font-size:13px;z-index:10001;transition:opacity .3s}
|
||
|
||
.mb-grid{display:grid;gap:10px}
|
||
.mb-grid.sm{grid-template-columns:repeat(auto-fill,minmax(120px,1fr))}
|
||
.mb-grid.lg{grid-template-columns:repeat(auto-fill,minmax(200px,1fr))}
|
||
.mb-grid-cell{background:#1e293b;border-radius:10px;padding:12px;text-align:center;cursor:pointer;transition:all .2s;border:1px solid transparent;min-height:100px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:6px}
|
||
.mb-grid-cell:hover{background:#1e3a5f;border-color:#3b82f6;transform:translateY(-2px)}
|
||
.mb-grid-cell .mb-grid-icon{font-size:40px}
|
||
.mb-grid-cell .mb-grid-label{font-size:12px;line-height:1.3;max-height:2.6em;overflow:hidden;word-break:break-word}
|
||
.mb-grid-cell .mb-grid-uuid{font-size:10px;color:#64748b;font-family:monospace}
|
||
|
||
#mb-overlay{display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.6);z-index:9999}
|
||
#mb-overlay.active{display:block}
|
||
#mb-detail{display:none;position:fixed;top:10%;left:10%;right:10%;bottom:10%;background:#1e293b;border:1px solid #334155;border-radius:12px;z-index:10000;padding:24px;overflow-y:auto;box-shadow:0 20px 60px rgba(0,0,0,.5)}
|
||
#mb-detail.active{display:block}
|
||
#mb-detail-close{position:absolute;top:12px;right:16px;background:none;border:none;color:#64748b;font-size:20px;cursor:pointer}
|
||
.mb-loc-tag{display:inline-block;padding:2px 8px;border-radius:4px;font-size:11px;margin:2px 4px 2px 0;font-family:monospace}
|
||
.mb-loc-tag.hot{background:#064e3b;color:#4ade80}
|
||
.mb-loc-tag.warm{background:#451a03;color:#fbbf24}
|
||
.mb-loc-tag.cold{background:#1e1b4b;color:#818cf8}
|
||
.mb-loc-tag.cloud{background:#0c4a6e;color:#38bdf8}
|
||
.mb-new-folder-btn{background:#1e3a5f;border:1px solid #3b82f6;color:#60a5fa;padding:2px 10px;border-radius:4px;cursor:pointer;font-size:11px;font-family:inherit;margin-left:8px}
|
||
.mb-new-folder-btn:hover{background:#1e40af;color:#93c5fd}
|
||
.mb-lock-btn{background:none;border:none;font-size:16px;cursor:pointer;padding:2px 6px;align-self:center}
|
||
.mb-rename-input{background:#1e293b;border:1px solid #60a5fa;border-radius:4px;color:#e2e8f0;padding:1px 6px;font-size:13px;font-family:inherit;width:180px}
|
||
.mb-icon-picker{display:grid;grid-template-columns:repeat(8,1fr);gap:4px;max-height:200px;overflow-y:auto;margin:8px 0}
|
||
.mb-icon-picker button{background:#0f172a;border:2px solid transparent;border-radius:6px;font-size:22px;padding:6px;cursor:pointer}
|
||
.mb-icon-picker button:hover{background:#1e3a5f;border-color:#60a5fa}
|
||
|
||
#mb-settings-panel{display:none;position:fixed;top:0;left:0;right:0;bottom:52px;background:#0f172a;z-index:9998;overflow-y:auto;padding:16px 24px}
|
||
#mb-settings-panel.active{display:block}
|
||
.mb-config-section{background:#1e293b;border-radius:8px;padding:16px;margin:8px 0}
|
||
.mb-config-header{color:#60a5fa;font-size:14px;font-weight:600;margin-bottom:12px;padding-bottom:8px;border-bottom:1px solid #334155}
|
||
.mb-config-item{display:flex;justify-content:space-between;align-items:center;padding:8px 0;border-bottom:1px solid #334155}
|
||
.mb-config-item:last-child{border-bottom:none}
|
||
.mb-config-label{color:#94a3b8;font-size:13px}
|
||
.mb-config-value{color:#e2e8f0;font-size:13px;font-family:monospace}
|
||
.mb-config-edit-btn{background:#334155;border:none;color:#60a5fa;padding:2px 8px;border-radius:4px;cursor:pointer;font-size:11px}
|
||
.mb-config-edit-btn:hover{background:#475569}
|
||
.mb-config-input{background:#0f172a;border:1px solid #60a5fa;border-radius:4px;color:#e2e8f0;padding:2px 8px;font-size:12px;font-family:monospace;width:150px}
|
||
.mb-config-save-btn{background:#064e3b;border:1px solid #4ade80;color:#4ade80;padding:2px 8px;border-radius:4px;cursor:pointer;font-size:11px}
|
||
.mb-config-cancel-btn{background:#451a03;border:1px solid #fbbf24;color:#fbbf24;padding:2px 8px;border-radius:4px;cursor:pointer;font-size:11px}
|
||
.mb-password-toggle{background:none;border:none;color:#60a5fa;cursor:pointer;font-size:14px;padding:0 4px}
|
||
.mb-password-toggle:hover{color:#3b82f6}
|
||
#mb-admin-modal{display:none;position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);
|
||
background:#1e293b;border:1px solid #334155;padding:24px;border-radius:8px;z-index:10000;min-width:280px}
|
||
#mb-admin-modal.active{display:block}
|
||
.mb-admin-title{color:#60a5fa;font-size:16px;font-weight:600;margin-bottom:16px}
|
||
.mb-admin-input{background:#0f172a;border:1px solid #60a5fa;border-radius:4px;
|
||
color:#e2e8f0;padding:8px 12px;width:100%;margin-bottom:12px;font-size:13px}
|
||
.mb-admin-btn{background:#064e3b;border:1px solid #4ade80;color:#4ade80;
|
||
padding:8px 16px;border-radius:4px;cursor:pointer;width:100%;font-size:13px}
|
||
.mb-admin-btn:hover{background:#4ade80;color:#064e3b}
|
||
.mb-admin-error{color:#ef4444;font-size:12px;margin-top:8px}
|
||
.mb-admin-close{position:absolute;top:12px;right:12px;background:none;border:none;
|
||
color:#64748b;font-size:18px;cursor:pointer}
|
||
</style>
|
||
<script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
|
||
<script>mermaid.initialize({startOnLoad:false,theme:'dark',themeVariables:{primaryColor:'#60a5fa',primaryTextColor:'#e2e8f0',lineColor:'#94a3b8'}})</script>
|
||
</head><body>
|
||
<div id=mb-content>{__CONTENT__}</div>
|
||
<div id=mb-overlay onclick="closeDetail()"></div>
|
||
<div id=mb-detail><button id=mb-detail-close onclick="closeDetail()">✕</button><div id=mb-detail-body></div></div>
|
||
<div id=mb-tree-panel><div id=mb-tree-body></div></div>
|
||
<div id=mb-settings-panel><div id=mb-settings-body></div></div>
|
||
|
||
<div id=mb-bar style="position:fixed;bottom:0;left:0;right:0;background:#1e293b;border-top:1px solid #334155;display:flex;justify-content:center;align-items:center;gap:5px;padding:5px 10px;z-index:9999;font-size:12px">
|
||
<button onclick="fetch('/command',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({cmd:'restart'})})" title="Restart">⏮</button>
|
||
<button onclick="fetch('/command',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({cmd:'prev'})})" title="Prev">◀</button>
|
||
<button onclick="fetch('/command',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({cmd:'next'})})" title="Next">▶</button>
|
||
<button onclick="fetch('/command',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({cmd:'end'})})" title="End">⏭</button>
|
||
<input id=mbgi type=number min=1 max=999 placeholder=N style="width:36px;background:#0f172a;border:1px solid #334155;border-radius:4px;color:white;padding:2px 4px;font-size:10px;text-align:center">
|
||
<button onclick="var n=document.getElementById('mbgi').value;if(n)fetch('/command',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({cmd:'goto',val:parseInt(n)})})" style=font-size:10px>GO</button>
|
||
<select id=mbstep onchange="if(this.value)fetch('/command',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({cmd:'goto',val:parseInt(this.value)})})" style="background:#0f172a;color:white;border:1px solid #334155;border-radius:4px;padding:1px 3px;font-size:10px;max-width:160px"><option value="">Step...</option></select>
|
||
<span style=color:#475569;font-size:10px>|</span>
|
||
<button onclick="toggleTree()" title="File Tree" style="background:none;border:none;color:#60a5fa;cursor:pointer;font-size:16px">🗂</button>
|
||
<span style=color:#475569;font-size:10px>|</span>
|
||
<button onclick="toggleSettings()" title="Settings" style="background:none;border:none;color:#60a5fa;cursor:pointer;font-size:16px">⚙️</button>
|
||
<span style=color:#475569;font-size:10px>|</span>
|
||
<button onclick="var t=this.textContent;this.textContent=t===String.fromCodePoint(0x1F50A)?String.fromCodePoint(0x1F507):String.fromCodePoint(0x1F50A)" id=mbvb title=Voice style=font-size:16px>🔊</button>
|
||
<select id=mbvl onchange="fetch('/command',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({cmd:'lang',val:this.value})})" style="background:#0f172a;color:white;border:1px solid #334155;border-radius:4px;padding:1px 3px;font-size:10px">
|
||
<option value=zh_TW>🇹🇼</option><option value=en_US>🇺🇸</option><option value=ja_JP>🇯🇵</option><option value=ko_KR>🇰🇷</option><option value=fr_FR>🇫🇷</option></select>
|
||
<select id=mbol onchange="fetch('/command',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({cmd:'audio_out',val:this.value})})" style="background:#0f172a;color:white;border:1px solid #334155;border-radius:4px;padding:1px 3px;font-size:10px;max-width:120px">{out_devs}</select>
|
||
<button onclick="var l=document.getElementById('mbvl').value;var o=document.getElementById('mbol').value;fetch('/command',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({cmd:'test_voice',val:l,out:o})})" style=font-size:14px title="Test Voice">🔊</button>
|
||
<select id=mbil onchange="fetch('/command',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({cmd:'audio_in',val:this.value})})" style="background:#0f172a;color:white;border:1px solid #334155;border-radius:4px;padding:1px 3px;font-size:10px;max-width:120px">{in_devs}</select>
|
||
<button onclick="fetch('/command',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({cmd:'vol_down'})})" style=font-size:13px title="Vol-">➖</button>
|
||
<span id=mbvlvl style=color:#4ade80;font-size:11px>--</span>
|
||
<button onclick="fetch('/command',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({cmd:'vol_up'})})" style=font-size:13px title="Vol+">➕</button>
|
||
<span id=mbsi style=color:#94a3b8;font-size:10px;margin-left:2px></span>
|
||
</div>
|
||
|
||
<script>
|
||
// ═══════════════ SETTINGS PANEL ═══════════════
|
||
var _sv=false;
|
||
|
||
function toggleSettings(){
|
||
var token=localStorage.getItem('admin_token');
|
||
var lastClose=localStorage.getItem('admin_close_time');
|
||
|
||
if(token){
|
||
// Check if closed more than 10 seconds ago
|
||
if(lastClose){
|
||
var now=Date.now();
|
||
var elapsed=(now-parseInt(lastClose))/1000;
|
||
if(elapsed>10){
|
||
// Token expired (>10s since close), clear and show login
|
||
localStorage.removeItem('admin_token');
|
||
localStorage.removeItem('admin_close_time');
|
||
showAdminLoginModal();
|
||
return;
|
||
}
|
||
}
|
||
|
||
// Verify token validity
|
||
fetch('/api/v2/admin/verify',{
|
||
headers:{'Authorization':'Bearer '+token}
|
||
})
|
||
.then(function(r){return r.json()})
|
||
.then(function(d){
|
||
if(d.ok){
|
||
// Token valid, open settings
|
||
_sv=!_sv;
|
||
document.getElementById("mb-settings-panel").classList.toggle("active",_sv);
|
||
if(_sv){
|
||
loadSettings();
|
||
// Clear close time when opening
|
||
localStorage.removeItem('admin_close_time');
|
||
}else{
|
||
// Record close time when closing
|
||
localStorage.setItem('admin_close_time',Date.now());
|
||
}
|
||
}else{
|
||
// Token invalid, remove and show login
|
||
localStorage.removeItem('admin_token');
|
||
localStorage.removeItem('admin_close_time');
|
||
showAdminLoginModal();
|
||
}
|
||
})
|
||
.catch(function(e){
|
||
localStorage.removeItem('admin_token');
|
||
localStorage.removeItem('admin_close_time');
|
||
showAdminLoginModal();
|
||
});
|
||
}else{
|
||
// No token, show login
|
||
showAdminLoginModal();
|
||
}
|
||
}
|
||
|
||
function showAdminLoginModal(){
|
||
var m=document.getElementById('mb-admin-modal');
|
||
|
||
if(!m){
|
||
m=document.createElement('div');
|
||
m.id='mb-admin-modal';
|
||
m.innerHTML='<button class=mb-admin-close onclick=this.parentElement.classList.remove("active")>✕</button>'+
|
||
'<div class=mb-admin-title>Admin Authentication Required</div>'+
|
||
'<div style="position:relative">'+
|
||
'<input class=mb-admin-input type=password id=admin-password placeholder="Enter admin password" onkeypress=handleAdminKeyPress(event)>'+
|
||
'<button class=mb-password-toggle style="position:absolute;right:8px;top:50%;transform:translateY(-50%)" onclick=toggleAdminPassword()>👁</button>'+
|
||
'</div>'+
|
||
'<button class=mb-admin-btn onclick=submitAdminLogin()>Login</button>'+
|
||
'<div class=mb-admin-error id=admin-error></div>';
|
||
document.body.appendChild(m);
|
||
}
|
||
|
||
document.getElementById('admin-password').value='';
|
||
document.getElementById('admin-password').type='password';
|
||
document.getElementById('admin-error').textContent='';
|
||
m.classList.add('active');
|
||
document.getElementById('admin-password').focus();
|
||
}
|
||
|
||
function toggleAdminPassword(){
|
||
var pwdInput=document.getElementById('admin-password');
|
||
var toggleBtn=pwdInput.parentElement.querySelector('.mb-password-toggle');
|
||
|
||
if(pwdInput.type==='password'){
|
||
pwdInput.type='text';
|
||
toggleBtn.textContent='🙈';
|
||
}else{
|
||
pwdInput.type='password';
|
||
toggleBtn.textContent='👁';
|
||
}
|
||
}
|
||
|
||
function handleAdminKeyPress(e){
|
||
if(e.key==='Enter'||e.keyCode===13){
|
||
submitAdminLogin();
|
||
}
|
||
}
|
||
|
||
function submitAdminLogin(){
|
||
var pwd=document.getElementById('admin-password').value;
|
||
|
||
if(!pwd){
|
||
document.getElementById('admin-error').textContent='Password required';
|
||
return;
|
||
}
|
||
|
||
fetch('/api/v2/admin/login',{
|
||
method:'POST',
|
||
headers:{'Content-Type':'application/json'},
|
||
body:JSON.stringify({username:'admin',password:pwd})
|
||
})
|
||
.then(function(r){return r.json()})
|
||
.then(function(d){
|
||
if(d.token){
|
||
localStorage.setItem('admin_token',d.token);
|
||
localStorage.removeItem('admin_close_time'); // Clear close time on new login
|
||
document.getElementById('mb-admin-modal').classList.remove('active');
|
||
toast('Admin authenticated ✓');
|
||
toggleSettings(); // Re-open settings
|
||
}else{
|
||
document.getElementById('admin-error').textContent=d.error||'Login failed';
|
||
document.getElementById('admin-password').value='';
|
||
document.getElementById('admin-password').focus();
|
||
}
|
||
})
|
||
.catch(function(e){
|
||
document.getElementById('admin-error').textContent='Connection error: '+e;
|
||
});
|
||
}
|
||
|
||
function loadSettings(){
|
||
var b=document.getElementById("mb-settings-body");
|
||
if(!b)return;
|
||
b.innerHTML="<div style=text-align:center;padding:40px;color:#64748b>Loading...</div>";
|
||
|
||
fetch("/api/v2/config").then(function(r){return r.json()}).then(function(d){
|
||
var h="<div class=mb-mode-bar>";
|
||
h+="<span style=color:#60a5fa;font-size:16px>⚙️</span>";
|
||
h+="<span style=font-size:14px;color:#e2e8f0;margin-left:8px>Settings</span>";
|
||
h+="<span style=flex:1></span>";
|
||
h+="<button class=mb-new-folder-btn onclick=validateSettings()>Validate</button>";
|
||
h+="<button onclick=toggleSettings() style='background:none;border:none;color:#64748b;font-size:18px;cursor:pointer'>✕</button>";
|
||
h+="</div>";
|
||
|
||
var sections=["server","postgresql","authentication","test","logging"];
|
||
sections.forEach(function(sec){
|
||
var secData=d[sec];
|
||
if(!secData)return;
|
||
|
||
h+="<div class=mb-config-section>";
|
||
h+="<div class=mb-config-header>"+sec.toUpperCase()+"</div>";
|
||
|
||
for(var key in secData){
|
||
if(secData.hasOwnProperty(key)){
|
||
var val=secData[key];
|
||
var ckey=sec+"."+key;
|
||
var dispVal=typeof val==="object"?JSON.stringify(val):String(val);
|
||
var safeId=ckey.replace(/[^a-zA-Z0-9]/g,"-");
|
||
var isPassword=key.toLowerCase().indexOf("password")!==-1;
|
||
|
||
h+="<div class=mb-config-item>";
|
||
h+="<span class=mb-config-label>"+key+"</span>";
|
||
h+="<div style=display:flex;gap:6px;align-items:center>";
|
||
|
||
if(isPassword){
|
||
h+="<span class=mb-config-value id=config-value-"+safeId+">••••••••</span>";
|
||
h+="<button class=mb-password-toggle onclick=togglePassword(\""+safeId+"\",\""+encodeURIComponent(dispVal)+"\") title=\"Show/Hide\">👁</button>";
|
||
}else{
|
||
h+="<span class=mb-config-value id=config-value-"+safeId+">"+dispVal+"</span>";
|
||
}
|
||
|
||
h+="<button class=mb-config-edit-btn onclick=editSetting(\""+ckey+"\",\""+encodeURIComponent(dispVal)+"\")>Edit</button>";
|
||
h+="</div>";
|
||
h+="</div>";
|
||
}
|
||
}
|
||
h+="</div>";
|
||
});
|
||
|
||
b.innerHTML=h;
|
||
}).catch(function(e){
|
||
b.innerHTML="<div style=padding:20px;color:#ef4444>Failed to load settings: "+e+"</div>";
|
||
});
|
||
}
|
||
|
||
function editSetting(key,currentVal){
|
||
var safeId=key.replace(/[^a-zA-Z0-9]/g,"-");
|
||
var valEl=document.getElementById("config-value-"+safeId);
|
||
if(!valEl)return;
|
||
|
||
var decodedVal=decodeURIComponent(currentVal);
|
||
var isPassword=key.toLowerCase().indexOf("password")!==-1;
|
||
|
||
var inputType=isPassword?"type=password ":"";
|
||
valEl.innerHTML="<input class=mb-config-input id=config-input-"+safeId+" "+inputType+"value='"+decodedVal+"'>";
|
||
|
||
// Remove password toggle button if exists
|
||
var toggleBtn=valEl.parentElement.querySelector(".mb-password-toggle");
|
||
if(toggleBtn)toggleBtn.remove();
|
||
|
||
var parent=valEl.parentElement;
|
||
var editBtn=parent.querySelector(".mb-config-edit-btn");
|
||
editBtn.outerHTML="<button class=mb-config-save-btn onclick=saveSetting(\""+key+"\",\""+safeId+"\",\""+isPassword+"\")>Save</button><button class=mb-config-cancel-btn onclick=cancelEdit(\""+key+"\",\""+currentVal+"\")>Cancel</button>";
|
||
}
|
||
|
||
function saveSetting(key,safeId,isPassword){
|
||
var input=document.getElementById("config-input-"+safeId);
|
||
if(!input)return;
|
||
|
||
var newVal=input.value;
|
||
|
||
fetch("/api/v2/config/edit?key="+encodeURIComponent(key)+"&value="+encodeURIComponent(newVal),{method:"POST"})
|
||
.then(function(r){return r.json()}).then(function(d){
|
||
if(d.ok){
|
||
toast("Saved: "+key);
|
||
loadSettings();
|
||
}else{
|
||
toast("Error: "+(d.error||"unknown"));
|
||
}
|
||
}).catch(function(e){toast("Save error: "+e)});
|
||
}
|
||
|
||
function cancelEdit(key,currentVal){
|
||
loadSettings();
|
||
}
|
||
|
||
function togglePassword(safeId,actualVal){
|
||
var valEl=document.getElementById("config-value-"+safeId);
|
||
if(!valEl)return;
|
||
|
||
var toggleBtn=valEl.parentElement.querySelector(".mb-password-toggle");
|
||
if(!toggleBtn)return;
|
||
|
||
if(valEl.textContent==="••••••••"){
|
||
valEl.textContent=decodeURIComponent(actualVal);
|
||
toggleBtn.textContent="🙈";
|
||
}else{
|
||
valEl.textContent="••••••••";
|
||
toggleBtn.textContent="👁";
|
||
}
|
||
}
|
||
|
||
function validateSettings(){
|
||
fetch("/api/v2/config/validate").then(function(r){return r.json()}).then(function(d){
|
||
if(d.ok)toast("✓ Settings valid");
|
||
else toast("✗ Invalid: "+(d.error||"unknown"));
|
||
}).catch(function(e){toast("Validate error: "+e)});
|
||
}
|
||
|
||
function toast(msg){
|
||
var t=document.createElement("div");
|
||
t.className="mb-toast";
|
||
t.textContent=msg;
|
||
document.body.appendChild(t);
|
||
setTimeout(function(){t.style.opacity="0";setTimeout(function(){t.remove()},300)},2000);
|
||
}
|
||
|
||
// Page version polling (skip while tree or detail panel is open)
|
||
var _v=-1;
|
||
setInterval(function(){
|
||
if(_tv)return;
|
||
var ov=document.getElementById("mb-overlay");
|
||
if(ov&&ov.style.display==="block")return;
|
||
fetch("/version").then(function(r){return r.json()}).then(function(d){
|
||
if(d.v!=_v){_v=d.v;
|
||
var ov2=document.getElementById("mb-overlay");
|
||
if(ov2&&ov2.style.display==="block")return;
|
||
if(_v>=0)fetch("/body").then(function(r){return r.text()}).then(function(h){
|
||
var ov3=document.getElementById("mb-overlay");
|
||
if(ov3&&ov3.style.display==="block")return;
|
||
var e=document.getElementById("mb-content");
|
||
if(e){e.innerHTML=h;mermaid.run()}})}})
|
||
},500)
|
||
|
||
setInterval(function(){
|
||
fetch("/status").then(function(r){return r.json()}).then(function(d){
|
||
var s="";d.id&&(s+="["+d.id+"] ");d.step&&(s+="Step "+d.step+"/"+d.total);d.label&&(s+=" "+d.label);
|
||
var e=document.getElementById("mbsi");if(e)e.textContent=s})
|
||
},1000)
|
||
|
||
fetch("/volume").then(function(r){return r.json()}).then(function(d){
|
||
var e=document.getElementById("mbvlvl");if(e){e.textContent=d.level}
|
||
})
|
||
fetch("/labels").then(function(r){return r.json()}).then(function(d){
|
||
var s=document.getElementById("mbstep");
|
||
d.forEach(function(x){var o=document.createElement("option");o.value=x.num;o.text=x.num+". "+x.label;s.appendChild(o)})
|
||
})
|
||
|
||
// ═══════════════ FILE TREE PANEL ═══════════════
|
||
var _tv=false, _tm="tree", _td=null, _tree_user=null;
|
||
|
||
function toggleTree(){
|
||
var token=localStorage.getItem('tree_token');
|
||
var savedUser=localStorage.getItem('tree_user');
|
||
|
||
if(token && savedUser){
|
||
// Verify token validity
|
||
fetch('/api/v2/auth/verify',{
|
||
headers:{'Authorization':'Bearer '+token}
|
||
})
|
||
.then(function(r){return r.json()})
|
||
.then(function(d){
|
||
if(d.ok && d.user_id===savedUser){
|
||
// Token valid, open tree
|
||
_tree_user=savedUser;
|
||
_tv=!_tv;
|
||
document.getElementById("mb-tree-panel").classList.toggle("active",_tv);
|
||
if(_tv)loadTree();
|
||
}else{
|
||
// Token invalid or user mismatch, clear and show login
|
||
localStorage.removeItem('tree_token');
|
||
localStorage.removeItem('tree_user');
|
||
showTreeLoginModal();
|
||
}
|
||
})
|
||
.catch(function(e){
|
||
localStorage.removeItem('tree_token');
|
||
localStorage.removeItem('tree_user');
|
||
showTreeLoginModal();
|
||
});
|
||
}else{
|
||
// No token, show login
|
||
showTreeLoginModal();
|
||
}
|
||
}
|
||
|
||
function showTreeLoginModal(){
|
||
var m=document.getElementById('mb-tree-login-modal');
|
||
|
||
if(!m){
|
||
m=document.createElement('div');
|
||
m.id='mb-tree-login-modal';
|
||
m.style.cssText='display:none;position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:#1e293b;border:1px solid #334155;padding:24px;border-radius:8px;z-index:10000;min-width:320px';
|
||
m.innerHTML='<button onclick="this.parentElement.style.display=\'none\';this.parentElement.classList.remove(\'active\')" style="position:absolute;top:12px;right:12px;background:none;border:none;color:#64748b;font-size:18px;cursor:pointer">✕</button>'+
|
||
'<div style="color:#60a5fa;font-size:16px;font-weight:600;margin-bottom:16px">File Tree Authentication</div>'+
|
||
'<div style="margin-bottom:12px">'+
|
||
'<label style="color:#94a3b8;font-size:13px;display:block;margin-bottom:4px">User ID</label>'+
|
||
'<input style="background:#0f172a;border:1px solid #60a5fa;border-radius:4px;color:#e2e8f0;padding:8px 12px;width:100%;font-size:13px" type=text id=tree-user placeholder="Enter user ID (e.g., demo)">'+
|
||
'</div>'+
|
||
'<div style="margin-bottom:12px;position:relative">'+
|
||
'<label style="color:#94a3b8;font-size:13px;display:block;margin-bottom:4px">Password</label>'+
|
||
'<input style="background:#0f172a;border:1px solid #60a5fa;border-radius:4px;color:#e2e8f0;padding:8px 12px;width:100%;font-size:13px;padding-right:36px" type=password id=tree-password placeholder="Enter password" onkeypress=handleTreeKeyPress(event)>'+
|
||
'<button onclick=toggleTreePassword() style="position:absolute;right:8px;top:28px;transform:translateY(-50%);background:none;border:none;color:#60a5fa;cursor:pointer;font-size:14px">👁</button>'+
|
||
'</div>'+
|
||
'<button onclick=submitTreeLogin() style="background:#064e3b;border:1px solid #4ade80;color:#4ade80;padding:8px 16px;border-radius:4px;cursor:pointer;width:100%;font-size:13px">Login</button>'+
|
||
'<div id=tree-error style="color:#ef4444;font-size:12px;margin-top:8px"></div>';
|
||
document.body.appendChild(m);
|
||
}
|
||
|
||
document.getElementById('tree-user').value='';
|
||
document.getElementById('tree-password').value='';
|
||
document.getElementById('tree-password').type='password';
|
||
document.getElementById('tree-error').textContent='';
|
||
m.classList.add('active');
|
||
m.style.display='block';
|
||
document.getElementById('tree-user').focus();
|
||
}
|
||
|
||
function handleTreeKeyPress(e){
|
||
if(e.key==='Enter'||e.keyCode===13){
|
||
submitTreeLogin();
|
||
}
|
||
}
|
||
|
||
function logoutTree(){
|
||
// Clear tree authentication data
|
||
localStorage.removeItem('tree_token');
|
||
localStorage.removeItem('tree_user');
|
||
localStorage.removeItem('tree_locked');
|
||
|
||
// Reset tree data
|
||
_td=null;
|
||
_tree_user=null;
|
||
|
||
// Close tree panel
|
||
document.getElementById("mb-tree-panel").classList.remove("active");
|
||
|
||
// Show login modal
|
||
showTreeLoginModal();
|
||
|
||
// Show toast
|
||
toast('Tree logout ✓');
|
||
}
|
||
|
||
function toggleTreePassword(){
|
||
var pwdInput=document.getElementById('tree-password');
|
||
var toggleBtn=pwdInput.parentElement.querySelector('button');
|
||
|
||
if(pwdInput.type==='password'){
|
||
pwdInput.type='text';
|
||
toggleBtn.textContent='🙈';
|
||
}else{
|
||
pwdInput.type='password';
|
||
toggleBtn.textContent='👁';
|
||
}
|
||
}
|
||
|
||
function submitTreeLogin(){
|
||
var user=document.getElementById('tree-user').value.trim();
|
||
var pwd=document.getElementById('tree-password').value;
|
||
|
||
if(!user){
|
||
document.getElementById('tree-error').textContent='User ID required';
|
||
return;
|
||
}
|
||
if(!pwd){
|
||
document.getElementById('tree-error').textContent='Password required';
|
||
return;
|
||
}
|
||
|
||
fetch('/api/v2/auth/login',{
|
||
method:'POST',
|
||
headers:{'Content-Type':'application/json'},
|
||
body:JSON.stringify({username:user,password:pwd})
|
||
})
|
||
.then(function(r){return r.json()})
|
||
.then(function(d){
|
||
if(d.token){
|
||
localStorage.setItem('tree_token',d.token);
|
||
localStorage.setItem('tree_user',user);
|
||
_tree_user=user;
|
||
document.getElementById('mb-tree-login-modal').style.display='none';
|
||
toast('Logged in as '+user+' ✓');
|
||
_tv=true;
|
||
document.getElementById("mb-tree-panel").classList.add("active");
|
||
loadTree();
|
||
}else{
|
||
document.getElementById('tree-error').textContent=d.error||'Login failed';
|
||
document.getElementById('tree-password').value='';
|
||
document.getElementById('tree-password').focus();
|
||
}
|
||
})
|
||
.catch(function(e){
|
||
document.getElementById('tree-error').textContent='Connection error: '+e;
|
||
});
|
||
}
|
||
|
||
function loadTree(){
|
||
var b=document.getElementById("mb-tree-body");
|
||
if(!b)return;
|
||
b.innerHTML="<div style=text-align:center;padding:40px;color:#64748b>Loading...</div>";
|
||
|
||
var token=localStorage.getItem('tree_token');
|
||
var user=_tree_user||localStorage.getItem('tree_user')||'demo';
|
||
|
||
fetch("/api/v2/tree/"+user+"?mode="+_tm,{
|
||
headers:{'Authorization':'Bearer '+token}
|
||
}).then(function(r){return r.json()}).then(function(d){
|
||
_td=d;
|
||
_td=d;
|
||
var h="";
|
||
// Mode buttons
|
||
var modes=[{k:"tree",i:"🌳",l:"Tree"},{k:"list",i:"📋",l:"List"},{k:"grid_sm",i:"🟦",l:"Icons"},{k:"grid_lg",i:"🔲",l:"Large"}];
|
||
h+="<div class=mb-mode-bar>";
|
||
modes.forEach(function(m){
|
||
h+="<button class=mb-mode-btn"+(_tm==m.k?" active":"")+" onclick='changeMode(\""+m.k+"\")'>";
|
||
h+="<span>"+m.i+"</span>"+m.l+"</button>";
|
||
});
|
||
h+="<span style=flex:1></span>";
|
||
h+="<button class=mb-lock-btn id=mb-lock-icon onclick=toggleLock() title='Toggle edit lock'>"+(_locked?"🔒":"🔓")+"</button>";
|
||
if(!_locked){
|
||
h+="<button class=mb-new-folder-btn onclick='document.getElementById(\"mb-file-input\").click()' style='background:#064e3b;border-color:#4ade80;color:#4ade80'>📤 Upload</button>";
|
||
h+="<input type=file id=mb-file-input style=display:none onchange=uploadFile(this)>";
|
||
}
|
||
if(!_locked){
|
||
h+="<button class=mb-new-folder-btn onclick=organizeTree() style='background:#0c4a6e;border-color:#38bdf8;color:#38bdf8'>⚡ Agent</button>";
|
||
}
|
||
h+="<button class=mb-new-folder-btn onclick=newFolder()>+ Folder</button>";
|
||
if(!_locked){
|
||
h+="<button class=mb-new-folder-btn onclick=restoreTree() style='background:#1e3a5f;border-color:#3b82f6;color:#93c5fd'>↻ Restore</button>";
|
||
h+="<button class=mb-new-folder-btn onclick=findDupes() style='background:#451a03;color:#fbbf24;border-color:#b45309'>🔍 Dupes</button>";
|
||
h+="<button class=mb-new-folder-btn onclick=deleteAll() style='background:#451a03;color:#fbbf24;border-color:#b45309'>✕ All</button>";
|
||
h+="<button class=mb-new-folder-btn onclick=logoutTree() style='background:#7f1d1d;color:#fca5a5;border-color:#dc2626'>🚪 Logout</button>";
|
||
}
|
||
h+="<span style=color:#64748b;font-size:12px;align-self:center>"+d.nodes.length+" nodes</span></div>";
|
||
|
||
if(_tm=="tree")h+=renderTree(d);
|
||
else if(_tm=="list")h+=renderList(d);
|
||
else h+=renderGrid(d,_tm);
|
||
b.innerHTML=h;
|
||
}).catch(function(e){
|
||
b.innerHTML="<div style=padding:20px;color:#ef4444>Failed to load tree: "+e+"</div>";
|
||
});
|
||
}
|
||
|
||
function changeMode(m){
|
||
_tm=m;localStorage.setItem("display_mode",m);
|
||
loadTree();
|
||
}
|
||
|
||
function dname(n){
|
||
var a=n.aliases||{};
|
||
for(var k in a){if(a.hasOwnProperty(k)&&a[k])return a[k];}
|
||
return n.label;
|
||
}
|
||
|
||
function fsize(b){
|
||
if(!b&&b!==0)return "-";
|
||
var s=b,i=0,u=["B","KB","MB","GB"];
|
||
while(s>=1024&&i<3){s/=1024;i++;}
|
||
return (i==0?s:s.toFixed(1))+" "+u[i];
|
||
}
|
||
|
||
// TREE MODE
|
||
var _clk=0;
|
||
function tgl(id){
|
||
var el=document.getElementById(id);
|
||
if(el){var v=el.style.display=="none";el.style.display=v?"":"none";
|
||
var c=document.getElementById("cr"+id);if(c)c.textContent=v?"▼":"▶";}
|
||
}
|
||
function renderTree(d){
|
||
var ch={};
|
||
d.nodes.forEach(function(n){var p=n.parent_id||"root";if(!ch[p])ch[p]=[];ch[p].push(n);});
|
||
function rc(pid,ind){
|
||
var lst=ch[pid];if(!lst||!lst.length)return"";
|
||
var h="";
|
||
lst.sort(function(a,b){if(a.node_type!=b.node_type)return a.node_type=="folder"?-1:1;return a.label.localeCompare(b.label);});
|
||
lst.forEach(function(n){
|
||
if(n.node_type=="folder"){
|
||
_clk++;var cid="tc"+_clk;var nid=n.node_id;
|
||
h+="<div class=mb-tree-node style=padding-left:"+(ind*20)+"px>";
|
||
h+="<span class=mb-tree-caret id=cr"+cid+" onclick='tgl(\""+cid+"\")'>"+(ind==0?"▼":"▶")+"</span>";
|
||
h+="<span class=mb-tree-label ondblclick='renameNode(\""+nid+"\")'>"+(n.icon||"📁")+" <b id=flb"+nid+">"+dname(n)+"</b></span>";
|
||
h+="<span class=mb-folder-actions>";
|
||
h+="<button class=mb-folder-btn onclick='pickIcon(\""+nid+"\")'>🎨</button>";
|
||
h+="<button class=mb-folder-btn onclick='renameNode(\""+nid+"\")'>✏️</button>";
|
||
h+="<button class=mb-folder-btn onclick='moveNode(\""+nid+"\")'>📦</button>";
|
||
h+="<button class=mb-folder-btn danger onclick=delNode(\""+nid+"\",\""+dname(n)+"\")>🗑</button></span>";;
|
||
if(ch[n.node_id]&&ch[n.node_id].length){
|
||
h+="<div id="+cid+" style=display:"+(ind==0?"block":"none")+">";
|
||
h+=rc(n.node_id,ind+1)+"</div>";
|
||
}
|
||
}else{
|
||
var nid=n.node_id;
|
||
h+="<div class=mb-tree-node style=padding-left:"+(ind*20+18)+"px>";
|
||
h+="<span class=mb-tree-label ondblclick='renameNode(\""+nid+"\")'>";
|
||
h+="<span class=mb-tree-file onclick='showDetail(\""+(n.file_uuid||"")+"\")'>";
|
||
h+=(n.icon||"📄")+" <b id=flb"+nid+">"+dname(n)+"</b></span>";
|
||
h+="<span class=mb-tree-meta>"+fsize(n.file_size)+"</span>";
|
||
h+="<span class=mb-folder-btn onclick='quickPreview(\""+(n.file_uuid||"")+"\")' title='Preview' style='display:inline-block;margin-left:4px;font-size:11px'>👁</span></span>";
|
||
h+="<span class=mb-folder-actions>";
|
||
h+="<button class=mb-folder-btn onclick='pickIcon(\""+nid+"\")'>🎨</button>";
|
||
h+="<button class=mb-folder-btn onclick='renameNode(\""+nid+"\")'>✏️</button>";
|
||
h+="<button class=mb-folder-btn onclick='moveNode(\""+nid+"\")'>📦</button>";
|
||
h+="<button class=mb-folder-btn danger onclick=delNode(\""+nid+"\",\""+dname(n)+"\")>🗑</button></span>";;
|
||
}
|
||
h+="</div>";
|
||
});
|
||
return h;
|
||
}
|
||
return rc("root",0);
|
||
}
|
||
|
||
function renderList(d){
|
||
var h="<table style=width:100%><thead><tr><th>Name</th><th>file_uuid</th><th>Size</th><th>Type</th></tr></thead><tbody>";
|
||
d.nodes.forEach(function(n){
|
||
var icon=n.icon||(n.node_type=="folder"?"📁":"📄");
|
||
var badge=n.node_type=="folder"?"<span style=color:#fbbf24>folder</span>":"<span style=color:#4ade80>file</span>";
|
||
h+="<tr onclick='"+(n.file_uuid?"showDetail(\""+n.file_uuid+"\")":"")+"' style=cursor:"+(n.file_uuid?"pointer":"default")+">";
|
||
h+="<td>"+icon+" "+dname(n)+"</td>";
|
||
h+="<td><code>"+(n.file_uuid||"-")+"</code></td>";
|
||
h+="<td>"+fsize(n.file_size)+"</td><td>"+badge+"</td></tr>";
|
||
});
|
||
h+="</tbody></table>";return h;
|
||
}
|
||
|
||
function renderGrid(d,mode){
|
||
var cls=mode=="grid_sm"?"sm":"lg";
|
||
var h="<div class='mb-grid "+cls+"'>";
|
||
d.nodes.forEach(function(n){
|
||
var icon=n.icon||(n.node_type=="folder"?"📁":"📄");
|
||
h+="<div class=mb-grid-cell onclick='"+(n.file_uuid?"showDetail(\""+n.file_uuid+"\")":"")+"'>";
|
||
h+="<div class=mb-grid-icon>"+icon+"</div>";
|
||
h+="<div class=mb-grid-label>"+dname(n)+"</div>";
|
||
if(n.file_uuid)h+="<div class=mb-grid-uuid>"+n.file_uuid+"</div>";
|
||
h+="</div>";
|
||
});
|
||
h+="</div>";return h;
|
||
}
|
||
|
||
// DETAIL PANEL
|
||
function showDetail(fuuid){
|
||
if(!fuuid)return;
|
||
var userId=localStorage.getItem("tree_user")||"demo";
|
||
document.getElementById("mb-overlay").style.display="block";
|
||
document.getElementById("mb-detail").style.display="block";
|
||
var b=document.getElementById("mb-detail-body");
|
||
b.innerHTML="<div style=text-align:center;padding:30px;color:#64748b>Loading...</div>";
|
||
fetch("/api/v2/files/"+userId+"/"+fuuid+"/info").then(function(r){return r.json()}).then(function(d){
|
||
var node=_td&&_td.nodes?_td.nodes.find(function(n){return n.file_uuid==fuuid}):null;
|
||
var label=node?dname(node):fuuid;
|
||
var sz=node?fsize(node.file_size):"-";
|
||
var sha=node?(node.sha256||"-").substring(0,16)+"...":"-";
|
||
var reg=node?(node.registered_at||"-").substring(0,10):"-";
|
||
var h="<h2 style=border:none;margin-bottom:16px>📄 "+label+"</h2>";
|
||
h+="<table style=width:100%><tr><th>file_uuid</th><td><code>"+d.file_uuid+"</code></td></tr>";
|
||
h+="<tr><th>SHA256</th><td><code>"+sha+"</code></td></tr>";
|
||
h+="<tr><th>Size</th><td>"+sz+"</td></tr>";
|
||
h+="<tr><th>Registered</th><td>"+reg+"</td></tr></table>";
|
||
h+="<h3>🌳 Virtual Paths</h3>";
|
||
if(d.virtual_paths&&d.virtual_paths.length) d.virtual_paths.forEach(function(vp){h+="<div>📁 "+vp+"</div>"});
|
||
else h+="<div style=color:#64748b>—</div>";
|
||
h+="<h3>💾 Real Locations ("+d.location_count+")</h3>";
|
||
if(d.real_locations&&d.real_locations.length){
|
||
d.real_locations.forEach(function(loc){
|
||
var lbl=loc.label||"",pth=loc.path||"";
|
||
var tier="hot";
|
||
if(lbl.indexOf("warm")>=0||lbl.indexOf("nas")>=0) tier="warm";
|
||
if(lbl.indexOf("cold")>=0||lbl.indexOf("lto")>=0||lbl.indexOf("glacier")>=0) tier="cold";
|
||
if(lbl.indexOf("s3")>=0||lbl.indexOf("cdn")>=0||lbl.indexOf("ipfs")>=0) tier="cloud";
|
||
h+="<div><span class=mb-loc-tag "+tier+">"+lbl+"</span><code style=font-size:11px>"+pth.substring(0,80)+"</code></div>";
|
||
});
|
||
}else h+="<div style=color:#64748b>—</div>";
|
||
h+="<h3>🔍 Probe Data <span id=mb-probe-status style=font-size:11px;color:#64748b>loading...</span></h3><div id=mb-probe-data style=color:#64748b>Loading...</div>";
|
||
h+="<h3>🖼️ Preview <span style=font-size:12px;color:#94a3b8>"+label+"</span> <span id=mb-preview-res style=font-size:12px;color:#64748b></span></h3><div style=margin-top:8px;position:relative;display:flex;align-items:center;gap:8px>";
|
||
var userId=localStorage.getItem("tree_user")||"demo";
|
||
var src="/api/v2/files/"+userId+"/"+fuuid+"/stream";
|
||
var ext=(label||"").split(".").pop().toLowerCase();
|
||
var isVideo=(ext=="mp4"||ext=="mov"||ext=="avi"||ext=="webm"||ext=="mkv");
|
||
var isTxt=(ext=="txt"||ext=="log"||ext=="csv"||ext=="json"||ext=="xml");
|
||
var isMd=(ext=="md"||ext=="markdown");
|
||
var isDocText=(ext=="docx"||ext=="doc"||ext=="rtf");
|
||
var isDocImg=(ext=="pages"||ext=="key"||ext=="numbers");
|
||
var isDocPdf=(ext=="pdf"||ext=="pptx"||ext=="xlsx"||ext=="odt"||ext=="xls"||ext=="ppt"||ext=="epub"||ext=="html");
|
||
var isDoc=isDocText||isDocImg||isDocPdf;
|
||
if(isVideo){
|
||
h+="<video controls style='max-width:100%;max-height:360px;border-radius:8px;background:#0f172a'><source src='"+src+"'></video>";
|
||
}else if(isTxt||isDocText){
|
||
h+="<pre id=mb-detail-txt style='max-height:300px;overflow:auto;background:#0f172a;color:#e2e8f0;padding:12px;border-radius:8px;font-size:13px;white-space:pre-wrap;width:100%'>Loading...</pre>";
|
||
}else if(isMd){
|
||
h+="<div id=mb-md-render style='max-height:400px;overflow:auto;background:#0f172a;padding:12px;border-radius:8px'>Loading...</div>";
|
||
}else if(isDocImg){
|
||
h+="<img id=mb-preview-img src='"+src+"' style='max-width:100%;max-height:400px;border-radius:8px' onerror=\"this.onerror=null;this.alt='No preview'\">";
|
||
}else if(isDocPdf){
|
||
h+="<iframe sandbox='allow-same-origin' src='"+src+"' style='width:100%;height:400px;border:none;border-radius:8px;background:#fff'></iframe>";
|
||
}else{
|
||
h+="<button id=mb-prev-btn onclick=navigatePhoto('prev') style='background:#1e293b;border:1px solid #334155;color:#94a3b8;font-size:24px;padding:8px 14px;border-radius:6px;cursor:pointer'>◀</button>";
|
||
h+="<img id=mb-preview-img src='"+src+"' style='max-width:100%;max-height:400px;min-height:100px;min-width:100px;border-radius:8px;background:#0f172a' onerror=\"this.onerror=null;this.alt='No preview'\">";
|
||
h+="<button id=mb-next-btn onclick=navigatePhoto('next') style='background:#1e293b;border:1px solid #334155;color:#94a3b8;font-size:24px;padding:8px 14px;border-radius:6px;cursor:pointer'>▶</button>";
|
||
h+="<span id=mb-photo-pos style='position:absolute;bottom:8px;right:50px;background:rgba(0,0,0,.7);color:#94a3b8;padding:2px 8px;border-radius:4px;font-size:11px'>1/1</span>";
|
||
}
|
||
h+="</div>";
|
||
b.innerHTML=h;
|
||
if(!isVideo&&!isTxt&&!isMd&&!isDocText&&!isDocImg&&!isDocPdf){_photoUuid=fuuid;setupPhotoNav(fuuid)}
|
||
if(isTxt||isDocText) fetch(src).then(function(r){return r.text()}).then(function(t){
|
||
var el=document.getElementById("mb-detail-txt");if(el)el.textContent=t||"(empty)";
|
||
});
|
||
if(isMd) fetch("/api/v2/render/"+fuuid+"/body").then(function(r){return r.text()}).then(function(h){
|
||
var el=document.getElementById("mb-md-render");if(el){el.innerHTML=h;setTimeout(function(){
|
||
var nodes=el.querySelectorAll(".mermaid");if(nodes.length)mermaid.run({nodes:Array.from(nodes)})
|
||
},100)}
|
||
});
|
||
fetch("/api/v2/files/"+userId+"/"+fuuid+"/probe").then(function(r){return r.json()}).then(function(p){
|
||
var pd=document.getElementById("mb-probe-data");
|
||
var ps=document.getElementById("mb-probe-status");
|
||
var pr=document.getElementById("mb-preview-res");
|
||
if(p.width&&p.height)pr.textContent="("+p.width+"×"+p.height+")";
|
||
else pr.textContent="";
|
||
if(p.probe){
|
||
ps.textContent="";
|
||
var ph="<table style=width:100%>";
|
||
if(p.duration)ph+="<tr><th>Duration</th><td>"+(p.duration).toFixed(1)+"s ("+Math.floor(p.duration/60)+"min "+(p.duration%60).toFixed(0)+"s)</td></tr>";
|
||
if(p.width&&p.height)ph+="<tr><th>Resolution</th><td>"+p.width+"×"+p.height+"</td></tr>";
|
||
if(p.fps)ph+="<tr><th>FPS</th><td>"+p.fps+"</td></tr>";
|
||
if(p.file_type)ph+="<tr><th>Codec</th><td>"+p.file_type+"</td></tr>";
|
||
if(p.total_frames)ph+="<tr><th>Total Frames</th><td>"+p.total_frames+"</td></tr>";
|
||
if(p.probe.format){
|
||
var fmt=p.probe.format;
|
||
if(fmt.format_name)ph+="<tr><th>Format</th><td>"+fmt.format_name+"</td></tr>";
|
||
if(fmt.size)ph+="<tr><th>Probe Size</th><td>"+fmt.size+"</td></tr>";
|
||
if(fmt.bit_rate)ph+="<tr><th>Bitrate</th><td>"+(fmt.bit_rate/1000000).toFixed(1)+" Mbps</td></tr>";
|
||
}
|
||
if(p.probe.streams){
|
||
ph+="<tr><th>Streams</th><td>";
|
||
p.probe.streams.forEach(function(s){ph+=" • "+s.codec_type+": "+s.codec_name+"<br>"});
|
||
ph+="</td></tr>";
|
||
}
|
||
ph+="</table>";pd.innerHTML=ph;
|
||
}else{
|
||
ps.textContent="(not available)";
|
||
pd.innerHTML="<div style=color:#64748b>No probe data for this file</div>";
|
||
}
|
||
}).catch(function(){document.getElementById("mb-probe-status").textContent="(error)"});
|
||
}).catch(function(e){b.innerHTML="<div style=color:#ef4444>Error: "+e+"</div>"});
|
||
}
|
||
|
||
function closeDetail(){
|
||
document.getElementById("mb-overlay").style.display="none";
|
||
document.getElementById("mb-detail").style.display="none";
|
||
}
|
||
|
||
// PHOTO NAVIGATION
|
||
var _photoUuid=null,_photoList=[];
|
||
|
||
function setupPhotoNav(fuuid){
|
||
if(!_td) return;
|
||
var imgs=["jpg","jpeg","png","gif","bmp","webp","tiff","svg"];
|
||
_photoList=_td.nodes.filter(function(n){
|
||
if(!n.file_uuid||n.node_type!="file")return false;
|
||
var e=(n.label||"").split(".").pop().toLowerCase();
|
||
return imgs.indexOf(e)>=0;
|
||
}).map(function(n){return n.file_uuid});
|
||
updatePhotoPos(fuuid);
|
||
}
|
||
|
||
function updatePhotoPos(fuuid){
|
||
_photoUuid=fuuid;
|
||
var idx=_photoList.indexOf(fuuid);
|
||
var pos=document.getElementById("mb-photo-pos");
|
||
if(pos)pos.textContent=(idx+1)+"/"+_photoList.length;
|
||
var prev=document.getElementById("mb-prev-btn");
|
||
var next=document.getElementById("mb-next-btn");
|
||
if(prev)prev.style.visibility=idx>0?"visible":"hidden";
|
||
if(next)next.style.visibility=idx<_photoList.length-1?"visible":"hidden";
|
||
}
|
||
|
||
function navigatePhoto(dir){
|
||
var idx=_photoList.indexOf(_photoUuid);
|
||
if(dir=="prev"&&idx>0)idx--;else if(dir=="next"&&idx<_photoList.length-1)idx++;
|
||
if(idx>=0&&idx<_photoList.length){
|
||
_photoUuid=_photoList[idx];
|
||
var img=document.getElementById("mb-preview-img");
|
||
if(img)img.src="/api/v2/files/"+_photoUuid+"/stream?_="+Date.now();
|
||
updatePhotoPos(_photoUuid);
|
||
}
|
||
}
|
||
|
||
// TOAST
|
||
function toast(msg){
|
||
var t=document.createElement("div");t.className="mb-toast";t.textContent=msg;
|
||
document.body.appendChild(t);
|
||
setTimeout(function(){t.style.opacity="0";setTimeout(function(){t.remove()},300)},1500);
|
||
}
|
||
|
||
// ICON PICKER
|
||
var ICONS=["📁","📂","📄","🎬","🎵","🖼️","📊","📝","📦","📸","🎨","📹","🎧","📚","🔧","⚙️","🌐","💾","📀","💿","🏠","🏢","🌟","⭐","💡","🔥","❤️","💚","💙","💛","🧡","💜","✅","❌","⚠️","🔒","🔓","🔑","📡","🔗"];
|
||
var _icoPicked=null,_icoNode=null;
|
||
|
||
function pickIcon(nid){
|
||
_icoPicked=null;_icoNode=nid;
|
||
var o=document.createElement("div");
|
||
o.style.cssText="position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.7);z-index:10002;display:flex;align-items:center;justify-content:center";
|
||
var bx=document.createElement("div");
|
||
bx.style.cssText="background:#1e293b;border:1px solid #334155;border-radius:12px;padding:24px;min-width:380px;max-width:440px";
|
||
var h="<h3>🎨 Pick Icon</h3><div class=mb-icon-picker id=mb-ipg>";
|
||
ICONS.forEach(function(ico,idx){h+="<button id=ipi"+idx+" onclick='selIcon("+idx+",\""+ico+"\")'>"+ico+"</button>"});
|
||
h+="</div>Custom: <input id=mbci placeholder='emoji or SVG url' style='width:100%;background:#0f172a;border:1px solid #334155;border-radius:6px;color:#e2e8f0;padding:6px 10px;font-size:18px;margin-top:8px' oninput='_icoPicked=document.getElementById(\"mbci\").value'>";
|
||
h+="<div style=margin-top:10px;display:flex;gap:6px;justify-content:flex-end>";
|
||
h+="<button id=idef style='background:#451a03;border:none;color:#fbbf24;padding:4px 14px;border-radius:6px;cursor:pointer'>Default</button>";
|
||
h+="<button id=icxl style='background:#334155;border:none;color:#94a3b8;padding:4px 14px;border-radius:6px;cursor:pointer'>Cancel</button>";
|
||
h+="<button id=iok style='background:#064e3b;border:none;color:#4ade80;padding:4px 14px;border-radius:6px;cursor:pointer'>OK</button>";
|
||
h+="</div>";
|
||
bx.innerHTML=h;o.appendChild(bx);document.body.appendChild(o);
|
||
document.getElementById("iok").onclick=function(){if(_icoPicked)applyIcon(_icoNode,_icoPicked);o.remove()};
|
||
document.getElementById("idef").onclick=function(){applyIcon(_icoNode,"");o.remove()};
|
||
document.getElementById("icxl").onclick=function(){o.remove()};
|
||
o.onclick=function(e){if(e.target==o)o.remove()};
|
||
}
|
||
|
||
function selIcon(idx,ico){
|
||
_icoPicked=ico;
|
||
ICONS.forEach(function(_,i){var el=document.getElementById("ipi"+i);if(el)el.style.border=i==idx?"2px solid #4ade80":"2px solid transparent"});
|
||
}
|
||
|
||
function applyIcon(nid,ico){
|
||
var token=localStorage.getItem('tree_token');
|
||
var user=_tree_user||localStorage.getItem('tree_user')||'demo';
|
||
fetch("/api/v2/tree/"+user+"/node/"+nid,{method:"PUT",headers:{"Content-Type":"application/json","Authorization":"Bearer "+token},
|
||
body:JSON.stringify({icon:ico})})
|
||
.then(function(r){return r.json()}).then(function(){
|
||
loadTree();toast(ico?"Icon → "+ico:"Icon reset to default");
|
||
});
|
||
}
|
||
|
||
// AGENT ORGANIZE
|
||
function organizeTree(){
|
||
if(!_td){toast("Load tree first");return}
|
||
var folders={};
|
||
_td.nodes.forEach(function(n){if(n.node_type=="folder")folders[n.label]=n.node_id});
|
||
var cats=["Movies","Marketing","Cartoons","Other"];
|
||
var targets={};
|
||
cats.forEach(function(c){if(folders[c])targets[c]=folders[c]});
|
||
if(Object.keys(targets).length<2){toast("Need at least 2 category folders");return}
|
||
|
||
var files=_td.nodes.filter(function(n){
|
||
if(n.node_type!="file"||!n.parent_id)return false;
|
||
var inCat=false;
|
||
for(var k in targets){if(n.parent_id==targets[k]){inCat=true;break}}
|
||
return !inCat;
|
||
});
|
||
if(!files.length){toast("All files already organized ✓");return}
|
||
|
||
var token=localStorage.getItem('tree_token');
|
||
var user=_tree_user||localStorage.getItem('tree_user')||'demo';
|
||
var mover=function(idx){
|
||
if(idx>=files.length){loadTree();toast("Agent: "+files.length+" files organized");return}
|
||
var f=files[idx];
|
||
var nl=(f.label||"").toLowerCase();
|
||
var t="Other";
|
||
if(/charade|film|clip|movie|comedy|filmriot/.test(nl))t="Movies";
|
||
else if(/exasan|gamma|thunderbolt|nab|koba|webinar|top colorist|accusys|a12t3/.test(nl))t="Marketing";
|
||
else if(/cartoon|alice|felix|disney|steamboat|animal/.test(nl))t="Cartoons";
|
||
var pid=targets[t];
|
||
if(!pid){mover(idx+1);return}
|
||
fetch("/api/v2/tree/"+user+"/node/"+f.node_id+"/move",{method:"PUT",headers:{"Content-Type":"application/json","Authorization":"Bearer "+token},
|
||
body:JSON.stringify({parent_id:pid})})
|
||
.then(function(r){return r.json()}).then(function(){mover(idx+1);})
|
||
.catch(function(){mover(idx+1);});
|
||
};
|
||
mover(0);
|
||
}
|
||
|
||
// DUPLICATE FINDER
|
||
function findDupes(){
|
||
var o=document.createElement("div");
|
||
o.style.cssText="position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.7);z-index:10002;display:flex;align-items:center;justify-content:center";
|
||
var bx=document.createElement("div");
|
||
bx.style.cssText="background:#1e293b;border:1px solid #334155;border-radius:12px;padding:24px;min-width:500px;max-width:700px;max-height:80vh;overflow-y:auto";
|
||
bx.innerHTML="<h3>🔍 Scanning for duplicates...</h3>";
|
||
o.appendChild(bx);document.body.appendChild(o);
|
||
o.onclick=function(e){if(e.target==o)o.remove()};
|
||
|
||
fetch("/api/v2/dupes/demo").then(function(r){return r.json()}).then(function(d){
|
||
if(!d.dupes||!d.dupes.length){
|
||
bx.innerHTML="<h3>✅ No Duplicates Found</h3><button onclick=this.parentElement.parentElement.remove() style='margin-top:12px;background:#334155;border:none;color:#94a3b8;padding:6px 16px;border-radius:6px;cursor:pointer'>Close</button>";
|
||
return;
|
||
}
|
||
var h="<h3>🔍 Duplicates ("+d.dup_groups+" groups)</h3>";
|
||
d.dupes.forEach(function(g,i){
|
||
h+="<div style='margin:10px 0;padding:10px;background:#0f172a;border-radius:8px'>";
|
||
h+="<b>"+(i+1)+". "+g.file_name+"</b> ("+g.count+"x)<br>";
|
||
g.uuids.forEach(function(uuid,j){
|
||
var st=g.statuses[j]||"?";
|
||
var badge=st=="completed"?"background:#064e3b;color:#4ade80":st=="pending"?"background:#451a03;color:#fbbf24":"background:#1e293b;color:#64748b";
|
||
h+="<div style='display:flex;align-items:center;gap:8px;margin:4px 0;padding:4px 8px;background:#1e293b;border-radius:4px'>";
|
||
h+="<code style='flex:1;font-size:11px'>"+uuid+"</code>";
|
||
h+="<span style='padding:1px 6px;border-radius:3px;font-size:10px;"+badge+"'>"+st+"</span>";
|
||
h+="<button onclick=unregDup('"+uuid+"') style='background:#451a03;border:none;color:#fbbf24;padding:2px 8px;border-radius:4px;cursor:pointer;font-size:11px'>✕</button>";
|
||
h+="</div>";
|
||
});
|
||
h+="</div>";
|
||
});
|
||
h+="<div style=margin-top:12px;display:flex;gap:6px;justify-content:flex-end><button onclick=this.parentElement.parentElement.parentElement.remove() style='background:#334155;border:none;color:#94a3b8;padding:4px 14px;border-radius:6px;cursor:pointer'>Close</button><button onclick='afterUnreg()' style='background:#1e3a5f;border:none;color:#93c5fd;padding:4px 14px;border-radius:6px;cursor:pointer'>Restore Tree</button></div>";
|
||
bx.innerHTML=h;
|
||
});
|
||
}
|
||
|
||
function unregDup(uuid){
|
||
if(!confirm("Unregister "+uuid+" ?"))return;
|
||
fetch("/api/v2/unregister/"+uuid,{method:"POST"})
|
||
.then(function(r){return r.json()}).then(function(d){
|
||
toast("Unregistered: "+uuid);
|
||
findDupes(); // re-scan
|
||
});
|
||
}
|
||
|
||
function afterUnreg(){
|
||
document.querySelectorAll("div[style*='z-index:10002']").forEach(function(el){el.remove()});
|
||
restoreTree();
|
||
}
|
||
|
||
function uploadFile(input){
|
||
var file=input.files[0];
|
||
if(!file)return;
|
||
var fd=new FormData();
|
||
fd.append("file",file);
|
||
toast("Uploading "+file.name+"...");
|
||
fetch("/api/v2/upload/demo",{method:"POST",body:fd})
|
||
.then(function(r){return r.json()}).then(function(d){
|
||
if(d.ok){toast("Uploaded: "+d.filename+" ("+fsize(d.size)+")");loadTree()}
|
||
else toast("Upload failed: "+(d.error||"unknown"));
|
||
}).catch(function(e){toast("Upload error: "+e)});
|
||
input.value="";
|
||
}
|
||
|
||
// QUICK PREVIEW
|
||
function quickPreview(fuuid){
|
||
if(!fuuid)return;
|
||
var node=(_td&&_td.nodes)?_td.nodes.find(function(n){return n.file_uuid==fuuid}):null;
|
||
var label=node?node.label:"";
|
||
var ext=(label||"").split(".").pop().toLowerCase();
|
||
var isVideo=ext=="mp4"||ext=="mov"||ext=="avi"||ext=="webm"||ext=="mkv";
|
||
var isTxt=ext=="txt"||ext=="log"||ext=="csv"||ext=="json"||ext=="xml";
|
||
var isMd=(ext=="md"||ext=="markdown");
|
||
var isDocText=(ext=="docx"||ext=="doc"||ext=="rtf");
|
||
var isDocImg=(ext=="pages"||ext=="key"||ext=="numbers");
|
||
var isDocPdf=(ext=="pdf"||ext=="pptx"||ext=="xlsx"||ext=="odt"||ext=="xls"||ext=="ppt"||ext=="epub"||ext=="html");
|
||
var userId=localStorage.getItem("tree_user")||"demo";
|
||
var src="/api/v2/files/"+userId+"/"+fuuid+"/stream";
|
||
|
||
var o=document.createElement("div");
|
||
o.style.cssText="position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.8);z-index:10002;display:flex;align-items:center;justify-content:center";
|
||
var inner="";
|
||
if(isVideo){
|
||
inner="<video controls autoplay style='max-width:90vw;max-height:85vh;border-radius:8px'><source src='"+src+"'></video>";
|
||
}else if(isTxt||isDocText){
|
||
inner="<pre id=mb-txt-preview style='max-width:90vw;max-height:85vh;overflow:auto;background:#0f172a;color:#e2e8f0;padding:24px;border-radius:8px;font-size:14px;white-space:pre-wrap'>Loading...</pre>";
|
||
}else if(isMd){
|
||
inner="<div id=mb-qmd-render style='max-width:90vw;max-height:85vh;overflow:auto;background:#0f172a;padding:24px;border-radius:8px'>Loading...</div>";
|
||
}else if(isDocImg){
|
||
inner="<img src='"+src+"' style='max-width:90vw;max-height:85vh;border-radius:8px' onerror=\"this.onerror=null;this.alt='No preview'\">";
|
||
}else if(isDocPdf){
|
||
inner="<iframe sandbox='allow-same-origin' src='"+src+"' style='width:90vw;height:85vh;border:none;border-radius:8px;background:#fff'></iframe>";
|
||
}else{
|
||
inner="<img src='"+src+"' style='max-width:90vw;max-height:85vh;min-height:100px;min-width:100px;border-radius:8px' onerror=\"this.onerror=null;this.alt='No preview'\">";
|
||
}
|
||
inner+="<div style='position:absolute;top:16px;right:16px'><button onclick=this.parentElement.parentElement.remove() style='background:rgba(0,0,0,.6);border:none;color:#fff;font-size:24px;cursor:pointer;padding:4px 12px;border-radius:6px'>✕</button></div>";
|
||
o.innerHTML=inner;
|
||
document.body.appendChild(o);
|
||
if(isTxt||isDocText) fetch(src).then(function(r){return r.text()}).then(function(t){
|
||
var el=document.getElementById("mb-txt-preview");
|
||
if(el)el.textContent=t||"(empty)";
|
||
});
|
||
if(isMd) fetch("/api/v2/render/"+fuuid+"/body").then(function(r){return r.text()}).then(function(h){
|
||
var el=document.getElementById("mb-qmd-render");if(el){el.innerHTML=h;setTimeout(function(){
|
||
var nodes=el.querySelectorAll(".mermaid");if(nodes.length)mermaid.run({nodes:Array.from(nodes)})
|
||
},100)}
|
||
});
|
||
o.onclick=function(e){if(e.target==o)o.remove()};
|
||
}
|
||
|
||
// LOCK TOGGLE
|
||
var _locked=true;
|
||
function toggleLock(){
|
||
_locked=!_locked;
|
||
localStorage.setItem("tree_locked",_locked?"1":"0");
|
||
document.body.classList.toggle("mb-locked",_locked);
|
||
var icon=document.getElementById("mb-lock-icon");
|
||
if(icon)icon.textContent=_locked?"🔒":"🔓";
|
||
loadTree();
|
||
}
|
||
|
||
// Init
|
||
(function(){
|
||
var s=localStorage.getItem("display_mode");
|
||
if(s&&["tree","list","grid_sm","grid_lg"].indexOf(s)>=0)_tm=s;
|
||
_locked=localStorage.getItem("tree_locked")!=="0";
|
||
document.body.classList.toggle("mb-locked",_locked);
|
||
})();
|
||
</script>
|
||
</body></html> |