add thumbnails in frontend

This commit is contained in:
lelo 2025-04-21 14:08:23 +02:00
parent 8260cf5ae3
commit ba0bf2d731
3 changed files with 119 additions and 82 deletions

14
app.py
View File

@ -100,6 +100,8 @@ def list_directory_contents(directory, subpath):
music_exts = ('.mp3',) music_exts = ('.mp3',)
image_exts = ('.jpg', '.jpeg', '.png', '.gif', '.bmp') image_exts = ('.jpg', '.jpeg', '.png', '.gif', '.bmp')
blocked_filenames = ['Thumbs.db']
try: try:
with os.scandir(directory) as it: with os.scandir(directory) as it:
# Sorting by name if required. # Sorting by name if required.
@ -108,6 +110,10 @@ def list_directory_contents(directory, subpath):
if entry.name.startswith('.'): if entry.name.startswith('.'):
continue continue
# Skip blocked_filenames
if entry.name in blocked_filenames:
continue
if entry.is_dir(follow_symlinks=False): if entry.is_dir(follow_symlinks=False):
if entry.name in ["Transkription", "@eaDir"]: if entry.name in ["Transkription", "@eaDir"]:
continue continue
@ -287,7 +293,7 @@ def serve_file(subpath):
img = ImageOps.exif_transpose(orig) img = ImageOps.exif_transpose(orig)
variants = { variants = {
orig_key: (1920, 1920), orig_key: (1920, 1920),
small_key: ( 192, 192), small_key: ( 480, 480),
} }
for key, size in variants.items(): for key, size in variants.items():
thumb = img.copy() thumb = img.copy()
@ -295,10 +301,8 @@ def serve_file(subpath):
if thumb.mode in ("RGBA", "P"): if thumb.mode in ("RGBA", "P"):
thumb = thumb.convert("RGB") thumb = thumb.convert("RGB")
bio = io.BytesIO() bio = io.BytesIO()
save_kwargs = {'format': 'JPEG', 'quality': 85} # dont pass exif_bytes here
if exif_bytes: thumb.save(bio, format='JPEG', quality=85)
save_kwargs['exif'] = exif_bytes
thumb.save(bio, **save_kwargs)
bio.seek(0) bio.seek(0)
cache.set(key, bio, read=True) cache.set(key, bio, read=True)
# Read back the variant we need # Read back the variant we need

View File

@ -237,3 +237,26 @@ footer {
opacity: 0; opacity: 0;
cursor: auto; cursor: auto;
} }
.images-grid {
display: grid;
/* make each column exactly 150px wide */
grid-template-columns: repeat(auto-fill, 150px);
/* force every row exactly 150px tall */
grid-auto-rows: 150px;
gap: 14px;
}
.image-item {
/* ensure the thumbnail cant overflow its cell */
overflow: hidden;
}
.thumbnail {
/* fill the full cell */
width: 100%;
height: 100%;
/* crop/scale to cover without stretching */
object-fit: cover;
border-radius: 4px;
}

View File

