/* A <file-browser> custom element that talks to /api/files */
class FileBrowser extends HTMLElement {
constructor ( ) {
super ( ) ;
this . attachShadow ( { mode : "open" } ) ;
this . path = "" ; // current virtual path ("" = ROOT)
this . offset = 0 ; // pagination offset
this . limit = 40 ; // items per request
}
connectedCallback ( ) {
this . path = this . getAttribute ( "path" ) || "" ;
this . renderShell ( ) ;
this . load ( ) ;
}
// ---------- UI scaffolding -------------------------------------------
renderShell ( ) {
this . shadowRoot . innerHTML = `
<style>
:host { display:block; font-family: system-ui, sans-serif; box-sizing: border-box; }
nav { display:flex; flex-wrap:wrap; gap:.5rem; margin:.5rem 0; align-items:center; }
button { padding:.35rem .65rem; border:none; border-radius:4px; background:#f05a28; color:#fff; cursor:pointer; font:inherit; }
button:disabled { background:#999; cursor:not-allowed; }
.crumb { font-weight:600; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(120px,1fr)); gap:1rem; }
.tile { border:1px solid #f05a28; border-radius:8px; padding:.5rem; background:#000000; text-align:center; cursor:pointer; transition:box-shadow .2s ease; }
.tile:hover { box-shadow:0 2px 8px rgba(0,0,0,.1); }
img.thumb { width:100%; height:90px; object-fit:cover; border-radius:6px; }
.icon { font-size:48px; line-height:90px; }
</style>
<nav>
<button id="up">⬅️ Up</button>
<span class="crumb" id="crumb"></span>
</nav>
<div class="grid" id="grid"></div>
<nav>
<button id="prev">Prev</button>
<button id="next">Next</button>
</nav>
` ;
this . shadowRoot
. getElementById ( "up" )
. addEventListener ( "click" , ( ) => this . goUp ( ) ) ;
this . shadowRoot . getElementById ( "prev" ) . addEventListener ( "click" , ( ) => {
if ( this . offset > 0 ) {
this . offset -= this . limit ;
this . load ( ) ;
}
} ) ;
this . shadowRoot . getElementById ( "next" ) . addEventListener ( "click" , ( ) => {
this . offset += this . limit ;
this . load ( ) ;
} ) ;
}
// ---------- Networking ----------------------------------------------
async load ( ) {
const r = await fetch (
` /drive.json?path= ${ encodeURIComponent ( this . path ) } &offset= ${ this . offset } &limit= ${ this . limit } ` ,
) ;
if ( ! r . ok ) {
console . error ( await r . text ( ) ) ;
return ;
}
const data = await r . json ( ) ;
this . renderTiles ( data . items ) ;
this . updateNav ( data . pagination ) ;
}
// ---------- Rendering -------------------------------------------------
renderTiles ( items ) {
const grid = this . shadowRoot . getElementById ( "grid" ) ;
grid . innerHTML = "" ;
items . forEach ( ( item ) => {
const tile = document . createElement ( "div" ) ;
tile . className = "tile" ;
if ( item . type === "directory" ) {
tile . innerHTML = ` <div class="icon">📂</div><div> ${ item . name } </div> ` ;
tile . addEventListener ( "click" , ( ) => {
this . path = item . path ;
this . offset = 0 ;
this . load ( ) ;
} ) ;
} else {
if ( item . mimetype ? . startsWith ( "image/" ) ) {
tile . innerHTML = ` <img class="thumb" src=" ${ item . url } " alt=" ${ item . name } "><div> ${ item . name } </div> ` ;
} else {
tile . innerHTML = ` <div class="icon">📄</div><div> ${ item . name } </div> ` ;
}
tile . addEventListener ( "click" , ( ) => window . open ( item . url , "_blank" ) ) ;
}
grid . appendChild ( tile ) ;
} ) ;
}
// ---------- Navigation + pagination ----------------------------------
updateNav ( { offset , limit , total } ) {
this . shadowRoot . getElementById ( "crumb" ) . textContent = ` / ${ this . path } ` ;
this . shadowRoot . getElementById ( "prev" ) . disabled = offset === 0 ;
this . shadowRoot . getElementById ( "next" ) . disabled = offset + limit >= total ;
this . shadowRoot . getElementById ( "up" ) . disabled = this . path === "" ;
}
goUp ( ) {
if ( ! this . path ) return ;
this . path = this . path . split ( "/" ) . slice ( 0 , - 1 ) . join ( "/" ) ;
this . offset = 0 ;
this . load ( ) ;
}
}
customElements . define ( "file-manager" , FileBrowser ) ;