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>📁</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')">☰ List</button> 81 <button id="btn-thumb" onclick="setView('thumb')">▦ 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">📁</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">📁</span><a href="{{ .Path }}">{{ .Name }}/</a> 115 {{ else if isImage .Ext }} 116 <span class="file-icon img">📷</span><a href="{{ .Path }}">{{ .Name }}</a> 117 {{ else }} 118 <span class="file-icon">📄</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">↑</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">📁</span> 149 {{ else if isImage .Ext }} 150 <img src="/__thumb{{ .Path }}" alt="{{ .Name }}" loading="lazy"> 151 {{ else }} 152 <span class="big-icon">📄</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) · {{ .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>