View switching feature

This commit is contained in:
2026-05-29 09:21:19 +02:00
parent 5844a870ce
commit f85c29ba42
6 changed files with 368 additions and 23 deletions
+13 -1
View File
@@ -8,7 +8,17 @@
<div class="content">{{.SpecialContent}}</div>
{{end}}
{{if .Entries}}
<h2 id="files">Files <button class="btn btn-small" data-companion-reveal hidden title="Open folder in file manager">open</button></h2>
<h2 id="files">Files <button class="btn btn-small" data-companion-reveal hidden title="Open folder in file manager">open</button>{{if .CanEdit}} <button class="btn btn-small" id="view-settings-btn" onclick="openViewSettings()" title="View &amp; sorting" data-view="{{.View}}" data-sort="{{.Sort}}" data-order="{{.Order}}">view</button>{{end}}</h2>
{{if eq .View "thumbnail"}}
<div class="thumb-grid">
{{range .Entries}}
<a class="thumb-tile" href="{{.URL}}" title="{{.Name}}">
{{if .ThumbURL}}<img class="thumb-img" src="{{.ThumbURL}}" alt="" loading="lazy" width="300">{{else}}<span class="thumb-icon">{{.Icon}}</span>{{end}}
<span class="thumb-label truncate">{{.Name}}</span>
</a>
{{end}}
</div>
{{else}}
<table class="data-table panel">
<tbody>
{{range .Entries}}
@@ -20,6 +30,8 @@
{{end}}
</tbody>
</table>
{{end}}
{{if .CanEdit}}<script src="/_/page/view-settings.js"></script>{{end}}
{{else if not .Content}}
{{if not .SpecialContent}}
<p class="empty">Empty folder — <a href="?edit">[CREATE]</a></p>
+85
View File
@@ -0,0 +1,85 @@
// View-settings modal: lets the user pick the folder listing's view style,
// sort key, and order, then persists them by POSTing to the folder with
// ?settings. Reuses openModal/closeModal and postReplace from page/actions.js.
function openViewSettings() {
var btn = document.getElementById('view-settings-btn');
var state = {
view: (btn && btn.dataset.view) || 'list',
sort: (btn && btn.dataset.sort) || 'name',
order: (btn && btn.dataset.order) || 'asc'
};
// segmented builds a row of mutually-exclusive .btn toggles bound to a
// single state key, marking the current choice with .is-active.
function segmented(key, options) {
var wrap = document.createElement('div');
wrap.className = 'row gap-1';
options.forEach(function (opt) {
var b = document.createElement('button');
b.type = 'button';
b.className = 'btn btn-small';
b.textContent = opt.label;
if (state[key] === opt.value) b.classList.add('is-active');
b.addEventListener('click', function () {
state[key] = opt.value;
wrap.querySelectorAll('button').forEach(function (x) {
x.classList.remove('is-active');
});
b.classList.add('is-active');
});
wrap.appendChild(b);
});
return wrap;
}
function field(labelText, control) {
var row = document.createElement('div');
row.className = 'col gap-1';
var label = document.createElement('span');
label.className = 'caption';
label.textContent = labelText;
row.appendChild(label);
row.appendChild(control);
return row;
}
var sortSelect = document.createElement('select');
sortSelect.className = 'input';
[['name', 'Name'], ['modified', 'Modified'], ['size', 'Size']].forEach(function (o) {
var opt = document.createElement('option');
opt.value = o[0];
opt.textContent = o[1];
if (state.sort === o[0]) opt.selected = true;
sortSelect.appendChild(opt);
});
sortSelect.addEventListener('change', function () { state.sort = sortSelect.value; });
var body = document.createElement('div');
body.className = 'col';
body.appendChild(field('View style', segmented('view', [
{ value: 'list', label: 'List' },
{ value: 'thumbnail', label: 'Thumbnail' }
])));
body.appendChild(field('Sort by', sortSelect));
body.appendChild(field('Order', segmented('order', [
{ value: 'asc', label: 'Asc' },
{ value: 'desc', label: 'Desc' }
])));
openModal({
title: 'View settings',
body: body,
confirm: {
label: 'SAVE',
onConfirm: function () {
var action = window.location.pathname + '?settings';
var formBody = 'view=' + encodeURIComponent(state.view) +
'&sort=' + encodeURIComponent(state.sort) +
'&order=' + encodeURIComponent(state.order);
var target = window.location.pathname;
closeModal();
postReplace(action, formBody, target);
}
}
});
}
+39
View File
@@ -192,6 +192,8 @@ footer {
.btn-fab:hover { background: var(--bg-panel-hover); color: var(--primary-hover); }
.danger { color: var(--danger); }
.danger:hover { color: var(--danger-hover); }
/* Selected segmented-toggle button (view-settings modal). */
.btn.is-active { color: var(--primary-hover); }
/* === Form controls ===
.input baseline is shared by search-input, modal inputs, and the editor
@@ -435,6 +437,43 @@ button.fab { display: none; }
background: var(--bg-panel) url("/_/icons/thumb-placeholder.svg") center/2rem no-repeat;
}
/* === Thumbnail listing grid ===
File-listing variant of .photo-grid: responsive tiles that pair a thumbnail
(or a file/folder icon for non-thumbnailable entries) with a truncated
name label beneath. */
.thumb-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: var(--space-3);
margin-top: var(--space-3);
}
.thumb-tile {
display: flex;
flex-direction: column;
gap: var(--space-1);
color: var(--text);
border: var(--border);
background: var(--bg-panel);
padding: var(--space-2);
}
.thumb-tile:hover { background: var(--bg-panel-hover); color: var(--primary-hover); }
.thumb-img {
width: 100%;
height: 150px;
object-fit: cover;
display: block;
background: var(--bg) url("/_/icons/thumb-placeholder.svg") center/2rem no-repeat;
}
.thumb-icon {
height: 150px;
display: flex;
align-items: center;
justify-content: center;
font-size: 3rem;
color: var(--secondary);
}
.thumb-label { font-size: var(--font-sm); }
.empty { padding: var(--space-4); text-align: center; }
/* === Scrollbars === */