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

View File

@ -237,3 +237,26 @@ footer {
opacity: 0;
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) {
// Render breadcrumbs, directories (grid view when appropriate), and files.
// Render breadcrumbs
let breadcrumbHTML = '';
data.breadcrumbs.forEach((crumb, index) => {
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;
// 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 = '';
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;
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>`;
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) {
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>';
}
}
// Render files.
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.
// Insert generated content
document.getElementById('content').innerHTML = contentHTML;
// Now attach the event listeners for directories.
document.querySelectorAll('div.directory-item').forEach(li => {
li.addEventListener('click', function(e) {
// Only trigger if the click did not start on an <a>.
// Attach event listeners for directories
document.querySelectorAll('.directory-item').forEach(item => {
item.addEventListener('click', function(e) {
if (!e.target.closest('a')) {
const anchor = li.querySelector('a.directory-link');
if (anchor) {
anchor.click();
}
const anchor = item.querySelector('a.directory-link');
if (anchor) anchor.click();
}
});
});
// Now attach the event listeners for directories.
document.querySelectorAll('li.directory-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.directory-link');
if (anchor) {
anchor.click();
// Attach event listeners for file items (including images)
if (isImageOnly) {
document.querySelectorAll('.image-item').forEach(item => {
item.addEventListener('click', function(e) {
if (!e.target.closest('a')) {
const anchor = item.querySelector('a.image-link');
if (anchor) anchor.click();
}
}
});
});
});
// Now attach the event listeners for files.
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();
} 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();
}
}
});
});
});
}
// Update global variable for gallery images (only image files).
// Update gallery images for lightbox or similar
currentGalleryImages = data.files
.filter(f => f.file_type === 'image')
.map(f => f.path);
attachEventListeners(); // Reattach event listeners for newly rendered elements.
attachEventListeners();
}
// Fetch directory data from the API.