add thumbnails in frontend
This commit is contained in:
parent
8260cf5ae3
commit
ba0bf2d731
14
app.py
14
app.py
@ -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}
|
# don’t 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
|
||||||
|
|||||||
@ -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 can’t 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;
|
||||||
}
|
}
|
||||||
164
static/app.js
164
static/app.js
@ -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">📄</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">📄</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.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user