From 2ae0d254a3cfb6a182f3581f80c9441f5ec08c72 Mon Sep 17 00:00:00 2001 From: Dominic Date: Sun, 20 Sep 2026 14:53:49 +0000 Subject: [PATCH] =?UTF-8?q?Bugfix=20+=20Feat=20Besseres=20Laden=20+=20Bild?= =?UTF-8?q?ervorschau=20und=20Uploadbalken=20+=20Popup=20wirklich=20bild?= =?UTF-8?q?=20l=C3=B6schen=3F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/__pycache__/app.cpython-312.pyc | Bin 78915 -> 86340 bytes backend/app.py | 143 ++++++++++++++++++++---- backend/static/styles.css | 95 ++++++++++++++++ backend/templates/gallery.html | 45 +++++++- backend/templates/upload.html | 137 ++++++++++++++++++++--- data/db/app.sqlite3 | Bin 65536 -> 106496 bytes docker-compose.yml | 1 + 7 files changed, 385 insertions(+), 36 deletions(-) diff --git a/backend/__pycache__/app.cpython-312.pyc b/backend/__pycache__/app.cpython-312.pyc index 13b0fe08d78114c1c383351d99723b9364686ce9..2532eb25adc2200c1cb0cac3afc8be4069418692 100644 GIT binary patch delta 32486 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 Wx8bGXfrX{>49}L%^D8X#5BPt bool: return "." in filename and filename.rsplit(".", 1)[1].lower() in ALLOWED_EXTENSIONS +def upload_error_response(message: str, status: int = 400): + if request.headers.get("X-Upload-Batch") == "1": + return jsonify(ok=False, error=message), status + flash(message) + return redirect(url_for("upload")) + + +def thumbnail_filename(filename: str) -> str | None: + if Path(filename).name != filename or "." not in filename: + return None + extension = filename.rsplit(".", 1)[1].lower() + if extension not in THUMBNAIL_EXTENSIONS: + return None + return f"{filename.rsplit('.', 1)[0]}.webp" + + +def ensure_thumbnail(filename: str) -> str | None: + thumbnail_name = thumbnail_filename(filename) + if thumbnail_name is None: + return None + + source_path = Path(app.config["UPLOAD_FOLDER"]) / filename + thumbnail_dir = Path(app.config["THUMBNAIL_FOLDER"]) + thumbnail_path = thumbnail_dir / thumbnail_name + if thumbnail_path.is_file(): + return thumbnail_name + if not source_path.is_file(): + return None + + thumbnail_dir.mkdir(parents=True, exist_ok=True) + temporary_path = thumbnail_dir / f".{thumbnail_name}.{uuid.uuid4().hex}.tmp" + try: + with Image.open(source_path) as source_image: + preview = ImageOps.exif_transpose(source_image) + if preview.mode not in {"RGB", "RGBA"}: + preview = preview.convert("RGBA" if "transparency" in preview.info else "RGB") + preview.thumbnail(THUMBNAIL_MAX_SIZE, Image.Resampling.LANCZOS) + preview.save(temporary_path, format="WEBP", quality=84, method=4) + os.replace(temporary_path, thumbnail_path) + return thumbnail_name + except (OSError, UnidentifiedImageError, ValueError): + app.logger.warning("Could not create thumbnail for %s", filename, exc_info=True) + return None + finally: + try: + temporary_path.unlink() + except FileNotFoundError: + pass + + @app.get("/health") def health(): return {"status": "ok"} @@ -1409,34 +1485,35 @@ def rsvp(): @login_required def upload(): if request.method == "POST": + saved_paths = [] try: files = [f for f in request.files.getlist("photo") if f and f.filename] if not files: - flash(t("flash_select_image")) - return redirect(url_for("upload")) + return upload_error_response(t("flash_select_image")) + validated_files = [] for file in files: if not is_allowed_file(file.filename): - flash(t("flash_allowed_types")) - return redirect(url_for("upload")) + return upload_error_response(t("flash_allowed_types")) mime_type = (file.mimetype or "").lower() if mime_type and mime_type not in ALLOWED_MIME_TYPES: - flash(t("flash_allowed_types")) - return redirect(url_for("upload")) + return upload_error_response(t("flash_allowed_types")) + safe_name = secure_filename(file.filename) + if "." not in safe_name: + return upload_error_response(t("flash_allowed_types")) + validated_files.append((file, safe_name.rsplit(".", 1)[1].lower())) upload_dir = app.config["UPLOAD_FOLDER"] os.makedirs(upload_dir, exist_ok=True) db = get_db() now = datetime.utcnow().isoformat() upload_rows = [] - for file in files: - safe_name = secure_filename(file.filename) - if "." not in safe_name: - flash(t("flash_allowed_types")) - return redirect(url_for("upload")) - ext = safe_name.rsplit(".", 1)[1].lower() + for file, ext in validated_files: stored_name = f"{uuid.uuid4().hex}.{ext}" - file.save(os.path.join(upload_dir, stored_name)) + stored_path = os.path.join(upload_dir, stored_name) + file.save(stored_path) + saved_paths.append(stored_path) + ensure_thumbnail(stored_name) upload_rows.append((stored_name, int(session["group_id"]), now)) db.executemany( @@ -1445,14 +1522,20 @@ def upload(): ) db.commit() + if request.headers.get("X-Upload-Batch") == "1": + return jsonify(ok=True, count=len(upload_rows)) flash(t("flash_upload_success_count").format(count=len(upload_rows))) return redirect(url_for("gallery")) except RequestEntityTooLarge: raise except Exception: app.logger.exception("Upload failed") - flash(t("flash_upload_failed")) - return redirect(url_for("upload")) + for saved_path in saved_paths: + try: + os.remove(saved_path) + except FileNotFoundError: + pass + return upload_error_response(t("flash_upload_failed"), 500) return render_template("upload.html") @@ -1500,6 +1583,11 @@ def delete_image(image_id: int): file_path = os.path.join(app.config["UPLOAD_FOLDER"], image["filename"]) if os.path.isfile(file_path): os.remove(file_path) + thumbnail_name = thumbnail_filename(str(image["filename"])) + if thumbnail_name: + thumbnail_path = os.path.join(app.config["THUMBNAIL_FOLDER"], thumbnail_name) + if os.path.isfile(thumbnail_path): + os.remove(thumbnail_path) flash(t("flash_image_deleted")) return redirect(url_for("gallery")) @@ -1517,6 +1605,19 @@ def serve_upload(filename: str): ) +@app.get("/uploads/thumbnails/") +@login_required +def serve_thumbnail(filename: str): + generated_name = ensure_thumbnail(filename) + if generated_name: + return send_from_directory( + app.config["THUMBNAIL_FOLDER"], + generated_name, + max_age=31536000, + ) + return send_from_directory(app.config["UPLOAD_FOLDER"], filename) + + @app.get("/info/") @login_required def info(page: str): diff --git a/backend/static/styles.css b/backend/static/styles.css index ffde7fb..3344695 100644 --- a/backend/static/styles.css +++ b/backend/static/styles.css @@ -634,6 +634,64 @@ input[type="file"]:focus { padding: 0; } +.upload-progress { + padding: 0.85rem 1rem; + border: 1px solid rgba(31, 58, 47, 0.14); + border-radius: 14px; + background: rgba(255, 253, 248, 0.9); +} + +.upload-progress[hidden] { + display: none; +} + +.upload-progress-track { + height: 0.65rem; + overflow: hidden; + border-radius: 999px; + background: rgba(31, 58, 47, 0.12); +} + +.upload-progress-track span { + display: block; + width: 0; + height: 100%; + border-radius: inherit; + background: linear-gradient(90deg, var(--gold), var(--forest)); + transition: width 0.25s ease; +} + +.upload-progress-count { + margin: 0.65rem 0 0; + color: var(--forest); + font-weight: 700; + text-align: center; +} + +.upload-progress-status { + margin: 0.2rem 0 0; + color: rgba(31, 31, 31, 0.7); + text-align: center; +} + +.upload-progress-status.is-error { + color: var(--burgundy); +} + +.upload-retry-btn[hidden] { + display: none; +} + +.form-grid.is-uploading .upload-picker { + cursor: wait; + opacity: 0.62; +} + +.upload-file-remove:disabled { + cursor: not-allowed; + opacity: 0.45; +} + .btn { display: inline-flex; align-items: center; @@ -1466,6 +1524,43 @@ input[type="file"]:focus { background: #5e2733; } +.gallery-delete-dialog { + width: min(24rem, calc(100% - 2rem)); + margin: auto; + padding: 1.5rem; + border: 1px solid rgba(109, 47, 61, 0.2); + border-radius: 18px; + background: #fffdf8; + color: var(--forest); + text-align: center; + box-shadow: 0 22px 60px rgba(18, 31, 24, 0.3); +} + +.gallery-delete-dialog::backdrop { + background: rgba(18, 31, 24, 0.62); + backdrop-filter: blur(2px); +} + +.gallery-delete-dialog h2 { + margin: 0 0 0.65rem; +} + +.gallery-delete-dialog p { + margin: 0; +} + +.gallery-delete-dialog-actions { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 0.75rem; + margin-top: 1.35rem; +} + +.gallery-delete-dialog-actions .btn { + min-width: 7.5rem; +} + .lightbox { position: fixed; inset: 0; diff --git a/backend/templates/gallery.html b/backend/templates/gallery.html index 3f12cb3..845fbcd 100644 --- a/backend/templates/gallery.html +++ b/backend/templates/gallery.html @@ -47,10 +47,14 @@ class="gallery-open" data-image-index="{{ loop.index0 }}" > - {{ t('gallery_image_alt').format(name=image['uploaded_by_name']) }} + {{ t('gallery_image_alt').format(name=image['uploaded_by_name']) }} {% if is_host or guest_id == image['uploaded_by'] %} -