From ed4c8853a61b9b0327f5bd07f4650de43be96e5e Mon Sep 17 00:00:00 2001
From: OpenClaw Sub-agent
Date: Sat, 1 Aug 2026 22:00:26 +1000
Subject: [PATCH] =?UTF-8?q?feat:=20Files=20page=20layout=20switcher=20?=
=?UTF-8?q?=E2=80=94=20grid=20(Finder-style)=20&=20carousel=20(cover-flow)?=
=?UTF-8?q?=20views?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Three layouts: list (unchanged) / grid / carousel, persisted in localStorage
- Grid: image thumbnails (lazy), video first-frame + play badge, doc icon fallback
- Carousel: scroll-snap horizontal track, desktop arrows, native swipe on mobile
- Click any card opens the existing preview modal (with prev/next navigation)
- Mobile-first: 2-column grid on phones, hidden carousel arrows on touch
- Pure frontend in list.html; no model/API changes
---
links/templates/links/files/list.html | 287 ++++++++++++++++++++++++--
1 file changed, 275 insertions(+), 12 deletions(-)
diff --git a/links/templates/links/files/list.html b/links/templates/links/files/list.html
index db5a573..00e2942 100644
--- a/links/templates/links/files/list.html
+++ b/links/templates/links/files/list.html
@@ -140,6 +140,131 @@
.file-actions { flex: none; display: flex; align-items: center; gap: 0.15rem; }
+ /* ---------- Layout switcher (list / grid / carousel) ---------- */
+ .layout-switcher {
+ display: inline-flex; align-items: center; gap: 2px;
+ background: rgba(0, 0, 0, 0.055); border-radius: 980px; padding: 3px;
+ border: 1px solid rgba(0, 0, 0, 0.04);
+ }
+ .layout-btn {
+ display: inline-flex; align-items: center; justify-content: center;
+ width: 40px; height: 34px; border-radius: 980px; border: none; cursor: pointer;
+ background: transparent; color: var(--apple-gray);
+ transition: background 0.25s ease, color 0.25s ease, box-shadow 0.25s ease;
+ }
+ .layout-btn:hover { color: var(--apple-text); }
+ .layout-btn.active {
+ background: #fff; color: var(--apple-blue);
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
+ }
+ .layout-btn svg { width: 1.15rem; height: 1.15rem; display: block; }
+ @media (max-width: 640px) {
+ .layout-btn { width: 38px; height: 32px; }
+ }
+
+ /* ---------- Grid view (macOS Finder style) ---------- */
+ .file-grid {
+ display: grid; gap: 0.9rem; padding: 0.6rem 1.8rem 1.6rem;
+ grid-template-columns: repeat(auto-fill, minmax(7.5rem, 1fr));
+ }
+ .grid-card {
+ display: flex; flex-direction: column; align-items: stretch; gap: 0.45rem;
+ padding: 0.65rem 0.65rem 0.8rem; border-radius: var(--apple-radius-md);
+ background: #fff; border: 1px solid rgba(0, 0, 0, 0.05);
+ box-shadow: var(--apple-shadow-card); cursor: pointer; text-align: left;
+ transition: transform 0.3s var(--apple-ease), box-shadow 0.3s var(--apple-ease);
+ }
+ .grid-card:hover { transform: translateY(-3px); box-shadow: var(--apple-shadow-lift); }
+ .grid-card:active { transform: scale(0.97); }
+ .grid-thumb {
+ position: relative; width: 100%; aspect-ratio: 1 / 1;
+ border-radius: 16px; overflow: hidden; background: #f0f0f2;
+ display: flex; align-items: center; justify-content: center;
+ }
+ .grid-thumb img, .grid-thumb video {
+ width: 100%; height: 100%; object-fit: cover; display: block;
+ }
+ .grid-thumb .thumb-badge {
+ position: absolute; bottom: 0.4rem; right: 0.4rem;
+ width: 1.6rem; height: 1.6rem; border-radius: 50%;
+ background: rgba(0, 0, 0, 0.55); color: #fff;
+ display: flex; align-items: center; justify-content: center;
+ -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px);
+ }
+ .grid-thumb .thumb-badge svg { width: 0.85rem; height: 0.85rem; }
+ .grid-file-icon { width: 3.4rem; height: 3.4rem; color: #c7c7cc; }
+ .grid-file-icon svg { width: 100%; height: 100%; }
+ .grid-name {
+ font-size: 0.82rem; font-weight: 600; color: var(--apple-text);
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
+ }
+ .grid-meta { font-size: 0.72rem; color: var(--apple-gray); }
+ .grid-badge {
+ align-self: flex-start; display: inline-flex; align-items: center; gap: 0.3rem;
+ padding: 0.18rem 0.6rem; border-radius: 980px;
+ font-size: 0.68rem; font-weight: 600;
+ }
+ .grid-badge.pub { background: rgba(48, 209, 88, 0.14); color: var(--apple-green); }
+ .grid-badge.priv { background: rgba(0, 0, 0, 0.05); color: var(--apple-gray); }
+ @media (max-width: 640px) {
+ .file-grid { gap: 0.65rem; padding: 0.5rem 1.1rem 1.2rem; grid-template-columns: repeat(auto-fill, minmax(6.4rem, 1fr)); }
+ .grid-card { padding: 0.5rem 0.5rem 0.65rem; }
+ .grid-thumb { border-radius: 13px; }
+ }
+
+ /* ---------- Carousel view (iOS cover-flow style) ---------- */
+ .file-carousel { position: relative; }
+ .carousel-track {
+ display: flex; gap: 1.1rem; overflow-x: auto;
+ padding: 0.9rem 1.8rem 1.4rem; margin-bottom: -1.4rem;
+ scroll-snap-type: x mandatory; -webkit-overflow-scrolling: touch;
+ scrollbar-width: none;
+ }
+ .carousel-track::-webkit-scrollbar { display: none; }
+ .carousel-card {
+ flex: none; width: 11.5rem; scroll-snap-align: center;
+ display: flex; flex-direction: column; gap: 0.5rem;
+ background: #fff; border-radius: var(--apple-radius-lg);
+ border: 1px solid rgba(0, 0, 0, 0.05); box-shadow: var(--apple-shadow-card);
+ padding: 0.8rem; cursor: pointer; text-align: left;
+ transition: transform 0.3s var(--apple-ease), box-shadow 0.3s var(--apple-ease);
+ }
+ .carousel-card:hover { transform: translateY(-4px) scale(1.02); box-shadow: var(--apple-shadow-lift); }
+ .carousel-cover {
+ position: relative; width: 100%; aspect-ratio: 1 / 1; border-radius: 18px;
+ overflow: hidden; background: #f0f0f2;
+ display: flex; align-items: center; justify-content: center;
+ }
+ .carousel-cover img, .carousel-cover video {
+ width: 100%; height: 100%; object-fit: cover; display: block;
+ }
+ .carousel-file-icon { width: 4rem; height: 4rem; color: #c7c7cc; }
+ .carousel-file-icon svg { width: 100%; height: 100%; }
+ .carousel-name {
+ font-size: 0.85rem; font-weight: 600; color: var(--apple-text);
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
+ }
+ .carousel-meta { font-size: 0.72rem; color: var(--apple-gray); }
+ .carousel-nav {
+ position: absolute; top: 42%; transform: translateY(-50%); z-index: 5;
+ width: 2.4rem; height: 2.4rem; border-radius: 50%;
+ background: rgba(255, 255, 255, 0.92); color: var(--apple-text);
+ border: 1px solid rgba(0, 0, 0, 0.06); box-shadow: var(--apple-shadow-card);
+ display: flex; align-items: center; justify-content: center; cursor: pointer;
+ -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px);
+ transition: background 0.2s ease, transform 0.2s ease;
+ }
+ .carousel-nav:hover { background: #fff; transform: translateY(-50%) scale(1.06); }
+ .carousel-nav svg { width: 1.1rem; height: 1.1rem; }
+ .carousel-nav.prev { left: 0.9rem; }
+ .carousel-nav.next { right: 0.9rem; }
+ @media (max-width: 640px) {
+ .carousel-track { gap: 0.8rem; padding: 0.7rem 1.1rem 1.2rem; }
+ .carousel-card { width: 9.5rem; padding: 0.6rem; }
+ .carousel-cover { border-radius: 14px; }
+ .carousel-nav { display: none; } /* native swipe on touch */
+ }
+
/* ---------- Empty state ---------- */
.empty-state {
padding: 2.75rem 1.5rem; text-align: center; color: var(--apple-gray);
@@ -327,6 +452,33 @@
+
+
+
+
+
+
-
-
-
-
- {% trans "No files yet" %}
- {% trans "Drag files anywhere on this page, or click Upload Files above." %}
-
+
+
+
+
+
+
+
+
+
![]()
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ●
+ ○
+
+
+
+
+
+
+
+
+
+
+
+
+
+
![]()
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{% trans "No files yet" %}
+
{% trans "Drag files anywhere on this page, or click Upload Files above." %}
+
@@ -690,6 +922,7 @@ function fileManager() {
visibleFiles: [],
uploads: [],
hudOpen: false,
+ layout: 'list',
toastMsg: '',
toastVisible: false,
_toastTimer: null,
@@ -699,6 +932,12 @@ function fileManager() {
currentIndex: -1,
init() {
+ // Restore persisted layout preference (list | grid | carousel)
+ try {
+ const saved = localStorage.getItem('links-files-layout');
+ if (saved === 'grid' || saved === 'carousel' || saved === 'list') this.layout = saved;
+ } catch (e) { /* localStorage unavailable — keep default */ }
+
// Seed initial file list from server-rendered JSON
try {
this.visibleFiles = JSON.parse(document.getElementById('initial-file-data').textContent);
@@ -756,6 +995,30 @@ function fileManager() {
document.getElementById('globalFileInput').click();
},
+ // ── Layout switching (list / grid / carousel) ─────────────────
+ setLayout(layout) {
+ this.layout = layout;
+ try { localStorage.setItem('links-files-layout', layout); } catch (e) { /* ignore */ }
+ // Re-snap the carousel to the start when switching into it
+ if (layout === 'carousel') {
+ this.$nextTick(() => {
+ const track = this.$refs.carouselTrack;
+ if (track && track.scrollTo) track.scrollTo({ left: 0, behavior: 'auto' });
+ });
+ }
+ },
+ isVideo(f) {
+ const mime = (f.mime_type || '').toLowerCase();
+ return mime.startsWith('video/') || /\.(mp4|webm|mov|m4v)$/i.test(f.name || '');
+ },
+ scrollCarousel(dir) {
+ const track = this.$refs.carouselTrack;
+ if (!track) return;
+ const card = track.querySelector('.carousel-card');
+ const step = card ? card.offsetWidth + 16 : track.clientWidth * 0.8;
+ track.scrollBy({ left: dir * step, behavior: 'smooth' });
+ },
+
// ── Uploads ─────────────────────────────────────────────────
uploadFiles(fileList) {
this.hudOpen = true;