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." %}

+
@@ -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;