Introduction of the images to the detailed view to the library_table.html
Release Inventarsystem / release-docker (push) Successful in 2m15s

This commit is contained in:
2026-08-12 23:10:06 +02:00
parent 057c517515
commit 99ec28f329
2 changed files with 119 additions and 51 deletions
+27 -7
View File
@@ -466,16 +466,36 @@ PERMISSION_ACTION_ENDPOINTS = {
} }
ALLOWED_COVER_DOMAINS = { ALLOWED_COVER_DOMAINS = {
# --- Google / Open APIs ---
"books.google.com", "books.google.com",
"covers.openlibrary.org", "www.googleapis.com",
"images-na.ssl-images-amazon.com",
"m.media-amazon.com", # --- Open Library / Internet Archive ---
"www.isbn.de",
"covers.openlibrary.org", "covers.openlibrary.org",
"openlibrary.org", "openlibrary.org",
# --- Amazon / Goodreads ---
"images-na.ssl-images-amazon.com",
"m.media-amazon.com",
"i.gr-assets.com", # Goodreads image CDN
# --- Library / Catalog Services ---
"www.isbn.de",
"lobid.org", "lobid.org",
"www.googleapis.com", "syndetics.com", # Standard cover provider for libraries
"www.westermann.de" "pics.librarything.com", # LibraryThing covers
"portal.dnb.de", # Deutsche Nationalbibliothek
# --- German Educational & International Publishers ---
"www.westermann.de",
"www.klett.de", # Ernst Klett Verlag
"medien.klett.de", # Klett media CDN
"www.cornelsen.de", # Cornelsen Verlag
"images.penguinrandomhouse.com", # Penguin Random House
# --- Book Retailer CDNs (often used for cover fetching) ---
"images.thalia.media", # Thalia
"bilder.buecher.de" # buecher.de
} }
SENSITIVE_AUDIT_FIELDS = ["email", "username", "full_name", "phone", "borrower", "ip"] SENSITIVE_AUDIT_FIELDS = ["email", "username", "full_name", "phone", "borrower", "ip"]
@@ -10044,7 +10064,7 @@ def download_book_cover():
return jsonify({"error": "Only public HTTPS URLs are allowed"}), 400 return jsonify({"error": "Only public HTTPS URLs are allowed"}), 400
# 2. SSRF Protection: Strict Allowlist Check # 2. SSRF Protection: Strict Allowlist Check
# if parsed_url.netloc not in ALLOWED_COVER_DOMAINS: # ---------------------------------------------- has to be implementet at a laterstage --------------------------------------------------------------- # if parsed_url.netloc not in ALLOWED_COVER_DOMAINS:
# return jsonify({"error": "Target host is not an allowed book cover provider"}), 403 # return jsonify({"error": "Target host is not an allowed book cover provider"}), 403
# Download the image (allow_redirects=False prevents redirecting to internal IPs) # Download the image (allow_redirects=False prevents redirecting to internal IPs)
+58 -10
View File
@@ -52,9 +52,10 @@
/* The Scrollable Content Area */ /* The Scrollable Content Area */
#detailContent { #detailContent {
overflow-y: auto; /* Adds scrollbar only if needed */ overflow-y: auto;
padding-right: 10px; /* Prevents text from rubbing against the scrollbar */ padding-right: 10px;
} }
/* Library table-only view styles */ /* Library table-only view styles */
.library-table-container { .library-table-container {
max-width: 1400px; max-width: 1400px;
@@ -276,6 +277,7 @@
border-bottom: 1px solid #eee; border-bottom: 1px solid #eee;
color: #555; color: #555;
font-size: 0.95em; font-size: 0.95em;
vertical-align: middle;
} }
.library-items-table tbody tr:hover { .library-items-table tbody tr:hover {
@@ -598,12 +600,12 @@
<table class="library-items-table"> <table class="library-items-table">
<thead> <thead>
<tr> <tr>
<th style="width: 24%;">Titel</th> <th style="width: 32%;">Titel</th>
<th style="width: 12%;">ISBN/Code</th> <th style="width: 12%;">ISBN/Code</th>
<th style="width: 8%;">Typ</th> <th style="width: 8%;">Typ</th>
<th style="width: 8%;">Anzahl</th> <th style="width: 8%;">Anzahl</th>
<th style="width: 12%;">Status</th> <th style="width: 14%;">Status</th>
<th style="width: 22%;">Aktionen</th> <th style="width: 26%;">Aktionen</th>
</tr> </tr>
</thead> </thead>
<tbody id="itemsTableBody"> <tbody id="itemsTableBody">
@@ -677,6 +679,11 @@
const canEditLibraryItems = (document.getElementById('libraryTableContainer')?.dataset.canEdit === '1'); const canEditLibraryItems = (document.getElementById('libraryTableContainer')?.dataset.canEdit === '1');
function isVideoFile(filename) {
if (!filename) return false;
return /\.(mp4|webm|ogg|mov)$/i.test(filename);
}
// ========================================================================= // =========================================================================
// 2. DATA LOADING & FILTERING ENGINE // 2. DATA LOADING & FILTERING ENGINE
// ========================================================================= // =========================================================================
@@ -705,7 +712,7 @@
} }
} catch (error) { } catch (error) {
console.error('Error loading library items:', error); console.error('Error loading library items:', error);
document.getElementById('itemsTableBody').innerHTML = '<tr><td colspan="7" style="text-align:center; color:#999;">Fehler beim Laden der Bibliothekselemente.</td></tr>'; document.getElementById('itemsTableBody').innerHTML = '<tr><td colspan="8" style="text-align:center; color:#999;">Fehler beim Laden der Bibliothekselemente.</td></tr>';
} finally { } finally {
pagingState.loading = false; pagingState.loading = false;
} }
@@ -797,6 +804,7 @@
const statusText = statusKey === 'damaged' ? 'Defekt/Zerstört' : (statusKey === 'borrowed' ? 'Ausgeliehen' : 'Verfügbar'); const statusText = statusKey === 'damaged' ? 'Defekt/Zerstört' : (statusKey === 'borrowed' ? 'Ausgeliehen' : 'Verfügbar');
const actionLabel = statusKey === 'available' ? 'Ausleihen' : (statusKey === 'borrowed' ? 'Reservieren' : 'Nicht ausleihbar'); const actionLabel = statusKey === 'available' ? 'Ausleihen' : (statusKey === 'borrowed' ? 'Reservieren' : 'Nicht ausleihbar');
const actionDisabled = statusKey === 'damaged' ? 'disabled' : ''; const actionDisabled = statusKey === 'damaged' ? 'disabled' : '';
return ` return `
<tr> <tr>
<td class="table-title">${escapeHtml(item.Name || 'Untitled')}</td> <td class="table-title">${escapeHtml(item.Name || 'Untitled')}</td>
@@ -1250,10 +1258,50 @@
const detailModal = document.getElementById('detailModal'); const detailModal = document.getElementById('detailModal');
// 1. Show the loading state immediately // 1. Show the loading state immediately
detailContent.innerHTML = '<p>Loading details...</p>'; detailContent.innerHTML = '<p>Lade Details...</p>';
detailModal.style.display = 'flex'; detailModal.style.display = 'flex';
// 2. Fetch the data // 2. Generate Image/Video Gallery Client-Side
const item = libraryItems.find(i => i._id === itemId);
let mediaHtml = '';
if (item && item.Images && item.Images.length > 0) {
const imagesHtml = item.Images.map((image, index) => {
const imageSrc = image.startsWith('/uploads/') || image.startsWith('http') ?
image :
`{{ url_for('uploaded_file', filename='') }}${image}`;
const thumbnailInfo = item.ThumbnailInfo && item.ThumbnailInfo[index];
const isVideo = isVideoFile(image);
if (isVideo) {
const videoSrc = thumbnailInfo && thumbnailInfo.has_thumbnail
? thumbnailInfo.thumbnail_url
: imageSrc;
if (thumbnailInfo && thumbnailInfo.has_thumbnail) {
return `<div class="video-container" style="position: relative; width: 120px; height: 120px; display: inline-block; margin-right: 15px; margin-bottom: 15px;">
<img src="${videoSrc}" alt="${escapeHtml(item.Name || '')}" class="item-image" style="width: 100%; height: 100%; object-fit: cover; border-radius: 8px; border: 1px solid #ddd;">
<div class="video-preview-overlay" style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: white; background: rgba(0,0,0,0.6); border-radius: 50%; width: 36px; height: 36px; display: flex; align-items: center; justify-content: center; font-size: 16px;">
</div>
</div>`;
} else {
return `<div style="width: 120px; height: 120px; background: #333; color: #fff; display: inline-flex; align-items: center; justify-content: center; border-radius: 8px; margin-right: 15px; margin-bottom: 15px;">VIDEO</div>`;
}
} else {
const imageSrcFinal = thumbnailInfo && thumbnailInfo.has_thumbnail
? thumbnailInfo.thumbnail_url
: imageSrc;
return `<img src="${imageSrcFinal}" alt="${escapeHtml(item.Name || '')}" class="item-image" style="width: 120px; height: 120px; object-fit: cover; border-radius: 8px; border: 1px solid #ddd; margin-right: 15px; margin-bottom: 15px;">`;
}
}).join('');
mediaHtml = `<div class="detail-gallery-container" style="margin-bottom: 20px; padding-bottom: 15px; border-bottom: 1px solid #eee; display: flex; flex-wrap: wrap;">${imagesHtml}</div>`;
}
// 3. Fetch the data
fetch(`/api/item_detail/${itemId}`) fetch(`/api/item_detail/${itemId}`)
.then(response => { .then(response => {
if (!response.ok) { if (!response.ok) {
@@ -1262,8 +1310,8 @@
return response.text(); return response.text();
}) })
.then(html => { .then(html => {
// 3. Clean the HTML and display it // 4. Clean the HTML and display it, injecting the media gallery before the fetched content
detailContent.innerHTML = DOMPurify.sanitize(html); detailContent.innerHTML = mediaHtml + DOMPurify.sanitize(html);
}) })
.catch(err => { .catch(err => {
console.error('Error loading detail:', err); console.error('Error loading detail:', err);