Compare commits

...

15 Commits

Author SHA1 Message Date
Aiirondev_dev 5136e40587 Implementation of a clean up function for a stray collection processing
Release Inventarsystem / release-docker (push) Successful in 2m15s
2026-08-16 23:08:10 +02:00
Aiirondev_dev c62b2b553d improvements in processing the library item uploading
Release Inventarsystem / release-docker (push) Successful in 2m16s
2026-08-16 22:50:17 +02:00
Aiirondev_dev 8783f97a09 improvements in displaying the Images in the detailed view
Release Inventarsystem / release-docker (push) Successful in 2m15s
2026-08-16 22:19:41 +02:00
Aiirondev_dev 43e09b41f1 Slight change to the Style for the scnaner
Release Inventarsystem / release-docker (push) Successful in 2m16s
2026-08-16 21:23:04 +02:00
Aiirondev_dev 84257dc289 Fix / implementation of the according wrapper for the scanner processing
Release Inventarsystem / release-docker (push) Successful in 2m18s
2026-08-16 21:07:47 +02:00
Aiirondev_dev c4b3850369 Merge remote-tracking branch 'origin/main'
Release Inventarsystem / release-docker (push) Successful in 2m15s
2026-08-16 20:06:12 +02:00
Aiirondev_dev 28e9487fe1 Improved implementation of the Klassen processing to havbe the already existing ones in a dropdown. 2026-08-16 20:06:05 +02:00
Aiirondev_dev fe075938d7 resolve camara scan stream failure
Release Inventarsystem / release-docker (push) Successful in 2m15s
2026-08-16 15:57:12 +02:00
Aiirondev_dev 0ef928efd8 slight fix of the scanner
Release Inventarsystem / release-docker (push) Successful in 2m14s
2026-08-16 15:50:48 +02:00
Aiirondev_dev 1299140823 Fix of the encryption processing with the Student class
Release Inventarsystem / release-docker (push) Successful in 2m14s
2026-08-16 13:17:20 +02:00
Aiirondev_dev ea48d5c28a decryption of the parsed fields
Release Inventarsystem / release-docker (push) Successful in 2m15s
2026-08-16 13:08:34 +02:00
Aiirondev_dev 514065f4af fixe of a list error
Release Inventarsystem / release-docker (push) Successful in 3m7s
2026-08-16 12:58:31 +02:00
Aiirondev_dev 90da8487f2 feat(student-cards): add class-specific PDF export with dynamic dropdown
Release Inventarsystem / release-docker (push) Successful in 2m16s
- Implement /student_card_class_barcode_download route to filter student cards by class
- Dynamically extract unique class names from the database for frontend selection
- Update student_cards_admin.html to replace the text input with a class selection dropdown
2026-08-15 18:10:05 +02:00
Aiirondev_dev b1ca358d41 Cleanup of the front end and backend of the upload function to only allow images and no videos because of space and memory reasons as well as hardening the uploading process.
Release Inventarsystem / release-docker (push) Successful in 2m15s
2026-08-15 01:38:35 +02:00
Aiirondev_dev 47cba5865d Removal of dupolicate function.
Release Inventarsystem / release-docker (push) Successful in 4m55s
2026-08-15 01:33:38 +02:00
5 changed files with 796 additions and 531 deletions
+436 -315
View File
File diff suppressed because it is too large Load Diff
+135 -1
View File
@@ -25,6 +25,7 @@ import datetime
import Web.modules.database.settings as cfg
from Web.modules.database.settings import MongoClient
import Web.modules.inventarsystem.data_protection as dp
import logging
def is_library_item(item):
@@ -1292,4 +1293,137 @@ def sync_group_codes(primary_obj_id, base_code, individual_codes_list):
return True
except Exception as e:
print(f"Error syncing group codes: {e}")
return False
return False
logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s')
def cleanup_orphaned_images(fs, dry_run=True):
"""
Finds images in GridFS that are no longer referenced by any item
and optionally deletes them.
Supported item fields:
- book_cover_image
- image
- images
The fields are expected to contain GridFS file ObjectIds.
:param fs: GridFS instance, e.g. gridfs.GridFS(db)
:param dry_run: If True, only reports what would be deleted.
If False, actually deletes the files.
"""
logging.info("Starte Cleanup-Skript...")
client = MongoClient(cfg.MONGODB_HOST, cfg.MONGODB_PORT)
try:
db = client[cfg.MONGODB_DB]
items_collection = db["items"]
referenced_files = set()
for item in items_collection.find(
{},
{
"Images": 1
}
):
images = item.get("Images")
if isinstance(images, list):
for img in images:
if img:
referenced_files.add(img)
referenced_files.discard(None)
logging.info(
f"{len(referenced_files)} referenzierte GridFS-Dateien gefunden."
)
orphaned_files = []
for grid_file in fs.find():
file_id = grid_file._id
filename = grid_file.filename or ""
if not filename.lower().endswith(
(".jpg", ".jpeg", ".png", ".gif", ".webp")
):
continue
if file_id not in referenced_files:
orphaned_files.append(
{
"_id": file_id,
"filename": filename,
"upload_date": grid_file.upload_date,
}
)
logging.info(
f"Gefundene verwaiste Bilder: {len(orphaned_files)}"
)
if dry_run:
logging.info(
"--- DRY RUN AKTIV - Es wird nichts gelöscht ---"
)
for file in orphaned_files:
logging.info(
f"Würde löschen: "
f"{file['filename']} "
f"(ID: {file['_id']}, "
f"Hochgeladen: {file['upload_date']})"
)
logging.info(
"--- Setze dry_run=False, um physisch zu löschen ---"
)
else:
logging.warning("--- LÖSCHVORGANG AKTIV ---")
deleted_count = 0
failed_count = 0
for file in orphaned_files:
try:
fs.delete(file["_id"])
deleted_count += 1
logging.info(
f"Gelöscht: {file['filename']} "
f"(ID: {file['_id']})"
)
except Exception as e:
failed_count += 1
logging.error(
f"Fehler beim Löschen von "
f"{file['filename']} "
f"(ID: {file['_id']}): {e}"
)
logging.info(
f"Cleanup beendet. "
f"{deleted_count} Bilder gelöscht, "
f"{failed_count} Fehler."
)
return {
"status": "success",
"referenced_count": len(referenced_files),
"orphaned_count": len(orphaned_files),
"dry_run": dry_run,
}
finally:
client.close()
+125 -104
View File
@@ -135,21 +135,6 @@
.library-scan-status.warn { color: #9a6700; }
.library-scan-status.error { color: #b42318; }
.library-scan-reader-wrap {
display: none;
margin-top: 12px;
max-width: 460px;
background: var(--ui-surface);
border: 1px solid #d9dde4;
border-radius: 8px;
overflow: hidden;
}
.library-scan-reader {
width: 100%;
min-height: 280px;
}
/* Filters */
.library-filter-toggle-btn {
padding: 10px 16px;
@@ -431,6 +416,40 @@
.library-scan-reader-wrap { max-width: 100%; }
}
.library-scan-reader-wrap {
display: none;
margin-top: 15px;
max-width: 640px;
margin-left: auto;
margin-right: auto;
background: #000;
border: 1px solid #d9dde4;
border-radius: 8px;
overflow: hidden;
grid-column: 1 / -1;
}
.library-scan-reader {
width: 100%;
min-height: 280px;
position: relative;
}
.library-scan-reader video {
width: 100%;
height: auto;
display: block;
object-fit: cover;
}
.library-scan-reader canvas.drawing,
.library-scan-reader canvas.drawingBuffer {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
</style>
<div class="library-table-container" id="libraryTableContainer" data-can-edit="{{ 1 if current_permissions.actions.get('can_edit', False) else 0 }}">
@@ -457,7 +476,6 @@
🔍 Filter
</button>
</div>
<div class="library-scan-controls">
<select id="scanModeSelect" aria-label="Scan-Modus">
<option value="card_only">Nur Ausweis erfassen</option>
@@ -475,6 +493,10 @@
</label>
<button id="manualActionBtn" class="button" type="button" style="margin-left:6px; background:#4f46e5; color:white;">Code verarbeiten</button>
</div>
<div class="library-scan-reader-wrap" id="scanReaderWrap" style="display: none; margin-top: 15px;">
<div class="library-scan-reader" style="width: 100%; max-width: 640px; margin: 0 auto; overflow: hidden; border-radius: 8px; border: 2px solid #ccc;">
</div>
</div>
<div id="filterPanel" class="library-filter-panel">
<div class="filter-row">
<div class="filter-item">
@@ -805,71 +827,92 @@
deleteLibraryItem(itemId);
}
// =========================================================================
// 3. CORE SCANNER ROUTING ENGINE (QUAGGA2)
// =========================================================================
function startScanner(targetCallback) {
const readerWrap = document.getElementById('scanReaderWrap');
async function startScanner(callback = null) {
if (scannerRunning) {
console.warn("Scanner is already running. Stopping it first...");
await stopScanner();
}
activeScannerCallback = callback;
const scannerWrap = document.querySelector('.library-scan-reader-wrap');
const toggleBtn = document.getElementById('toggleScannerBtn');
activeScannerCallback = targetCallback;
// 1. CRITICAL FIX: Make the container visible BEFORE Quagga initializes
// Quagga cannot calculate video dimensions if display is 'none'
if (scannerWrap) {
scannerWrap.style.display = 'block';
}
if (readerWrap) readerWrap.style.display = 'block';
setScanStatus('Initializing camera...', 'warn');
setScanStatus('Starte Kamera...', 'warn');
Quagga.init({
inputStream: {
name: "Live",
type: "LiveStream",
target: document.querySelector('#library-scanner-container'),
constraints: {
width: 640,
height: 480,
facingMode: "environment"
// 2. Initialize Quagga with a slight delay to allow the DOM to render the block
setTimeout(() => {
Quagga.init({
inputStream: {
name: "Live",
type: "LiveStream",
// This MUST match the inner div where the video should appear
target: document.querySelector('.library-scan-reader'),
constraints: {
width: 640,
height: 480,
facingMode: "environment" // Prefer back camera on mobile
}
},
},
decoder: {
readers: [
"code_128_reader",
"ean_reader",
"code_39_reader",
"upc_reader",
"codabar_reader",
"i2of5_reader"
]
}
}, function(err) {
if (err) {
console.error('Scanner start failed:', err);
if (readerWrap) readerWrap.style.display = 'none';
const detail = (err && (err.message || err.name)) ? ` (${err.message || err.name})` : '';
setScanStatus(`Scanner konnte nicht gestartet werden${detail}`, 'error');
return;
}
locator: {
patchSize: "medium",
halfSample: true
},
decoder: {
// Keep only the barcode types you actually use to improve performance
readers: ["code_128_reader", "ean_reader", "code_39_reader"]
},
locate: true
}, function(err) {
if (err) {
// This will finally log the actual error (e.g., NotAllowedError) if permissions fail
console.error("Quagga initialization failed:", err);
setScanStatus('Kamera-Fehler: ' + (err.name || err), 'error');
Quagga.start();
scannerRunning = true;
if (scannerWrap) scannerWrap.style.display = 'none';
scannerRunning = false;
return;
}
if (!targetCallback && toggleBtn) {
toggleBtn.textContent = 'Scanner stoppen';
}
setScanStatus('Scanner aktiv. Jetzt Code scannen.', 'warn');
});
Quagga.start();
scannerRunning = true;
setScanStatus('Scanner bereit.', 'ok');
if (toggleBtn) {
toggleBtn.textContent = 'Scanner stoppen';
}
});
}, 50);
}
function stopScanner() {
async function stopScanner() {
if (!scannerRunning) return;
const readerWrap = document.getElementById('scanReaderWrap');
const toggleBtn = document.getElementById('toggleScannerBtn');
try {
Quagga.stop();
} catch (e) {
console.warn("Error stopping Quagga (it may not have been running):", e);
}
Quagga.stop();
scannerRunning = false;
activeScannerCallback = null;
if (readerWrap) readerWrap.style.display = 'none';
if (toggleBtn) toggleBtn.textContent = 'Scanner starten';
setScanStatus('Scanner gestoppt.', 'warn');
// Hide the container to free up UI space
const scannerWrap = document.querySelector('.library-scan-reader-wrap');
if (scannerWrap) {
scannerWrap.style.display = 'none';
}
const toggleBtn = document.getElementById('toggleScannerBtn');
if (toggleBtn) {
toggleBtn.textContent = 'Kamera Scanner'; // Reset button text
}
}
async function isStudentCardBarcode(code) {
@@ -1288,51 +1331,29 @@
detailContent.innerHTML = '<p>Lade Details...</p>';
detailModal.style.display = 'flex';
// Sicherer Zugriff auf das Item mit Fallback
const item = libraryItems.find(i => i._id === itemId);
let mediaHtml = '';
// Robuste Prüfung: Wir testen gängige Benennungen aus deinem Backend
const imageArray = item.Images || item.Bilder || item.images;
if (item) {
// Prüfe gängige Array-Namen aus dem Backend
const imageArray = item.Images || item.Bilder || item.images;
if (item && Array.isArray(imageArray) && imageArray.length > 0) {
const imagesHtml = imageArray.map((image, index) => {
if (Array.isArray(imageArray) && imageArray.length > 0) {
const imagesHtml = imageArray.map(image => {
// Direkter, robuster Pfad zur Upload-Route
const imageSrc = image.startsWith('/uploads/') || image.startsWith('http')
? image
: `/uploads/${image}`;
// Dein neuer Code für die exakte Routen-Generierung
const imageSrc = image.startsWith('/uploads/') || image.startsWith('http') ?
image :
`{{ url_for('uploaded_file', filename='') }}${image}`;
return `<img src="${imageSrc}" alt="${escapeHtml(item.Name || 'Medium')}" 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('');
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 || 'Medium')}" 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 || 'Medium')}" 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>`;
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>`;
}
}
// Zusätzliche Details vom Backend laden
fetch(`/api/item_detail/${itemId}`)
.then(response => {
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
@@ -1347,7 +1368,7 @@
});
}
// Closes the modal via the 'x' button
// Schließt das Modal über den 'x'-Button
function closeDetailModal() {
document.getElementById('detailModal').style.display = 'none';
}
+18 -2
View File
@@ -232,6 +232,16 @@
<h1>📚 Bibliotheksausweise (Bibliothek)</h1>
</div>
<div class="export-buttons">
<form method="GET" action="{{ url_for('student_card_class_barcode_download') }}" style="display: inline-flex; gap: 5px; align-items: center; background: white; padding: 2px; border-radius: 4px; border: 1px solid #ddd;">
<select name="class_name" required style="border: none; padding: 8px; outline: none; font-size: 14px; background: transparent; cursor: pointer;">
<option value="" disabled selected>-- Klasse wählen --</option>
{% for cls in available_classes %}
<option value="{{ cls }}">{{ cls }}</option>
{% endfor %}
</select>
<button type="submit" class="btn-print" style="background: #17a2b8; padding: 8px 12px; margin: 0;">📤 PDF</button>
</form>
<a href="{{ url_for('student_card_barcode_download') }}" class="btn-print" style="background: #28a745;">📥 Alle Ausweise (PDF)</a>
<a href="{{ url_for('library_admin') }}" class="btn btn-primary">← Zur Bibliotheks-Upload</a>
</div>
@@ -283,9 +293,15 @@
</div>
<div class="form-group">
<label for="class_name">Klasse</label>
<input type="text" id="class_name" name="class_name"
<input type="text" id="class_name" name="class_name" list="class_list"
value="{{ form_data.get('class_name', '') }}"
placeholder="z.B. 10A">
placeholder="z.B. 10A (Tippen oder Auswählen)">
<datalist id="class_list">
{% for cls in available_classes %}
<option value="{{ cls }}">
{% endfor %}
</datalist>
</div>
</div>
+82 -109
View File
@@ -836,7 +836,7 @@
<!-- Options will be loaded by JavaScript -->
</select>
</div>
<div class="formupload_admin-group">
<div class="form-group">
<label for="filter2-4">Wert 4:</label>
<select id="filter2-4" name="filter2" class="filter-dropdown-select">
<option value="">-- Optional --</option>
@@ -876,12 +876,14 @@
</div>
<!-- Image upload (hidden for library mode) -->
<div class="form-group">
<label for="images">Bilder/Videos:</label>
<input type="file" id="images" name="images" accept=".jpg, .jpeg, .png, .gif, .mp4, .mov, .avi, .mkv, .webm, .flv, .m4v, .3gp" multiple>
<div class="allowed-formats">Erlaubte Formate: JPG, JPEG, PNG, GIF, MP4, MOV, AVI, MKV, WEBM, FLV, M4V, 3GP</div>
<!-- Add image preview area -->
<label>Buchcover (automatisch):</label>
<div id="book-cover-preview-container"></div>
</div>
<div class="form-group">
<label for="images"> Bilder hinzufügen:</label>
<input type="file" id="images" name="images" accept=".jpg, .jpeg, .png, .gif" multiple>
<div class="allowed-formats">Erlaubte Formate: JPG, JPEG, PNG, GIF</div>
<div class="image-preview-container" id="image-preview-container"></div>
</div>
@@ -1669,23 +1671,24 @@
}, 3000);
}
// Function to download book cover image
function downloadBookCover(imageUrl) {
if (!imageUrl) {
console.log('No image URL provided');
return;
}
// Show loading indicator for image download
const imagePreviewContainer = document.getElementById('image-preview-container');
if (imagePreviewContainer) {
const loadingDiv = document.createElement('div');
loadingDiv.className = 'image-loading';
loadingDiv.innerHTML = '<div class="loading-spinner">Buchcover wird heruntergeladen...</div>';
imagePreviewContainer.appendChild(loadingDiv);
const coverPreviewContainer = document.getElementById('book-cover-preview-container');
if (!coverPreviewContainer) {
console.error('Error: "book-cover-preview-container" not found in the DOM.');
return;
}
// Download the image via backend
const loadingDiv = document.createElement('div');
loadingDiv.className = 'image-loading';
loadingDiv.innerHTML = '<div class="loading-spinner">Buchcover wird heruntergeladen...</div>';
coverPreviewContainer.appendChild(loadingDiv);
fetch('/download_book_cover', {
method: 'POST',
headers: {
@@ -1693,78 +1696,63 @@
},
body: JSON.stringify({ url: imageUrl })
})
.then(response => response.json())
.then(data => {
// Remove loading indicator
const loadingDiv = imagePreviewContainer?.querySelector('.image-loading');
if (loadingDiv) {
loadingDiv.remove();
}
if (data.success) {
// Create a preview of the downloaded image
const imagePreview = document.createElement('div');
imagePreview.className = 'book-cover-preview';
imagePreview.innerHTML = `
<div class="preview-item">
<img src="{{ url_for('uploaded_file', filename='') }}${data.filename}"
alt="Buchcover" class="book-cover-thumbnail">
<p class="book-cover-caption">Buchcover automatisch heruntergeladen</p>
<input type="hidden" name="book_cover_image" value="${data.filename}">
<button type="button" onclick="removeBookCover(this)"
class="remove-book-cover-button">
Entfernen
</button>
</div>
`;
if (imagePreviewContainer) {
imagePreviewContainer.appendChild(imagePreview);
.then(response => response.json())
.then(data => {
const currentLoadingDiv = coverPreviewContainer.querySelector('.image-loading');
if (currentLoadingDiv) {
currentLoadingDiv.remove();
}
console.log('Book cover downloaded successfully:', data.filename);
} else {
console.error('Failed to download book cover:', data.error);
// Show error message to user
if (imagePreviewContainer) {
const errorDiv = document.createElement('div');
errorDiv.className = 'error-message';
errorDiv.textContent = 'Fehler beim Herunterladen des Buchcovers: ' + data.error;
errorDiv.style.fontSize = '0.8em';
errorDiv.style.padding = '5px';
errorDiv.style.marginTop = '5px';
imagePreviewContainer.appendChild(errorDiv);
// Remove error message after 5 seconds
setTimeout(() => errorDiv.remove(), 5000);
if (data.success) {
coverPreviewContainer.innerHTML = '';
const imagePreview = document.createElement('div');
imagePreview.className = 'book-cover-preview';
imagePreview.innerHTML = `
<div class="preview-item">
<img src="/uploads/${data.filename}"
alt="Buchcover" class="book-cover-thumbnail" style="max-width: 150px; border-radius: 4px;">
<p class="book-cover-caption" style="font-size: 0.9em; color: #555;">Buchcover automatisch heruntergeladen</p>
<input type="hidden" name="book_cover_image" value="${data.filename}">
<button type="button" onclick="removeBookCover(this)"
class="remove-book-cover-button btn btn-sm btn-danger">
Entfernen
</button>
</div>
`;
coverPreviewContainer.appendChild(imagePreview);
console.log('Book cover downloaded successfully:', data.filename);
} else {
console.error('Failed to download book cover:', data.error);
showCoverError(coverPreviewContainer, 'Fehler beim Herunterladen des Buchcovers: ' + data.error);
}
}
})
.catch(error => {
console.error('Error downloading book cover:', error);
// Remove loading indicator
const loadingDiv = imagePreviewContainer?.querySelector('.image-loading');
if (loadingDiv) {
loadingDiv.remove();
}
// Show error message
if (imagePreviewContainer) {
const errorDiv = document.createElement('div');
errorDiv.className = 'error-message';
errorDiv.textContent = 'Netzwerkfehler beim Herunterladen des Buchcovers';
errorDiv.style.fontSize = '0.8em';
errorDiv.style.padding = '5px';
errorDiv.style.marginTop = '5px';
imagePreviewContainer.appendChild(errorDiv);
// Remove error message after 5 seconds
setTimeout(() => errorDiv.remove(), 5000);
}
});
})
.catch(error => {
console.error('Error downloading book cover:', error);
const currentLoadingDiv = coverPreviewContainer.querySelector('.image-loading');
if (currentLoadingDiv) {
currentLoadingDiv.remove();
}
showCoverError(coverPreviewContainer, 'Netzwerkfehler beim Herunterladen des Buchcovers');
});
}
// Function to remove downloaded book cover
function showCoverError(container, message) {
const errorDiv = document.createElement('div');
errorDiv.className = 'error-message';
errorDiv.textContent = message;
errorDiv.style.fontSize = '0.8em';
errorDiv.style.color = 'red';
errorDiv.style.padding = '5px';
errorDiv.style.marginTop = '5px';
container.appendChild(errorDiv);
setTimeout(() => errorDiv.remove(), 5000);
}
function removeBookCover(button) {
const previewItem = button.closest('.preview-item');
if (previewItem) {
@@ -1772,7 +1760,6 @@
}
}
// Code validation functions
function checkCodeUnique(code, excludeId, callback) {
if (!code || code.trim() === '') {
callback(true);
@@ -1857,28 +1844,25 @@
function setupImagePreview() {
const imageInput = document.getElementById('images');
const previewContainer = document.getElementById('image-preview-container');
if (imageInput && previewContainer) {
imageInput.addEventListener('change', function(e) {
previewContainer.innerHTML = '';
const files = e.target.files;
// Validate file types before preview
const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif',
'video/mp4', 'video/quicktime', 'video/x-msvideo', 'video/x-matroska',
'video/webm', 'video/x-flv', 'video/mp4', 'video/3gpp'];
let hasInvalidFile = false;
// Validate file types before preview (Strictly Images)
const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif'];
for (let i = 0; i < files.length; i++) {
if (!allowedTypes.includes(files[i].type)) {
hasInvalidFile = true;
// Clear the file input to prevent submission
imageInput.value = '';
previewContainer.innerHTML = '<div class="error-message">Fehler: Datei "' + files[i].name + '" hat ein nicht unterstütztes Format. Erlaubte Formate: JPG, JPEG, PNG, GIF, MP4, MOV, AVI, MKV, WEBM, FLV, M4V, 3GP</div>';
previewContainer.innerHTML = '<div class="error-message">Fehler: Datei "' + files[i].name + '" hat ein nicht unterstütztes Format. Erlaubte Formate: JPG, JPEG, PNG, GIF</div>';
return; // Stop processing
}
}
for (let i = 0; i < files.length; i++) {
const file = files[i];
if (file.type.startsWith('image/')) {
@@ -1893,17 +1877,6 @@
previewContainer.appendChild(preview);
};
reader.readAsDataURL(file);
} else if (file.type.startsWith('video/')) {
const preview = document.createElement('div');
preview.className = 'image-preview video-preview';
preview.innerHTML = `
<div class="video-placeholder">
<div class="video-icon">🎥</div>
<div class="video-name">${file.name}</div>
</div>
<button type="button" class="remove-image" onclick="removeImagePreview(this, ${i})">×</button>
`;
previewContainer.appendChild(preview);
}
}
});