mirror of
https://github.com/rclone/rclone.git
synced 2026-10-10 15:01:37 -04:00
The listing now shows the folder path with clickable breadcrumbs above a card containing the entries: - A summary of the directories, files and total size - The directory and file counts are toggles - The search box sits beside the Name heading (focussable with /) - Sizes right aligned, empty columns are gone and the icons are simpler. - The colours are CSS variables The template data is unchanged so custom templates still work, and the zip download links and ?sort= parameters work as before.
356 lines
14 KiB
HTML
356 lines
14 KiB
HTML
<!--
|
|
// Copyright 2015 Matthew Holt and The Caddy Authors
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
Modifications: Adapted to rclone markup -->
|
|
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<title>{{html .Name}}</title>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<meta name="google" content="notranslate">
|
|
<style>
|
|
:root {
|
|
--page: #f3f4f6;
|
|
--card: #ffffff;
|
|
--text: #1f2328;
|
|
--muted: #6b7280;
|
|
--faint: #9ca3af;
|
|
--rule: #e5e7eb;
|
|
--rule-soft: #eef0f3;
|
|
--hover: #fbfbe6;
|
|
--link: #006ed3;
|
|
--link-hover: #319cff;
|
|
--folder: #f4b400;
|
|
--file: #8b95a5;
|
|
--input: #ffffff;
|
|
--input-border: #cfd4dc;
|
|
--shadow: 0 1px 2px rgba(16, 24, 40, .06), 0 1px 3px rgba(16, 24, 40, .1);
|
|
}
|
|
@media (prefers-color-scheme: dark) {
|
|
:root {
|
|
color-scheme: dark;
|
|
--page: #111114;
|
|
--card: #18181b;
|
|
--text: #f4f4f5;
|
|
--muted: #a1a1aa;
|
|
--faint: #71717a;
|
|
--rule: #3f3f46;
|
|
--rule-soft: #27272a;
|
|
--hover: #27272a;
|
|
--link: #99c7fb;
|
|
--link-hover: #cce3fd;
|
|
--file: #a1a1aa;
|
|
--input: #27272a;
|
|
--input-border: #52525b;
|
|
--shadow: 0 1px 2px rgba(0, 0, 0, .5);
|
|
}
|
|
}
|
|
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
[hidden] { display: none !important; }
|
|
body {
|
|
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
font-size: 14px;
|
|
line-height: 1.4;
|
|
color: var(--text);
|
|
background: var(--page);
|
|
}
|
|
a { color: var(--link); text-decoration: none; }
|
|
a:hover { color: var(--link-hover); }
|
|
a:focus-visible, button:focus-visible, input:focus-visible { outline: 2px solid var(--link); outline-offset: 2px; }
|
|
svg { fill: currentColor; }
|
|
|
|
/* Header: folder path */
|
|
header {
|
|
background: var(--card);
|
|
border-bottom: 1px solid var(--rule);
|
|
padding: 14px 5% 12px;
|
|
}
|
|
.eyebrow {
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
letter-spacing: .08em;
|
|
text-transform: uppercase;
|
|
color: var(--muted);
|
|
}
|
|
h1 {
|
|
font-size: 20px;
|
|
font-weight: 400;
|
|
color: var(--faint);
|
|
white-space: nowrap;
|
|
overflow-x: auto;
|
|
margin-top: 4px;
|
|
}
|
|
h1 a { color: var(--text); margin: 0 3px; }
|
|
h1 a:first-child { margin-left: 0; }
|
|
h1 a:hover { text-decoration: underline; }
|
|
.zip-root { margin-left: 8px; vertical-align: middle; }
|
|
.zip-root svg { width: 1.3em; height: 1.3em; }
|
|
|
|
/* The listing card */
|
|
main { padding: 24px 5% 40px; }
|
|
.card {
|
|
background: var(--card);
|
|
border: 1px solid var(--rule);
|
|
border-radius: 8px;
|
|
box-shadow: var(--shadow);
|
|
overflow: hidden;
|
|
}
|
|
#summary {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
align-items: center;
|
|
gap: 6px 10px;
|
|
padding: 10px 16px;
|
|
border-bottom: 1px solid var(--rule);
|
|
color: var(--muted);
|
|
}
|
|
#summary b { color: var(--text); font-weight: 600; font-variant-numeric: tabular-nums; }
|
|
.toggle {
|
|
font: inherit;
|
|
color: var(--text);
|
|
background: var(--rule-soft);
|
|
border: 1px solid var(--rule);
|
|
border-radius: 999px;
|
|
cursor: pointer;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
padding: 3px 12px 3px 9px;
|
|
}
|
|
.toggle svg { width: 1.15em; height: 1.15em; }
|
|
#toggle-dirs svg { color: var(--folder); }
|
|
#toggle-files svg { color: var(--file); }
|
|
.toggle:hover { border-color: var(--faint); }
|
|
.toggle[aria-pressed="false"] { background: none; color: var(--faint); text-decoration: line-through; }
|
|
.toggle[aria-pressed="false"] b { color: var(--faint); font-weight: 400; }
|
|
.toggle[aria-pressed="false"] svg { opacity: .35; }
|
|
.total { margin-left: auto; }
|
|
|
|
.listing { overflow-x: auto; }
|
|
table { width: 100%; border-collapse: collapse; }
|
|
th, td { text-align: left; padding: 9px 12px; white-space: nowrap; }
|
|
th {
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
letter-spacing: .04em;
|
|
text-transform: uppercase;
|
|
color: var(--text);
|
|
border-bottom: 1px solid var(--rule);
|
|
}
|
|
th a { color: inherit; }
|
|
th[aria-sort="ascending"] a::after { content: " \25B4"; }
|
|
th[aria-sort="descending"] a::after { content: " \25BE"; }
|
|
th.name { display: flex; align-items: center; gap: 14px; }
|
|
.search {
|
|
position: relative;
|
|
font-weight: 400;
|
|
letter-spacing: 0;
|
|
text-transform: none;
|
|
}
|
|
.search svg {
|
|
position: absolute;
|
|
left: 8px;
|
|
top: 50%;
|
|
width: 13px;
|
|
height: 13px;
|
|
margin-top: -6.5px;
|
|
color: var(--faint);
|
|
pointer-events: none;
|
|
}
|
|
#filter {
|
|
font: inherit;
|
|
font-size: 13px;
|
|
width: 220px;
|
|
max-width: 100%;
|
|
padding: 5px 8px 5px 26px;
|
|
border: 1px solid var(--input-border);
|
|
border-radius: 6px;
|
|
background: var(--input);
|
|
color: var(--text);
|
|
}
|
|
#filter::placeholder { color: var(--faint); }
|
|
th.size, td.size { text-align: right; font-variant-numeric: tabular-nums; }
|
|
tbody tr { border-bottom: 1px solid var(--rule-soft); }
|
|
tbody tr:last-child { border-bottom: 0; }
|
|
tbody tr:hover { background: var(--hover); }
|
|
td.name { width: 100%; white-space: normal; word-break: break-all; }
|
|
td.name svg { width: 1.15em; height: 1.15em; vertical-align: -.2em; margin-right: 8px; }
|
|
tr.dir td.name svg { color: var(--folder); }
|
|
tr:not(.dir) td.name svg { color: var(--file); }
|
|
td.time { font-variant-numeric: tabular-nums; }
|
|
td.name .zip { opacity: 0; margin: 0 0 0 6px; transition: opacity 0.15s ease-in-out; }
|
|
tr.file:hover td.name .zip { opacity: 1; }
|
|
tr.empty td { text-align: center; color: var(--muted); padding: 28px 12px; }
|
|
.goup a { color: var(--muted); }
|
|
.goup a:hover { color: var(--link-hover); }
|
|
@media (prefers-reduced-motion: reduce) {
|
|
td.name .zip { transition: none; }
|
|
}
|
|
@media (max-width: 640px) {
|
|
header, main { padding-left: 16px; padding-right: 16px; }
|
|
main { padding-top: 16px; }
|
|
th, td { padding: 8px 6px; }
|
|
th.name { gap: 10px; }
|
|
#filter { width: 130px; }
|
|
.total { margin-left: 0; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<svg width="0" height="0" style="position:absolute" aria-hidden="true">
|
|
<defs>
|
|
<symbol id="folder" viewBox="0 0 24 24"><path d="M10 4H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-8l-2-2z"/></symbol>
|
|
<symbol id="file" viewBox="0 0 24 24"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-6-6zm-1 7V3.5L18.5 9H13z"/></symbol>
|
|
<symbol id="search" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" d="M10.5 4a6.5 6.5 0 1 1 0 13 6.5 6.5 0 0 1 0-13zm5 11.5L20 20"/></symbol>
|
|
<symbol id="zip-folder" viewBox="0 -960 960 960"><path d="M640-480v-80h80v80h-80Zm0 80h-80v-80h80v80Zm0 80v-80h80v80h-80ZM447-640l-80-80H160v480h400v-80h80v80h160v-400H640v80h-80v-80H447ZM160-160q-33 0-56.5-23.5T80-240v-480q0-33 23.5-56.5T160-800h240l80 80h320q33 0 56.5 23.5T880-640v400q0 33-23.5 56.5T800-160H160Zm0-80v-480 480Z"/></symbol>
|
|
</defs>
|
|
</svg>
|
|
<header>
|
|
<div class="eyebrow">Folder path</div>
|
|
<h1>
|
|
{{- range $i, $crumb := .Breadcrumb}}<a href="{{html $crumb.Link}}">{{html $crumb.Text}}</a>{{if ne $i 0}}/{{end}}{{end}}
|
|
{{- if not .DisableZip}}
|
|
<a class="zip-root" href="{{html .ZipURL}}" title="Download root as .zip"><svg><use href="#zip-folder"></use></svg></a>
|
|
{{- end}}
|
|
</h1>
|
|
</header>
|
|
<main>
|
|
<div class="card">
|
|
<div id="summary">
|
|
<button class="toggle" id="toggle-dirs" aria-pressed="true" title="Show or hide directories"><svg><use href="#folder"></use></svg><b id="ndirs">{{.NumDirs}}</b> directories</button>
|
|
<button class="toggle" id="toggle-files" aria-pressed="true" title="Show or hide files"><svg><use href="#file"></use></svg><b id="nfiles">{{.NumFiles}}</b> files</button>
|
|
<span class="total"><b><size>{{.TotalSize}}</size></b> total</span>
|
|
</div>
|
|
<div class="listing">
|
|
<table aria-describedby="summary">
|
|
<thead>
|
|
<tr>
|
|
<th class="name" data-key="name">
|
|
<a href="#" onclick="return sortBy(this.parentNode)">Name</a>
|
|
<span class="search"><svg><use href="#search"></use></svg><input type="search" id="filter" placeholder="Search" aria-label="Search this directory" autocomplete="off" oninput="filter()"></span>
|
|
</th>
|
|
<th class="size" data-key="size"><a href="#" onclick="return sortBy(this.parentNode)">Size</a></th>
|
|
<th class="time" data-key="time"><a href="#" onclick="return sortBy(this.parentNode)">Modified</a></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{{- if not (and .Static .IsRoot)}}
|
|
<tr class="goup"><td class="name"><a href="{{html .UpLink}}">Go up</a></td><td class="size">—</td><td class="time">—</td></tr>
|
|
{{- end}}
|
|
{{- range .Entries}}
|
|
<tr class="file{{if .IsDir}} dir{{end}}" data-size="{{.Size}}" data-time="{{if .ModTime | afterEpoch}}{{.ModTime}}{{end}}">
|
|
<td class="name">
|
|
{{- if .IsDir}}<svg><use href="#folder"></use></svg>{{else}}<svg><use href="#file"></use></svg>{{end -}}
|
|
<a href="{{html .URL}}">{{html .Leaf}}</a>
|
|
{{- if and .IsDir (not $.DisableZip)}}
|
|
<a class="zip" href="{{html .ZipURL}}" title="Download folder as .zip"><svg><use href="#zip-folder"></use></svg></a>
|
|
{{- end}}
|
|
</td>
|
|
<td class="size">{{if .IsDir}}—{{else}}<size>{{.Size}}</size>{{end}}</td>
|
|
<td class="time">{{if .ModTime | afterEpoch}}<time datetime="{{.ModTime}}">{{.ModTime}}</time>{{else}}—{{end}}</td>
|
|
</tr>
|
|
{{- end}}
|
|
<tr class="empty" hidden><td colspan="3"></td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
<script>
|
|
var filterEl = document.getElementById('filter');
|
|
var toggles = {dirs: document.getElementById('toggle-dirs'), files: document.getElementById('toggle-files')};
|
|
var rows = function() { return Array.prototype.slice.call(document.querySelectorAll('tr.file')); };
|
|
// Apply the search text and the directory and file toggles to the rows
|
|
function filter() {
|
|
var q = filterEl.value.trim().toLowerCase();
|
|
var shown = {dirs: 0, files: 0}, total = {dirs: 0, files: 0};
|
|
rows().forEach(function(tr) {
|
|
var kind = tr.classList.contains('dir') ? 'dirs' : 'files';
|
|
var show = toggles[kind].getAttribute('aria-pressed') == 'true' && (!q || tr.querySelector('.name').textContent.toLowerCase().indexOf(q) != -1);
|
|
tr.hidden = !show;
|
|
total[kind]++;
|
|
if (show) shown[kind]++;
|
|
});
|
|
['dirs', 'files'].forEach(function(kind) {
|
|
document.getElementById('n' + kind).textContent = shown[kind] == total[kind] ? total[kind] : shown[kind] + ' of ' + total[kind];
|
|
});
|
|
var empty = document.querySelector('tr.empty');
|
|
empty.hidden = shown.dirs + shown.files > 0;
|
|
empty.firstChild.textContent = total.dirs + total.files ? 'No matching entries' : 'Empty directory';
|
|
}
|
|
function toggle(kind) {
|
|
toggles[kind].setAttribute('aria-pressed', toggles[kind].getAttribute('aria-pressed') == 'true' ? 'false' : 'true');
|
|
filter();
|
|
}
|
|
toggles.dirs.onclick = function() { toggle('dirs'); };
|
|
toggles.files.onclick = function() { toggle('files'); };
|
|
// Sort by the column of th, toggling the direction on repeated clicks
|
|
function sortBy(th) {
|
|
var key = th.getAttribute('data-key');
|
|
var asc = th.getAttribute('aria-sort') != 'ascending';
|
|
document.querySelectorAll('th[aria-sort]').forEach(function(el) { el.removeAttribute('aria-sort'); });
|
|
th.setAttribute('aria-sort', asc ? 'ascending' : 'descending');
|
|
var value = function(tr) {
|
|
if (key == 'name') return tr.querySelector('.name').textContent.trim();
|
|
if (key == 'size') return Number(tr.getAttribute('data-size'));
|
|
return tr.getAttribute('data-time');
|
|
};
|
|
var sorted = rows().sort(function(a, b) {
|
|
// Directories first when sorting by name
|
|
var byDir = key == 'name' ? b.classList.contains('dir') - a.classList.contains('dir') : 0;
|
|
var va = value(a), vb = value(b);
|
|
var cmp = typeof va == 'string' ? va.localeCompare(vb, undefined, {numeric: true, sensitivity: 'base'}) : va - vb;
|
|
return byDir || (asc ? cmp : -cmp);
|
|
});
|
|
var empty = document.querySelector('tr.empty');
|
|
sorted.forEach(function(tr) { empty.parentNode.insertBefore(tr, empty); });
|
|
return false;
|
|
}
|
|
// Show how the server sorted the listing from the ?sort= and ?order= parameters
|
|
var getUrlParameter = function(name) {
|
|
var params = window.location.search.substring(1).split('&');
|
|
for (var i = 0; i < params.length; i++) {
|
|
var param = params[i].split('=');
|
|
if (param[0] === name) return param[1] === undefined ? true : decodeURIComponent(param[1]);
|
|
}
|
|
};
|
|
var sorted = document.querySelector('th[data-key="' + (getUrlParameter('sort') || 'name').replace('dirfirst', '') + '"]');
|
|
if (sorted) sorted.setAttribute('aria-sort', getUrlParameter('order') == 'desc' ? 'descending' : 'ascending');
|
|
// Press / to search
|
|
document.addEventListener('keydown', function(e) {
|
|
if (e.key == '/' && document.activeElement != filterEl) { filterEl.focus(); e.preventDefault(); }
|
|
});
|
|
function localizeDatetime(e) {
|
|
var d = new Date(e.getAttribute('datetime'));
|
|
if (isNaN(d)) {
|
|
d = new Date(e.textContent);
|
|
if (isNaN(d)) return;
|
|
}
|
|
e.textContent = d.toLocaleString([], {day: "2-digit", month: "2-digit", year: "numeric", hour: "2-digit", minute: "2-digit", second: "2-digit"});
|
|
}
|
|
Array.prototype.slice.call(document.getElementsByTagName("time")).forEach(localizeDatetime);
|
|
function readableFileSize(size) {
|
|
var units = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
|
|
var i = 0;
|
|
while (size >= 1024) { size /= 1024; ++i; }
|
|
return (i ? size.toFixed(1) : size) + ' ' + units[i];
|
|
}
|
|
Array.prototype.slice.call(document.getElementsByTagName("size")).forEach(function(el) { el.textContent = readableFileSize(Number(el.textContent)); });
|
|
filter();
|
|
</script>
|
|
</body>
|
|
</html>
|