bruschetta

A simple golang web based file browser
Log | Files | Refs | LICENSE

listing.html (10740B)


      1 <!DOCTYPE html>
      2 <html lang="en">
      3 <head>
      4 <meta charset="UTF-8">
      5 <meta name="viewport" content="width=device-width, initial-scale=1.0">
      6 <title>{{ .CurrentPath }}</title>
      7 <style>
      8 *,::after,::before{box-sizing:border-box;margin:0;padding:0}
      9 a:hover{text-decoration:underline}
     10 body,html{height:100%;overflow:hidden}
     11 body{background:#1a1a1a;color:#d4d4d4;font-family:Consolas,Menlo,'DejaVu Sans Mono',monospace;font-size:13px;display:flex;flex-direction:column}
     12 footer{padding:6px 16px;color:#777}
     13 header{border-bottom:1px solid #333;padding:10px 16px;display:flex;align-items:center;gap:12px}
     14 @media (max-width: 640px) { .sidebar { display: none; } .file-table td.date { display: none; } .file-table td.size { min-width: 50px; } .thumb-grid { grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 8px; padding: 8px; } }
     15 .breadcrumb .current{color:#777}
     16 .breadcrumb .sep{color:#777;margin:0 2px}
     17 .breadcrumb a,.file-table thead th:hover,.thumb-label a{color:#d4d4d4}
     18 .breadcrumb a:hover,.thumb-label a:hover,a{color:#5b9bd5;text-decoration:none}
     19 .breadcrumb,.tree-item,.view-toggle{display:flex;gap:4px}
     20 .breadcrumb{align-items:center;flex-wrap:wrap;flex:1}
     21 .empty{padding:48px;text-align:center;color:#777}
     22 .file-icon.dir,.thumb-preview .dir-icon{color:#c8a94e}
     23 .file-icon.img{color:#7cbf7c}
     24 .file-icon{margin-right:6px;color:#777}
     25 .file-table tbody tr,.file-table thead th{border-bottom:1px solid #333}
     26 .file-table tbody tr:hover,.tree-item.active,.tree-item:hover{background:#2e2e2e}
     27 .file-table td.date{color:#777;min-width:130px}
     28 .file-table td.name{width:100%;white-space:normal;word-break:break-all}
     29 .file-table td.size{color:#777;text-align:right;min-width:70px}
     30 .file-table td{padding:7px 12px;white-space:nowrap}
     31 .file-table thead th,.tree-item{cursor:pointer;user-select:none;white-space:nowrap}
     32 .file-table thead th.sort-asc::after{content:' ↑';color:#5b9bd5}
     33 .file-table thead th.sort-desc::after{content:' ↓';color:#5b9bd5}
     34 .file-table thead th{background:#222;padding:8px 12px;text-align:left;font-weight:400;color:#777;font-size:11px;letter-spacing:.08em;text-transform:uppercase}
     35 .file-table thead{position:sticky;top:0}
     36 .file-table{width:100%;border-collapse:collapse}
     37 .layout{display:flex;flex:1;overflow:hidden}
     38 .main{flex:1;overflow-y:auto}
     39 .sidebar,.thumb-item,footer,header{background:#222}
     40 .sidebar{width:220px;min-width:220px;border-right:1px solid #333;overflow-y:auto;overflow-x:hidden;padding:8px 0;flex-shrink:0}
     41 .sidebar-title{font-size:10px;letter-spacing:.1em;color:#777;padding:4px 12px 8px;text-transform:uppercase}
     42 .thumb-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:12px;padding:16px}
     43 .thumb-item:hover{border-color:#5b9bd5}
     44 .thumb-item{border:1px solid #333;overflow:hidden}
     45 .thumb-label,footer{border-top:1px solid #333;font-size:11px}
     46 .thumb-label{padding:6px 8px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
     47 .thumb-meta{font-size:10px;color:#777;padding:0 8px 6px}
     48 .thumb-preview .big-icon{font-size:36px;color:#777}
     49 .thumb-preview img{width:100%;height:100%;object-fit:cover}
     50 .thumb-preview{width:100%;height:110px;display:flex;align-items:center;justify-content:center;background:#2a2a2a;overflow:hidden}
     51 .tree-children.open{display:block}
     52 .tree-children{display:none;padding-left:12px}
     53 .tree-icon{color:#777;flex-shrink:0}
     54 .tree-item.active{color:#5b9bd5}
     55 .tree-item{align-items:center;padding:3px 8px;overflow:hidden;color:#d4d4d4;font-size:12px}
     56 .tree-label{overflow:hidden;text-overflow:ellipsis}
     57 .tree-root{list-style:none}
     58 .tree-toggle{width:12px;text-align:center;color:#777;flex-shrink:0;font-size:10px}
     59 .view-toggle button.active{background:#1a1a1a;color:#5b9bd5;border-color:#5b9bd5}
     60 .view-toggle button:hover{background:#2e2e2e;color:#d4d4d4}
     61 .view-toggle button{background:#2a2a2a;border:1px solid #333;color:#777;padding:4px 10px;cursor:pointer;font-size:12px;font-family:inherit}
     62 .view-toggle{margin-left:auto}
     63 </style>
     64 </head>
     65 <body>
     66 
     67 <header>
     68   <span>&#128193;</span>
     69   <div class="breadcrumb">
     70     {{ range $i, $crumb := .Breadcrumbs }}
     71       {{ if gt $i 0 }}<span class="sep">/</span>{{ end }}
     72       {{ if eq $i (sub1 (len $.Breadcrumbs)) }}
     73         <span class="current">{{ $crumb.Name }}</span>
     74       {{ else }}
     75         <a href="{{ $crumb.Path }}">{{ $crumb.Name }}</a>
     76       {{ end }}
     77     {{ end }}
     78   </div>
     79   <div class="view-toggle">
     80     <button id="btn-list" class="active" onclick="setView('list')">&#9776; List</button>
     81     <button id="btn-thumb" onclick="setView('thumb')">&#9638; Thumbs</button>
     82   </div>
     83 </header>
     84 
     85 <div class="layout">
     86   <nav class="sidebar">
     87     <div class="sidebar-title">Folders</div>
     88     <ul class="tree-root" id="tree-root"></ul>
     89   </nav>
     90 
     91   <main class="main">
     92     <div id="view-list">
     93       {{ if .Files }}
     94       <table class="file-table" id="file-table">
     95         <thead>
     96           <tr>
     97             <th class="name" onclick="sortTable(0)">Name</th>
     98             <th class="size" onclick="sortTable(1)">Size</th>
     99             <th class="date" onclick="sortTable(2)">Modified</th>
    100           </tr>
    101         </thead>
    102         <tbody>
    103           {{ if ne .CurrentPath "/" }}
    104           <tr>
    105             <td class="name"><span class="file-icon dir">&#128193;</span><a href="../">../</a></td>
    106             <td class="size">—</td>
    107             <td class="date">—</td>
    108           </tr>
    109           {{ end }}
    110           {{ range .Files }}
    111           <tr data-name="{{ .Name }}" data-size="{{ .Size }}" data-date="{{ .ModTime.Unix }}">
    112             <td class="name">
    113               {{ if .IsDir }}
    114                 <span class="file-icon dir">&#128193;</span><a href="{{ .Path }}">{{ .Name }}/</a>
    115               {{ else if isImage .Ext }}
    116                 <span class="file-icon img">&#128247;</span><a href="{{ .Path }}">{{ .Name }}</a>
    117               {{ else }}
    118                 <span class="file-icon">&#128196;</span><a href="{{ .Path }}">{{ .Name }}</a>
    119               {{ end }}
    120             </td>
    121             <td class="size">{{ if .IsDir }}—{{ else }}{{ formatSize .Size }}{{ end }}</td>
    122             <td class="date">{{ formatTime .ModTime }}</td>
    123           </tr>
    124           {{ end }}
    125         </tbody>
    126       </table>
    127       {{ else }}
    128       <div class="empty">Empty directory</div>
    129       {{ end }}
    130     </div>
    131 
    132     <div id="view-thumb" style="display:none">
    133       {{ if .Files }}
    134       <div class="thumb-grid">
    135         {{ if ne .CurrentPath "/" }}
    136         <div class="thumb-item">
    137           <a href="../" style="display:block">
    138             <div class="thumb-preview"><span class="big-icon dir-icon">&#8593;</span></div>
    139             <div class="thumb-label">../</div>
    140           </a>
    141         </div>
    142         {{ end }}
    143         {{ range .Files }}
    144         <div class="thumb-item">
    145           <a href="{{ .Path }}" style="display:block">
    146             <div class="thumb-preview">
    147               {{ if .IsDir }}
    148                 <span class="big-icon dir-icon">&#128193;</span>
    149               {{ else if isImage .Ext }}
    150                 <img src="/__thumb{{ .Path }}" alt="{{ .Name }}" loading="lazy">
    151               {{ else }}
    152                 <span class="big-icon">&#128196;</span>
    153               {{ end }}
    154             </div>
    155             <div class="thumb-label" title="{{ .Name }}">{{ .Name }}{{ if .IsDir }}/{{ end }}</div>
    156             <div class="thumb-meta">{{ if .IsDir }}folder{{ else }}{{ formatSize .Size }}{{ end }}</div>
    157           </a>
    158         </div>
    159         {{ end }}
    160       </div>
    161       {{ else }}
    162       <div class="empty">Empty directory</div>
    163       {{ end }}
    164     </div>
    165   </main>
    166 </div>
    167 
    168 <footer>{{ len .Files }} item(s) &nbsp;·&nbsp; {{ .CurrentPath }}</footer>
    169 
    170 <script>
    171 const TREE_DATA = {{ .TreeJSON }};
    172 const CURRENT_PATH = {{ printf "%q" .CurrentPath }};
    173 
    174 function setView(v) {
    175   const isList = v === 'list';
    176   document.getElementById('view-list').style.display = isList ? '' : 'none';
    177   document.getElementById('view-thumb').style.display = isList ? 'none' : '';
    178   document.getElementById('btn-list').className = isList ? 'active' : '';
    179   document.getElementById('btn-thumb').className = isList ? '' : 'active';
    180 }
    181 
    182 let sortCol = -1, sortDir = 1;
    183 function sortTable(col) {
    184   const tbody = document.querySelector('#file-table tbody');
    185   const rows = Array.from(tbody.querySelectorAll('tr'));
    186   const parentRow = rows.find(r => r.querySelector('a[href="../"]'));
    187   const dataRows = rows.filter(r => r !== parentRow);
    188 
    189   if (sortCol === col) sortDir *= -1;
    190   else { sortCol = col; sortDir = 1; }
    191 
    192   dataRows.sort((a, b) => {
    193     let av, bv;
    194     if (col === 0) { av = (a.dataset.name||'').toLowerCase(); bv = (b.dataset.name||'').toLowerCase(); }
    195     else if (col === 1) { av = parseInt(a.dataset.size||0); bv = parseInt(b.dataset.size||0); }
    196     else { av = parseInt(a.dataset.date||0); bv = parseInt(b.dataset.date||0); }
    197     return av < bv ? -sortDir : av > bv ? sortDir : 0;
    198   });
    199 
    200   document.querySelectorAll('#file-table thead th').forEach((th, i) => {
    201     th.className = th.className.replace(/ ?sort-(asc|desc)/g, '');
    202     if (i === col) th.className += sortDir > 0 ? ' sort-asc' : ' sort-desc';
    203   });
    204 
    205   tbody.innerHTML = '';
    206   if (parentRow) tbody.appendChild(parentRow);
    207   dataRows.forEach(r => tbody.appendChild(r));
    208 }
    209 
    210 function buildTree(nodes, container, depth) {
    211   if (!nodes || !nodes.length) return;
    212   nodes.forEach(node => {
    213     const li = document.createElement('li');
    214     const item = document.createElement('div');
    215     item.className = 'tree-item' + (node.path === CURRENT_PATH ? ' active' : '');
    216     item.style.paddingLeft = (8 + depth * 12) + 'px';
    217 
    218     const toggle = document.createElement('span');
    219     toggle.className = 'tree-toggle';
    220     toggle.textContent = (node.children && node.children.length) ? '▶' : ' ';
    221 
    222     const icon = document.createElement('span');
    223     icon.className = 'tree-icon';
    224     icon.textContent = '📁';
    225 
    226     const label = document.createElement('span');
    227     label.className = 'tree-label';
    228     label.textContent = node.name;
    229 
    230     item.append(toggle, icon, label);
    231     li.appendChild(item);
    232 
    233     let childUl = null;
    234     if (node.children && node.children.length) {
    235       childUl = document.createElement('ul');
    236       childUl.className = 'tree-children';
    237       buildTree(node.children, childUl, depth + 1);
    238       li.appendChild(childUl);
    239       toggle.addEventListener('click', e => {
    240         e.stopPropagation();
    241         toggle.textContent = childUl.classList.toggle('open') ? '▼' : '▶';
    242       });
    243     }
    244 
    245     item.addEventListener('click', () => { window.location.href = node.path; });
    246 
    247     if (CURRENT_PATH.startsWith(node.path) && childUl) {
    248       childUl.classList.add('open');
    249       toggle.textContent = '▼';
    250     }
    251 
    252     container.appendChild(li);
    253   });
    254 }
    255 
    256 buildTree(TREE_DATA, document.getElementById('tree-root'), 0);
    257 </script>
    258 </body>
    259 </html>