@ -34,8 +34,7 @@ function paintFile() {
} }
function renderContent(data) { function renderContent(data) {
// Render breadcrumbs
// Render breadcrumbs, directories (grid view when appropriate), and files.
let breadcrumbHTML = ''; let breadcrumbHTML = '';
data.breadcrumbs.forEach((crumb, index) => { data.breadcrumbs.forEach((crumb, index) => {
breadcrumbHTML += `<a href="#" class="breadcrumb-link" data-path="${crumb.path}">${crumb.name}</a>`; breadcrumbHTML += `<a href="#" class="breadcrumb-link" data-path="${crumb.path}">${crumb.name}</a>`;
@ -45,14 +44,32 @@ function renderContent(data) {
}); });
document.getElementById('breadcrumbs').innerHTML = breadcrumbHTML; document.getElementById('breadcrumbs').innerHTML = breadcrumbHTML;
// Check for image-only directory (no subdirectories, at least one file, all images)
const isImageOnly = data.directories.length === 0
&& data.files.length > 0
&& data.files.every(file => file.file_type === 'image');
// Render directories.
let contentHTML = ''; let contentHTML = '';
if (isImageOnly) {
// Display thumbnails grid
contentHTML += '<div class="images-grid">';
data.files.forEach(file => {
const thumbUrl = `${file.path}?thumbnail=true`;
contentHTML += `
<div class="image-item">
<a href="#" class="play-file image-link" data-url="${file.path}" data-file-type="${file.file_type}">
<img src="/media/${thumbUrl}" alt="${file.name}" class="thumbnail" />
</a>
</div>
`;
});
contentHTML += '</div>';
} else {
// Render directories normally
if (data.directories.length > 0) { if (data.directories.length > 0) {
contentHTML += '<ul>';
// Check if every directory name is short (≤15 characters) and no files are present
const areAllShort = data.directories.every(dir => dir.name.length <= 15) && data.files.length === 0; const areAllShort = data.directories.every(dir => dir.name.length <= 15) && data.files.length === 0;
if (areAllShort & data.breadcrumbs.length != 1) { if (areAllShort && data.breadcrumbs.length !== 1) {
contentHTML += '<div class="directories-grid">'; contentHTML += '<div class="directories-grid">';
data.directories.forEach(dir => { data.directories.forEach(dir => {
contentHTML += `<div class="directory-item">📁 <a href="#" class="directory-link" data-path="${dir.path}">${dir.name}</a></div>`; contentHTML += `<div class="directory-item">📁 <a href="#" class="directory-link" data-path="${dir.path}">${dir.name}</a></div>`;
@ -63,15 +80,15 @@ function renderContent(data) {
data.directories.forEach(dir => { data.directories.forEach(dir => {
contentHTML += `<li class="directory-item">📁 <a href="#" class="directory-link" data-path="${dir.path}">${dir.name}</a></li>`; contentHTML += `<li class="directory-item">📁 <a href="#" class="directory-link" data-path="${dir.path}">${dir.name}</a></li>`;
}); });
if (data.breadcrumbs.length == 1) { if (data.breadcrumbs.length === 1) {
contentHTML += `<li class="link-item" onclick="window.location.href='/search'">🔎 <a href="/search" class="link-link">Suche</a></li>`; contentHTML += `<li class="link-item" onclick="window.location.href='/search'">🔎 <a href="/search" class="link-link">Suche</a></li>`;
} }
contentHTML += '</ul>'; contentHTML += '</ul>';
} }
} }
// Render files. // Render files (including music and non-image files)
currentMusicFiles = []; // Reset the music files array. currentMusicFiles = [];
if (data.files.length > 0) { if (data.files.length > 0) {
contentHTML += '<ul>'; contentHTML += '<ul>';
data.files.forEach((file, idx) => { data.files.forEach((file, idx) => {
@ -92,55 +109,48 @@ function renderContent(data) {
}); });
contentHTML += '</ul>'; contentHTML += '</ul>';
} }
}
// Insert the generated HTML into a container in the DOM. // Insert generated content
document.getElementById('content').innerHTML = contentHTML; document.getElementById('content').innerHTML = contentHTML;
// Now attach the event listeners for directories. // Attach event listeners for directories
document.querySelectorAll('div.directory-item').forEach(li => { document.querySelectorAll('.directory-item').forEach(item => {
li.addEventListener('click', function(e) { item.addEventListener('click', function(e) {
// Only trigger if the click did not start on an <a>.
if (!e.target.closest('a')) { if (!e.target.closest('a')) {
const anchor = li.querySelector('a.directory-link'); const anchor = item.querySelector('a.directory-link');
if (anchor) { if (anchor) anchor.click();
anchor.click();
}
} }
}); });
}); });
// Now attach the event listeners for directories. // Attach event listeners for file items (including images)
document.querySelectorAll('li.directory-item').forEach(li => { if (isImageOnly) {
li.addEventListener('click', function(e) { document.querySelectorAll('.image-item').forEach(item => {
// Only trigger if the click did not start on an <a>. item.addEventListener('click', function(e) {
if (!e.target.closest('a')) { if (!e.target.closest('a')) {
const anchor = li.querySelector('a.directory-link'); const anchor = item.querySelector('a.image-link');
if (anchor) { if (anchor) anchor.click();
anchor.click();
}
} }
}); });
}); });
} else {
// Now attach the event listeners for files. document.querySelectorAll('li.file-item').forEach(item => {
document.querySelectorAll('li.file-item').forEach(li => { item.addEventListener('click', function(e) {
li.addEventListener('click', function(e) {
// Only trigger if the click did not start on an <a>.
if (!e.target.closest('a')) { if (!e.target.closest('a')) {
const anchor = li.querySelector('a.play-file'); const anchor = item.querySelector('a.play-file');
if (anchor) { if (anchor) anchor.click();
anchor.click();
}
} }
}); });
}); });
}
// Update global variable for gallery images (only image files). // Update gallery images for lightbox or similar
currentGalleryImages = data.files currentGalleryImages = data.files
.filter(f => f.file_type === 'image') .filter(f => f.file_type === 'image')
.map(f => f.path); .map(f => f.path);
attachEventListeners(); // Reattach event listeners for newly rendered elements. attachEventListeners();
} }
// Fetch directory data from the API. // Fetch directory data from the API.