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.
@ -107,6 +109,10 @@ def list_directory_contents(directory, subpath):
# Skip hidden files and directories. # Skip hidden files and directories.
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"]:
@ -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

@ -236,4 +236,27 @@ footer {
transition: opacity 1s ease; transition: opacity 1s ease;
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,102 +44,113 @@ 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 (data.directories.length > 0) {
contentHTML += '<ul>'; if (isImageOnly) {
// Check if every directory name is short (≤15 characters) and no files are present // Display thumbnails grid
const areAllShort = data.directories.every(dir => dir.name.length <= 15) && data.files.length === 0; contentHTML += '<div class="images-grid">';
if (areAllShort & data.breadcrumbs.length != 1) { data.files.forEach(file => {
contentHTML += '<div class="directories-grid">'; const thumbUrl = `${file.path}?thumbnail=true`;
data.directories.forEach(dir => { contentHTML += `
contentHTML += `<div class="directory-item">📁 <a href="#" class="directory-link" data-path="${dir.path}">${dir.name}</a></div>`; <div class="image-item">
}); <a href="#" class="play-file image-link" data-url="${file.path}" data-file-type="${file.file_type}">
contentHTML += '</div>'; <img src="/media/${thumbUrl}" alt="${file.name}" class="thumbnail" />
} else { </a>
contentHTML += '<ul>'; </div>
data.directories.forEach(dir => { `;
contentHTML += `<li class="directory-item">📁 <a href="#" class="directory-link" data-path="${dir.path}">${dir.name}</a></li>`; });
}); contentHTML += '</div>';
if (data.breadcrumbs.length == 1) { } else {
contentHTML += `<li class="link-item" onclick="window.location.href='/search'">🔎 <a href="/search" class="link-link">Suche</a></li>`; // Render directories normally
if (data.directories.length > 0) {
const areAllShort = data.directories.every(dir => dir.name.length <= 15) && data.files.length === 0;
if (areAllShort && data.breadcrumbs.length !== 1) {
contentHTML += '<div class="directories-grid">';
data.directories.forEach(dir => {
contentHTML += `<div class="directory-item">📁 <a href="#" class="directory-link" data-path="${dir.path}">${dir.name}</a></div>`;
});
contentHTML += '</div>';
} else {
contentHTML += '<ul>';
data.directories.forEach(dir => {
contentHTML += `<li class="directory-item">📁 <a href="#" class="directory-link" data-path="${dir.path}">${dir.name}</a></li>`;
});
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 += '</ul>';
} }
}
// Render files (including music and non-image files)
currentMusicFiles = [];
if (data.files.length > 0) {
contentHTML += '<ul>';
data.files.forEach((file, idx) => {
let symbol = '📄';
if (file.file_type === 'music') {
symbol = '🔊';
currentMusicFiles.push({ path: file.path, index: idx, title: file.name.replace('.mp3', '') });
} else if (file.file_type === 'image') {
symbol = '🖼️';
}
const indexAttr = file.file_type === 'music' ? ` data-index="${currentMusicFiles.length - 1}"` : '';
contentHTML += `<li class="file-item">
<a href="#" class="play-file"${indexAttr} data-url="${file.path}" data-file-type="${file.file_type}">${symbol} ${file.name.replace('.mp3', '')}</a>`;
if (file.has_transcript) {
contentHTML += `<a href="#" class="show-transcript" data-url="${file.transcript_url}" title="Show Transcript">&#128196;</a>`;
}
contentHTML += `</li>`;
});
contentHTML += '</ul>'; contentHTML += '</ul>';
} }
} }
// Render files. // Insert generated content
currentMusicFiles = []; // Reset the music files array.
if (data.files.length > 0) {
contentHTML += '<ul>';
data.files.forEach((file, idx) => {
let symbol = '📄';
if (file.file_type === 'music') {
symbol = '🔊';
currentMusicFiles.push({ path: file.path, index: idx, title: file.name.replace('.mp3', '') });
} else if (file.file_type === 'image') {
symbol = '🖼️';
}
const indexAttr = file.file_type === 'music' ? ` data-index="${currentMusicFiles.length - 1}"` : '';
contentHTML += `<li class="file-item">
<a href="#" class="play-file"${indexAttr} data-url="${file.path}" data-file-type="${file.file_type}">${symbol} ${file.name.replace('.mp3', '')}</a>`;
if (file.has_transcript) {
contentHTML += `<a href="#" class="show-transcript" data-url="${file.transcript_url}" title="Show Transcript">&#128196;</a>`;
}
contentHTML += `</li>`;
});
contentHTML += '</ul>';
}
// Insert the generated HTML into a container in the DOM.
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 {
document.querySelectorAll('li.file-item').forEach(item => {
item.addEventListener('click', function(e) {
if (!e.target.closest('a')) {
const anchor = item.querySelector('a.play-file');
if (anchor) anchor.click();
}
});
});
}
// Now attach the event listeners for files. // Update gallery images for lightbox or similar
document.querySelectorAll('li.file-item').forEach(li => {
li.addEventListener('click', function(e) {
// Only trigger if the click did not start on an <a>.
if (!e.target.closest('a')) {
const anchor = li.querySelector('a.play-file');
if (anchor) {
anchor.click();
}
}
});
});
// Update global variable for gallery images (only image files).
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.