Compare commits

...

3 Commits

Author SHA1 Message Date
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
Aiirondev_dev 4a776872e0 Improvement of the style for the Library Module
Release Inventarsystem / release-docker (push) Successful in 2m15s
2026-08-15 00:08:13 +02:00
3 changed files with 149 additions and 330 deletions
+37 -144
View File
@@ -5239,15 +5239,15 @@ def debug_favorites():
def upload_item():
"""
Route for adding new items to the inventory.
Handles file uploads and creates QR codes.
Handles image file uploads and creates QR codes.
Enhanced for mobile browser compatibility.
Returns:
flask.Response: Redirect to admin homepage
"""
if 'username' not in session:
return jsonify({'success': False, 'message': 'Nicht angemeldet'}), 401
item_is_library = False
# Check if user may insert items
@@ -5265,20 +5265,20 @@ def upload_item():
# Detect if request is from mobile device
is_mobile = 'Mobile' in request.headers.get('User-Agent', '')
# Log mobile request for debugging
if is_mobile:
app.logger.info(f"Mobile upload from {request.headers.get('User-Agent', 'unknown')} by {encrypt_text(username)}")
app.logger.info(
f"Mobile upload from {request.headers.get('User-Agent', 'unknown')} by {encrypt_text(username)}")
try:
# Strip whitespace from all text fields
name = sanitize_form_value(request.form['name'])
ort = sanitize_form_value(request.form['ort'])
beschreibung = sanitize_form_value(request.form['beschreibung'])
# Check both possible image field names
images = request.files.getlist('images') or request.files.getlist('new_images')
filter_upload = sanitize_form_value(request.form.getlist('filter'))
filter_upload2 = sanitize_form_value(request.form.getlist('filter2'))
filter_upload3 = sanitize_form_value(request.form.getlist('filter3'))
@@ -5291,7 +5291,7 @@ def upload_item():
item_count_raw = sanitize_form_value(request.form.get('item_count', '1'))
item_type_input = sanitize_form_value(request.form.get('item_type_input', ''))
library_category = sanitize_form_value(request.form.get('library_category', ''))
if library_category != '':
item_is_library = True
@@ -5301,46 +5301,30 @@ def upload_item():
item_count = 1
item_count = max(1, min(item_count, 100))
# Optional list of per-item codes (one code per line)
# Optional list of per-item codes
individual_codes = []
if individual_codes_raw:
individual_codes = [c.strip() for c in str(individual_codes_raw).replace(',', '\n').splitlines() if c.strip()]
individual_codes = [c.strip() for c in str(individual_codes_raw).replace(',', '\n').splitlines() if
c.strip()]
# Check if this is a duplication
is_duplicating = request.form.get('is_duplicating') == 'true'
# Get duplicate_images if duplicating
duplicate_images = request.form.getlist('duplicate_images') if is_duplicating else []
# Make sure duplicate_images is always a list, even if there's only one
if is_duplicating and duplicate_images and not isinstance(duplicate_images, list):
duplicate_images = [duplicate_images]
# Log details about each image
if is_duplicating and duplicate_images:
for i, img in enumerate(duplicate_images):
print(f"DEBUG: Duplicate image {i+1}/{len(duplicate_images)}: {img}")
# Get book cover image if downloaded
book_cover_image = request.form.get('book_cover_image')
# Special handling for mobile browsers that might send data differently
# Special handling for mobile browsers
if is_mobile and 'mobile_data' in request.form:
try:
mobile_data = json.loads(request.form['mobile_data'])
# Override values with mobile data if available
if 'filters' in mobile_data:
filter_upload = mobile_data.get('filters', [])
if 'filters2' in mobile_data:
filter_upload2 = mobile_data.get('filters2', [])
if 'filters3' in mobile_data:
filter_upload3 = mobile_data.get('filters3', [])
if 'duplicate_images' in mobile_data and mobile_data['duplicate_images']:
duplicate_images = mobile_data.get('duplicate_images', [])
except json.JSONDecodeError as e:
app.logger.error(f"Error parsing mobile data: {str(e)}")
except:
except Exception as e:
app.logger.error(f"Upload Form Data Parsing Error: {str(e)}")
flash('Fehler beim Verarbeiten der Formulardaten. Bitte versuchen Sie es erneut.', 'error')
return redirect(url_for(success_redirect_endpoint))
@@ -5348,7 +5332,7 @@ def upload_item():
filter_upload = expand_filter_selection(filter_upload, 1)
filter_upload2 = expand_filter_selection(filter_upload2, 2)
# Validation
# Base Validation
if not name or not ort or not beschreibung:
error_msg = 'Bitte füllen Sie alle erforderlichen Felder aus'
if is_mobile:
@@ -5384,9 +5368,8 @@ def upload_item():
flash(error_msg, 'error')
return redirect(url_for('library_admin'))
# Only check for images if not duplicating and no duplicate images provided and no book cover
# For library mode, skip this check as images come only from ISBN fetch
if upload_mode != 'library' and not is_duplicating and not images and not duplicate_images and not book_cover_image:
# Strict Image Validation (Since duplication is removed)
if upload_mode != 'library' and not images and not book_cover_image:
error_msg = 'Bitte laden Sie mindestens ein Bild hoch'
if is_mobile:
return jsonify({'success': False, 'message': error_msg}), 400
@@ -5395,36 +5378,20 @@ def upload_item():
return redirect(url_for(success_redirect_endpoint))
primary_code_raw = request.form.get('code_4', '').strip()
individual_codes_raw = request.form.get('individual_codes', '').strip()
all_item_codes = []
if primary_code_raw:
all_item_codes.append(primary_code_raw)
if individual_codes_raw:
extra_codes = [c.strip() for c in individual_codes_raw.replace('\r', '').split('\n') if c.strip()]
for c in extra_codes:
if individual_codes:
for c in individual_codes:
if c not in all_item_codes:
all_item_codes.append(c)
if len(all_item_codes) > 0:
item_count = len(all_item_codes)
else:
try:
item_count = int(request.form.get('item_count', 1))
except ValueError:
item_count = 1
if upload_mode != 'library' and not is_duplicating and not images and not duplicate_images and not book_cover_image:
error_msg = 'Bitte laden Sie mindestens ein Bild hoch'
if is_mobile:
return jsonify({'success': False, 'message': error_msg}), 400
else:
flash(error_msg, 'error')
return redirect(url_for(success_redirect_endpoint))
# Check unique codes
for code in all_item_codes:
if not it.is_code_unique(code):
app.logger.info(f"DEBUG: Code '{code}' is not unique.")
@@ -5434,12 +5401,12 @@ def upload_item():
error_msg = 'Der Code wird bereits verwendet. Umleitung zum Eintrag.'
if is_mobile:
return jsonify({
'success': False,
'success': False,
'message': error_msg,
'existing_item_id': str(existing_item['_id']),
'redirect_to_item': True
}), 400
flash(error_msg, 'info')
return redirect(url_for(success_redirect_endpoint, open_item=str(existing_item['_id'])))
@@ -5451,14 +5418,11 @@ def upload_item():
return redirect(url_for(success_redirect_endpoint))
def generate_unique_batch_code(base_code, position):
"""Generate a unique code for every item in a batch if no specific code is provided."""
if not base_code:
return None
candidate = base_code if position == 1 else f"{base_code}"
if it.is_code_unique(candidate):
return candidate
suffix = 1
while suffix <= 1000:
alternative = f"{candidate}"
@@ -5473,14 +5437,11 @@ def upload_item():
skipped_count = 0
upload_session_id = str(uuid.uuid4())[:8]
app.logger.info(f"Starting image upload session {upload_session_id} - Files: {len(images)}, User: {encrypt_text(username)}")
app.logger.info(
f"Starting image upload session {upload_session_id} - Files: {len(images)}, User: {encrypt_text(username)}")
# 1. Process Manual Image Uploads
for index, image in enumerate(images):
#if upload_mode == 'library':
# app.logger.info(f"[Upload {upload_session_id}] Skipping manual upload (Library Mode)")
# skipped_count += 1
# continue
if not image or not image.filename:
skipped_count += 1
continue
@@ -5498,9 +5459,7 @@ def upload_item():
continue
secure_name = secure_filename(image.filename)
image.seek(0)
image_bytes = image.read()
if not image_bytes:
@@ -5535,7 +5494,6 @@ def upload_item():
)
image_filenames.append(new_filename)
processed_count += 1
final_size_kb = len(optimized_io.getvalue()) / 1024
app.logger.info(
@@ -5545,79 +5503,13 @@ def upload_item():
app.logger.error(f"{image_log_prefix} Processing failed: {str(e)}")
error_count += 1
app.logger.info(f"Upload session {upload_session_id} completed: {processed_count} processed, {error_count} errors, {skipped_count} skipped")
# 1. Duplikate verarbeiten (Direkt via MongoDB / GridFS)
if duplicate_images:
app.logger.info(f"Processing {len(duplicate_images)} duplicate images from GridFS: {duplicate_images}")
duplicate_image_copies = []
placeholder_used = False
for i, dup_img in enumerate(duplicate_images):
try:
# Suche das Originalbild direkt in der GridFS-Datenbank
existing_file = fs.find_one({"filename": dup_img})
# Fallback: Falls der exakte Name nicht gefunden wird, suche über Regex nach dem Basisnamen
if not existing_file:
base_name = re.sub(r'_\d+(?=\.[^.]+$)', '', dup_img) # Entfernt Suffixe wie _800
base_name_no_ext = os.path.splitext(base_name)[0]
if len(base_name_no_ext) > 5:
app.logger.info(f"Trying regex search for base name: {base_name_no_ext}")
existing_file = fs.find_one({"filename": {"$regex": f"^{re.escape(base_name_no_ext)}"}})
new_filename = f"{uuid.uuid4().hex}_{int(time.time())}.webp"
if existing_file:
app.logger.info(f"Found image in GridFS for {dup_img}. Duplicating...")
# Bilddaten in den Speicher laden und direkt als neue Datei in GridFS ablegen
image_data = existing_file.read()
fs.put(
image_data,
filename=new_filename,
content_type=existing_file.content_type,
metadata={'original_duplicate_of': dup_img}
)
duplicate_image_copies.append(new_filename)
processed_count += 1
else:
app.logger.warning(f"Could not find {dup_img} in GridFS. Using placeholder.")
placeholder_path = os.path.join(app.static_folder, 'img', 'no-image.png')
if os.path.exists(placeholder_path):
with open(placeholder_path, 'rb') as pf:
placeholder_data = pf.read()
fs.put(
placeholder_data,
filename=new_filename,
content_type='image/png',
metadata={'is_placeholder': True}
)
duplicate_image_copies.append(new_filename)
placeholder_used = True
except Exception as e:
app.logger.error(f"Error duplicating image {dup_img}: {str(e)}")
error_count += 1
if placeholder_used:
app.logger.warning("Used placeholders for some missing images during duplication")
image_filenames.extend(duplicate_image_copies)
# 2. Buchcover verarbeiten
# 2. Process Book Cover
if book_cover_image:
new_filename = f"{uuid.uuid4().hex}_{int(time.time())}_book_cover.webp"
# Prüfen, ob das Cover noch als lokale temporäre Datei existiert (z.B. von einer API geladen)
local_path = os.path.join(app.config.get('UPLOAD_FOLDER', ''), book_cover_image)
try:
if os.path.exists(local_path):
# Lokales Bild einlesen, zu WebP optimieren und in GridFS schieben
with Image.open(local_path) as img:
if img.mode not in ('RGB', 'RGBA'):
img = img.convert('RGBA')
@@ -5635,7 +5527,6 @@ def upload_item():
image_filenames.append(new_filename)
app.logger.info(f"Processed local book cover and saved to GridFS: {new_filename}")
else:
# Falls es bereits in GridFS liegt, einfach duplizieren
existing_cover = fs.find_one({"filename": book_cover_image})
if existing_cover:
fs.put(
@@ -5651,7 +5542,10 @@ def upload_item():
except Exception as e:
app.logger.error(f"Error processing book cover {book_cover_image}: {str(e)}")
# 3. Item-Erstellung (Logik bleibt identisch, da it.add_item nun die GridFS-Filenames erhält)
app.logger.info(
f"Upload session {upload_session_id} completed: {processed_count} processed, {error_count} errors, {skipped_count} skipped")
# 3. Item Creation
predefined_locations = it.get_predefined_locations()
if ort and ort not in predefined_locations:
it.add_predefined_location(ort)
@@ -5677,7 +5571,6 @@ def upload_item():
parent_item_id = str(created_item_ids[0]) if created_item_ids else None
# image_filenames enthält jetzt ausschließlich GridFS-Filenames (oder ObjectIds, je nach deinem Setup)
item_id = it.add_item(
name, ort, beschreibung, image_filenames, filter_upload,
filter_upload2, filter_upload3,
@@ -5723,8 +5616,8 @@ def upload_item():
'codes': created_item_ids and [it.get_item(cid).get('Code_4') for cid in created_item_ids] or []
}
)
except Exception:
app.logger.warning('Audit write failed for library_item_created')
except Exception as e:
app.logger.warning(f"Audit write failed for library_item_created: {str(e)}")
flash(success_msg, 'success')
return redirect(url_for(success_redirect_endpoint))
+99 -159
View File
@@ -4,59 +4,60 @@
{% block content %}
<style>
/* The Dark Background Overlay */
/* =========================================
1. Modals & Overlays
========================================= */
.modal {
display: none; /* Managed by JS (none/flex) */
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background-color: rgba(0, 0, 0, 0.6);
/* Display is managed by JS (none/flex) */
align-items: center;
justify-content: center;
inset: 0; /* Modern shorthand for top: 0, right: 0, bottom: 0, left: 0 */
z-index: 1000;
background-color: rgba(0, 0, 0, 0.6);
align-items: center;
justify-content: center;
padding: 20px;
}
/* The White Modal Box */
.modal-content {
background-color: #fff;
padding: 20px;
width: 80%;
max-width: 600px;
border-radius: 8px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
/* Flexbox allows the header to stay fixed while the body scrolls */
position: relative;
display: flex;
flex-direction: column;
max-height: 85vh;
width: 100%;
max-width: 900px; /* Consolidated from conflicting 600px/900px rules */
max-height: 90vh;
background-color: var(--ui-surface, #fff);
border-radius: 8px;
padding: 20px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
overflow-y: auto;
}
/* The Close Button */
.close {
align-self: flex-end;
position: absolute;
top: 15px;
right: 20px;
color: #aaa;
font-size: 28px;
font-weight: bold;
cursor: pointer;
line-height: 1;
margin-bottom: 10px;
cursor: pointer;
transition: color 0.2s ease;
}
.close:hover,
.close:focus {
color: #000;
color: var(--ui-text, #000);
text-decoration: none;
}
/* The Scrollable Content Area */
#detailContent {
overflow-y: auto;
padding-right: 10px;
}
/* Library table-only view styles */
/* =========================================
2. Layout & Headers
========================================= */
.library-table-container {
max-width: 1400px;
margin: 0 auto;
@@ -79,13 +80,15 @@
font-size: 0.95em;
}
/* Library search bar */
/* =========================================
3. Search, Filter & Scan Panels
========================================= */
.library-search-bar {
margin-bottom: 20px;
display: flex;
gap: 10px;
flex-wrap: wrap;
align-items: center;
gap: 10px;
margin-bottom: 20px;
}
.library-search-input {
@@ -95,10 +98,9 @@
border: 1px solid #ddd;
border-radius: 6px;
font-size: 16px;
background: white;
background: #fff;
}
/* Scanner panel */
.library-scan-panel {
background: var(--ui-surface);
border: 1px solid #d9dde4;
@@ -129,26 +131,18 @@
color: #4b5563;
}
.library-scan-status.ok {
color: #1f7a38;
}
.library-scan-status.warn {
color: #9a6700;
}
.library-scan-status.error {
color: #b42318;
}
.library-scan-status.ok { color: #1f7a38; }
.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;
background: var(--ui-surface);
max-width: 460px;
}
.library-scan-reader {
@@ -156,14 +150,15 @@
min-height: 280px;
}
/* Filters */
.library-filter-toggle-btn {
padding: 10px 16px;
background: #f0f2f5;
border: 1px solid #ddd;
border-radius: 6px;
cursor: pointer;
font-weight: 500;
color: var(--ui-text);
cursor: pointer;
transition: all 0.2s ease;
}
@@ -178,10 +173,9 @@
color: #4f46e5;
}
/* Filter panel */
.library-filter-panel {
display: none;
background: white;
background: #fff;
border: 1px solid #ddd;
border-radius: 8px;
padding: 20px;
@@ -210,10 +204,10 @@
}
.filter-item label {
margin-bottom: 6px;
font-weight: 600;
font-size: 0.9em;
color: var(--ui-text);
margin-bottom: 6px;
}
.filter-item input,
@@ -222,7 +216,8 @@
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
background: white;
background: #fff;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.filter-item input:focus,
@@ -236,8 +231,8 @@
display: flex;
gap: 10px;
margin-top: 15px;
border-top: 1px solid #eee;
padding-top: 15px;
border-top: 1px solid #eee;
}
.filter-buttons .button {
@@ -246,11 +241,13 @@
font-size: 14px;
}
/* Library table */
/* =========================================
4. Tables & Data Display
========================================= */
.library-items-table {
width: 100%;
background: #fff;
border-collapse: collapse;
background: white;
border: 1px solid #ddd;
border-radius: 8px;
overflow: hidden;
@@ -266,8 +263,8 @@
padding: 14px 16px;
text-align: left;
font-weight: 600;
color: var(--ui-text);
font-size: 0.9em;
color: var(--ui-text);
text-transform: uppercase;
letter-spacing: 0.5px;
}
@@ -288,7 +285,6 @@
border-bottom: none;
}
/* Table cells */
.table-title {
font-weight: 600;
color: var(--ui-text);
@@ -302,26 +298,14 @@
font-weight: 500;
}
.status-available {
background: #e8f5e9;
color: #2e7d32;
}
.status-available { background: #e8f5e9; color: #2e7d32; }
.status-borrowed { background: #fff3e0; color: #e65100; }
.status-damaged { background: #fee2e2; color: #991b1b; }
.status-borrowed {
background: #fff3e0;
color: #e65100;
}
.status-damaged {
background: #fee2e2;
color: #991b1b;
}
/* Actions */
.table-actions {
display: flex;
gap: 8px;
flex-wrap: wrap;
gap: 8px;
}
.table-actions .button {
@@ -330,26 +314,25 @@
white-space: nowrap;
}
/* Empty state */
.library-empty-state {
text-align: center;
padding: 60px 20px;
text-align: center;
color: #999;
}
.library-empty-state h3 {
font-size: 1.3em;
margin-bottom: 10px;
font-size: 1.3em;
color: #666;
}
.library-load-row {
margin-top: 12px;
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 12px;
flex-wrap: wrap;
margin-top: 12px;
}
.library-load-hint {
@@ -357,54 +340,9 @@
color: #6b7280;
}
/* Small confirmation popup (toast) */
.small-popup {
position: fixed;
bottom: 24px;
left: 50%;
transform: translateX(-50%);
background: rgba(17, 24, 39, 0.96);
color: #fff;
padding: 12px 16px;
border-radius: 8px;
box-shadow: 0 6px 24px rgba(2,6,23,0.6);
z-index: 2000;
display: flex;
gap: 10px;
align-items: center;
max-width: 90%;
font-size: 0.95em;
}
.small-popup.ok { background: rgba(16,185,129,0.95); color: #032; }
.small-popup.error { background: rgba(239,68,68,0.95); color: #210; }
.small-popup .close-x { margin-left: 8px; cursor: pointer; font-weight: 700; }
/* Modal styles */
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
align-items: center;
justify-content: center;
padding: 20px;
}
.modal-content {
background-color: var(--ui-surface);
border-radius: 8px;
padding: 20px;
max-width: 900px;
max-height: 90vh;
overflow-y: auto;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
position: relative;
}
/* =========================================
5. Edit Grid Forms
========================================= */
.edit-grid {
display: grid;
grid-template-columns: 1fr 1fr;
@@ -416,10 +354,10 @@
}
.edit-grid label {
display: block;
margin-bottom: 4px;
font-weight: 600;
font-size: 0.9em;
margin-bottom: 4px;
display: block;
}
.edit-grid input,
@@ -437,37 +375,44 @@
margin-top: 14px;
}
.close {
position: absolute;
right: 20px;
top: 10px;
font-size: 28px;
font-weight: bold;
color: #999;
/* =========================================
6. Notifications / Toasts
========================================= */
.small-popup {
position: fixed;
bottom: 24px;
left: 50%;
z-index: 2000;
display: flex;
align-items: center;
gap: 10px;
max-width: 90%;
padding: 12px 16px;
background: rgba(17, 24, 39, 0.96);
color: #fff;
border-radius: 8px;
font-size: 0.95em;
transform: translateX(-50%);
box-shadow: 0 6px 24px rgba(2,6,23,0.6);
}
.small-popup.ok { background: rgba(16,185,129,0.95); color: #032; }
.small-popup.error { background: rgba(239,68,68,0.95); color: #210; }
.small-popup .close-x {
margin-left: 8px;
font-weight: 700;
cursor: pointer;
}
.close:hover {
color: var(--ui-text);
}
/* Mobile responsive */
/* =========================================
7. Mobile Responsiveness
========================================= */
@media (max-width: 768px) {
.library-table-container {
padding: 10px;
}
.library-search-bar {
flex-direction: column;
}
.library-search-input {
width: 100%;
}
.library-items-table {
font-size: 0.85em;
}
.library-table-container { padding: 10px; }
.library-search-bar { flex-direction: column; }
.library-search-input { width: 100%; }
.library-items-table { font-size: 0.85em; }
.library-items-table th,
.library-items-table td {
@@ -479,17 +424,12 @@
font-size: 0.75em;
}
.filter-row {
grid-template-columns: 1fr;
}
.filter-row,
.library-scan-controls {
grid-template-columns: 1fr;
}
.library-scan-reader-wrap {
max-width: 100%;
}
.library-scan-reader-wrap { max-width: 100%; }
}
</style>
+13 -27
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,11 +876,11 @@
</div>
<!-- Image upload (hidden for library mode) -->
<!-- Image upload -->
<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>
<label for="images">Bilder:</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>
<!-- Add image preview area -->
<div class="image-preview-container" id="image-preview-container"></div>
</div>
@@ -1857,28 +1857,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 +1890,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);
}
}
});