Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 542caa520f | |||
| 5136e40587 | |||
| c62b2b553d | |||
| 8783f97a09 | |||
| 43e09b41f1 | |||
| 84257dc289 |
+30
-24
@@ -3828,7 +3828,10 @@ def api_item_detail(item_id):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
client.close()
|
client.close()
|
||||||
return detail_html, 200
|
return jsonify({
|
||||||
|
'html': detail_html,
|
||||||
|
'images': item.get('Images', item.get('Bilder', []))
|
||||||
|
}), 200
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
app.logger.error(f"Error fetching item detail: {e}")
|
app.logger.error(f"Error fetching item detail: {e}")
|
||||||
return jsonify({'error': 'An error occurred while fetching the item detail'}), 500
|
return jsonify({'error': 'An error occurred while fetching the item detail'}), 500
|
||||||
@@ -5481,7 +5484,7 @@ def upload_item():
|
|||||||
fs = get_gridfs()
|
fs = get_gridfs()
|
||||||
|
|
||||||
if cfg.MODULES.is_enabled('library') and sanitize_form_value(request.form.get('item_type_input', '')) != "other":
|
if cfg.MODULES.is_enabled('library') and sanitize_form_value(request.form.get('item_type_input', '')) != "other":
|
||||||
success_redirect_endpoint = 'library'
|
success_redirect_endpoint = 'library_view'
|
||||||
else:
|
else:
|
||||||
success_redirect_endpoint = 'home_admin'
|
success_redirect_endpoint = 'home_admin'
|
||||||
|
|
||||||
@@ -5825,7 +5828,9 @@ def upload_item():
|
|||||||
|
|
||||||
if item_id:
|
if item_id:
|
||||||
success_msg = f'Element wurde erfolgreich hinzugefügt ({len(created_item_ids)} erstellt)'
|
success_msg = f'Element wurde erfolgreich hinzugefügt ({len(created_item_ids)} erstellt)'
|
||||||
|
fs = get_gridfs() # Deine GridFS Verbindung
|
||||||
|
|
||||||
|
it.cleanup_orphaned_images(fs, dry_run=True)
|
||||||
if upload_mode == 'library':
|
if upload_mode == 'library':
|
||||||
try:
|
try:
|
||||||
_append_audit_event_standalone(
|
_append_audit_event_standalone(
|
||||||
@@ -9958,18 +9963,21 @@ def fetch_book_info(isbn):
|
|||||||
app.logger.error(f"Error fetching book data: {e}")
|
app.logger.error(f"Error fetching book data: {e}")
|
||||||
return jsonify({"error": f"Failed to fetch book information"}), 500
|
return jsonify({"error": f"Failed to fetch book information"}), 500
|
||||||
|
|
||||||
|
|
||||||
@app.route('/download_book_cover', methods=['POST'])
|
@app.route('/download_book_cover', methods=['POST'])
|
||||||
def download_book_cover():
|
def download_book_cover():
|
||||||
"""
|
"""
|
||||||
API endpoint to download and save a book cover image from URL
|
API endpoint to download a book cover image from URL
|
||||||
|
and save it directly to MongoDB GridFS.
|
||||||
"""
|
"""
|
||||||
if 'username' not in session:
|
if 'username' not in session:
|
||||||
return jsonify({"error": "Not authorized"}), 403
|
return jsonify({"error": "Not authorized"}), 403
|
||||||
|
|
||||||
current_permissions = us.get_effective_permissions(session['username'])
|
current_permissions = us.get_effective_permissions(session['username'])
|
||||||
|
|
||||||
if not current_permissions['actions'].get('can_insert', False):
|
if not current_permissions['actions'].get('can_insert', False):
|
||||||
flash('Ihnen fehlen die nötigen Berechtigungen, um diese Aktion auszuführen.', 'error')
|
return jsonify({"error": "Ihnen fehlen die nötigen Berechtigungen."}), 403
|
||||||
return redirect(url_for('library_view'))
|
|
||||||
if not cfg.MODULES.is_enabled('library'):
|
if not cfg.MODULES.is_enabled('library'):
|
||||||
return jsonify({"error": "Bibliotheks-Modul ist deaktiviert."}), 403
|
return jsonify({"error": "Bibliotheks-Modul ist deaktiviert."}), 403
|
||||||
|
|
||||||
@@ -9984,17 +9992,11 @@ def download_book_cover():
|
|||||||
if parsed_url.scheme != 'https' or not parsed_url.netloc:
|
if parsed_url.scheme != 'https' or not parsed_url.netloc:
|
||||||
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
|
|
||||||
# if parsed_url.netloc not in ALLOWED_COVER_DOMAINS:
|
|
||||||
# return jsonify({"error": "Target host is not an allowed book cover provider"}), 403
|
|
||||||
|
|
||||||
# Download the image (allow_redirects=False prevents redirecting to internal IPs)
|
|
||||||
response = requests.get(image_url, stream=True, timeout=10, allow_redirects=False)
|
response = requests.get(image_url, stream=True, timeout=10, allow_redirects=False)
|
||||||
|
|
||||||
if response.status_code != 200:
|
if response.status_code != 200:
|
||||||
return jsonify({"error": f"Failed to download image: Status {response.status_code}"}), 400
|
return jsonify({"error": f"Failed to download image: Status {response.status_code}"}), 400
|
||||||
|
|
||||||
# Check content type
|
|
||||||
content_type = response.headers.get('content-type', '')
|
content_type = response.headers.get('content-type', '')
|
||||||
allowed_types = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif']
|
allowed_types = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif']
|
||||||
|
|
||||||
@@ -10003,16 +10005,14 @@ def download_book_cover():
|
|||||||
"error": f"Nicht unterstütztes Bildformat: {content_type}. Erlaubte Formate: JPG, JPEG, PNG, GIF"
|
"error": f"Nicht unterstütztes Bildformat: {content_type}. Erlaubte Formate: JPG, JPEG, PNG, GIF"
|
||||||
}), 400
|
}), 400
|
||||||
|
|
||||||
# Check content length header
|
|
||||||
content_length = response.headers.get('Content-Length')
|
content_length = response.headers.get('Content-Length')
|
||||||
if content_length:
|
if content_length:
|
||||||
try:
|
try:
|
||||||
if int(content_length) > 5 * 1024 * 1024:
|
if int(content_length) > 5 * 1024 * 1024:
|
||||||
return jsonify({"error": "Image is too large"}), 413
|
return jsonify({"error": "Image is too large (max 5MB)"}), 413
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Generate a fully unique filename
|
|
||||||
unique_id = str(uuid.uuid4())
|
unique_id = str(uuid.uuid4())
|
||||||
timestamp = time.strftime("%Y%m%d%H%M%S")
|
timestamp = time.strftime("%Y%m%d%H%M%S")
|
||||||
|
|
||||||
@@ -10023,23 +10023,29 @@ def download_book_cover():
|
|||||||
extension = '.gif'
|
extension = '.gif'
|
||||||
|
|
||||||
filename = f"book_cover_{unique_id}_{timestamp}{extension}"
|
filename = f"book_cover_{unique_id}_{timestamp}{extension}"
|
||||||
filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
|
|
||||||
|
|
||||||
# Save image in chunks (prevents memory exhaustion and enforces size limits)
|
image_data = io.BytesIO()
|
||||||
with open(filepath, 'wb') as f:
|
|
||||||
written = 0
|
written = 0
|
||||||
|
|
||||||
for chunk in response.iter_content(chunk_size=8192):
|
for chunk in response.iter_content(chunk_size=8192):
|
||||||
written += len(chunk)
|
written += len(chunk)
|
||||||
if written > 5 * 1024 * 1024:
|
if written > 5 * 1024 * 1024:
|
||||||
# Clean up the partial file before aborting
|
return jsonify({"error": "Image is too large (max 5MB)"}), 413
|
||||||
os.remove(filepath)
|
image_data.write(chunk)
|
||||||
return jsonify({"error": "Image is too large"}), 413
|
|
||||||
f.write(chunk)
|
image_data.seek(0)
|
||||||
|
|
||||||
|
fs = get_gridfs()
|
||||||
|
fs.put(
|
||||||
|
image_data,
|
||||||
|
filename=filename,
|
||||||
|
content_type=content_type
|
||||||
|
)
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"success": True,
|
"success": True,
|
||||||
"filename": filename,
|
"filename": filename,
|
||||||
"message": "Image downloaded successfully"
|
"message": "Image downloaded and stored directly in database"
|
||||||
})
|
})
|
||||||
|
|
||||||
except requests.exceptions.RequestException as e:
|
except requests.exceptions.RequestException as e:
|
||||||
@@ -10047,8 +10053,8 @@ def download_book_cover():
|
|||||||
return jsonify({"error": "Netzwerkfehler beim Herunterladen des Bildes."}), 500
|
return jsonify({"error": "Netzwerkfehler beim Herunterladen des Bildes."}), 500
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
app.logger.error(f"Error downloading book cover: {e}")
|
app.logger.error(f"Error downloading book cover: {e}")
|
||||||
# Fixed syntax here: Removed the injected HTML that was appended to this line
|
return jsonify({"error": "Failed to download image"}), 500
|
||||||
return jsonify({"error": f"Failed to download image"}), 500
|
|
||||||
"""
|
"""
|
||||||
@app.route('/proxy_image')
|
@app.route('/proxy_image')
|
||||||
def proxy_image():
|
def proxy_image():
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import datetime
|
|||||||
import Web.modules.database.settings as cfg
|
import Web.modules.database.settings as cfg
|
||||||
from Web.modules.database.settings import MongoClient
|
from Web.modules.database.settings import MongoClient
|
||||||
import Web.modules.inventarsystem.data_protection as dp
|
import Web.modules.inventarsystem.data_protection as dp
|
||||||
|
import logging
|
||||||
|
|
||||||
|
|
||||||
def is_library_item(item):
|
def is_library_item(item):
|
||||||
@@ -1293,3 +1294,136 @@ def sync_group_codes(primary_obj_id, base_code, individual_codes_list):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error syncing group codes: {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()
|
||||||
@@ -135,21 +135,6 @@
|
|||||||
.library-scan-status.warn { color: #9a6700; }
|
.library-scan-status.warn { color: #9a6700; }
|
||||||
.library-scan-status.error { color: #b42318; }
|
.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 */
|
/* Filters */
|
||||||
.library-filter-toggle-btn {
|
.library-filter-toggle-btn {
|
||||||
padding: 10px 16px;
|
padding: 10px 16px;
|
||||||
@@ -431,6 +416,40 @@
|
|||||||
|
|
||||||
.library-scan-reader-wrap { max-width: 100%; }
|
.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>
|
</style>
|
||||||
|
|
||||||
<div class="library-table-container" id="libraryTableContainer" data-can-edit="{{ 1 if current_permissions.actions.get('can_edit', False) else 0 }}">
|
<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
|
🔍 Filter
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="library-scan-controls">
|
<div class="library-scan-controls">
|
||||||
<select id="scanModeSelect" aria-label="Scan-Modus">
|
<select id="scanModeSelect" aria-label="Scan-Modus">
|
||||||
<option value="card_only">Nur Ausweis erfassen</option>
|
<option value="card_only">Nur Ausweis erfassen</option>
|
||||||
@@ -475,6 +493,10 @@
|
|||||||
</label>
|
</label>
|
||||||
<button id="manualActionBtn" class="button" type="button" style="margin-left:6px; background:#4f46e5; color:white;">Code verarbeiten</button>
|
<button id="manualActionBtn" class="button" type="button" style="margin-left:6px; background:#4f46e5; color:white;">Code verarbeiten</button>
|
||||||
</div>
|
</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 id="filterPanel" class="library-filter-panel">
|
||||||
<div class="filter-row">
|
<div class="filter-row">
|
||||||
<div class="filter-item">
|
<div class="filter-item">
|
||||||
@@ -1309,51 +1331,29 @@
|
|||||||
detailContent.innerHTML = '<p>Lade Details...</p>';
|
detailContent.innerHTML = '<p>Lade Details...</p>';
|
||||||
detailModal.style.display = 'flex';
|
detailModal.style.display = 'flex';
|
||||||
|
|
||||||
|
// Sicherer Zugriff auf das Item mit Fallback
|
||||||
const item = libraryItems.find(i => i._id === itemId);
|
const item = libraryItems.find(i => i._id === itemId);
|
||||||
let mediaHtml = '';
|
let mediaHtml = '';
|
||||||
|
|
||||||
// Robuste Prüfung: Wir testen gängige Benennungen aus deinem Backend
|
if (item) {
|
||||||
|
// Prüfe gängige Array-Namen aus dem Backend
|
||||||
const imageArray = item.Images || item.Bilder || item.images;
|
const imageArray = item.Images || item.Bilder || item.images;
|
||||||
|
|
||||||
if (item && Array.isArray(imageArray) && imageArray.length > 0) {
|
if (Array.isArray(imageArray) && imageArray.length > 0) {
|
||||||
const imagesHtml = imageArray.map((image, index) => {
|
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
|
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;">`;
|
||||||
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 || '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('');
|
}).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}`)
|
fetch(`/api/item_detail/${itemId}`)
|
||||||
.then(response => {
|
.then(response => {
|
||||||
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
@@ -1368,7 +1368,7 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Closes the modal via the 'x' button
|
// Schließt das Modal über den 'x'-Button
|
||||||
function closeDetailModal() {
|
function closeDetailModal() {
|
||||||
document.getElementById('detailModal').style.display = 'none';
|
document.getElementById('detailModal').style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -876,12 +876,14 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<!-- Image upload -->
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="images">Bilder:</label>
|
<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>
|
<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="allowed-formats">Erlaubte Formate: JPG, JPEG, PNG, GIF</div>
|
||||||
<!-- Add image preview area -->
|
|
||||||
<div class="image-preview-container" id="image-preview-container"></div>
|
<div class="image-preview-container" id="image-preview-container"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1669,23 +1671,24 @@
|
|||||||
}, 3000);
|
}, 3000);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to download book cover image
|
|
||||||
function downloadBookCover(imageUrl) {
|
function downloadBookCover(imageUrl) {
|
||||||
if (!imageUrl) {
|
if (!imageUrl) {
|
||||||
console.log('No image URL provided');
|
console.log('No image URL provided');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show loading indicator for image download
|
const coverPreviewContainer = document.getElementById('book-cover-preview-container');
|
||||||
const imagePreviewContainer = document.getElementById('image-preview-container');
|
|
||||||
if (imagePreviewContainer) {
|
if (!coverPreviewContainer) {
|
||||||
|
console.error('Error: "book-cover-preview-container" not found in the DOM.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const loadingDiv = document.createElement('div');
|
const loadingDiv = document.createElement('div');
|
||||||
loadingDiv.className = 'image-loading';
|
loadingDiv.className = 'image-loading';
|
||||||
loadingDiv.innerHTML = '<div class="loading-spinner">Buchcover wird heruntergeladen...</div>';
|
loadingDiv.innerHTML = '<div class="loading-spinner">Buchcover wird heruntergeladen...</div>';
|
||||||
imagePreviewContainer.appendChild(loadingDiv);
|
coverPreviewContainer.appendChild(loadingDiv);
|
||||||
}
|
|
||||||
|
|
||||||
// Download the image via backend
|
|
||||||
fetch('/download_book_cover', {
|
fetch('/download_book_cover', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@@ -1695,76 +1698,61 @@
|
|||||||
})
|
})
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
// Remove loading indicator
|
const currentLoadingDiv = coverPreviewContainer.querySelector('.image-loading');
|
||||||
const loadingDiv = imagePreviewContainer?.querySelector('.image-loading');
|
if (currentLoadingDiv) {
|
||||||
if (loadingDiv) {
|
currentLoadingDiv.remove();
|
||||||
loadingDiv.remove();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
// Create a preview of the downloaded image
|
coverPreviewContainer.innerHTML = '';
|
||||||
|
|
||||||
const imagePreview = document.createElement('div');
|
const imagePreview = document.createElement('div');
|
||||||
imagePreview.className = 'book-cover-preview';
|
imagePreview.className = 'book-cover-preview';
|
||||||
imagePreview.innerHTML = `
|
imagePreview.innerHTML = `
|
||||||
<div class="preview-item">
|
<div class="preview-item">
|
||||||
<img src="{{ url_for('uploaded_file', filename='') }}${data.filename}"
|
<img src="/uploads/${data.filename}"
|
||||||
alt="Buchcover" class="book-cover-thumbnail">
|
alt="Buchcover" class="book-cover-thumbnail" style="max-width: 150px; border-radius: 4px;">
|
||||||
<p class="book-cover-caption">Buchcover automatisch heruntergeladen</p>
|
<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}">
|
<input type="hidden" name="book_cover_image" value="${data.filename}">
|
||||||
<button type="button" onclick="removeBookCover(this)"
|
<button type="button" onclick="removeBookCover(this)"
|
||||||
class="remove-book-cover-button">
|
class="remove-book-cover-button btn btn-sm btn-danger">
|
||||||
Entfernen
|
Entfernen
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
if (imagePreviewContainer) {
|
coverPreviewContainer.appendChild(imagePreview);
|
||||||
imagePreviewContainer.appendChild(imagePreview);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Book cover downloaded successfully:', data.filename);
|
console.log('Book cover downloaded successfully:', data.filename);
|
||||||
} else {
|
} else {
|
||||||
console.error('Failed to download book cover:', data.error);
|
console.error('Failed to download book cover:', data.error);
|
||||||
// Show error message to user
|
showCoverError(coverPreviewContainer, 'Fehler beim Herunterladen des Buchcovers: ' + data.error);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error('Error downloading book cover:', error);
|
console.error('Error downloading book cover:', error);
|
||||||
// Remove loading indicator
|
|
||||||
const loadingDiv = imagePreviewContainer?.querySelector('.image-loading');
|
const currentLoadingDiv = coverPreviewContainer.querySelector('.image-loading');
|
||||||
if (loadingDiv) {
|
if (currentLoadingDiv) {
|
||||||
loadingDiv.remove();
|
currentLoadingDiv.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show error message
|
showCoverError(coverPreviewContainer, 'Netzwerkfehler beim Herunterladen des Buchcovers');
|
||||||
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);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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) {
|
function removeBookCover(button) {
|
||||||
const previewItem = button.closest('.preview-item');
|
const previewItem = button.closest('.preview-item');
|
||||||
if (previewItem) {
|
if (previewItem) {
|
||||||
@@ -1772,7 +1760,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Code validation functions
|
|
||||||
function checkCodeUnique(code, excludeId, callback) {
|
function checkCodeUnique(code, excludeId, callback) {
|
||||||
if (!code || code.trim() === '') {
|
if (!code || code.trim() === '') {
|
||||||
callback(true);
|
callback(true);
|
||||||
|
|||||||
Reference in New Issue
Block a user