Introduction of the images to the detailed view to the library_table.html
Release Inventarsystem / release-docker (push) Successful in 2m15s
Release Inventarsystem / release-docker (push) Successful in 2m15s
This commit is contained in:
+27
-7
@@ -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)
|
||||||
|
|||||||
@@ -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 {
|
||||||
@@ -500,14 +502,14 @@
|
|||||||
|
|
||||||
<!-- Search and Filter Toggle -->
|
<!-- Search and Filter Toggle -->
|
||||||
<div class="library-search-bar">
|
<div class="library-search-bar">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
id="librarySearch"
|
id="librarySearch"
|
||||||
class="library-search-input"
|
class="library-search-input"
|
||||||
placeholder="Nach Titel, ISBN suchen..."
|
placeholder="Nach Titel, ISBN suchen..."
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
id="filterToggleBtn"
|
id="filterToggleBtn"
|
||||||
class="library-filter-toggle-btn"
|
class="library-filter-toggle-btn"
|
||||||
aria-label="Filter öffnen/schließen"
|
aria-label="Filter öffnen/schließen"
|
||||||
>
|
>
|
||||||
@@ -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">
|
||||||
@@ -656,11 +658,11 @@
|
|||||||
const RENDER_BATCH_COUNT = 120;
|
const RENDER_BATCH_COUNT = 120;
|
||||||
let renderedCount = INITIAL_RENDER_COUNT;
|
let renderedCount = INITIAL_RENDER_COUNT;
|
||||||
let filterPanelOpen = false;
|
let filterPanelOpen = false;
|
||||||
|
|
||||||
// Scanner Related State Variables
|
// Scanner Related State Variables
|
||||||
let scannerInstance = null;
|
let scannerInstance = null;
|
||||||
let scannerRunning = false;
|
let scannerRunning = false;
|
||||||
let activeScannerCallback = null;
|
let activeScannerCallback = null;
|
||||||
let activeStudentCardId = '';
|
let activeStudentCardId = '';
|
||||||
let lastScanValue = '';
|
let lastScanValue = '';
|
||||||
let lastScanAt = 0;
|
let lastScanAt = 0;
|
||||||
@@ -674,9 +676,14 @@
|
|||||||
seriesGroupId: '',
|
seriesGroupId: '',
|
||||||
groupMembers: []
|
groupMembers: []
|
||||||
};
|
};
|
||||||
|
|
||||||
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>
|
||||||
@@ -877,33 +885,33 @@
|
|||||||
function startScanner(targetCallback) {
|
function startScanner(targetCallback) {
|
||||||
const readerWrap = document.getElementById('scanReaderWrap');
|
const readerWrap = document.getElementById('scanReaderWrap');
|
||||||
const toggleBtn = document.getElementById('toggleScannerBtn');
|
const toggleBtn = document.getElementById('toggleScannerBtn');
|
||||||
|
|
||||||
activeScannerCallback = targetCallback;
|
activeScannerCallback = targetCallback;
|
||||||
|
|
||||||
if (readerWrap) readerWrap.style.display = 'block';
|
if (readerWrap) readerWrap.style.display = 'block';
|
||||||
setScanStatus('Initializing camera...', 'warn');
|
setScanStatus('Initializing camera...', 'warn');
|
||||||
|
|
||||||
Quagga.init({
|
Quagga.init({
|
||||||
inputStream: {
|
inputStream: {
|
||||||
name: "Live",
|
name: "Live",
|
||||||
type: "LiveStream",
|
type: "LiveStream",
|
||||||
target: document.querySelector('#library-scanner-container'),
|
target: document.querySelector('#library-scanner-container'),
|
||||||
constraints: {
|
constraints: {
|
||||||
width: 640,
|
width: 640,
|
||||||
height: 480,
|
height: 480,
|
||||||
facingMode: "environment"
|
facingMode: "environment"
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
decoder: {
|
decoder: {
|
||||||
readers: [
|
readers: [
|
||||||
"code_128_reader",
|
"code_128_reader",
|
||||||
"ean_reader",
|
"ean_reader",
|
||||||
"code_39_reader",
|
"code_39_reader",
|
||||||
"upc_reader",
|
"upc_reader",
|
||||||
"codabar_reader",
|
"codabar_reader",
|
||||||
"i2of5_reader"
|
"i2of5_reader"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}, function(err) {
|
}, function(err) {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.error('Scanner start failed:', err);
|
console.error('Scanner start failed:', err);
|
||||||
@@ -912,41 +920,41 @@
|
|||||||
setScanStatus(`Scanner konnte nicht gestartet werden${detail}`, 'error');
|
setScanStatus(`Scanner konnte nicht gestartet werden${detail}`, 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Quagga.start();
|
Quagga.start();
|
||||||
scannerRunning = true;
|
scannerRunning = true;
|
||||||
|
|
||||||
if (!targetCallback && toggleBtn) {
|
if (!targetCallback && toggleBtn) {
|
||||||
toggleBtn.textContent = 'Scanner stoppen';
|
toggleBtn.textContent = 'Scanner stoppen';
|
||||||
}
|
}
|
||||||
setScanStatus('Scanner aktiv. Jetzt Code scannen.', 'warn');
|
setScanStatus('Scanner aktiv. Jetzt Code scannen.', 'warn');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function stopScanner() {
|
function stopScanner() {
|
||||||
if (!scannerRunning) return;
|
if (!scannerRunning) return;
|
||||||
|
|
||||||
const readerWrap = document.getElementById('scanReaderWrap');
|
const readerWrap = document.getElementById('scanReaderWrap');
|
||||||
const toggleBtn = document.getElementById('toggleScannerBtn');
|
const toggleBtn = document.getElementById('toggleScannerBtn');
|
||||||
|
|
||||||
Quagga.stop();
|
Quagga.stop();
|
||||||
scannerRunning = false;
|
scannerRunning = false;
|
||||||
activeScannerCallback = null;
|
activeScannerCallback = null;
|
||||||
|
|
||||||
if (readerWrap) readerWrap.style.display = 'none';
|
if (readerWrap) readerWrap.style.display = 'none';
|
||||||
if (toggleBtn) toggleBtn.textContent = 'Scanner starten';
|
if (toggleBtn) toggleBtn.textContent = 'Scanner starten';
|
||||||
setScanStatus('Scanner gestoppt.', 'warn');
|
setScanStatus('Scanner gestoppt.', 'warn');
|
||||||
}
|
}
|
||||||
|
|
||||||
Quagga.onDetected(function(data) {
|
Quagga.onDetected(function(data) {
|
||||||
if (!data || !data.codeResult || !data.codeResult.code) return;
|
if (!data || !data.codeResult || !data.codeResult.code) return;
|
||||||
|
|
||||||
const barcode = String(data.codeResult.code || '').trim();
|
const barcode = String(data.codeResult.code || '').trim();
|
||||||
console.log("Barcode detected:", barcode);
|
console.log("Barcode detected:", barcode);
|
||||||
|
|
||||||
const currentCallback = activeScannerCallback;
|
const currentCallback = activeScannerCallback;
|
||||||
stopScanner();
|
stopScanner();
|
||||||
|
|
||||||
const returnOnly = (document.getElementById('returnOnlyToggle') || {}).checked;
|
const returnOnly = (document.getElementById('returnOnlyToggle') || {}).checked;
|
||||||
if (returnOnly) {
|
if (returnOnly) {
|
||||||
// direct return flow
|
// direct return flow
|
||||||
@@ -1008,21 +1016,21 @@
|
|||||||
function handleScanSuccess(decodedText) {
|
function handleScanSuccess(decodedText) {
|
||||||
const scannedCode = normalizeScannedCode(decodedText);
|
const scannedCode = normalizeScannedCode(decodedText);
|
||||||
if (!scannedCode) return;
|
if (!scannedCode) return;
|
||||||
|
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
if (scannedCode === lastScanValue && (now - lastScanAt) < 1500) {
|
if (scannedCode === lastScanValue && (now - lastScanAt) < 1500) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
lastScanValue = scannedCode;
|
lastScanValue = scannedCode;
|
||||||
lastScanAt = now;
|
lastScanAt = now;
|
||||||
|
|
||||||
const mode = (document.getElementById('scanModeSelect') || {}).value || 'card_only';
|
const mode = (document.getElementById('scanModeSelect') || {}).value || 'card_only';
|
||||||
if (mode === 'card_only') {
|
if (mode === 'card_only') {
|
||||||
setActiveStudentCard(scannedCode);
|
setActiveStudentCard(scannedCode);
|
||||||
setScanStatus(`Ausweis gesetzt: ${activeStudentCardId}`, 'ok');
|
setScanStatus(`Ausweis gesetzt: ${activeStudentCardId}`, 'ok');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
processQuickToggleScan(scannedCode);
|
processQuickToggleScan(scannedCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user