Scalino84 commited on
Commit
1e7308f
·
0 Parent(s):

Initial commit

Browse files
main.py ADDED
@@ -0,0 +1,715 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/env python3.11
2
+ import os
3
+ import sqlite3
4
+ import replicate
5
+ import argparse
6
+ import requests
7
+ from datetime import datetime
8
+ from fastapi import FastAPI, WebSocket, WebSocketDisconnect, HTTPException, Request, Form, Query, Response
9
+ from fastapi.templating import Jinja2Templates
10
+ from fastapi.responses import FileResponse
11
+ from fastapi.staticfiles import StaticFiles
12
+ from pydantic import BaseModel
13
+ from typing import Optional, List
14
+ import uvicorn
15
+ from asyncio import gather, Semaphore, create_task
16
+ from mistralai import Mistral
17
+ from dotenv import load_dotenv
18
+ from contextlib import contextmanager
19
+ from io import BytesIO
20
+ import zipfile
21
+ import logging
22
+
23
+ # Logging-Konfiguration (gut!)
24
+ logging.basicConfig(level=logging.INFO)
25
+ logger = logging.getLogger("uvicorn.error")
26
+
27
+ import sys
28
+ print(f"Arguments: {sys.argv}")
29
+
30
+ load_dotenv()
31
+
32
+ # ANSI Escape Codes für farbige Ausgabe (kann entfernt werden, falls nicht benötigt)
33
+ HEADER = "\033[38;2;255;255;153m"
34
+ TITLE = "\033[38;2;255;255;153m"
35
+ MENU = "\033[38;2;255;165;0m"
36
+ SUCCESS = "\033[38;2;153;255;153m"
37
+ ERROR = "\033[38;2;255;69;0m"
38
+ MAIN = "\033[38;2;204;204;255m"
39
+ SPEAKER1 = "\033[38;2;173;216;230m"
40
+ SPEAKER2 = "\033[38;2;255;179;102m"
41
+ RESET = "\033[0m"
42
+
43
+ DOWNLOAD_DIR = "/mnt/d/ai/dialog/2/flux-pics" # Pfad zu deinen Bildern (sollte korrekt sein)
44
+ DATABASE_PATH = "flux_logs_neu.db" # Datenbank-Pfad
45
+ TIMEOUT_DURATION = 900 # Timeout-Dauer in Sekunden (scheint angemessen)
46
+
47
+ # WICHTIG: Stelle sicher, dass dieses Verzeichnis existiert und die Bilder enthält.
48
+ IMAGE_STORAGE_PATH = DOWNLOAD_DIR
49
+
50
+ app = FastAPI()
51
+
52
+ # StaticFiles Middleware hinzufügen (korrekt und wichtig!)
53
+ app.mount("/static", StaticFiles(directory="static"), name="static")
54
+ app.mount("/flux-pics", StaticFiles(directory=IMAGE_STORAGE_PATH), name="flux-pics")
55
+
56
+ templates = Jinja2Templates(directory="templates")
57
+
58
+ # Datenbank-Hilfsfunktionen (sehen gut aus)
59
+ @contextmanager
60
+ def get_db_connection(db_path=DATABASE_PATH):
61
+ conn = sqlite3.connect(db_path)
62
+ try:
63
+ yield conn
64
+ finally:
65
+ conn.close()
66
+
67
+ def initialize_database(db_path=DATABASE_PATH):
68
+ with get_db_connection(db_path) as conn:
69
+ cursor = conn.cursor()
70
+ # Tabellen-Erstellung (scheint korrekt, keine Auffälligkeiten)
71
+ cursor.execute("""
72
+ CREATE TABLE IF NOT EXISTS generation_logs (
73
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
74
+ timestamp TEXT,
75
+ prompt TEXT,
76
+ optimized_prompt TEXT,
77
+ hf_lora TEXT,
78
+ lora_scale REAL,
79
+ aspect_ratio TEXT,
80
+ guidance_scale REAL,
81
+ output_quality INTEGER,
82
+ prompt_strength REAL,
83
+ num_inference_steps INTEGER,
84
+ output_file TEXT,
85
+ album_id INTEGER,
86
+ category_id INTEGER
87
+ )
88
+ """)
89
+ cursor.execute("""
90
+ CREATE TABLE IF NOT EXISTS albums (
91
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
92
+ name TEXT NOT NULL
93
+ )
94
+ """)
95
+ cursor.execute("""
96
+ CREATE TABLE IF NOT EXISTS categories (
97
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
98
+ name TEXT NOT NULL
99
+ )
100
+ """)
101
+ cursor.execute("""
102
+ CREATE TABLE IF NOT EXISTS pictures (
103
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
104
+ timestamp TEXT,
105
+ file_path TEXT,
106
+ file_name TEXT,
107
+ album_id INTEGER,
108
+ FOREIGN KEY (album_id) REFERENCES albums(id)
109
+ )
110
+ """)
111
+ cursor.execute("""
112
+ CREATE TABLE IF NOT EXISTS picture_categories (
113
+ picture_id INTEGER,
114
+ category_id INTEGER,
115
+ FOREIGN KEY (picture_id) REFERENCES pictures(id),
116
+ FOREIGN KEY (category_id) REFERENCES categories(id),
117
+ PRIMARY KEY (picture_id, category_id)
118
+ )
119
+ """)
120
+ conn.commit()
121
+ def log_generation(args, optimized_prompt, image_file):
122
+ file_path, file_name = os.path.split(image_file)
123
+ try:
124
+ with get_db_connection() as conn:
125
+ cursor = conn.cursor()
126
+ cursor.execute("""
127
+ INSERT INTO generation_logs (
128
+ timestamp, prompt, optimized_prompt, hf_lora, lora_scale, aspect_ratio, guidance_scale,
129
+ output_quality, prompt_strength, num_inference_steps, output_file, album_id, category_id
130
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
131
+ """, (
132
+ datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
133
+ args.prompt,
134
+ optimized_prompt,
135
+ args.hf_lora,
136
+ args.lora_scale,
137
+ args.aspect_ratio,
138
+ args.guidance_scale,
139
+ args.output_quality,
140
+ args.prompt_strength,
141
+ args.num_inference_steps,
142
+ image_file,
143
+ args.album_id,
144
+ args.category_ids[0] if args.category_ids else None # Hier auf erstes Element zugreifen
145
+ ))
146
+ picture_id = cursor.lastrowid # Dies scheint nicht korrekt zu sein, da die ID für die Tabelle pictures benötigt wird
147
+ cursor.execute("""
148
+ INSERT INTO pictures (
149
+ timestamp, file_path, file_name, album_id
150
+ ) VALUES (?, ?, ?, ?)
151
+ """, (
152
+ datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
153
+ file_path,
154
+ file_name,
155
+ args.album_id
156
+ ))
157
+ picture_id = cursor.lastrowid # Korrekte Zeile
158
+
159
+ # Insert multiple categories
160
+ for category_id in args.category_ids:
161
+ cursor.execute("""
162
+ INSERT INTO picture_categories (picture_id, category_id)
163
+ VALUES (?, ?)
164
+ """, (picture_id, category_id))
165
+
166
+ conn.commit()
167
+ except sqlite3.Error as e:
168
+ print(f"Error logging generation: {e}") # Sollte durch logger.error ersetzt werden.
169
+
170
+ @app.on_event("startup")
171
+ def startup_event():
172
+ initialize_database()
173
+
174
+ @app.get("/")
175
+ def read_root(request: Request):
176
+ with get_db_connection() as conn:
177
+ cursor = conn.cursor()
178
+ cursor.execute("SELECT id, name FROM albums")
179
+ albums = cursor.fetchall()
180
+ cursor.execute("SELECT id, name FROM categories")
181
+ categories = cursor.fetchall()
182
+ return templates.TemplateResponse("index.html", {"request": request, "albums": albums, "categories": categories})
183
+
184
+ @app.get("/archive")
185
+ def read_archive(
186
+ request: Request,
187
+ album: Optional[str] = Query(None),
188
+ category: Optional[List[str]] = Query(None),
189
+ search: Optional[str] = None,
190
+ items_per_page: int = Query(30),
191
+ page: int = Query(1)
192
+ ):
193
+ album_id = int(album) if album and album.isdigit() else None
194
+ category_ids = [int(cat) for cat in category] if category else []
195
+ offset = (page - 1) * items_per_page
196
+
197
+ with get_db_connection() as conn:
198
+ cursor = conn.cursor()
199
+ query = """
200
+ SELECT gl.timestamp, gl.prompt, gl.optimized_prompt, gl.output_file, a.name as album, c.name as category
201
+ FROM generation_logs gl
202
+ LEFT JOIN albums a ON gl.album_id = a.id
203
+ LEFT JOIN categories c ON gl.category_id = c.id
204
+ WHERE 1=1
205
+ """
206
+ params = []
207
+
208
+ if album_id is not None:
209
+ query += " AND gl.album_id = ?"
210
+ params.append(album_id)
211
+
212
+ if category_ids:
213
+ # Hier ist die Verknüpfungstabelle picture_categories notwendig
214
+ query = """
215
+ SELECT gl.timestamp, gl.prompt, gl.optimized_prompt, gl.output_file, a.name as album, GROUP_CONCAT(c.name) as categories
216
+ FROM generation_logs gl
217
+ LEFT JOIN albums a ON gl.album_id = a.id
218
+ LEFT JOIN picture_categories pc ON gl.id = pc.picture_id
219
+ LEFT JOIN categories c ON pc.category_id = c.id
220
+ WHERE 1=1
221
+ """
222
+ if album_id is not None:
223
+ query += " AND gl.album_id = ?"
224
+ params.append(album_id)
225
+
226
+ query += " AND pc.category_id IN ({})".format(','.join('?' for _ in category_ids))
227
+ params.extend(category_ids)
228
+
229
+ if search:
230
+ query += " AND (gl.prompt LIKE ? OR gl.optimized_prompt LIKE ?)"
231
+ params.append(f'%{search}%')
232
+ params.append(f'%{search}%')
233
+
234
+ query += " GROUP BY gl.id, gl.timestamp, gl.prompt, gl.optimized_prompt, gl.output_file, a.name ORDER BY gl.timestamp DESC LIMIT ? OFFSET ?"
235
+ params.extend([items_per_page, offset])
236
+ cursor.execute(query, params)
237
+ logs = cursor.fetchall()
238
+
239
+ logs = [{
240
+ "timestamp": log[0],
241
+ "prompt": log[1],
242
+ "optimized_prompt": log[2],
243
+ "output_file": log[3],
244
+ "album": log[4],
245
+ "category": log[5]
246
+ } for log in logs]
247
+
248
+ cursor.execute("SELECT id, name FROM albums")
249
+ albums = cursor.fetchall()
250
+
251
+ cursor.execute("SELECT id, name FROM categories")
252
+ categories = cursor.fetchall()
253
+
254
+ return templates.TemplateResponse("archive.html", {
255
+ "request": request,
256
+ "logs": logs,
257
+ "albums": albums,
258
+ "categories": categories,
259
+ "selected_album": album,
260
+ "selected_categories": category_ids,
261
+ "search_query": search,
262
+ "items_per_page": items_per_page,
263
+ "page": page
264
+ })
265
+
266
+
267
+ @app.get("/backend")
268
+ def read_backend(request: Request):
269
+ with get_db_connection() as conn:
270
+ cursor = conn.cursor()
271
+ cursor.execute("SELECT id, name FROM albums")
272
+ albums = cursor.fetchall()
273
+ cursor.execute("SELECT id, name FROM categories")
274
+ categories = cursor.fetchall()
275
+ return templates.TemplateResponse("backend.html", {"request": request, "albums": albums, "categories": categories})
276
+
277
+ @app.get("/backend/stats")
278
+ async def get_backend_stats():
279
+ with get_db_connection() as conn:
280
+ cursor = conn.cursor()
281
+
282
+ # Anzahl der Bilder (aus der pictures-Tabelle)
283
+ cursor.execute("SELECT COUNT(*) FROM pictures")
284
+ total_images = cursor.fetchone()[0]
285
+
286
+ # Alben-Statistiken (Anzahl)
287
+ cursor.execute("SELECT COUNT(*) FROM albums")
288
+ total_albums = cursor.fetchone()[0]
289
+
290
+ # Kategorie-Statistiken (Anzahl)
291
+ cursor.execute("SELECT COUNT(*) FROM categories")
292
+ total_categories = cursor.fetchone()[0]
293
+
294
+ # Monatliche Statistiken (Anzahl der Bilder pro Monat)
295
+ cursor.execute("""
296
+ SELECT strftime('%Y-%m', timestamp) as month, COUNT(*)
297
+ FROM pictures
298
+ GROUP BY month
299
+ ORDER BY month
300
+ """)
301
+ monthly_stats = [{"month": row[0], "count": row[1]} for row in cursor.fetchall()]
302
+
303
+ # Speicherplatzberechnung
304
+ total_size = 0
305
+ for filename in os.listdir(IMAGE_STORAGE_PATH):
306
+ filepath = os.path.join(IMAGE_STORAGE_PATH, filename)
307
+ if os.path.isfile(filepath):
308
+ total_size += os.path.getsize(filepath)
309
+ total_size_mb = total_size / (1024 * 1024)
310
+
311
+ # Daten für die Kategorien-Statistik (Beispiel: Anzahl der Bilder pro Kategorie)
312
+ cursor.execute("""
313
+ SELECT c.name, COUNT(pc.picture_id)
314
+ FROM categories c
315
+ LEFT JOIN picture_categories pc ON c.id = pc.category_id
316
+ GROUP BY c.name
317
+ """)
318
+ category_stats = [{"name": row[0], "count": row[1]} for row in cursor.fetchall()]
319
+
320
+ return {
321
+ "total_images": total_images,
322
+ "albums": {
323
+ "total": total_albums
324
+ },
325
+ "categories": {
326
+ "total": total_categories,
327
+ "data": category_stats
328
+ },
329
+ "storage_usage_mb": total_size_mb,
330
+ "monthly": monthly_stats
331
+ } # Hier war die Klammer falsch gesetzt
332
+
333
+ # Neue Routen für Alben
334
+ @app.get("/albums")
335
+ async def get_albums():
336
+ with get_db_connection() as conn:
337
+ cursor = conn.cursor()
338
+ cursor.execute("SELECT id, name FROM albums")
339
+ result = cursor.fetchall()
340
+ albums = [{"id": row[0], "name": row[1]} for row in result]
341
+ return albums
342
+
343
+ @app.post("/create_album")
344
+ async def create_album_route(name: str = Form(...), description: Optional[str] = Form(None)):
345
+ try:
346
+ with get_db_connection() as conn:
347
+ cursor = conn.cursor()
348
+ cursor.execute("INSERT INTO albums (name) VALUES (?)", (name,))
349
+ conn.commit()
350
+ new_album_id = cursor.lastrowid
351
+ return {"message": "Album erstellt", "id": new_album_id, "name": name}
352
+ except sqlite3.Error as e:
353
+ raise HTTPException(status_code=500, detail=f"Error creating album: {e}")
354
+
355
+ @app.delete("/delete_album/{album_id}")
356
+ async def delete_album(album_id: int):
357
+ try:
358
+ with get_db_connection() as conn:
359
+ cursor = conn.cursor()
360
+ # Lösche die Verknüpfungen in picture_categories
361
+ cursor.execute("DELETE FROM picture_categories WHERE picture_id IN (SELECT id FROM pictures WHERE album_id = ?)", (album_id,))
362
+ # Lösche die Bilder aus der pictures-Tabelle
363
+ cursor.execute("DELETE FROM pictures WHERE album_id = ?", (album_id,))
364
+ # Lösche die Einträge aus generation_logs
365
+ cursor.execute("DELETE FROM generation_logs WHERE album_id = ?", (album_id,))
366
+ # Lösche das Album aus der albums-Tabelle
367
+ cursor.execute("DELETE FROM albums WHERE id = ?", (album_id,))
368
+ conn.commit()
369
+ return {"message": f"Album {album_id} und zugehörige Einträge gelöscht"}
370
+ except sqlite3.Error as e:
371
+ raise HTTPException(status_code=500, detail=f"Error deleting album: {e}")
372
+
373
+ @app.put("/update_album/{album_id}")
374
+ async def update_album(album_id: int, request: Request):
375
+ data = await request.json()
376
+ try:
377
+ with get_db_connection() as conn:
378
+ cursor = conn.cursor()
379
+ cursor.execute("UPDATE albums SET name = ? WHERE id = ?", (data["name"], album_id))
380
+ conn.commit()
381
+ if cursor.rowcount == 0:
382
+ raise HTTPException(status_code=404, detail=f"Album {album_id} nicht gefunden")
383
+ return {"message": f"Album {album_id} aktualisiert"}
384
+ except sqlite3.Error as e:
385
+ raise HTTPException(status_code=500, detail=f"Error updating album: {e}")
386
+
387
+ # Neue Routen für Kategorien
388
+ @app.get("/categories")
389
+ async def get_categories():
390
+ with get_db_connection() as conn:
391
+ cursor = conn.cursor()
392
+ cursor.execute("SELECT id, name FROM categories")
393
+ result = cursor.fetchall()
394
+ categories = [{"id": row[0], "name": row[1]} for row in result]
395
+ return categories
396
+
397
+ @app.post("/create_category")
398
+ async def create_category_route(name: str = Form(...)):
399
+ try:
400
+ with get_db_connection() as conn:
401
+ cursor = conn.cursor()
402
+ cursor.execute("INSERT INTO categories (name) VALUES (?)", (name,))
403
+ conn.commit()
404
+ new_category_id = cursor.lastrowid
405
+ return {"message": "Kategorie erstellt", "id": new_category_id, "name": name}
406
+ except sqlite3.Error as e:
407
+ raise HTTPException(status_code=500, detail=f"Error creating category: {e}")
408
+
409
+ @app.delete("/delete_category/{category_id}")
410
+ async def delete_category(category_id: int):
411
+ try:
412
+ with get_db_connection() as conn:
413
+ cursor = conn.cursor()
414
+ # Lösche die Verknüpfungen in picture_categories
415
+ cursor.execute("DELETE FROM picture_categories WHERE category_id = ?", (category_id,))
416
+ # Lösche die Kategorie aus der categories-Tabelle
417
+ cursor.execute("DELETE FROM categories WHERE id = ?", (category_id,))
418
+ conn.commit()
419
+ return {"message": f"Kategorie {category_id} und zugehörige Einträge gelöscht"}
420
+ except sqlite3.Error as e:
421
+ raise HTTPException(status_code=500, detail=f"Error deleting category: {e}")
422
+
423
+ @app.put("/update_category/{category_id}")
424
+ async def update_category(category_id: int, request: Request):
425
+ data = await request.json()
426
+ try:
427
+ with get_db_connection() as conn:
428
+ cursor = conn.cursor()
429
+ cursor.execute("UPDATE categories SET name = ? WHERE id = ?", (data["name"], category_id))
430
+ conn.commit()
431
+ if cursor.rowcount == 0:
432
+ raise HTTPException(status_code=404, detail=f"Kategorie {category_id} nicht gefunden")
433
+ return {"message": f"Kategorie {category_id} aktualisiert"}
434
+ except sqlite3.Error as e:
435
+ raise HTTPException(status_code=500, detail=f"Error updating category: {e}")
436
+
437
+ @app.post("/flux-pics")
438
+ async def download_images(request: Request):
439
+ try:
440
+ body = await request.json()
441
+ logger.info(f"Received request body: {body}")
442
+
443
+ image_files = body.get("selectedImages", [])
444
+ if not image_files:
445
+ raise HTTPException(status_code=400, detail="Keine Bilder ausgewählt.")
446
+
447
+ logger.info(f"Processing image files: {image_files}")
448
+
449
+ # Überprüfe ob Download-Verzeichnis existiert
450
+ if not os.path.exists(IMAGE_STORAGE_PATH):
451
+ logger.error(f"Storage path not found: {IMAGE_STORAGE_PATH}")
452
+ raise HTTPException(status_code=500, detail="Storage path not found")
453
+
454
+ zip_buffer = BytesIO()
455
+ with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zip_file:
456
+ for image_file in image_files:
457
+ image_path = os.path.join(IMAGE_STORAGE_PATH, image_file)
458
+ logger.info(f"Processing file: {image_path}")
459
+
460
+ if os.path.exists(image_path):
461
+ zip_file.write(image_path, arcname=image_file)
462
+ else:
463
+ logger.error(f"File not found: {image_path}")
464
+ raise HTTPException(status_code=404, detail=f"Bild {image_file} nicht gefunden.")
465
+
466
+ zip_buffer.seek(0)
467
+
468
+ # Korrekter Response mit Buffer
469
+ return Response(
470
+ content=zip_buffer.getvalue(),
471
+ media_type="application/zip",
472
+ headers={
473
+ "Content-Disposition": f"attachment; filename=images.zip"
474
+ }
475
+ )
476
+
477
+ except Exception as e:
478
+ logger.error(f"Error in download_images: {str(e)}")
479
+ raise HTTPException(status_code=500, detail=str(e))
480
+
481
+ @app.post("/flux-pics/single")
482
+ async def download_single_image(request: Request):
483
+ try:
484
+ data = await request.json()
485
+ filename = data.get("filename")
486
+ logger.info(f"Requested file download: {filename}")
487
+
488
+ if not filename:
489
+ logger.error("No filename provided")
490
+ raise HTTPException(status_code=400, detail="Kein Dateiname angegeben")
491
+
492
+ file_path = os.path.join(IMAGE_STORAGE_PATH, filename)
493
+ logger.info(f"Full file path: {file_path}")
494
+
495
+ if not os.path.exists(file_path):
496
+ logger.error(f"File not found: {file_path}")
497
+ raise HTTPException(status_code=404, detail=f"Datei {filename} nicht gefunden")
498
+
499
+ # Determine MIME type
500
+ file_extension = filename.lower().split('.')[-1]
501
+ mime_types = {
502
+ 'png': 'image/png',
503
+ 'jpg': 'image/jpeg',
504
+ 'jpeg': 'image/jpeg',
505
+ 'gif': 'image/gif',
506
+ 'webp': 'image/webp'
507
+ }
508
+ media_type = mime_types.get(file_extension, 'application/octet-stream')
509
+ logger.info(f"Serving file with media type: {media_type}")
510
+
511
+ return FileResponse(
512
+ path=file_path,
513
+ filename=filename,
514
+ media_type=media_type,
515
+ headers={
516
+ "Content-Disposition": f"attachment; filename={filename}"
517
+ }
518
+ )
519
+ except Exception as e:
520
+ logger.error(f"Error in download_single_image: {str(e)}")
521
+ raise HTTPException(status_code=500, detail=str(e))
522
+
523
+ @app.websocket("/ws")
524
+ async def websocket_endpoint(websocket: WebSocket):
525
+ await websocket.accept()
526
+ try:
527
+ data = await websocket.receive_json()
528
+ prompts = data.get("prompts", [data])
529
+
530
+ for prompt_data in prompts:
531
+ prompt_data["lora_scale"] = float(prompt_data["lora_scale"])
532
+ prompt_data["guidance_scale"] = float(prompt_data["guidance_scale"])
533
+ prompt_data["prompt_strength"] = float(prompt_data["prompt_strength"])
534
+ prompt_data["num_inference_steps"] = int(prompt_data["num_inference_steps"])
535
+ prompt_data["num_outputs"] = int(prompt_data["num_outputs"])
536
+ prompt_data["output_quality"] = int(prompt_data["output_quality"])
537
+
538
+ # Handle new album and category creation
539
+ album_name = prompt_data.get("album_id")
540
+ category_names = prompt_data.get("category_ids", [])
541
+
542
+ if album_name and not album_name.isdigit():
543
+ with get_db_connection() as conn:
544
+ cursor = conn.cursor()
545
+ cursor.execute(
546
+ "INSERT INTO albums (name) VALUES (?)", (album_name,)
547
+ )
548
+ conn.commit()
549
+ prompt_data["album_id"] = cursor.lastrowid
550
+ else:
551
+ prompt_data["album_id"] = int(album_name) if album_name else None
552
+
553
+ category_ids = []
554
+ for category_name in category_names:
555
+ if not category_name.isdigit():
556
+ with get_db_connection() as conn:
557
+ cursor = conn.cursor()
558
+ cursor.execute(
559
+ "INSERT INTO categories (name) VALUES (?)", (category_name,)
560
+ )
561
+ conn.commit()
562
+ category_ids.append(cursor.lastrowid)
563
+ else:
564
+ category_ids.append(int(category_name) if category_name else None)
565
+ prompt_data["category_ids"] = category_ids
566
+
567
+ args = argparse.Namespace(**prompt_data)
568
+
569
+ # await websocket.send_json({"message": "Optimiere Prompt..."})
570
+ optimized_prompt = (
571
+ optimize_prompt(args.prompt)
572
+ if getattr(args, "agent", False)
573
+ else args.prompt
574
+ )
575
+ await websocket.send_json({"optimized_prompt": optimized_prompt})
576
+
577
+ if prompt_data.get("optimize_only"):
578
+ continue
579
+
580
+ await generate_and_download_image(websocket, args, optimized_prompt)
581
+ except WebSocketDisconnect:
582
+ print("Client disconnected")
583
+ except Exception as e:
584
+ await websocket.send_json({"message": str(e)})
585
+ raise e
586
+ finally:
587
+ await websocket.close()
588
+
589
+ async def fetch_image(item, index, args, filenames, semaphore, websocket, timestamp):
590
+ async with semaphore:
591
+ try:
592
+ response = requests.get(item, timeout=TIMEOUT_DURATION)
593
+ if response.status_code == 200:
594
+ filename = (
595
+ f"{DOWNLOAD_DIR}/image_{timestamp}_{index}.{args.output_format}"
596
+ )
597
+ with open(filename, "wb") as file:
598
+ file.write(response.content)
599
+ filenames.append(
600
+ f"/flux-pics/image_{timestamp}_{index}.{args.output_format}"
601
+ )
602
+ progress = int((index + 1) / args.num_outputs * 100)
603
+ await websocket.send_json({"progress": progress})
604
+ else:
605
+ await websocket.send_json(
606
+ {
607
+ "message": f"Fehler beim Herunterladen des Bildes {index + 1}: {response.status_code}"
608
+ }
609
+ )
610
+ except requests.exceptions.Timeout:
611
+ await websocket.send_json(
612
+ {"message": f"Timeout beim Herunterladen des Bildes {index + 1}"}
613
+ )
614
+
615
+ async def generate_and_download_image(websocket: WebSocket, args, optimized_prompt):
616
+ try:
617
+ input_data = {
618
+ "prompt": optimized_prompt,
619
+ "hf_lora": getattr(
620
+ args, "hf_lora", None
621
+ ), # Use getattr to safely access hf_lora
622
+ "lora_scale": args.lora_scale,
623
+ "num_outputs": args.num_outputs,
624
+ "aspect_ratio": args.aspect_ratio,
625
+ "output_format": args.output_format,
626
+ "guidance_scale": args.guidance_scale,
627
+ "output_quality": args.output_quality,
628
+ "prompt_strength": args.prompt_strength,
629
+ "num_inference_steps": args.num_inference_steps,
630
+ "disable_safety_checker": False,
631
+ }
632
+
633
+ # await websocket.send_json({"message": "Generiere Bilder..."})
634
+
635
+ # Debug: Log the start of the replication process
636
+ print(
637
+ f"Starting replication process for {args.num_outputs} outputs with timeout {TIMEOUT_DURATION}"
638
+ )
639
+
640
+ output = replicate.run(
641
+ "lucataco/flux-dev-lora:091495765fa5ef2725a175a57b276ec30dc9d39c22d30410f2ede68a3eab66b3",
642
+ input=input_data,
643
+ timeout=TIMEOUT_DURATION,
644
+ )
645
+
646
+ if not os.path.exists(DOWNLOAD_DIR):
647
+ os.makedirs(DOWNLOAD_DIR)
648
+
649
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
650
+ filenames = []
651
+ semaphore = Semaphore(3) # Limit concurrent downloads
652
+
653
+ tasks = [
654
+ create_task(
655
+ fetch_image(
656
+ item, index, args, filenames, semaphore, websocket, timestamp
657
+ )
658
+ )
659
+ for index, item in enumerate(output)
660
+ ]
661
+ await gather(*tasks)
662
+
663
+ for file in filenames:
664
+ log_generation(args, optimized_prompt, file)
665
+
666
+ await websocket.send_json(
667
+ {"message": "Bilder erfolgreich generiert", "generated_files": filenames}
668
+ )
669
+ except requests.exceptions.Timeout:
670
+ await websocket.send_json(
671
+ {"message": "Fehler bei der Bildgenerierung: Timeout überschritten"}
672
+ )
673
+ except Exception as e:
674
+ await websocket.send_json(
675
+ {"message": f"Fehler bei der Bildgenerierung: {str(e)}"}
676
+ )
677
+ raise Exception(f"Fehler bei der Bildgenerierung: {str(e)}")
678
+
679
+ def optimize_prompt(prompt):
680
+ api_key = os.environ.get("MISTRAL_API_KEY")
681
+ agent_id = os.environ.get("MISTRAL_FLUX_AGENT")
682
+
683
+ if not api_key or not agent_id:
684
+ raise ValueError("MISTRAL_API_KEY oder MISTRAL_FLUX_AGENT nicht gesetzt")
685
+
686
+ client = Mistral(api_key=api_key)
687
+ chat_response = client.agents.complete(
688
+ agent_id=agent_id,
689
+ messages=[
690
+ {
691
+ "role": "user",
692
+ "content": f"Optimiere folgenden Prompt für Flux Lora: {prompt}",
693
+ }
694
+ ],
695
+ )
696
+
697
+ return chat_response.choices[0].message.content
698
+
699
+ if __name__ == "__main__":
700
+ # Parse command line arguments
701
+ parser = argparse.ArgumentParser(description="Beschreibung")
702
+ parser.add_argument('--hf_lora', default=None, help='HF LoRA Model')
703
+ args = parser.parse_args()
704
+
705
+ # Pass arguments to the FastAPI application
706
+ app.state.args = args
707
+
708
+ # Run the Uvicorn server
709
+ uvicorn.run(
710
+ "main:app",
711
+ host="0.0.0.0",
712
+ port=8000,
713
+ reload=True,
714
+ log_level="trace"
715
+ )
static/api.js ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // static/components/api.js
2
+ export const API = {
3
+ albums: {
4
+ list: '/albums', // Vermutung basierend auf AlbumManager.js
5
+ create: '/create_album',
6
+ delete: (id) => `/delete_album/${id}`,
7
+ update: (id) => `/update_album/${id}`,
8
+ },
9
+ categories: {
10
+ list: '/categories', // Vermutung basierend auf CategoryManager.js
11
+ create: '/create_category',
12
+ delete: (id) => `/delete_category/${id}`,
13
+ update: (id) => `/update_category/${id}`,
14
+ merge: '/categories/merge'
15
+ },
16
+ statistics: {
17
+ images: '/backend/stats',
18
+ storage: '/backend/stats'
19
+ }
20
+ };
21
+
22
+ export const APIHandler = {
23
+ get: async (url) => {
24
+ const response = await fetch(url);
25
+ if (!response.ok) {
26
+ throw new Error('Network response was not ok');
27
+ }
28
+ return response.json();
29
+ },
30
+ post: async (url, data) => {
31
+ const response = await fetch(url, {
32
+ method: 'POST',
33
+ headers: {
34
+ 'Content-Type': 'application/json'
35
+ },
36
+ body: JSON.stringify(data)
37
+ });
38
+ if (!response.ok) {
39
+ throw new Error('Network response was not ok');
40
+ }
41
+ return response.json();
42
+ },
43
+ put: async (url, data) => {
44
+ //PUT hinzufügen
45
+ const response = await fetch(url, {
46
+ method: 'PUT',
47
+ headers: {
48
+ 'Content-Type': 'application/json'
49
+ },
50
+ body: JSON.stringify(data)
51
+ });
52
+
53
+ if (!response.ok) {
54
+ throw new Error('Network response was not ok');
55
+ }
56
+ return response.json();
57
+ },
58
+ delete: async (url) => {
59
+ const response = await fetch(url, {
60
+ method: 'DELETE'
61
+ });
62
+ if (!response.ok) {
63
+ throw new Error('Network response was not ok');
64
+ }
65
+ return response.json();
66
+ }
67
+ };
static/arrow-down1.png ADDED
static/arrow-down2.png ADDED
static/arrow-up1.png ADDED
static/arrow-up2.png ADDED
static/backend.js ADDED
@@ -0,0 +1,159 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ class Logger {
2
+ static isDebugMode = false;
3
+
4
+ static debug(message, data = null) {
5
+ if (this.isDebugMode) {
6
+ console.log(`[Debug] ${message}`, data || '');
7
+ }
8
+ }
9
+
10
+ static error(message, error = null) {
11
+ console.error(`[Error] ${message}`, error || '');
12
+ }
13
+
14
+ static initializeDebugMode() {
15
+ try {
16
+ const urlParams = new URLSearchParams(window.location.search);
17
+ this.isDebugMode = urlParams.has('debug');
18
+ } catch (error) {
19
+ console.error('Fehler beim Initialisieren des Debug-Modus:', error);
20
+ this.isDebugMode = false;
21
+ }
22
+ }
23
+ }
24
+
25
+ const utils = {
26
+ showLoading() {
27
+ if (document.body) {
28
+ document.body.classList.add('loading');
29
+ Logger.debug('Loading-Status aktiviert');
30
+ } else {
31
+ Logger.error('document.body nicht verfügbar');
32
+ }
33
+ },
34
+
35
+ hideLoading() {
36
+ if (document.body) {
37
+ document.body.classList.remove('loading');
38
+ Logger.debug('Loading-Status deaktiviert');
39
+ } else {
40
+ Logger.error('document.body nicht verfügbar');
41
+ }
42
+ },
43
+
44
+ safeGetElement(id) {
45
+ const element = document.getElementById(id);
46
+ if (!element) {
47
+ Logger.error(`Element mit ID '${id}' nicht gefunden`);
48
+ return null;
49
+ }
50
+ return element;
51
+ },
52
+
53
+ async withLoading(asyncFn) {
54
+ try {
55
+ this.showLoading();
56
+ await asyncFn();
57
+ } finally {
58
+ this.hideLoading();
59
+ }
60
+ }
61
+ };
62
+
63
+ class BackendManager {
64
+ constructor() {
65
+ this.selectedItems = new Set();
66
+ this.initializeEventListeners();
67
+ }
68
+
69
+ initializeEventListeners() {
70
+ // Event-Listener für Dark Mode Toggle
71
+ const darkModeToggle = document.getElementById('darkModeToggle');
72
+ if (darkModeToggle) {
73
+ darkModeToggle.addEventListener('click', () => this.toggleDarkMode());
74
+ }
75
+
76
+ // Event-Listener für Aktualisieren-Button
77
+ const refreshButton = document.querySelector(".btn-primary[onclick='refreshData()']");
78
+ if (refreshButton) {
79
+ refreshButton.addEventListener("click", () => {
80
+ this.refreshData();
81
+ });
82
+ }
83
+ }
84
+
85
+ toggleDarkMode() {
86
+ document.body.classList.toggle('dark-mode');
87
+ const isDarkMode = document.body.classList.contains('dark-mode');
88
+ localStorage.setItem('darkMode', isDarkMode);
89
+
90
+ // Aktualisiere das Icon
91
+ const icon = document.querySelector('#darkModeToggle i');
92
+ if (isDarkMode) {
93
+ icon.classList.remove('bi-moon');
94
+ icon.classList.add('bi-sun');
95
+ } else {
96
+ icon.classList.remove('bi-sun');
97
+ icon.classList.add('bi-moon');
98
+ }
99
+ }
100
+
101
+ async updateStats() {
102
+ try {
103
+ const response = await fetch('/backend/stats');
104
+ if (!response.ok) {
105
+ throw new Error('Network response was not ok');
106
+ }
107
+ const stats = await response.json();
108
+ console.log("Stats from backend:", stats);
109
+
110
+ // Aktualisiere die Statistik-Anzeige
111
+ document.getElementById('totalImages').textContent = stats.total_images;
112
+ document.getElementById('totalAlbums').textContent = stats.albums.total;
113
+ document.getElementById('totalCategories').textContent = stats.categories.total;
114
+ document.getElementById('storageUsage').textContent = `${(stats.storage_usage_mb).toFixed(2)} MB`;
115
+
116
+ } catch (error) {
117
+ console.error('Fehler beim Laden der Statistiken:', error);
118
+ }
119
+ }
120
+
121
+ async refreshData() {
122
+ try {
123
+ await this.updateStats();
124
+ this.showToast('Erfolg', 'Daten erfolgreich aktualisiert', 'success');
125
+ } catch (error) {
126
+ this.showToast('Fehler', 'Fehler beim Aktualisieren der Daten', 'danger');
127
+ }
128
+ }
129
+
130
+ showToast(title, message, type = 'info') {
131
+ const toast = document.getElementById('toast');
132
+ const toastTitle = document.getElementById('toastTitle');
133
+ const toastMessage = document.getElementById('toastMessage');
134
+
135
+ toast.classList.remove('bg-success', 'bg-danger', 'bg-info', 'bg-warning');
136
+ toast.classList.add(`bg-${type}`);
137
+ toastTitle.textContent = title;
138
+ toastMessage.textContent = message;
139
+
140
+ const bsToast = new bootstrap.Toast(toast);
141
+ bsToast.show();
142
+ }
143
+ }
144
+
145
+ // Initialisierung
146
+ document.addEventListener('DOMContentLoaded', () => {
147
+ new BackendManager();
148
+ updateStatistics(); // Statistiken beim Laden der Seite abrufen
149
+
150
+ // Dark Mode aus localStorage wiederherstellen, falls vorhanden
151
+ if (localStorage.getItem('darkMode') === 'true') {
152
+ document.body.classList.add('dark-mode');
153
+ const icon = document.querySelector('#darkModeToggle i');
154
+ if (icon) {
155
+ icon.classList.remove('bi-moon');
156
+ icon.classList.add('bi-sun');
157
+ }
158
+ }
159
+ });
static/bg.webp ADDED
static/bg2.webp ADDED
static/components/AlbumManager.js ADDED
@@ -0,0 +1,260 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect, useCallback } from 'react';
2
+ import { API, APIHandler, Validators } from './api.js';
3
+
4
+ const AlbumManager = () => {
5
+ const [albums, setAlbums] = useState([]);
6
+ const [loading, setLoading] = useState(true);
7
+ const [error, setError] = useState(null);
8
+ const [selectedAlbums, setSelectedAlbums] = useState(new Set());
9
+ const [editingAlbum, setEditingAlbum] = useState(null);
10
+
11
+ // Daten laden
12
+ const fetchAlbums = useCallback(async () => {
13
+ try {
14
+ setLoading(true);
15
+ const data = await APIHandler.get(API.albums.list);
16
+ setAlbums(data);
17
+ setError(null);
18
+ } catch (err) {
19
+ setError('Fehler beim Laden der Alben: ' + err.message);
20
+ console.error('Fetch error:', err);
21
+ } finally {
22
+ setLoading(false);
23
+ }
24
+ }, []);
25
+
26
+ useEffect(() => {
27
+ fetchAlbums();
28
+ }, [fetchAlbums]);
29
+
30
+
31
+ // Album erstellen
32
+ const handleCreate = async (name, description = '') => {
33
+ try {
34
+ //const validationErrors = Validators.album({ name });
35
+ //if (Object.keys(validationErrors).length > 0) {
36
+ // throw new Error(Object.values(validationErrors).join(', '));
37
+ //}
38
+
39
+ await APIHandler.post(API.albums.create, { name, description });
40
+ await fetchAlbums();
41
+ window.showToast('Erfolg', 'Album wurde erstellt', 'success');
42
+ } catch (err) {
43
+ window.showToast('Fehler', err.message, 'danger');
44
+ }
45
+ };
46
+
47
+ // Album aktualisieren
48
+ const handleUpdate = async (id, updates) => {
49
+ try {
50
+ //const validationErrors = Validators.album(updates);
51
+ //if (Object.keys(validationErrors).length > 0) {
52
+ // throw new Error(Object.values(validationErrors).join(', '));
53
+ //}
54
+
55
+ await APIHandler.put(API.albums.update(id), updates);
56
+ await fetchAlbums();
57
+ setEditingAlbum(null);
58
+ window.showToast('Erfolg', 'Album wurde aktualisiert', 'success');
59
+ } catch (err) {
60
+ window.showToast('Fehler', err.message, 'danger');
61
+ }
62
+ };
63
+
64
+
65
+ // Album aktualisieren
66
+ const handleUpdate = async (id, updates) => {
67
+ try {
68
+ const validationErrors = Validators.album(updates);
69
+ if (Object.keys(validationErrors).length > 0) {
70
+ throw new Error(Object.values(validationErrors).join(', '));
71
+ }
72
+
73
+ await APIHandler.put(API.albums.update(id), updates);
74
+ await fetchAlbums();
75
+ setEditingAlbum(null);
76
+ window.showToast('Erfolg', 'Album wurde aktualisiert', 'success');
77
+ } catch (err) {
78
+ window.showToast('Fehler', err.message, 'danger');
79
+ }
80
+ };
81
+
82
+ // Massenbearbeitung
83
+ const handleBulkDelete = async () => {
84
+ if (selectedAlbums.size === 0) return;
85
+ if (!window.confirm(`Möchten Sie ${selectedAlbums.size} Alben wirklich löschen?`)) return;
86
+
87
+ try {
88
+ await Promise.all(
89
+ Array.from(selectedAlbums).map(id =>
90
+ APIHandler.delete(API.albums.delete(id))
91
+ )
92
+ );
93
+ setSelectedAlbums(new Set());
94
+ await fetchAlbums();
95
+ window.showToast('Erfolg', 'Ausgewählte Alben wurden gelöscht', 'success');
96
+ } catch (err) {
97
+ window.showToast('Fehler', err.message, 'danger');
98
+ }
99
+ };
100
+
101
+ const handleSelectAll = (event) => {
102
+ if (event.target.checked) {
103
+ setSelectedAlbums(new Set(albums.map(album => album.id)));
104
+ } else {
105
+ setSelectedAlbums(new Set());
106
+ }
107
+ };
108
+
109
+ if (loading) {
110
+ return (
111
+ <div className="d-flex justify-content-center p-5">
112
+ <div className="spinner-border text-primary" role="status">
113
+ <span className="visually-hidden">Laden...</span>
114
+ </div>
115
+ </div>
116
+ );
117
+ }
118
+
119
+ if (error) {
120
+ return (
121
+ <div className="alert alert-danger m-3" role="alert">
122
+ <h4 className="alert-heading">Fehler</h4>
123
+ <p>{error}</p>
124
+ <button
125
+ className="btn btn-outline-danger"
126
+ onClick={fetchAlbums}
127
+ >
128
+ Erneut versuchen
129
+ </button>
130
+ </div>
131
+ );
132
+ }
133
+
134
+ return (
135
+ <div className="card-body">
136
+ {/* Toolbar */}
137
+ <div className="d-flex justify-content-between mb-3">
138
+ <button
139
+ className="btn btn-primary"
140
+ onClick={() => setEditingAlbum({ name: '', description: '' })}
141
+ >
142
+ <i className="bi bi-plus-lg"></i> Neues Album
143
+ </button>
144
+ {selectedAlbums.size > 0 && (
145
+ <button
146
+ className="btn btn-danger"
147
+ onClick={handleBulkDelete}
148
+ >
149
+ <i className="bi bi-trash"></i>
150
+ {selectedAlbums.size} Alben löschen
151
+ </button>
152
+ )}
153
+ </div>
154
+
155
+ {/* Album Liste */}
156
+ <div className="table-responsive">
157
+ <table className="table table-hover">
158
+ <thead>
159
+ <tr>
160
+ <th>
161
+ <input
162
+ type="checkbox"
163
+ className="form-check-input"
164
+ onChange={handleSelectAll}
165
+ checked={selectedAlbums.size === albums.length}
166
+ />
167
+ </th>
168
+ <th>Name</th>
169
+ <th>Bilder</th>
170
+ <th>Erstellt</th>
171
+ <th>Aktionen</th>
172
+ </tr>
173
+ </thead>
174
+ <tbody>
175
+ {albums.map(album => (
176
+ <tr key={album.id}>
177
+ <td>
178
+ <input
179
+ type="checkbox"
180
+ className="form-check-input"
181
+ checked={selectedAlbums.has(album.id)}
182
+ onChange={(e) => {
183
+ const newSelected = new Set(selectedAlbums);
184
+ if (e.target.checked) {
185
+ newSelected.add(album.id);
186
+ } else {
187
+ newSelected.delete(album.id);
188
+ }
189
+ setSelectedAlbums(newSelected);
190
+ }}
191
+ />
192
+ </td>
193
+ <td>
194
+ {editingAlbum?.id === album.id ? (
195
+ <input
196
+ type="text"
197
+ className="form-control"
198
+ value={editingAlbum.name}
199
+ onChange={(e) => setEditingAlbum({
200
+ ...editingAlbum,
201
+ name: e.target.value
202
+ })}
203
+ />
204
+ ) : album.name}
205
+ </td>
206
+ <td>{album.imageCount}</td>
207
+ <td>{new Date(album.createdAt).toLocaleDateString()}</td>
208
+ <td>
209
+ <div className="btn-group">
210
+ {editingAlbum?.id === album.id ? (
211
+ <>
212
+ <button
213
+ className="btn btn-sm btn-success"
214
+ onClick={() => handleUpdate(album.id, editingAlbum)}
215
+ >
216
+ <i className="bi bi-check"></i>
217
+ </button>
218
+ <button
219
+ className="btn btn-sm btn-secondary"
220
+ onClick={() => setEditingAlbum(null)}
221
+ >
222
+ <i className="bi bi-x"></i>
223
+ </button>
224
+ </>
225
+ ) : (
226
+ <>
227
+ <button
228
+ className="btn btn-sm btn-outline-primary"
229
+ onClick={() => setEditingAlbum(album)}
230
+ >
231
+ <i className="bi bi-pencil"></i>
232
+ </button>
233
+ <button
234
+ className="btn btn-sm btn-outline-danger"
235
+ onClick={() => handleDelete(album.id)}
236
+ >
237
+ <i className="bi bi-trash"></i>
238
+ </button>
239
+ </>
240
+ )}
241
+ </div>
242
+ </td>
243
+ </tr>
244
+ ))}
245
+ </tbody>
246
+ </table>
247
+ </div>
248
+
249
+ {albums.length === 0 && (
250
+ <div className="text-center text-muted p-5">
251
+ <i className="bi bi-folder-x display-4"></i>
252
+ <p className="mt-3">Keine Alben vorhanden</p>
253
+ </div>
254
+ )}
255
+ </div>
256
+ );
257
+ };
258
+
259
+ export default AlbumManager;
260
+ window.AlbumManager = AlbumManager; // <-- Diese Zeile ans Ende setzen
static/components/CategoryManager.js ADDED
@@ -0,0 +1,463 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect, useCallback } from 'react';
2
+ import { API, APIHandler, Validators } from './api.js';
3
+
4
+ const CategoryManager = () => {
5
+ const [categories, setCategories] = useState([]);
6
+ const [newCategoryName, setNewCategoryName] = useState('');
7
+ const [loading, setLoading] = useState(true);
8
+
9
+ useEffect(() => {
10
+ fetchCategories();
11
+ }, []);
12
+
13
+ const fetchCategories = async () => {
14
+ try {
15
+ const response = await fetch('/backend/categories');
16
+ const data = await response.json();
17
+ setCategories(data);
18
+ } catch (error) {
19
+ console.error('Fehler beim Laden der Kategorien:', error);
20
+ } finally {
21
+ setLoading(false);
22
+ }
23
+ };
24
+
25
+ const handleSubmit = async (e) => {
26
+ e.preventDefault();
27
+ try {
28
+ const response = await fetch('/create_category', {
29
+ method: 'POST',
30
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
31
+ body: `name=${encodeURIComponent(newCategoryName)}`
32
+ });
33
+
34
+ if (!response.ok) throw new Error('Fehler beim Erstellen');
35
+
36
+ await fetchCategories();
37
+ setNewCategoryName('');
38
+ } catch (error) {
39
+ console.error('Fehler:', error);
40
+ }
41
+ };
42
+
43
+ const handleDelete = async (id) => {
44
+ if (!confirm('Kategorie wirklich löschen?')) return;
45
+
46
+ try {
47
+ const response = await fetch(`/delete_category/${id}`, {
48
+ method: 'DELETE'
49
+ });
50
+
51
+ if (!response.ok) throw new Error('Fehler beim Löschen');
52
+
53
+ await fetchCategories();
54
+ } catch (error) {
55
+ console.error('Fehler:', error);
56
+ }
57
+ };
58
+
59
+ if (loading) return <div>Lade...</div>;
60
+
61
+ return (
62
+ <div className="card-body">
63
+ <form onSubmit={handleSubmit} className="mb-3">
64
+ <div className="input-group">
65
+ <input
66
+ type="text"
67
+ className="form-control"
68
+ value={newCategoryName}
69
+ onChange={(e) => setNewCategoryName(e.target.value)}
70
+ placeholder="Neue Kategorie"
71
+ required
72
+ />
73
+ <button type="submit" className="btn btn-primary">Erstellen</button>
74
+ </div>
75
+ </form>
76
+
77
+ <div className="table-responsive">
78
+ <table className="table table-hover">
79
+ <thead>
80
+ <tr>
81
+ <th>Name</th>
82
+ <th>Bilder</th>
83
+ <th>Aktionen</th>
84
+ </tr>
85
+ </thead>
86
+ <tbody>
87
+ {categories.map(category => (
88
+ <tr key={category.id}>
89
+ <td>{category.name}</td>
90
+ <td>{category.imageCount}</td>
91
+ <td>
92
+ <button
93
+ onClick={() => handleDelete(category.id)}
94
+ className="btn btn-sm btn-outline-danger"
95
+ >
96
+ Löschen
97
+ </button>
98
+ </td>
99
+ </tr>
100
+ ))}
101
+ </tbody>
102
+ </table>
103
+ </div>
104
+ </div>
105
+ );
106
+ };
107
+
108
+ export default CategoryManager;
109
+ ### END: category-manager.txt
110
+
111
+ ### START: category-manager-updated.txt
112
+ import React, { useState, useEffect, useCallback } from 'react';
113
+ import { API, APIHandler, Validators } from '@/api';
114
+
115
+ const CategoryManager = () => {
116
+ const [categories, setCategories] = useState([]);
117
+ const [loading, setLoading] = useState(true);
118
+ const [error, setError] = useState(null);
119
+ const [selectedCategories, setSelectedCategories] = useState(new Set());
120
+ const [editingCategory, setEditingCategory] = useState(null);
121
+ const [searchTerm, setSearchTerm] = useState('');
122
+ const [sortConfig, setSortConfig] = useState({ key: 'name', direction: 'asc' });
123
+
124
+ // Daten laden
125
+ const fetchCategories = useCallback(async () => {
126
+ try {
127
+ setLoading(true);
128
+ const data = await APIHandler.get(API.categories.list);
129
+ setCategories(data);
130
+ setError(null);
131
+ } catch (err) {
132
+ setError('Fehler beim Laden der Kategorien: ' + err.message);
133
+ console.error('Fetch error:', err);
134
+ } finally {
135
+ setLoading(false);
136
+ }
137
+ }, []);
138
+
139
+ useEffect(() => {
140
+ fetchCategories();
141
+ }, [fetchCategories]);
142
+
143
+ // Sortierung
144
+ const sortedCategories = React.useMemo(() => {
145
+ const sorted = [...categories];
146
+ sorted.sort((a, b) => {
147
+ if (sortConfig.key === 'imageCount') {
148
+ return sortConfig.direction === 'asc'
149
+ ? a.imageCount - b.imageCount
150
+ : b.imageCount - a.imageCount;
151
+ }
152
+ return sortConfig.direction === 'asc'
153
+ ? a[sortConfig.key].localeCompare(b[sortConfig.key])
154
+ : b[sortConfig.key].localeCompare(a[sortConfig.key]);
155
+ });
156
+ return sorted;
157
+ }, [categories, sortConfig]);
158
+
159
+ // Kategorie erstellen
160
+ const handleCreate = async (name) => {
161
+ try {
162
+ //const validationErrors = Validators.category({ name });
163
+ //if (Object.keys(validationErrors).length > 0) {
164
+ // throw new Error(Object.values(validationErrors).join(', '));
165
+ //}
166
+
167
+ await APIHandler.post(API.categories.create, { name });
168
+ await fetchCategories();
169
+ window.showToast('Erfolg', 'Kategorie wurde erstellt', 'success');
170
+ } catch (err) {
171
+ window.showToast('Fehler', err.message, 'danger');
172
+ }
173
+ };
174
+
175
+
176
+ // Kategorie löschen
177
+ const handleDelete = async (id) => {
178
+ if (!window.confirm('Möchten Sie diese Kategorie wirklich löschen?')) return;
179
+
180
+ try {
181
+ await APIHandler.delete(API.categories.delete(id));
182
+ await fetchCategories();
183
+ window.showToast('Erfolg', 'Kategorie wurde gelöscht', 'success');
184
+ } catch (err) {
185
+ window.showToast('Fehler', err.message, 'danger');
186
+ }
187
+ };
188
+
189
+ // Kategorie aktualisieren
190
+ const handleUpdate = async (id, updates) => {
191
+ try {
192
+ //const validationErrors = Validators.category(updates);
193
+ //if (Object.keys(validationErrors).length > 0) {
194
+ // throw new Error(Object.values(validationErrors).join(', '));
195
+ //}
196
+
197
+ await APIHandler.put(API.categories.update(id), updates);
198
+ await fetchCategories();
199
+ setEditingCategory(null);
200
+ window.showToast('Erfolg', 'Kategorie wurde aktualisiert', 'success');
201
+ } catch (err) {
202
+ window.showToast('Fehler', err.message, 'danger');
203
+ }
204
+ };
205
+
206
+
207
+ // Massenbearbeitung
208
+ const handleBulkDelete = async () => {
209
+ if (selectedCategories.size === 0) return;
210
+ if (!window.confirm(`Möchten Sie ${selectedCategories.size} Kategorien wirklich löschen?`)) return;
211
+
212
+ try {
213
+ await Promise.all(
214
+ Array.from(selectedCategories).map(id =>
215
+ APIHandler.delete(API.categories.delete(id))
216
+ )
217
+ );
218
+ setSelectedCategories(new Set());
219
+ await fetchCategories();
220
+ window.showToast('Erfolg', 'Ausgewählte Kategorien wurden gelöscht', 'success');
221
+ } catch (err) {
222
+ window.showToast('Fehler', err.message, 'danger');
223
+ }
224
+ };
225
+
226
+ // Massenbearbeitung - Kategorien zusammenführen
227
+ const handleMergeCategories = async (targetId) => {
228
+ const selectedIds = Array.from(selectedCategories);
229
+ if (selectedIds.length < 2) return;
230
+
231
+ try {
232
+ await APIHandler.post(API.categories.merge, {
233
+ targetId,
234
+ sourceIds: selectedIds.filter(id => id !== targetId)
235
+ });
236
+ setSelectedCategories(new Set());
237
+ await fetchCategories();
238
+ window.showToast('Erfolg', 'Kategorien wurden zusammengeführt', 'success');
239
+ } catch (err) {
240
+ window.showToast('Fehler', err.message, 'danger');
241
+ }
242
+ };
243
+
244
+ const handleSelectAll = (event) => {
245
+ if (event.target.checked) {
246
+ setSelectedCategories(new Set(categories.map(cat => cat.id)));
247
+ } else {
248
+ setSelectedCategories(new Set());
249
+ }
250
+ };
251
+
252
+ const handleSort = (key) => {
253
+ setSortConfig(current => ({
254
+ key,
255
+ direction: current.key === key && current.direction === 'asc' ? 'desc' : 'asc'
256
+ }));
257
+ };
258
+
259
+ if (loading) {
260
+ return (
261
+ <div className="d-flex justify-content-center p-5">
262
+ <div className="spinner-border text-primary" role="status">
263
+ <span className="visually-hidden">Laden...</span>
264
+ </div>
265
+ </div>
266
+ );
267
+ }
268
+
269
+ if (error) {
270
+ return (
271
+ <div className="alert alert-danger m-3" role="alert">
272
+ <h4 className="alert-heading">Fehler</h4>
273
+ <p>{error}</p>
274
+ <button
275
+ className="btn btn-outline-danger"
276
+ onClick={fetchCategories}
277
+ >
278
+ Erneut versuchen
279
+ </button>
280
+ </div>
281
+ );
282
+ }
283
+
284
+ // Filtern basierend auf Suchbegriff
285
+ const filteredCategories = searchTerm
286
+ ? sortedCategories.filter(cat =>
287
+ cat.name.toLowerCase().includes(searchTerm.toLowerCase())
288
+ )
289
+ : sortedCategories;
290
+
291
+ return (
292
+ <div className="card-body">
293
+ {/* Toolbar */}
294
+ <div className="d-flex flex-wrap justify-content-between mb-3 gap-2">
295
+ <div className="d-flex gap-2">
296
+ <button
297
+ className="btn btn-primary"
298
+ onClick={() => setEditingCategory({ name: '' })}
299
+ >
300
+ <i className="bi bi-plus-lg"></i> Neue Kategorie
301
+ </button>
302
+ {selectedCategories.size > 0 && (
303
+ <div className="btn-group">
304
+ <button
305
+ className="btn btn-danger"
306
+ onClick={handleBulkDelete}
307
+ >
308
+ <i className="bi bi-trash"></i>
309
+ {selectedCategories.size} löschen
310
+ </button>
311
+ {selectedCategories.size > 1 && (
312
+ <button
313
+ className="btn btn-warning"
314
+ onClick={() => {
315
+ const firstId = Array.from(selectedCategories)[0];
316
+ handleMergeCategories(firstId);
317
+ }}
318
+ >
319
+ <i className="bi bi-arrow-join"></i>
320
+ Zusammenführen
321
+ </button>
322
+ )}
323
+ </div>
324
+ )}
325
+ </div>
326
+ <div className="flex-grow-1 max-w-xs">
327
+ <input
328
+ type="search"
329
+ className="form-control"
330
+ placeholder="Kategorien durchsuchen..."
331
+ value={searchTerm}
332
+ onChange={(e) => setSearchTerm(e.target.value)}
333
+ />
334
+ </div>
335
+ </div>
336
+
337
+ {/* Kategorie Liste */}
338
+ <div className="table-responsive">
339
+ <table className="table table-hover">
340
+ <thead>
341
+ <tr>
342
+ <th>
343
+ <input
344
+ type="checkbox"
345
+ className="form-check-input"
346
+ onChange={handleSelectAll}
347
+ checked={selectedCategories.size === categories.length}
348
+ />
349
+ </th>
350
+ <th
351
+ className="cursor-pointer"
352
+ onClick={() => handleSort('name')}
353
+ >
354
+ Name {sortConfig.key === 'name' && (
355
+ <i className={`bi bi-arrow-${sortConfig.direction === 'asc' ? 'up' : 'down'}`}></i>
356
+ )}
357
+ </th>
358
+ <th
359
+ className="cursor-pointer"
360
+ onClick={() => handleSort('imageCount')}
361
+ >
362
+ Bilder {sortConfig.key === 'imageCount' && (
363
+ <i className={`bi bi-arrow-${sortConfig.direction === 'asc' ? 'up' : 'down'}`}></i>
364
+ )}
365
+ </th>
366
+ <th>Aktionen</th>
367
+ </tr>
368
+ </thead>
369
+ <tbody>
370
+ {filteredCategories.map(category => (
371
+ <tr key={category.id}>
372
+ <td>
373
+ <input
374
+ type="checkbox"
375
+ className="form-check-input"
376
+ checked={selectedCategories.has(category.id)}
377
+ onChange={(e) => {
378
+ const newSelected = new Set(selectedCategories);
379
+ if (e.target.checked) {
380
+ newSelected.add(category.id);
381
+ } else {
382
+ newSelected.delete(category.id);
383
+ }
384
+ setSelectedCategories(newSelected);
385
+ }}
386
+ />
387
+ </td>
388
+ <td>
389
+ {editingCategory?.id === category.id ? (
390
+ <input
391
+ type="text"
392
+ className="form-control"
393
+ value={editingCategory.name}
394
+ onChange={(e) => setEditingCategory({
395
+ ...editingCategory,
396
+ name: e.target.value
397
+ })}
398
+ onKeyPress={(e) => {
399
+ if (e.key === 'Enter') {
400
+ handleUpdate(category.id, editingCategory);
401
+ }
402
+ }}
403
+ />
404
+ ) : category.name}
405
+ </td>
406
+ <td>{category.imageCount}</td>
407
+ <td>
408
+ <div className="btn-group">
409
+ {editingCategory?.id === category.id ? (
410
+ <>
411
+ <button
412
+ className="btn btn-sm btn-success"
413
+ onClick={() => handleUpdate(category.id, editingCategory)}
414
+ >
415
+ <i className="bi bi-check"></i>
416
+ </button>
417
+ <button
418
+ className="btn btn-sm btn-secondary"
419
+ onClick={() => setEditingCategory(null)}
420
+ >
421
+ <i className="bi bi-x"></i>
422
+ </button>
423
+ </>
424
+ ) : (
425
+ <>
426
+ <button
427
+ className="btn btn-sm btn-outline-primary"
428
+ onClick={() => setEditingCategory(category)}
429
+ >
430
+ <i className="bi bi-pencil"></i>
431
+ </button>
432
+ <button
433
+ className="btn btn-sm btn-outline-danger"
434
+ onClick={() => handleDelete(category.id)}
435
+ >
436
+ <i className="bi bi-trash"></i>
437
+ </button>
438
+ </>
439
+ )}
440
+ </div>
441
+ </td>
442
+ </tr>
443
+ ))}
444
+ </tbody>
445
+ </table>
446
+ </div>
447
+
448
+ {filteredCategories.length === 0 && (
449
+ <div className="text-center text-muted p-5">
450
+ <i className="bi bi-tags display-4"></i>
451
+ <p className="mt-3">
452
+ {searchTerm
453
+ ? 'Keine Kategorien gefunden'
454
+ : 'Keine Kategorien vorhanden'}
455
+ </p>
456
+ </div>
457
+ )}
458
+ </div>
459
+ );
460
+ };
461
+
462
+ export default CategoryManager;
463
+ window.CategoryManager = CategoryManager; // <-- Diese Zeile ans Ende setzen
static/components/StatsVisualization.js ADDED
@@ -0,0 +1,435 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useEffect } from 'react';
2
+ import { LineChart, Line, BarChart, Bar, PieChart, Pie, Cell, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
3
+
4
+ const COLORS = ['#0088FE', '#00C49F', '#FFBB28', '#FF8042'];
5
+
6
+ const StatsVisualization = () => {
7
+ const [stats, setStats] = useState(null);
8
+ const [storageStats, setStorageStats] = useState(null);
9
+
10
+ useEffect(() => {
11
+ fetchStats();
12
+ const interval = setInterval(fetchStats, 30000);
13
+ return () => clearInterval(interval);
14
+ }, []);
15
+
16
+ const fetchStats = async () => {
17
+ try {
18
+ const [statsResponse, storageResponse] = await Promise.all([
19
+ fetch('/backend/image-stats'),
20
+ fetch('/backend/storage-stats')
21
+ ]);
22
+
23
+ const statsData = await statsResponse.json();
24
+ const storageData = await storageResponse.json();
25
+
26
+ setStats(statsData);
27
+ setStorageStats(storageData);
28
+ } catch (error) {
29
+ console.error('Fehler beim Laden der Statistiken:', error);
30
+ }
31
+ };
32
+
33
+ if (!stats || !storageStats) return <div>Lade Statistiken...</div>;
34
+
35
+ return (
36
+ <div className="container-fluid p-4">
37
+ <div className="row g-4">
38
+ {/* Monatliche Bilderzahl */}
39
+ <div className="col-12">
40
+ <div className="card">
41
+ <div className="card-body">
42
+ <h5 className="card-title">Bilder pro Monat</h5>
43
+ <div style={{ height: '300px' }}>
44
+ <ResponsiveContainer width="100%" height="100%">
45
+ <LineChart data={stats.monthly}>
46
+ <CartesianGrid strokeDasharray="3 3" />
47
+ <XAxis dataKey="month" />
48
+ <YAxis />
49
+ <Tooltip />
50
+ <Legend />
51
+ <Line
52
+ type="monotone"
53
+ dataKey="count"
54
+ stroke="#8884d8"
55
+ name="Anzahl Bilder"
56
+ />
57
+ </LineChart>
58
+ </ResponsiveContainer>
59
+ </div>
60
+ </div>
61
+ </div>
62
+ </div>
63
+
64
+ {/* Album & Kategorie Verteilung */}
65
+ <div className="col-md-6">
66
+ <div className="card">
67
+ <div className="card-body">
68
+ <h5 className="card-title">Alben Verteilung</h5>
69
+ <div style={{ height: '300px' }}>
70
+ <ResponsiveContainer width="100%" height="100%">
71
+ <PieChart>
72
+ <Pie
73
+ data={[
74
+ { name: 'In Album', value: stats.albums.with },
75
+ { name: 'Ohne Album', value: stats.albums.without }
76
+ ]}
77
+ cx="50%"
78
+ cy="50%"
79
+ labelLine={false}
80
+ label={({name, percent}) => `${name}: ${(percent * 100).toFixed(0)}%`}
81
+ outerRadius={80}
82
+ fill="#8884d8"
83
+ dataKey="value"
84
+ >
85
+ {stats.albums.map((entry, index) => (
86
+ <Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
87
+ ))}
88
+ </Pie>
89
+ <Tooltip />
90
+ <Legend />
91
+ </PieChart>
92
+ </ResponsiveContainer>
93
+ </div>
94
+ </div>
95
+ </div>
96
+ </div>
97
+
98
+ <div className="col-md-6">
99
+ <div className="card">
100
+ <div className="card-body">
101
+ <h5 className="card-title">Kategorie Verteilung</h5>
102
+ <div style={{ height: '300px' }}>
103
+ <ResponsiveContainer width="100%" height="100%">
104
+ <PieChart>
105
+ <Pie
106
+ data={[
107
+ { name: 'Mit Kategorie', value: stats.categories.with },
108
+ { name: 'Ohne Kategorie', value: stats.categories.without }
109
+ ]}
110
+ cx="50%"
111
+ cy="50%"
112
+ labelLine={false}
113
+ label={({name, percent}) => `${name}: ${(percent * 100).toFixed(0)}%`}
114
+ outerRadius={80}
115
+ fill="#8884d8"
116
+ dataKey="value"
117
+ >
118
+ {stats.categories.map((entry, index) => (
119
+ <Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
120
+ ))}
121
+ </Pie>
122
+ <Tooltip />
123
+ <Legend />
124
+ </PieChart>
125
+ </ResponsiveContainer>
126
+ </div>
127
+ </div>
128
+ </div>
129
+ </div>
130
+
131
+ {/* Speichernutzung nach Format */}
132
+ <div className="col-12">
133
+ <div className="card">
134
+ <div className="card-body">
135
+ <h5 className="card-title">Speichernutzung nach Format</h5>
136
+ <div style={{ height: '300px' }}>
137
+ <ResponsiveContainer width="100%" height="100%">
138
+ <BarChart data={storageStats}>
139
+ <CartesianGrid strokeDasharray="3 3" />
140
+ <XAxis dataKey="format" />
141
+ <YAxis yAxisId="left" orientation="left" stroke="#8884d8" />
142
+ <YAxis yAxisId="right" orientation="right" stroke="#82ca9d" />
143
+ <Tooltip />
144
+ <Legend />
145
+ <Bar yAxisId="left" dataKey="count" name="Anzahl" fill="#8884d8" />
146
+ <Bar yAxisId="right" dataKey="sizeMB" name="Größe (MB)" fill="#82ca9d" />
147
+ </BarChart>
148
+ </ResponsiveContainer>
149
+ </div>
150
+ </div>
151
+ </div>
152
+ </div>
153
+ </div>
154
+ </div>
155
+ );
156
+ };
157
+
158
+ export default StatsVisualization;
159
+ ### END: stats-visualization.txt
160
+
161
+ ### START: stats-visualization-updated.txt
162
+ import React, { useState, useEffect, useCallback } from 'react';
163
+ import { API, APIHandler } from '@/api';
164
+ import {
165
+ LineChart, Line, BarChart, Bar, PieChart, Pie,
166
+ XAxis, YAxis, CartesianGrid, Tooltip, Legend,
167
+ ResponsiveContainer, Cell
168
+ } from 'recharts';
169
+
170
+ // Farbpalette für Charts
171
+ const COLORS = ['#0088FE', '#00C49F', '#FFBB28', '#FF8042', '#8884d8', '#82ca9d'];
172
+
173
+ const StatsVisualization = () => {
174
+ const [stats, setStats] = useState(null);
175
+ const [storageStats, setStorageStats] = useState(null);
176
+ const [loading, setLoading] = useState(true);
177
+ const [error, setError] = useState(null);
178
+ const [refreshInterval, setRefreshInterval] = useState(30000); // 30 Sekunden
179
+
180
+ // Daten laden
181
+ const fetchStats = useCallback(async () => {
182
+ try {
183
+ setLoading(true);
184
+ const [statsData, storageData] = await Promise.all([
185
+ APIHandler.get(API.statistics.images),
186
+ APIHandler.get(API.statistics.storage)
187
+ ]);
188
+ setStats(statsData);
189
+ setStorageStats(storageData);
190
+ setError(null);
191
+ } catch (err) {
192
+ setError('Fehler beim Laden der Statistiken: ' + err.message);
193
+ console.error('Fetch error:', err);
194
+ } finally {
195
+ setLoading(false);
196
+ }
197
+ }, []);
198
+
199
+ // Auto-Refresh
200
+ useEffect(() => {
201
+ fetchStats();
202
+ if (refreshInterval > 0) {
203
+ const interval = setInterval(fetchStats, refreshInterval);
204
+ return () => clearInterval(interval);
205
+ }
206
+ }, [fetchStats, refreshInterval]);
207
+
208
+ // Format für Byte-Größen
209
+ const formatBytes = (bytes) => {
210
+ if (bytes === 0) return '0 B';
211
+ const k = 1024;
212
+ const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
213
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
214
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
215
+ };
216
+
217
+ // Custom Tooltip für Charts
218
+ const CustomTooltip = ({ active, payload, label, valueFormatter }) => {
219
+ if (!active || !payload || !payload.length) return null;
220
+
221
+ return (
222
+ <div className="custom-tooltip bg-white p-3 rounded shadow">
223
+ <p className="label mb-2">{`${label}`}</p>
224
+ {payload.map((entry, index) => (
225
+ <p key={index} style={{ color: entry.color }} className="mb-1">
226
+ {`${entry.name}: ${valueFormatter ? valueFormatter(entry.value) : entry.value}`}
227
+ </p>
228
+ ))}
229
+ </div>
230
+ );
231
+ };
232
+
233
+ if (loading) {
234
+ return (
235
+ <div className="d-flex justify-content-center p-5">
236
+ <div className="spinner-border text-primary" role="status">
237
+ <span className="visually-hidden">Laden...</span>
238
+ </div>
239
+ </div>
240
+ );
241
+ }
242
+
243
+ if (error) {
244
+ return (
245
+ <div className="alert alert-danger m-3" role="alert">
246
+ <h4 className="alert-heading">Fehler</h4>
247
+ <p>{error}</p>
248
+ <button
249
+ className="btn btn-outline-danger"
250
+ onClick={fetchStats}
251
+ >
252
+ Erneut versuchen
253
+ </button>
254
+ </div>
255
+ );
256
+ }
257
+
258
+ return (
259
+ <div className="container-fluid p-4">
260
+ {/* Toolbar */}
261
+ <div className="d-flex justify-content-between align-items-center mb-4">
262
+ <div className="btn-group">
263
+ <button
264
+ className="btn btn-primary"
265
+ onClick={fetchStats}
266
+ >
267
+ <i className="bi bi-arrow-clockwise"></i> Aktualisieren
268
+ </button>
269
+ <select
270
+ className="form-select"
271
+ value={refreshInterval}
272
+ onChange={(e) => setRefreshInterval(Number(e.target.value))}
273
+ >
274
+ <option value="0">Kein Auto-Refresh</option>
275
+ <option value="15000">Alle 15 Sekunden</option>
276
+ <option value="30000">Alle 30 Sekunden</option>
277
+ <option value="60000">Jede Minute</option>
278
+ </select>
279
+ </div>
280
+ </div>
281
+
282
+ <div className="row g-4">
283
+ {/* Monatliche Bilder */}
284
+ <div className="col-12">
285
+ <div className="card">
286
+ <div className="card-body">
287
+ <h5 className="card-title">Bilder pro Monat</h5>
288
+ <div style={{ height: '300px' }}>
289
+ <ResponsiveContainer width="100%" height="100%">
290
+ <LineChart data={stats?.monthly}>
291
+ <CartesianGrid strokeDasharray="3 3" />
292
+ <XAxis dataKey="month" />
293
+ <YAxis />
294
+ <Tooltip content={<CustomTooltip />} />
295
+ <Legend />
296
+ <Line
297
+ type="monotone"
298
+ dataKey="count"
299
+ name="Anzahl Bilder"
300
+ stroke={COLORS[0]}
301
+ strokeWidth={2}
302
+ dot={{ r: 4 }}
303
+ activeDot={{ r: 6 }}
304
+ />
305
+ </LineChart>
306
+ </ResponsiveContainer>
307
+ </div>
308
+ </div>
309
+ </div>
310
+ </div>
311
+
312
+ {/* Album & Kategorie Verteilung */}
313
+ <div className="col-md-6">
314
+ <div className="card">
315
+ <div className="card-body">
316
+ <h5 className="card-title">Album Verteilung</h5>
317
+ <div style={{ height: '300px' }}>
318
+ <ResponsiveContainer width="100%" height="100%">
319
+ <PieChart>
320
+ <Pie
321
+ data={[
322
+ { name: 'In Alben', value: stats?.albums.with },
323
+ { name: 'Ohne Album', value: stats?.albums.without }
324
+ ]}
325
+ cx="50%"
326
+ cy="50%"
327
+ labelLine={false}
328
+ label={({name, percent}) =>
329
+ `${name}: ${(percent * 100).toFixed(1)}%`
330
+ }
331
+ outerRadius={80}
332
+ fill="#8884d8"
333
+ dataKey="value"
334
+ >
335
+ {stats?.albums.map((entry, index) => (
336
+ <Cell
337
+ key={`cell-${index}`}
338
+ fill={COLORS[index % COLORS.length]}
339
+ />
340
+ ))}
341
+ </Pie>
342
+ <Tooltip content={<CustomTooltip />} />
343
+ <Legend />
344
+ </PieChart>
345
+ </ResponsiveContainer>
346
+ </div>
347
+ </div>
348
+ </div>
349
+ </div>
350
+
351
+ <div className="col-md-6">
352
+ <div className="card">
353
+ <div className="card-body">
354
+ <h5 className="card-title">Kategorie Verteilung</h5>
355
+ <div style={{ height: '300px' }}>
356
+ <ResponsiveContainer width="100%" height="100%">
357
+ <PieChart>
358
+ <Pie
359
+ data={[
360
+ { name: 'Mit Kategorie', value: stats?.categories.with },
361
+ { name: 'Ohne Kategorie', value: stats?.categories.without }
362
+ ]}
363
+ cx="50%"
364
+ cy="50%"
365
+ labelLine={false}
366
+ label={({name, percent}) =>
367
+ `${name}: ${(percent * 100).toFixed(1)}%`
368
+ }
369
+ outerRadius={80}
370
+ fill="#8884d8"
371
+ dataKey="value"
372
+ >
373
+ {stats?.categories.map((entry, index) => (
374
+ <Cell
375
+ key={`cell-${index}`}
376
+ fill={COLORS[index % COLORS.length]}
377
+ />
378
+ ))}
379
+ </Pie>
380
+ <Tooltip content={<CustomTooltip />} />
381
+ <Legend />
382
+ </PieChart>
383
+ </ResponsiveContainer>
384
+ </div>
385
+ </div>
386
+ </div>
387
+ </div>
388
+
389
+ {/* Speichernutzung */}
390
+ <div className="col-12">
391
+ <div className="card">
392
+ <div className="card-body">
393
+ <h5 className="card-title">Speichernutzung nach Format</h5>
394
+ <div style={{ height: '300px' }}>
395
+ <ResponsiveContainer width="100%" height="100%">
396
+ <BarChart data={storageStats}>
397
+ <CartesianGrid strokeDasharray="3 3" />
398
+ <XAxis dataKey="format" />
399
+ <YAxis
400
+ yAxisId="left"
401
+ orientation="left"
402
+ stroke={COLORS[0]}
403
+ />
404
+ <YAxis
405
+ yAxisId="right"
406
+ orientation="right"
407
+ stroke={COLORS[1]}
408
+ />
409
+ <Tooltip content={<CustomTooltip valueFormatter={formatBytes} />} />
410
+ <Legend />
411
+ <Bar
412
+ yAxisId="left"
413
+ dataKey="count"
414
+ name="Anzahl"
415
+ fill={COLORS[0]}
416
+ />
417
+ <Bar
418
+ yAxisId="right"
419
+ dataKey="size"
420
+ name="Größe"
421
+ fill={COLORS[1]}
422
+ />
423
+ </BarChart>
424
+ </ResponsiveContainer>
425
+ </div>
426
+ </div>
427
+ </div>
428
+ </div>
429
+ </div>
430
+ </div>
431
+ );
432
+ };
433
+
434
+ export default StatsVisualization;
435
+ window.StatsVisualization = StatsVisualization; // <-- Diese Zeile ans Ende setzen
static/components/api.js ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // static/components/api.js
2
+ export const API = {
3
+ albums: {
4
+ list: '/albums', // Vermutung basierend auf AlbumManager.js
5
+ create: '/create_album',
6
+ delete: (id) => `/delete_album/${id}`,
7
+ update: (id) => `/update_album/${id}`,
8
+ },
9
+ categories: {
10
+ list: '/categories', // Vermutung basierend auf CategoryManager.js
11
+ create: '/create_category',
12
+ delete: (id) => `/delete_category/${id}`,
13
+ update: (id) => `/update_category/${id}`,
14
+ merge: '/categories/merge'
15
+ },
16
+ statistics: {
17
+ images: '/backend/stats',
18
+ storage: '/backend/stats'
19
+ }
20
+ };
21
+
22
+ export const APIHandler = {
23
+ get: async (url) => {
24
+ const response = await fetch(url);
25
+ if (!response.ok) {
26
+ throw new Error('Network response was not ok');
27
+ }
28
+ return response.json();
29
+ },
30
+ post: async (url, data) => {
31
+ const response = await fetch(url, {
32
+ method: 'POST',
33
+ headers: {
34
+ 'Content-Type': 'application/json'
35
+ },
36
+ body: JSON.stringify(data)
37
+ });
38
+ if (!response.ok) {
39
+ throw new Error('Network response was not ok');
40
+ }
41
+ return response.json();
42
+ },
43
+ put: async (url, data) => {
44
+ //PUT hinzufügen
45
+ const response = await fetch(url, {
46
+ method: 'PUT',
47
+ headers: {
48
+ 'Content-Type': 'application/json'
49
+ },
50
+ body: JSON.stringify(data)
51
+ });
52
+
53
+ if (!response.ok) {
54
+ throw new Error('Network response was not ok');
55
+ }
56
+ return response.json();
57
+ },
58
+ delete: async (url) => {
59
+ const response = await fetch(url, {
60
+ method: 'DELETE'
61
+ });
62
+ if (!response.ok) {
63
+ throw new Error('Network response was not ok');
64
+ }
65
+ return response.json();
66
+ }
67
+ };
static/components/statistic.js ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ document.addEventListener('DOMContentLoaded', () => {
2
+ const refreshButton = document.getElementById('refreshButton');
3
+ const darkModeToggle = document.getElementById('darkModeToggle');
4
+ const newAlbumButton = document.getElementById('newAlbumButton');
5
+ const createAlbumButton = document.getElementById('createAlbumButton');
6
+ const albumTable = document.getElementById('albumTable').querySelector('tbody'); // tbody auswählen
7
+ const albumPageSizeSelect = document.getElementById('albumPageSize');
8
+ const selectAllAlbumsCheckbox = document.getElementById('selectAllAlbums');
9
+
10
+ // Mock-Daten (ersetzen durch echte Daten vom Backend)
11
+ let albums = JSON.parse(localStorage.getItem('albums')) || [
12
+ { id: 1, name: 'Urlaub 2022', images: 120, created: '2022-08-15' },
13
+ { id: 2, name: 'Familienfeier', images: 85, created: '2023-01-20' },
14
+ ];
15
+
16
+ function saveAlbumsToLocalStorage() {
17
+ localStorage.setItem('albums', JSON.stringify(albums));
18
+ }
19
+
20
+ function renderAlbums() {
21
+ albumTable.innerHTML = ''; // Tabelle leeren
22
+ albums.forEach(album => {
23
+ const row = albumTable.insertRow();
24
+ row.innerHTML = `
25
+ <td><input type="checkbox" class="form-check-input item-checkbox" data-type="album" data-id="${album.id}"></td>
26
+ <td>${album.name}</td>
27
+ <td>${album.images}</td>
28
+ <td>${album.created}</td>
29
+ <td><button class="btn btn-danger btn-sm delete-album" data-id="${album.id}"><i class="bi bi-trash"></i></button></td>
30
+ `;
31
+ });
32
+ addDeleteAlbumEventListeners();
33
+ }
34
+
35
+ function addDeleteAlbumEventListeners(){
36
+ document.querySelectorAll('.delete-album').forEach(button => {
37
+ button.addEventListener('click', () => {
38
+ const idToDelete = parseInt(button.dataset.id);
39
+ albums = albums.filter(album => album.id !== idToDelete);
40
+ saveAlbumsToLocalStorage();
41
+ renderAlbums();
42
+ });
43
+ });
44
+ }
45
+
46
+ function showToast(title, message, type = 'info') {
47
+ const toast = document.getElementById('toast');
48
+ const toastTitle = document.getElementById('toastTitle');
49
+ const toastMessage = document.getElementById('toastMessage');
50
+
51
+ toast.classList.remove('bg-success', 'bg-danger', 'bg-info');
52
+ toast.classList.add(`bg-${type}`);
53
+ toastTitle.textContent = title;
54
+ toastMessage.textContent = message;
55
+
56
+ const bsToast = new bootstrap.Toast(toast);
57
+ bsToast.show();
58
+ }
59
+
60
+ refreshButton.addEventListener('click', () => {
61
+ // Hier würde normalerweise der Datenabruf vom Backend erfolgen
62
+ showToast('Info', 'Daten wurden simuliert aktualisiert.', 'info');
63
+ renderAlbums();
64
+ });
65
+
66
+ darkModeToggle.addEventListener('click', () => {
67
+ document.body.classList.toggle('dark-mode');
68
+ localStorage.setItem('darkMode', document.body.classList.contains('dark-mode'));
69
+ });
70
+
71
+ if (localStorage.getItem('darkMode') === 'true') {
72
+ document.body.classList.add('dark-mode');
73
+ }
74
+
75
+ newAlbumButton.addEventListener('click', () => {
76
+ const newAlbumModal = new bootstrap.Modal(document.getElementById('newAlbumModal'));
77
+ newAlbumModal.show();
78
+ });
79
+
80
+ createAlbumButton.addEventListener('click', () => {
81
+ const albumName = document.getElementById('albumName').value;
82
+ const albumDescription = document.getElementById('albumDescription').value;
83
+ if (albumName.trim() === '') {
84
+ showToast("Fehler", "Bitte geben Sie einen Album Namen ein", "danger")
85
+ return;
86
+ }
87
+
88
+ const newAlbum = {
89
+ id: albums.length + 1, // Provisorische ID
90
+ name: albumName,
91
+ images: 0,
92
+ created: new Date().toISOString().slice(0, 10),
93
+ };
94
+ albums.push(newAlbum);
95
+ saveAlbumsToLocalStorage();
96
+ renderAlbums();
97
+ document.getElementById('newAlbumForm').reset();
98
+ bootstrap.Modal.getInstance(document.getElementById('newAlbumModal')).hide();
99
+ showToast('Erfolg', 'Album erfolgreich erstellt.', 'success');
100
+ });
101
+
102
+ selectAllAlbumsCheckbox.addEventListener('change', () => {
103
+ const itemCheckboxes = document.querySelectorAll(`.item-checkbox[data-type="album"]`);
104
+ itemCheckboxes.forEach(item => item.checked = selectAllAlbumsCheckbox.checked);
105
+ });
106
+
107
+ renderAlbums();
108
+ });
static/favicon.ico ADDED
static/script.js ADDED
@@ -0,0 +1,761 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // 1. Logger-Klasse
2
+ class Logger {
3
+ static isDebugMode = false;
4
+
5
+ static debug(message, data = null) {
6
+ if (this.isDebugMode) {
7
+ console.log(`[Debug] ${message}`, data || '');
8
+ }
9
+ }
10
+
11
+ static error(message, error = null) {
12
+ console.error(`[Error] ${message}`, error || '');
13
+ }
14
+
15
+ static initializeDebugMode() {
16
+ try {
17
+ const urlParams = new URLSearchParams(window.location.search);
18
+ this.isDebugMode = urlParams.has('debug');
19
+ } catch (error) {
20
+ console.error('Fehler beim Initialisieren des Debug-Modus:', error);
21
+ this.isDebugMode = false;
22
+ }
23
+ }
24
+ }
25
+
26
+ // 2. Utils
27
+ const utils = {
28
+ showLoading() {
29
+ if (document.body) {
30
+ document.body.classList.add('loading');
31
+ Logger.debug('Loading-Status aktiviert');
32
+ } else {
33
+ Logger.error('document.body nicht verfügbar');
34
+ }
35
+ },
36
+
37
+ hideLoading() {
38
+ if (document.body) {
39
+ document.body.classList.remove('loading');
40
+ Logger.debug('Loading-Status deaktiviert');
41
+ } else {
42
+ Logger.error('document.body nicht verfügbar');
43
+ }
44
+ },
45
+
46
+ safeGetElement(id) {
47
+ const element = document.getElementById(id);
48
+ if (!element) {
49
+ Logger.error(`Element mit ID '${id}' nicht gefunden`);
50
+ return null;
51
+ }
52
+ return element;
53
+ },
54
+
55
+ async withLoading(asyncFn) {
56
+ try {
57
+ this.showLoading();
58
+ await asyncFn();
59
+ } finally {
60
+ this.hideLoading();
61
+ }
62
+ }
63
+ };
64
+
65
+ // In der ImageModal-Klasse:
66
+ class ImageModal {
67
+ constructor() {
68
+ this.modal = null;
69
+ this.modalImg = null;
70
+ this.currentImageIndex = 0;
71
+ this.selectedImages = [];
72
+ this.initialize();
73
+ }
74
+
75
+ initialize() {
76
+ if (typeof bootstrap === 'undefined') {
77
+ Logger.error('Bootstrap ist nicht verfügbar');
78
+ return;
79
+ }
80
+
81
+ const modalElement = utils.safeGetElement('imageModal');
82
+ if (!modalElement) return;
83
+
84
+ this.modal = new bootstrap.Modal(modalElement);
85
+ this.modalImg = utils.safeGetElement('modalImage');
86
+
87
+ // Event-Listener für Modal-Schließen
88
+ modalElement.addEventListener('hidden.bs.modal', () => {
89
+ this.cleanupModal();
90
+ });
91
+
92
+ // Klick-Handler für Modal-Container
93
+ const imageContainer = document.querySelector('.image-container');
94
+ if (imageContainer && this.modalImg) {
95
+ imageContainer.addEventListener('click', (e) => {
96
+ if (e.target === this.modalImg) {
97
+ this.hide();
98
+ }
99
+ });
100
+ }
101
+
102
+ // Event-Listener für Tastendruck (Pfeiltasten)
103
+ document.addEventListener('keydown', (event) => {
104
+ if (this.modal && this.modal._isShown) { // Modal muss geöffnet sein
105
+ if (event.key === 'ArrowLeft') {
106
+ event.stopPropagation(); // Verhindere Standardverhalten und Bubbling
107
+ this.showPreviousImage();
108
+ } else if (event.key === 'ArrowRight') {
109
+ event.stopPropagation(); // Verhindere Standardverhalten und Bubbling
110
+ this.showNextImage();
111
+ }
112
+ }
113
+ });
114
+
115
+ // Download-Button Handler
116
+ const downloadBtn = utils.safeGetElement('modalDownloadBtn');
117
+ if (downloadBtn) {
118
+ downloadBtn.addEventListener('click', async () => {
119
+ const filename = this.modalImg?.dataset?.filename;
120
+ if (filename) {
121
+ await this.downloadImage(filename);
122
+ } else {
123
+ Logger.error('Kein Dateiname für Download verfügbar');
124
+ }
125
+ });
126
+ }
127
+ }
128
+
129
+ async downloadImage(filename) {
130
+ await utils.withLoading(async () => {
131
+ try {
132
+ const response = await fetch(`/flux-pics/${filename}`); // Direkt über StaticFiles
133
+
134
+ if (!response.ok) {
135
+ throw new Error(`HTTP error! status: ${response.status}`);
136
+ }
137
+
138
+ const blob = await response.blob();
139
+ const url = window.URL.createObjectURL(blob);
140
+ const a = document.createElement('a');
141
+ a.style.display = 'none';
142
+ a.href = url;
143
+ a.download = filename;
144
+ document.body.appendChild(a);
145
+ a.click();
146
+ window.URL.revokeObjectURL(url);
147
+ document.body.removeChild(a);
148
+ } catch (error) {
149
+ Logger.error('Download-Fehler:', error);
150
+ alert('Ein Fehler ist beim Download aufgetreten: ' + error.message);
151
+ }
152
+ });
153
+ }
154
+
155
+ open(img, selectedImages = []) {
156
+ if (!this.modal || !this.modalImg) {
157
+ Logger.error('Modal nicht korrekt initialisiert');
158
+ return;
159
+ }
160
+
161
+ Logger.debug('Öffne Bild-Modal', img);
162
+
163
+ // Setze die ausgewählten Bilder und den Index des aktuellen Bildes
164
+ this.selectedImages = selectedImages;
165
+ this.currentImageIndex = this.selectedImages.indexOf(img.dataset.filename);
166
+
167
+ this.modalImg.src = img.src;
168
+ this.modalImg.dataset.filename = img.dataset.filename;
169
+
170
+ const metadataFields = ['format', 'timestamp', 'album', 'category', 'prompt', 'optimized_prompt'];
171
+ metadataFields.forEach(field => {
172
+ const element = utils.safeGetElement(`modal${field.charAt(0).toUpperCase() + field.slice(1)}`);
173
+ if (element) {
174
+ element.textContent = img.dataset[field] || 'Nicht verfügbar';
175
+ }
176
+ });
177
+
178
+ // Füge einen Event-Listener hinzu, um den Fokus auf das Modal zu setzen,
179
+ // nachdem es vollständig angezeigt wurde (shown.bs.modal)
180
+ this.modal._element.addEventListener('shown.bs.modal', () => {
181
+ this.modal._element.focus();
182
+ });
183
+
184
+ this.modal.show();
185
+ }
186
+
187
+ hide() {
188
+ this.modal?.hide();
189
+ }
190
+
191
+ cleanupModal() {
192
+ document.body.classList.remove('modal-open');
193
+ const backdrop = document.querySelector('.modal-backdrop');
194
+ if (backdrop) {
195
+ backdrop.remove();
196
+ }
197
+ document.body.style.overflow = '';
198
+ document.body.style.paddingRight = '';
199
+ }
200
+
201
+ showPreviousImage() {
202
+ if (this.currentImageIndex > 0) {
203
+ this.currentImageIndex--;
204
+ this.updateModalImage();
205
+ }
206
+ }
207
+
208
+ showNextImage() {
209
+ if (this.currentImageIndex < this.selectedImages.length - 1) {
210
+ this.currentImageIndex++;
211
+ this.updateModalImage();
212
+ }
213
+ }
214
+
215
+ updateModalImage() {
216
+ const filename = this.selectedImages[this.currentImageIndex];
217
+ const imgElement = document.querySelector(`.image-thumbnail[data-filename="${filename}"]`);
218
+
219
+ if (imgElement) {
220
+ this.modalImg.src = imgElement.src;
221
+ this.modalImg.dataset.filename = filename;
222
+
223
+ // Metadaten aktualisieren
224
+ const metadataFields = ['format', 'timestamp', 'album', 'category', 'prompt', 'optimized_prompt'];
225
+ metadataFields.forEach(field => {
226
+ const element = utils.safeGetElement(`modal${field.charAt(0).toUpperCase() + field.slice(1)}`);
227
+ if (element) {
228
+ element.textContent = imgElement.dataset[field] || 'Nicht verfügbar';
229
+ }
230
+ });
231
+ } else {
232
+ Logger.error('Bild-Element für Dateiname nicht gefunden:', filename);
233
+ }
234
+ }
235
+ }
236
+
237
+ class GalleryManager {
238
+ constructor() {
239
+ this.selectedImages = new Set();
240
+ this.imageModal = new ImageModal();
241
+ this.initialize();
242
+ }
243
+
244
+ initialize() {
245
+ this.initializeSelectionHandling();
246
+ this.initializeGalleryViews();
247
+ this.initializeDownloadHandling();
248
+ }
249
+
250
+ initializeSelectionHandling() {
251
+ // "Alle auswählen" Funktionalität
252
+ const selectAllCheckbox = utils.safeGetElement('selectAll');
253
+ if (selectAllCheckbox) {
254
+ selectAllCheckbox.addEventListener('change', () => {
255
+ const itemCheckboxes = document.querySelectorAll('.select-item');
256
+ itemCheckboxes.forEach(checkbox => {
257
+ checkbox.checked = selectAllCheckbox.checked;
258
+ this.updateSelectedImages(checkbox);
259
+ });
260
+ });
261
+ }
262
+
263
+ // Einzelne Bildauswahl
264
+ document.querySelectorAll('.select-item').forEach(checkbox => {
265
+ checkbox.addEventListener('change', () => this.updateSelectedImages(checkbox));
266
+ });
267
+ }
268
+
269
+ updateSelectedImages(checkbox) {
270
+ const card = checkbox.closest('.card');
271
+ if (!card) return;
272
+
273
+ const img = card.querySelector('img');
274
+ if (!img || !img.dataset.filename) {
275
+ Logger.error('Ungültiges Bild-Element in der Karte');
276
+ return;
277
+ }
278
+
279
+ if (checkbox.checked) {
280
+ this.selectedImages.add(img.dataset.filename);
281
+ } else {
282
+ this.selectedImages.delete(img.dataset.filename);
283
+ }
284
+
285
+ Logger.debug(`Ausgewählte Bilder aktualisiert: ${this.selectedImages.size} Bilder`);
286
+ }
287
+
288
+ getSelectedImages() {
289
+ return Array.from(this.selectedImages);
290
+ }
291
+
292
+ initializeGalleryViews() {
293
+ // Thumbnail-Galerie
294
+ const thumbGalleryBtn = utils.safeGetElement('thumbgalleryBtn');
295
+ if (thumbGalleryBtn) {
296
+ thumbGalleryBtn.addEventListener('click', () => this.openThumbnailGallery());
297
+ }
298
+
299
+ // Grid Layout
300
+ const gridLayout = utils.safeGetElement('gridLayout');
301
+ if (gridLayout) {
302
+ gridLayout.addEventListener('change', () => this.updateGridLayout(gridLayout.value));
303
+ }
304
+
305
+ // Bild-Thumbnails
306
+ document.querySelectorAll('.image-thumbnail').forEach(img => {
307
+ img.addEventListener('click', () => this.imageModal.open(img));
308
+ });
309
+ }
310
+
311
+ async openThumbnailGallery() {
312
+ const selectedImages = this.getSelectedImages();
313
+ if (selectedImages.length === 0) {
314
+ alert('Keine Bilder ausgewählt.');
315
+ return;
316
+ }
317
+
318
+ const galleryModal = new bootstrap.Modal(utils.safeGetElement('thumbGalleryModal'));
319
+ const container = utils.safeGetElement('thumbGalleryContainer');
320
+ if (!container) return;
321
+
322
+ container.innerHTML = '';
323
+
324
+ selectedImages.forEach(filename => {
325
+ const thumbContainer = this.createThumbnailElement(filename);
326
+ if (thumbContainer) {
327
+ container.appendChild(thumbContainer);
328
+ }
329
+ });
330
+
331
+ galleryModal.show();
332
+ }
333
+
334
+ createThumbnailElement(filename) {
335
+ const container = document.createElement('div');
336
+ container.className = 'thumb-container m-2';
337
+
338
+ const img = document.createElement('img');
339
+ img.src = `/flux-pics/${filename}`;
340
+ img.className = 'img-thumbnail thumbnail-img';
341
+ img.dataset.filename = filename;
342
+ img.style.maxWidth = '150px';
343
+ img.style.cursor = 'pointer';
344
+
345
+ const downloadBtn = document.createElement('button');
346
+ downloadBtn.className = 'btn btn-sm btn-primary download-thumb';
347
+ downloadBtn.innerHTML = '<i class="fas fa-download"></i>';
348
+
349
+ // Event-Listener
350
+ img.addEventListener('click', () => this.imageModal.open(img));
351
+ downloadBtn.addEventListener('click', async () => {
352
+ await this.imageModal.downloadImage(filename);
353
+ });
354
+
355
+ container.appendChild(img);
356
+ container.appendChild(downloadBtn);
357
+
358
+ return container;
359
+ }
360
+
361
+ updateGridLayout(columns) {
362
+ const imageGrid = utils.safeGetElement('imageGrid');
363
+ if (imageGrid) {
364
+ const validColumns = Math.max(1, Math.min(6, parseInt(columns) || 3));
365
+ imageGrid.className = `row row-cols-1 row-cols-md-${validColumns}`;
366
+ Logger.debug(`Grid-Layout aktualisiert: ${validColumns} Spalten`);
367
+ }
368
+ }
369
+
370
+ initializeDownloadHandling() {
371
+ const downloadBtn = utils.safeGetElement('downloadSelected');
372
+ if (downloadBtn) {
373
+ downloadBtn.addEventListener('click', () => this.handleBulkDownload());
374
+ }
375
+ }
376
+
377
+ async handleBulkDownload() {
378
+ const selectedImages = this.getSelectedImages();
379
+ if (selectedImages.length === 0) {
380
+ alert('Keine Bilder ausgewählt.');
381
+ return;
382
+ }
383
+
384
+ const useZip = selectedImages.length > 1 &&
385
+ confirm('Möchten Sie die Bilder als ZIP-Datei herunterladen?\nKlicken Sie "OK" für ZIP oder "Abbrechen" für Einzeldownloads.');
386
+
387
+ await utils.withLoading(async () => {
388
+ try {
389
+ if (useZip) {
390
+ await this.downloadAsZip(selectedImages);
391
+ } else {
392
+ await this.downloadIndividually(selectedImages);
393
+ }
394
+ /*alert('Download erfolgreich abgeschlossen.');*/
395
+ } catch (error) {
396
+ Logger.error('Bulk-Download Fehler:', error);
397
+ alert('Ein Fehler ist aufgetreten: ' + error.message);
398
+ }
399
+ });
400
+ }
401
+
402
+ async downloadAsZip(files) {
403
+ const response = await fetch('/flux-pics', {
404
+ method: 'POST',
405
+ headers: { 'Content-Type': 'application/json' },
406
+ body: JSON.stringify({ selectedImages: files })
407
+ });
408
+
409
+ if (!response.ok) {
410
+ throw new Error(`HTTP error! status: ${response.status}`);
411
+ }
412
+
413
+ const blob = await response.blob();
414
+ const url = window.URL.createObjectURL(blob);
415
+ const a = document.createElement('a');
416
+ a.style.display = 'none';
417
+ a.href = url;
418
+ a.download = 'images.zip';
419
+ document.body.appendChild(a);
420
+ a.click();
421
+ window.URL.revokeObjectURL(url);
422
+ document.body.removeChild(a);
423
+ }
424
+
425
+ async downloadIndividually(files) {
426
+ for (const filename of files) {
427
+ await this.imageModal.downloadImage(filename);
428
+ // Kleine Pause zwischen Downloads
429
+ await new Promise(resolve => setTimeout(resolve, 500));
430
+ }
431
+ }
432
+ }
433
+ // Slideshow-Verwaltung
434
+ // In der SlideshowManager-Klasse:
435
+ class SlideshowManager {
436
+ constructor(gallery) {
437
+ if (!gallery) {
438
+ Logger.error('GalleryManager ist erforderlich');
439
+ throw new Error('GalleryManager ist erforderlich');
440
+ }
441
+ this.gallery = gallery;
442
+ this.slideInterval = 3000;
443
+ this.currentSlideIndex = 0;
444
+ this.slideshowInterval = null;
445
+ this.carousel = null;
446
+ this.slideshowModal = null;
447
+
448
+ // Initialisierung direkt im Konstruktor
449
+ const slideshowBtn = utils.safeGetElement('slideshowBtn');
450
+ if (slideshowBtn) {
451
+ slideshowBtn.addEventListener('click', () => this.openSlideshow());
452
+ }
453
+ }
454
+
455
+ async openSlideshow() {
456
+ const selectedImages = this.gallery.getSelectedImages();
457
+ if (selectedImages.length === 0) {
458
+ alert('Keine Bilder ausgewählt.');
459
+ return;
460
+ }
461
+
462
+ const modalElement = utils.safeGetElement('slideshowModal');
463
+ if (!modalElement) return;
464
+
465
+ this.slideshowModal = new bootstrap.Modal(modalElement);
466
+ const container = utils.safeGetElement('slideshowContainer');
467
+ if (!container) return;
468
+
469
+ container.innerHTML = '';
470
+ this.createSlides(container, selectedImages);
471
+
472
+ const carouselElement = utils.safeGetElement('carouselExampleControls');
473
+ if (carouselElement) {
474
+ this.carousel = new bootstrap.Carousel(carouselElement, {
475
+ interval: false
476
+ });
477
+ }
478
+
479
+ // Event-Listener für Modal-Schließen
480
+ modalElement.addEventListener('hidden.bs.modal', () => {
481
+ this.cleanupSlideshow();
482
+ });
483
+
484
+ this.setupSlideshowControls();
485
+ this.slideshowModal.show();
486
+ }
487
+
488
+ createSlides(container, images) {
489
+ images.forEach((filename, index) => {
490
+ const div = document.createElement('div');
491
+ div.classList.add('carousel-item');
492
+ if (index === 0) div.classList.add('active');
493
+
494
+ const img = document.createElement('img');
495
+ img.src = `/flux-pics/${filename}`;
496
+ img.classList.add('d-block', 'w-100');
497
+ img.dataset.filename = filename;
498
+ img.onerror = () => {
499
+ Logger.error(`Fehler beim Laden des Bildes: ${filename}`);
500
+ // Optional: Anstelle des Bildes ein Placeholder-Element anzeigen
501
+ const errorPlaceholder = document.createElement('div');
502
+ errorPlaceholder.classList.add('error-placeholder');
503
+ errorPlaceholder.textContent = `Fehler beim Laden des Bildes: ${filename}`;
504
+ div.replaceChild(errorPlaceholder, img);
505
+ };
506
+
507
+ div.appendChild(img);
508
+ container.appendChild(div);
509
+ });
510
+ }
511
+
512
+ setupSlideshowControls() {
513
+ const playBtn = utils.safeGetElement('playSlideshow');
514
+ const pauseBtn = utils.safeGetElement('pauseSlideshow');
515
+
516
+ if (playBtn && pauseBtn) {
517
+ playBtn.addEventListener('click', () => this.startSlideshow());
518
+ pauseBtn.addEventListener('click', () => this.pauseSlideshow());
519
+ }
520
+
521
+ const fullscreenBtn = utils.safeGetElement('fullscreenBtn');
522
+ if (fullscreenBtn) {
523
+ fullscreenBtn.addEventListener('click', () => this.toggleFullscreen());
524
+ }
525
+
526
+ const downloadBtn = utils.safeGetElement('downloadCurrentSlide');
527
+ if (downloadBtn) {
528
+ downloadBtn.addEventListener('click', () => this.downloadCurrentSlide());
529
+ }
530
+ }
531
+
532
+ startSlideshow() {
533
+ if (!this.carousel) return;
534
+
535
+ this.slideshowInterval = setInterval(() => {
536
+ this.carousel.next();
537
+ }, this.slideInterval);
538
+
539
+ const playBtn = utils.safeGetElement('playSlideshow');
540
+ const pauseBtn = utils.safeGetElement('pauseSlideshow');
541
+ if (playBtn && pauseBtn) {
542
+ playBtn.style.display = 'none';
543
+ pauseBtn.style.display = 'block';
544
+ }
545
+ }
546
+
547
+ pauseSlideshow() {
548
+ if (this.slideshowInterval) {
549
+ clearInterval(this.slideshowInterval);
550
+ this.slideshowInterval = null;
551
+ }
552
+
553
+ const playBtn = utils.safeGetElement('playSlideshow');
554
+ const pauseBtn = utils.safeGetElement('pauseSlideshow');
555
+ if (playBtn && pauseBtn) {
556
+ pauseBtn.style.display = 'none';
557
+ playBtn.style.display = 'block';
558
+ }
559
+ }
560
+
561
+ async toggleFullscreen() {
562
+ const modalElement = utils.safeGetElement('slideshowModal');
563
+ if (!modalElement) return;
564
+
565
+ try {
566
+ if (!document.fullscreenElement) {
567
+ if (modalElement.requestFullscreen) {
568
+ await modalElement.requestFullscreen();
569
+ } else if (modalElement.webkitRequestFullscreen) {
570
+ await modalElement.webkitRequestFullscreen();
571
+ } else if (modalElement.msRequestFullscreen) {
572
+ await modalElement.msRequestFullscreen();
573
+ }
574
+ } else {
575
+ if (document.exitFullscreen) {
576
+ await document.exitFullscreen();
577
+ }
578
+ }
579
+ } catch (error) {
580
+ Logger.error('Vollbild-Fehler:', error);
581
+ }
582
+ }
583
+
584
+ async downloadCurrentSlide() {
585
+ const activeSlide = document.querySelector('.carousel-item.active img');
586
+ if (activeSlide?.dataset?.filename) {
587
+ try {
588
+ const filename = activeSlide.dataset.filename;
589
+ const response = await fetch(`/flux-pics/${filename}`);
590
+
591
+ if (!response.ok) {
592
+ throw new Error(`Fehler beim Herunterladen des Bildes: ${response.status} ${response.statusText}`);
593
+ }
594
+
595
+ const blob = await response.blob();
596
+ const link = document.createElement('a');
597
+ link.href = URL.createObjectURL(blob);
598
+ link.download = filename;
599
+ link.style.display = 'none';
600
+ document.body.appendChild(link);
601
+ link.click();
602
+ document.body.removeChild(link);
603
+ URL.revokeObjectURL(link.href);
604
+
605
+ } catch (error) {
606
+ Logger.error('Fehler beim Herunterladen des Bildes:', error);
607
+ }
608
+ } else {
609
+ Logger.error('Kein aktives Bild gefunden');
610
+ }
611
+ }
612
+
613
+ cleanupSlideshow() {
614
+ this.pauseSlideshow();
615
+
616
+ document.body.classList.remove('modal-open');
617
+ const backdrop = document.querySelector('.modal-backdrop');
618
+ if (backdrop) {
619
+ backdrop.remove();
620
+ }
621
+
622
+ document.body.style.overflow = '';
623
+ document.body.style.paddingRight = '';
624
+
625
+ if (document.fullscreenElement) {
626
+ document.exitFullscreen().catch(err => {
627
+ Logger.error('Fehler beim Beenden des Vollbildmodus:', err);
628
+ });
629
+ }
630
+ }
631
+ }
632
+ // 3. AppInitializer-Klasse
633
+ class AppInitializer {
634
+ constructor() {
635
+ this.gallery = null;
636
+ this.slideshow = null;
637
+ }
638
+
639
+ initialize() {
640
+ try {
641
+ Logger.initializeDebugMode();
642
+
643
+ document.addEventListener('DOMContentLoaded', () => {
644
+ try {
645
+ this.initializeComponents();
646
+ this.setupGlobalEventListeners();
647
+ Logger.debug('Anwendung erfolgreich initialisiert');
648
+
649
+ // >>> Ab hier Deine zusätzlichen Funktionen:
650
+ // -----------------------------------------------------
651
+
652
+ // 1) "Alles auswählen" (nur falls du es separat brauchst):
653
+ const selectAllCheckbox = document.getElementById('selectAll');
654
+ if (selectAllCheckbox) {
655
+ selectAllCheckbox.addEventListener('change', function() {
656
+ const checkboxes = document.querySelectorAll('.select-item');
657
+ checkboxes.forEach(cb => {
658
+ cb.checked = selectAllCheckbox.checked;
659
+ });
660
+ });
661
+ }
662
+
663
+ // 2) Items-per-page-Select: Bei Änderung URL manipulieren und Seite neuladen
664
+ const itemsPerPageSelect = document.getElementById('itemsPerPageSelect');
665
+ if (itemsPerPageSelect) {
666
+ itemsPerPageSelect.addEventListener('change', function() {
667
+ const newVal = this.value;
668
+ // Aktuelle URL analysieren
669
+ const url = new URL(window.location.href);
670
+
671
+ // items_per_page setzen
672
+ url.searchParams.set('items_per_page', newVal);
673
+
674
+ // Page zurücksetzen (falls "page" existiert)
675
+ url.searchParams.delete('page');
676
+
677
+ // Seite neuladen
678
+ window.location.href = url.toString();
679
+ });
680
+ }
681
+
682
+ // -----------------------------------------------------
683
+ // >>> Ende deiner zusätzlichen Funktionen
684
+
685
+ } catch (error) {
686
+ Logger.error('Fehler bei der Initialisierung:', error);
687
+ alert('Es gab ein Problem beim Laden der Anwendung. Bitte laden Sie die Seite neu.');
688
+ }
689
+ });
690
+ } catch (error) {
691
+ console.error('Kritischer Fehler bei der Initialisierung:', error);
692
+ }
693
+ }
694
+
695
+
696
+ initializeComponents() {
697
+ try {
698
+ this.gallery = new GalleryManager();
699
+ this.slideshow = new SlideshowManager(this.gallery);
700
+ Logger.debug('Komponenten initialisiert');
701
+ } catch (error) {
702
+ Logger.error('Fehler bei der Komponenten-Initialisierung:', error);
703
+ throw error;
704
+ }
705
+ }
706
+
707
+ setupGlobalEventListeners() {
708
+ try {
709
+ this.setupScrollToTop();
710
+ this.setupKeyboardNavigation();
711
+ Logger.debug('Globale Event-Listener eingerichtet');
712
+ } catch (error) {
713
+ Logger.error('Fehler beim Einrichten der Event-Listener:', error);
714
+ throw error;
715
+ }
716
+ }
717
+
718
+ setupScrollToTop() {
719
+ const scrollTopBtn = utils.safeGetElement('scrollTopBtn');
720
+ if (scrollTopBtn) {
721
+ window.addEventListener('scroll', () => {
722
+ scrollTopBtn.style.display = window.scrollY > 300 ? 'block' : 'none';
723
+ });
724
+
725
+ scrollTopBtn.addEventListener('click', () => {
726
+ window.scrollTo({
727
+ top: 0,
728
+ behavior: 'smooth'
729
+ });
730
+ });
731
+ }
732
+ }
733
+
734
+ setupKeyboardNavigation() {
735
+ document.addEventListener('keydown', (e) => this.handleKeyboardNavigation(e));
736
+ }
737
+
738
+ handleKeyboardNavigation(e) {
739
+ if (e.key === 'Escape') {
740
+ this.closeAllModals();
741
+ }
742
+ }
743
+
744
+
745
+
746
+ checkBootstrapAvailability() {
747
+ if (typeof bootstrap === 'undefined') {
748
+ Logger.error('Bootstrap ist nicht verfügbar');
749
+ return false;
750
+ }
751
+ return true;
752
+ }
753
+ }
754
+
755
+ // 4. Anwendung starten
756
+ try {
757
+ const app = new AppInitializer();
758
+ app.initialize();
759
+ } catch (error) {
760
+ console.error('Kritischer Fehler beim Erstellen der Anwendung:', error);
761
+ }
static/statistic.js ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ document.addEventListener('DOMContentLoaded', () => {
2
+ const refreshButton = document.getElementById('refreshButton');
3
+ const darkModeToggle = document.getElementById('darkModeToggle');
4
+ const newAlbumButton = document.getElementById('newAlbumButton');
5
+ const createAlbumButton = document.getElementById('createAlbumButton');
6
+ const albumTable = document.getElementById('albumTable').querySelector('tbody'); // tbody auswählen
7
+ const albumPageSizeSelect = document.getElementById('albumPageSize');
8
+ const selectAllAlbumsCheckbox = document.getElementById('selectAllAlbums');
9
+
10
+ // Mock-Daten (ersetzen durch echte Daten vom Backend)
11
+ let albums = JSON.parse(localStorage.getItem('albums')) || [
12
+ { id: 1, name: 'Urlaub 2022', images: 120, created: '2022-08-15' },
13
+ { id: 2, name: 'Familienfeier', images: 85, created: '2023-01-20' },
14
+ ];
15
+
16
+ function saveAlbumsToLocalStorage() {
17
+ localStorage.setItem('albums', JSON.stringify(albums));
18
+ }
19
+
20
+ function renderAlbums() {
21
+ albumTable.innerHTML = ''; // Tabelle leeren
22
+ albums.forEach(album => {
23
+ const row = albumTable.insertRow();
24
+ row.innerHTML = `
25
+ <td><input type="checkbox" class="form-check-input item-checkbox" data-type="album" data-id="${album.id}"></td>
26
+ <td>${album.name}</td>
27
+ <td>${album.images}</td>
28
+ <td>${album.created}</td>
29
+ <td><button class="btn btn-danger btn-sm delete-album" data-id="${album.id}"><i class="bi bi-trash"></i></button></td>
30
+ `;
31
+ });
32
+ addDeleteAlbumEventListeners();
33
+ }
34
+
35
+ function addDeleteAlbumEventListeners(){
36
+ document.querySelectorAll('.delete-album').forEach(button => {
37
+ button.addEventListener('click', () => {
38
+ const idToDelete = parseInt(button.dataset.id);
39
+ albums = albums.filter(album => album.id !== idToDelete);
40
+ saveAlbumsToLocalStorage();
41
+ renderAlbums();
42
+ });
43
+ });
44
+ }
45
+
46
+ function showToast(title, message, type = 'info') {
47
+ const toast = document.getElementById('toast');
48
+ const toastTitle = document.getElementById('toastTitle');
49
+ const toastMessage = document.getElementById('toastMessage');
50
+
51
+ toast.classList.remove('bg-success', 'bg-danger', 'bg-info');
52
+ toast.classList.add(`bg-${type}`);
53
+ toastTitle.textContent = title;
54
+ toastMessage.textContent = message;
55
+
56
+ const bsToast = new bootstrap.Toast(toast);
57
+ bsToast.show();
58
+ }
59
+
60
+ refreshButton.addEventListener('click', () => {
61
+ // Hier würde normalerweise der Datenabruf vom Backend erfolgen
62
+ showToast('Info', 'Daten wurden simuliert aktualisiert.', 'info');
63
+ renderAlbums();
64
+ });
65
+
66
+ darkModeToggle.addEventListener('click', () => {
67
+ document.body.classList.toggle('dark-mode');
68
+ localStorage.setItem('darkMode', document.body.classList.contains('dark-mode'));
69
+ });
70
+
71
+ if (localStorage.getItem('darkMode') === 'true') {
72
+ document.body.classList.add('dark-mode');
73
+ }
74
+
75
+ newAlbumButton.addEventListener('click', () => {
76
+ const newAlbumModal = new bootstrap.Modal(document.getElementById('newAlbumModal'));
77
+ newAlbumModal.show();
78
+ });
79
+
80
+ createAlbumButton.addEventListener('click', () => {
81
+ const albumName = document.getElementById('albumName').value;
82
+ const albumDescription = document.getElementById('albumDescription').value;
83
+ if (albumName.trim() === '') {
84
+ showToast("Fehler", "Bitte geben Sie einen Album Namen ein", "danger")
85
+ return;
86
+ }
87
+
88
+ const newAlbum = {
89
+ id: albums.length + 1, // Provisorische ID
90
+ name: albumName,
91
+ images: 0,
92
+ created: new Date().toISOString().slice(0, 10),
93
+ };
94
+ albums.push(newAlbum);
95
+ saveAlbumsToLocalStorage();
96
+ renderAlbums();
97
+ document.getElementById('newAlbumForm').reset();
98
+ bootstrap.Modal.getInstance(document.getElementById('newAlbumModal')).hide();
99
+ showToast('Erfolg', 'Album erfolgreich erstellt.', 'success');
100
+ });
101
+
102
+ selectAllAlbumsCheckbox.addEventListener('change', () => {
103
+ const itemCheckboxes = document.querySelectorAll(`.item-checkbox[data-type="album"]`);
104
+ itemCheckboxes.forEach(item => item.checked = selectAllAlbumsCheckbox.checked);
105
+ });
106
+
107
+ renderAlbums();
108
+ });
static/style.css ADDED
@@ -0,0 +1,376 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ /* Farben */
3
+ --primary-color: #007bff; /* Hauptfarbe (Blau) */
4
+ --secondary-color: #6c757d; /* Sekundärfarbe (Grau) */
5
+ --success-color: #28a745; /* Erfolgsfarbe (Grün) */
6
+ --error-color: #dc3545; /* Fehlerfarbe (Rot) */
7
+ --info-color: #17a2b8; /* Info-Farbe (Hellblau) */
8
+ --light-gray: #f8f9fa; /* Helles Grau für Hintergründe */
9
+ --medium-gray: #ced4da; /* Mittleres Grau für Rahmen */
10
+ --dark-gray: #333; /* Dunkles Grau für Text */
11
+ --body-bg-color: #f4f4f4;
12
+ --text-color: #333;
13
+ --blink-text-color: #2eb82e;
14
+
15
+ /* Typografie */
16
+ --base-font-size: 1rem;
17
+ --font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
18
+
19
+ /* Abstände */
20
+ --base-padding: 10px;
21
+ --base-margin: 10px;
22
+
23
+ /* Rahmen */
24
+ --border-radius: 5px;
25
+
26
+ /* Schatten */
27
+ --box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
28
+
29
+ /* Transparenz */
30
+ --base-opacity: 0.8;
31
+ }
32
+
33
+ /* Grundlegende Styles */
34
+ body {
35
+ font-family: var(--font-family);
36
+ background-image: url('bg.webp');
37
+ background-size: cover;
38
+ background-repeat: no-repeat;
39
+ background-position: center;
40
+ background-attachment: fixed;
41
+ /* background-color: var(--body-bg-color); */
42
+ color: var(--text-color);
43
+ margin: 0;
44
+ padding: 0;
45
+ font-size: var(--base-font-size);
46
+ }
47
+
48
+ .container {
49
+ max-width: 1200px;
50
+ margin: auto;
51
+ padding: 20px;
52
+ }
53
+
54
+ /* Header- und Navigationsbereich */
55
+ .banner img {
56
+ border-radius: var(--border-radius);
57
+ box-shadow: var(--box-shadow);
58
+ }
59
+
60
+ .nav-buttons a {
61
+ background-color: var(--primary-color);
62
+ color: white;
63
+ border: none;
64
+ padding: var(--base-padding) calc(var(--base-padding) * 2);
65
+ text-decoration: none;
66
+ font-weight: bold;
67
+ border-radius: var(--border-radius);
68
+ transition: background-color 0.3s ease;
69
+ }
70
+
71
+ .nav-buttons a:hover {
72
+ background-color: color-mix(in srgb, var(--primary-color) 40%, black);
73
+ }
74
+
75
+ /* Formulare */
76
+ .custom-bg {
77
+ background-color: var(--light-gray);
78
+ border-radius: var(--border-radius);
79
+ box-shadow: var(--box-shadow);
80
+ padding: var(--base-padding);
81
+ }
82
+
83
+ .form-label {
84
+ font-weight: 600;
85
+ }
86
+
87
+ .form-control {
88
+ border-radius: var(--border-radius);
89
+ border: 1px solid var(--medium-gray);
90
+ padding: var(--base-padding);
91
+ }
92
+
93
+ .form-control:focus {
94
+ border-color: var(--primary-color);
95
+ box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
96
+ }
97
+
98
+ .btn-primary {
99
+ background-color: var(--primary-color);
100
+ border-color: var(--primary-color);
101
+ }
102
+
103
+ .btn-primary:hover {
104
+ background-color: color-mix(in srgb, var(--primary-color) 40%, black);
105
+ border-color: color-mix(in srgb, var(--primary-color) 40%, black);
106
+ }
107
+
108
+ .btn-secondary {
109
+ background-color: var(--secondary-color);
110
+ border-color: var(--secondary-color);
111
+ }
112
+
113
+ .btn-secondary:hover {
114
+ background-color: color-mix(in srgb, var(--secondary-color) 40%, black);
115
+ border-color: color-mix(in srgb, var(--secondary-color) 40%, black);
116
+ }
117
+
118
+ /* Akkordeon */
119
+ .accordion-button {
120
+ font-weight: bold;
121
+ }
122
+
123
+ .accordion-button:not(.collapsed) {
124
+ color: var(--primary-color);
125
+ }
126
+
127
+ /* Fortschrittsnachricht */
128
+ #progressMessage {
129
+ background-color: rgb(40 167 69 / 10%); /* Leicht transparentes Grün */
130
+ border: 1px solid rgb(40 167 69 / 80%); /* Passender Rahmen */
131
+ color: var(--blink-text-color);
132
+ padding: 1rem;
133
+ border-radius: var(--border-radius);
134
+ font-weight: bold;
135
+ text-align: center;
136
+ animation: blink 1.5s infinite;
137
+ margin-top: 1rem;
138
+ }
139
+
140
+ /* Blinken-Animation */
141
+ @keyframes blink {
142
+ 0% { opacity: 1; }
143
+ 50% { opacity: 0; }
144
+ 100% { opacity: 1; }
145
+ }
146
+ /* Fortschrittsbalken */
147
+ #progressContainer {
148
+ height: 30px;
149
+ background-color: var(--light-gray); /* Hintergrund des Containers */
150
+ border-radius: var(--border-radius); /* Abgerundete Ecken */
151
+ overflow: hidden; /* Versteckt den Fortschrittsbalken, der über den Container hinausragt */
152
+ }
153
+
154
+ #progressBar {
155
+ background-color: var(--success-color);
156
+ height: 100%; /* Füllt die Höhe des Containers */
157
+ line-height: 30px; /* Vertikal zentriert den Text */
158
+ color: white;
159
+ text-align: center; /* Horizontal zentriert den Text */
160
+ transition: width 0.4s ease-in-out; /* Sanfter Übergang für die Breite */
161
+ }
162
+
163
+ /* Bilder und Galerie */
164
+ #output img {
165
+ margin-bottom: var(--base-margin);
166
+ border-radius: var(--border-radius);
167
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
168
+ }
169
+
170
+ .image-thumbnail {
171
+ border-radius: var(--border-radius);
172
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
173
+ transition: transform 0.3s ease;
174
+ cursor: pointer;
175
+ }
176
+
177
+ .image-thumbnail:hover {
178
+ transform: scale(1.05);
179
+ }
180
+
181
+ .thumbnail-img {
182
+ transition: transform 0.3s ease;
183
+ cursor: pointer;
184
+ }
185
+
186
+ .thumbnail-img:hover {
187
+ transform: scale(1.05);
188
+ }
189
+
190
+ /* Karten */
191
+ .card {
192
+ border: none;
193
+ border-radius: var(--border-radius);
194
+ box-shadow: var(--box-shadow);
195
+ }
196
+
197
+ /* Modal */
198
+ .modal-content {
199
+ border-radius: var(--border-radius);
200
+ }
201
+
202
+ .modal-header {
203
+ background-color: var(--light-gray);
204
+ }
205
+
206
+ /* Toolbars */
207
+ .toolbar {
208
+ background-color: white;
209
+ padding: var(--base-padding);
210
+ border: 1px solid var(--medium-gray);
211
+ border-radius: var(--border-radius);
212
+ margin-bottom: calc(var(--base-margin) * 2);
213
+ display: flex;
214
+ align-items: center;
215
+ gap: var(--base-margin);
216
+ }
217
+
218
+ /* Auswahl-Checkboxes */
219
+ .select-item {
220
+ cursor: pointer;
221
+ }
222
+
223
+ /* "Nach oben"-Button */
224
+ #scrollTopBtn {
225
+ display: none;
226
+ position: fixed;
227
+ bottom: 20px;
228
+ right: 20px;
229
+ z-index: 99;
230
+ border: none;
231
+ background: transparent;
232
+ cursor: pointer;
233
+ transition: all 0.3s ease;
234
+ opacity: var(--base-opacity);
235
+ }
236
+
237
+ #scrollTopBtn:hover {
238
+ transform: translateY(-5px);
239
+ box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.2);
240
+ }
241
+
242
+ /* "Nach oben"-Pfeil */
243
+ #scrollTopBtn img {
244
+ width: 50px;
245
+ height: 50px;
246
+ border-radius: 50%;
247
+ }
248
+
249
+ /* Benachrichtigungs-Container */
250
+ .notification {
251
+ position: fixed;
252
+ top: 20px;
253
+ left: 50%;
254
+ transform: translateX(-50%);
255
+ padding: var(--base-padding) calc(var(--base-padding) * 2);
256
+ border: 1px solid var(--medium-gray);
257
+ z-index: 1000;
258
+ border-radius: var(--border-radius);
259
+ color: white;
260
+ font-weight: bold;
261
+ opacity: var(--base-opacity);
262
+ }
263
+
264
+ .notification.success {
265
+ background-color: var(--success-color);
266
+ }
267
+
268
+ .notification.error {
269
+ background-color: var(--error-color);
270
+ }
271
+
272
+ .notification.info {
273
+ background-color: var(--info-color);
274
+ }
275
+
276
+ /* Slideshow */
277
+ .carousel-item img {
278
+ max-height: 85vh;
279
+ object-fit: contain;
280
+ }
281
+
282
+ .btn-primary, .btn-secondary {
283
+ padding: 8px 16px;
284
+ font-size: 0.9rem;
285
+ border-radius: var(--border-radius);
286
+ }
287
+
288
+ /* Zusätzliche Anpassungen für Slideshow */
289
+ .slideshow-modal .modal-content {
290
+ background-color: #333;
291
+ color: #fff;
292
+ }
293
+
294
+ .slideshow-modal .modal-header {
295
+ border-bottom: 1px solid #666;
296
+ }
297
+
298
+ .slideshow-modal .modal-footer {
299
+ border-top: 1px solid #666;
300
+ }
301
+
302
+ .slideshow-modal .btn-close {
303
+ background-color: #fff;
304
+ }
305
+
306
+ .btn-sm {
307
+ padding: 5px 10px;
308
+ font-size: 0.8rem;
309
+ }
310
+
311
+ .btn-group .btn {
312
+ margin-right: 5px;
313
+ }
314
+
315
+ #albumStats {
316
+ max-width: 100%;
317
+ width: 100%;
318
+ height: auto;
319
+ }
320
+
321
+ #categoryStats {
322
+ max-width: 100%;
323
+ width: 100%;
324
+ }
325
+
326
+ #playSlideshow, #pauseSlideshow {
327
+ background-color: transparent;
328
+ border: none;
329
+ padding: 0;
330
+ }
331
+
332
+ #playSlideshow i, #pauseSlideshow i {
333
+ font-size: 1.5rem;
334
+ color: var(--primary-color);
335
+ }
336
+
337
+ #playSlideshow:hover i, #pauseSlideshow:hover i {
338
+ color: color-mix(in srgb, var(--primary-color) 40%, black);
339
+ }
340
+
341
+ /* Hover-Effekte für Download-Buttons */
342
+ .download-thumb:hover,
343
+ .btn:hover {
344
+ opacity: 1;
345
+ transform: translateY(-2px);
346
+ box-shadow: 0 2px 5px rgba(0,0,0,0.2);
347
+ }
348
+
349
+ .download-thumb {
350
+ transition: all 0.3s ease;
351
+ opacity: var(--base-opacity);
352
+ }
353
+
354
+ .btn {
355
+ transition: all 0.3s ease;
356
+ }
357
+
358
+ .modal-body {
359
+ text-align: center;
360
+ }
361
+
362
+ .error-placeholder {
363
+ color: var(--error-color);
364
+ font-weight: bold;
365
+ margin-top: var(--base-margin);
366
+ }
367
+
368
+ .blink-text {
369
+ animation: blinker 1s linear infinite;
370
+ }
371
+
372
+ @keyframes blinker {
373
+ 50% {
374
+ opacity: 0;
375
+ }
376
+ }
templates/archive.html ADDED
@@ -0,0 +1,577 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}Flux Bildarchiv{% endblock %}
4
+
5
+ {% block content %}
6
+
7
+ <div class="container-fluid bg-light p-4 rounded shadow-sm">
8
+ <!-- Suche -->
9
+ <div class="mb-3 search-container">
10
+ <form id="searchForm" action="/archive" method="get" class="d-flex flex-column">
11
+ <div class="mb-3 w-100">
12
+ <label for="search" class="form-label">Suche:</label>
13
+ <input
14
+ type="text"
15
+ class="form-control"
16
+ id="search"
17
+ name="search"
18
+ value="{{ search_query }}"
19
+ >
20
+ </div>
21
+ <div class="d-flex flex-wrap mt-3">
22
+ <button type="submit" class="btn btn-primary flex-fill" style="width: 33.33%;">Suchen</button>
23
+ <button type="reset" class="btn btn-secondary flex-fill ms-2" style="width: 33.33%;">Zurücksetzen</button>
24
+ </div>
25
+ </form>
26
+ </div>
27
+
28
+ <!-- Filter Accordion -->
29
+
30
+ <!-- Archiv Anzeige -->
31
+ <!-- Archiv Anzeige -->
32
+ <div id="archive" class="container-fluid"><div id="archive" class="container">
33
+ <div class="accordion" id="filterAccordion">
34
+ <div class="accordion-item">
35
+ <h2 class="accordion-header" id="headingOne">
36
+ <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseOne" aria-expanded="false" aria-controls="collapseOne">
37
+ Filter- und Anzeigeoptionen
38
+ </button>
39
+ </h2>
40
+ <div id="collapseOne" class="accordion-collapse collapse" aria-labelledby="headingOne">
41
+ <div class="accordion-body">
42
+ <!-- Filterformular -->
43
+ <form id="filterForm" action="/archive" method="get" class="d-flex flex-wrap gap-3 align-items-center">
44
+ <div class="mb-3 flex-grow-1">
45
+ <label for="album_filter" class="form-label">Album:</label>
46
+ <select class="form-control" id="album_filter" name="album">
47
+ <option value="">Alle</option>
48
+ {% for album in albums %}
49
+ <option value="{{ album[0] }}" {% if album[0] == selected_album %}selected{% endif %}>{{ album[1] }}</option>
50
+ {% endfor %}
51
+ </select>
52
+ </div>
53
+ <div class="mb-3 flex-grow-1">
54
+ <label for="category_filter" class="form-label">Kategorie:</label>
55
+ <select class="form-control" id="category_filter" name="category" multiple>
56
+ <option value="">Alle</option>
57
+ {% for category in categories %}
58
+ <option value="{{ category[0] }}" {% if category[0] in selected_categories %}selected{% endif %}>{{ category[1] }}</option>
59
+ {% endfor %}
60
+ </select>
61
+ </div>
62
+ <button type="submit" class="btn btn-primary flex-fill">Filtern</button>
63
+ </form>
64
+ </div>
65
+ </div>
66
+ </div>
67
+ </div>
68
+ <div id="tollbarAll" class="toolbar">
69
+ <button id="thumbgalleryBtn" class="btn btn-secondary">Thumbgallery</button>
70
+ <button id="slideshowBtn" class="btn btn-secondary">Slideshow</button>
71
+ <select id="gridLayout" class="form-select">
72
+ <option value="2">2 Bilder</option>
73
+ <option value="3" selected>3 Bilder</option>
74
+ <option value="4">4 Bilder</option>
75
+ <option value="5">5 Bilder</option>
76
+ <option value="6">6 Bilder</option>
77
+ </select>
78
+ <div class="dropdown">
79
+ <button class="btn btn-secondary dropdown-toggle" type="button" id="actionMenu" data-bs-toggle="dropdown" aria-expanded="false">
80
+ Optionen
81
+ </button>
82
+ <ul class="dropdown-menu" aria-labelledby="actionMenu">
83
+ <li><a class="dropdown-item" href="#" id="deleteSelected">Löschen</a></li>
84
+ <li><a class="dropdown-item" href="#" id="addToCategory">Zu Kategorie hinzufügen</a></li>
85
+ <li><a class="dropdown-item" href="#" id="addToAlbum">Zu Album hinzufügen</a></li>
86
+ <li><a class="dropdown-item" href="#" id="downloadSelected">Aktuelle Auswahl downloaden</a></li>
87
+ </ul>
88
+ </div>
89
+ </div>
90
+ <div class="d-flex justify-content-between align-items-center mb-3 mt-3">
91
+ <!-- Links: Checkbox "Alles auswählen" -->
92
+ <div class="d-flex align-items-center">
93
+ <input type="checkbox" id="selectAll" />
94
+ <label for="selectAll" class="ms-1 mb-0">Alles auswählen</label>
95
+ </div>
96
+
97
+ <!-- Rechts: Items-per-page-Auswahl -->
98
+ <div class="d-flex align-items-center">
99
+ <label for="itemsPerPageSelect" class="me-2 mb-0">Bilder pro Seite:</label>
100
+ <select id="itemsPerPageSelect" class="form-select form-select-sm" style="width: auto;">
101
+ <option value="15" {% if items_per_page == 15 %}selected{% endif %}>15</option>
102
+ <option value="30" {% if items_per_page == 30 %}selected{% endif %}>30</option>
103
+ <option value="50" {% if items_per_page == 50 %}selected{% endif %}>50</option>
104
+ <option value="75" {% if items_per_page == 75 %}selected{% endif %}>75</option>
105
+ <option value="100" {% if items_per_page == 100 %}selected{% endif %}>100</option>
106
+ </select>
107
+ </div>
108
+ </div>
109
+ </div>
110
+ <!-- Bildgrid -->
111
+ <div class="row row-cols-3" id="imageGrid">
112
+ {% for log in logs %}
113
+ <div class="col mb-3">
114
+ <div class="card custom-bg">
115
+ <div class="card-body p-0 position-relative">
116
+ <img src="{{ log.output_file }}"
117
+ class="img-fluid image-thumbnail"
118
+ alt="Generiertes Bild"
119
+ data-id="{{ log.id }}"
120
+ data-filename="{{ log.output_file.split('/')[-1] }}"
121
+ data-format="{{ log.output_file.split('.')[-1] }}"
122
+ data-timestamp="{{ log.timestamp }}"
123
+ data-album="{{ log.album }}"
124
+ data-category="{{ log.category }}"
125
+ data-prompt="{{ log.prompt }}"
126
+ data-optimized_prompt="{{ log.optimized_prompt }}">
127
+ <input type="checkbox" class="form-check-input select-item position-absolute top-0 end-0 m-2">
128
+ </div>
129
+ </div>
130
+ </div>
131
+ {% endfor %}
132
+ </div>
133
+ </div>
134
+ </div>
135
+
136
+ <!-- Paginierung -->
137
+ <div class="d-flex justify-content-center mt-4">
138
+ {% if page > 1 %}
139
+ <a class="btn btn-secondary me-2" href="?page={{ page - 1 }}&items_per_page={{ items_per_page }}{% if search_query %}&search={{ search_query }}{% endif %}{% if selected_album %}&album={{ selected_album }}{% endif %}{% if selected_categories %}&category={{ selected_categories | join(',') }}{% endif %}">Vorherige Seite</a>
140
+ {% endif %}
141
+ {% if logs|length == items_per_page %}
142
+ <a class="btn btn-secondary" href="?page={{ page + 1 }}&items_per_page={{ items_per_page }}{% if search_query %}&search={{ search_query }}{% endif %}{% if selected_album %}&album={{ selected_album }}{% endif %}{% if selected_categories %}&category={{ selected_categories | join(',') }}{% endif %}">Nächste Seite</a>
143
+ {% endif %}
144
+ </div>
145
+
146
+ <button id="scrollTopBtn" style="display: none; position: fixed; bottom: 20px; right: 20px; z-index: 99; border: none; background: transparent;">
147
+ <img src="/static/arrow-up1.png" alt="Nach oben" style="width: 50px; height: 50px;">
148
+ </button>
149
+
150
+ <!-- "Nach oben"-Button -->
151
+ <!-- <button id="scrollTopBtn" class="btn btn-primary" style="display: none; position: fixed; bottom: 20px; right: 20px; width: 100px; z-index: 99;">
152
+ Nach oben
153
+ </button> -->
154
+
155
+ <!-- Bild-Detail Modal -->
156
+ <div id="imageModal" class="modal fade" tabindex="-1" aria-labelledby="imageModalLabel" aria-hidden="true">
157
+ <div class="modal-dialog modal-lg">
158
+ <div class="modal-content">
159
+ <div class="modal-header">
160
+ <h5 class="modal-title" id="imageModalLabel">Bilddetails</h5>
161
+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Schließen"></button>
162
+ </div>
163
+ <div class="modal-body">
164
+ <div class="image-container" style="cursor: pointer;">
165
+ <img id="modalImage" src="" class="img-fluid mb-3" alt="Bild">
166
+ </div>
167
+ <div class="image-details">
168
+ <p><strong>Dateiname:</strong> <span id="modalFilename"></span></p>
169
+ <p><strong>Bildformat:</strong> <span id="modalFormat"></span></p>
170
+ <p><strong>Datum:</strong> <span id="modalTimestamp"></span></p>
171
+ <p><strong>Album:</strong> <span id="modalAlbum"></span></p>
172
+ <p><strong>Kategorie:</strong> <span id="modalCategory"></span></p>
173
+ <p><strong>Eingabeaufforderung:</strong> <span id="modalPrompt"></span></p>
174
+ <p><strong>Optimierte Eingabeaufforderung:</strong> <span id="modalOptimizedPrompt"></span></p>
175
+ </div>
176
+ </div>
177
+ <div class="modal-footer">
178
+ <button type="button" class="btn btn-primary" id="modalDownloadBtn">
179
+ <i class="fas fa-download"></i> Download
180
+ </button>
181
+ <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Schließen</button>
182
+ </div>
183
+ </div>
184
+ </div>
185
+ </div>
186
+
187
+ <!-- Thumbnail-Galerie Modal -->
188
+ <div id="thumbGalleryModal" class="modal fade" tabindex="-1">
189
+ <div class="modal-dialog modal-lg">
190
+ <div class="modal-content">
191
+ <div class="modal-header">
192
+ <h5 class="modal-title">Thumbnail-Galerie</h5>
193
+ <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
194
+ </div>
195
+ <div class="modal-body">
196
+ <div id="thumbGalleryContainer" class="d-flex flex-wrap justify-content-center">
197
+ </div>
198
+ </div>
199
+ <div class="modal-footer">
200
+ <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Schließen</button>
201
+ </div>
202
+ </div>
203
+ </div>
204
+ </div>
205
+
206
+ <!-- Slideshow Modal -->
207
+ <div id="slideshowModal" class="modal fade" tabindex="-1">
208
+ <div class="modal-dialog modal-lg">
209
+ <div class="modal-content">
210
+ <div class="modal-header">
211
+ <h5 class="modal-title">Diashow</h5>
212
+ <div class="btn-group ms-auto me-2">
213
+ <button class="btn btn-primary btn-sm" id="playSlideshow">
214
+ <i class="fas fa-play"></i>
215
+ </button>
216
+ <button class="btn btn-primary btn-sm" id="pauseSlideshow" style="display: none;">
217
+ <i class="fas fa-pause"></i>
218
+ </button>
219
+ <button class="btn btn-primary btn-sm" id="fullscreenBtn">
220
+ <i class="fas fa-expand"></i>
221
+ </button>
222
+ <button class="btn btn-primary btn-sm" id="downloadCurrentSlide">
223
+ <i class="fas fa-download"></i>
224
+ </button>
225
+ </div>
226
+ <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
227
+ </div>
228
+ <div class="modal-body">
229
+ <div id="carouselExampleControls" class="carousel slide" data-bs-interval="false">
230
+ <div id="slideshowContainer" class="carousel-inner"></div>
231
+ <button class="carousel-control-prev" type="button" data-bs-target="#carouselExampleControls" data-bs-slide="prev">
232
+ <span class="carousel-control-prev-icon" aria-hidden="true"></span>
233
+ <span class="visually-hidden">Vorherige</span>
234
+ </button>
235
+ <button class="carousel-control-next" type="button" data-bs-target="#carouselExampleControls" data-bs-slide="next">
236
+ <span class="carousel-control-next-icon" aria-hidden="true"></span>
237
+ <span class="visually-hidden">Nächste</span>
238
+ </button>
239
+ </div>
240
+ </div>
241
+ </div>
242
+ </div>
243
+ </div>
244
+
245
+ <!-- Modal für Zuweisung zu Album/Kategorie -->
246
+ <div id="assignAlbumModal" class="modal fade" tabindex="-1">
247
+ <div class="modal-dialog">
248
+ <div class="modal-content">
249
+ <div class="modal-header">
250
+ <h5 class="modal-title">Zu Album hinzufügen</h5>
251
+ <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
252
+ </div>
253
+ <div class="modal-body">
254
+ <select class="form-control" id="albumSelect">
255
+ {% for album in albums %}
256
+ <option value="{{ album[0] }}">{{ album[1] }}</option>
257
+ {% endfor %}
258
+ </select>
259
+ </div>
260
+ <div class="modal-footer">
261
+ <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
262
+ <button type="button" class="btn btn-primary" id="assignAlbumBtn">Hinzufügen</button>
263
+ </div>
264
+ </div>
265
+ </div>
266
+ </div>
267
+
268
+ <div id="assignCategoryModal" class="modal fade" tabindex="-1">
269
+ <div class="modal-dialog">
270
+ <div class="modal-content">
271
+ <div class="modal-header">
272
+ <h5 class="modal-title">Zu Kategorie hinzufügen</h5>
273
+ <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
274
+ </div>
275
+ <div class="modal-body">
276
+ <select class="form-control" id="categorySelect" multiple>
277
+ {% for category in categories %}
278
+ <option value="{{ category[0] }}">{{ category[1] }}</option>
279
+ {% endfor %}
280
+ </select>
281
+ </div>
282
+ <div class="modal-footer">
283
+ <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
284
+ <button type="button" class="btn btn-primary" id="assignCategoryBtn">Hinzufügen</button>
285
+ </div>
286
+ </div>
287
+ </div>
288
+ </div>
289
+
290
+ <!-- CSS Styles -->
291
+ <style>
292
+ .image-container {
293
+ position: relative;
294
+ text-align: center;
295
+ max-height: 80vh;
296
+ overflow: auto;
297
+ }
298
+
299
+ .image-container img {
300
+ max-width: 100%;
301
+ height: auto;
302
+ transition: transform 0.2s;
303
+ }
304
+
305
+ .image-container img:hover {
306
+ transform: scale(1.02);
307
+ }
308
+
309
+ .thumb-container {
310
+ position: relative;
311
+ display: inline-block;
312
+ }
313
+
314
+ .download-thumb {
315
+ position: absolute;
316
+ bottom: 5px;
317
+ right: 5px;
318
+ opacity: 0;
319
+ transition: opacity 0.3s;
320
+ }
321
+
322
+ .thumb-container:hover .download-thumb {
323
+ opacity: 1;
324
+ }
325
+
326
+ .carousel-item img {
327
+ max-height: 80vh;
328
+ object-fit: contain;
329
+ }
330
+
331
+ #slideshowModal.fullscreen .modal-dialog {
332
+ max-width: 100%;
333
+ margin: 0;
334
+ height: 100vh;
335
+ }
336
+
337
+ #slideshowModal.fullscreen .modal-content {
338
+ height: 100%;
339
+ border: none;
340
+ border-radius: 0;
341
+ }
342
+
343
+ .carousel-control-prev,
344
+ .carousel-control-next {
345
+ width: 10%;
346
+ opacity: 0;
347
+ transition: opacity 0.3s;
348
+ }
349
+
350
+ .carousel:hover .carousel-control-prev,
351
+ .carousel:hover .carousel-control-next {
352
+ opacity: 0.5;
353
+ }
354
+
355
+ .modal-dialog {
356
+ max-width: 90vw;
357
+ margin: 1.75rem auto;
358
+ }
359
+
360
+ .image-details {
361
+ margin-top: 1rem;
362
+ padding: 1rem;
363
+ background-color: rgba(0,0,0,0.02);
364
+ border-radius: 4px;
365
+ }
366
+
367
+ .modal-footer {
368
+ justify-content: space-between;
369
+ }
370
+
371
+ .container-fluid.bg-light {
372
+ background-color: rgba(248, 249, 250, 0.8) !important; /* 50% Transparenz */
373
+ }
374
+
375
+ /* Für Geräte mit einer maximalen Breite von 768px (Tablets und kleiner) */
376
+ @media (max-width: 768px) {
377
+ #tollbarAll {
378
+ display: none;
379
+ }
380
+
381
+ #imageGrid {
382
+ display: grid;
383
+ grid-template-columns: 1fr; /* 1 Bild pro Reihe */
384
+ gap: 15px; /* Abstand zwischen Bildern */
385
+ }
386
+
387
+ /* Bildkarten auf volle Breite skalieren */
388
+ .card {
389
+ width: 100%; /* Volle Breite */
390
+ margin: 0 auto;
391
+ }
392
+
393
+ .card img {
394
+ width: 100%; /* Bild nimmt gesamte Breite der Karte ein */
395
+ height: auto;
396
+ }
397
+ }
398
+
399
+
400
+ </style>
401
+
402
+ <!-- {% block scripts %}
403
+ <script src="/static/script.js"></script>
404
+ {% endblock %} -->
405
+ <script>
406
+ document.addEventListener('DOMContentLoaded', function () {
407
+ // Hilfsfunktion für Einzelbild-Download
408
+ async function downloadSingleImage(filename) {
409
+ try {
410
+ const response = await fetch(`/flux-pics/${filename}`);
411
+ if (!response.ok) {
412
+ const errorText = await response.text();
413
+ console.error('Server Error:', errorText);
414
+ throw new Error(`HTTP error! status: ${response.status}`);
415
+ }
416
+
417
+ const blob = await response.blob();
418
+ const url = window.URL.createObjectURL(blob);
419
+ const a = document.createElement('a');
420
+ a.style.display = 'none';
421
+ a.href = url;
422
+ a.download = filename;
423
+ document.body.appendChild(a);
424
+ a.click();
425
+ window.URL.revokeObjectURL(url);
426
+ } catch (error) {
427
+ console.error('Fehler beim Download:', error);
428
+ alert('Ein Fehler ist aufgetreten: ' + error.message);
429
+ }
430
+ }
431
+
432
+ // Funktion zur automatischen Anpassung des Layouts für mobile Geräte
433
+ function updateLayoutForMobile() {
434
+ const gridLayout = document.getElementById('gridLayout');
435
+ const imageGrid = document.getElementById('imageGrid');
436
+ if (window.innerWidth <= 768) {
437
+ gridLayout.value = 1; // Ein Bild pro Zeile auf mobilen Geräten
438
+ imageGrid.className = 'row row-cols-1';
439
+ } else {
440
+ const columns = parseInt(gridLayout.value);
441
+ imageGrid.className = `row row-cols-1 row-cols-md-${columns}`;
442
+ }
443
+ }
444
+
445
+ // Initiale Layout-Anpassung
446
+ updateLayoutForMobile();
447
+
448
+ // Event Listener für Fenstergrößenänderung (Responsive Verhalten)
449
+ window.addEventListener('resize', updateLayoutForMobile);
450
+
451
+ // Event Listener für Grid Layout
452
+ document.getElementById('gridLayout').addEventListener('change', function () {
453
+ const columns = parseInt(this.value);
454
+ const imageGrid = document.getElementById('imageGrid');
455
+ imageGrid.className = `row row-cols-1 row-cols-md-${columns}`;
456
+ });
457
+
458
+ // Funktion zur Öffnung des Bilddetails-Modals
459
+ function openImageModal(img) {
460
+ const modal = new bootstrap.Modal(document.getElementById('imageModal'));
461
+ const modalImg = document.getElementById('modalImage');
462
+ const filename = img.dataset.filename;
463
+
464
+ modalImg.src = img.src;
465
+ document.getElementById('modalFilename').textContent = filename;
466
+ document.getElementById('modalFormat').textContent = img.dataset.format;
467
+ document.getElementById('modalTimestamp').textContent = img.dataset.timestamp;
468
+ document.getElementById('modalAlbum').textContent = img.dataset.album;
469
+ document.getElementById('modalCategory').textContent = img.dataset.category;
470
+ document.getElementById('modalPrompt').textContent = img.dataset.prompt;
471
+ document.getElementById('modalOptimizedPrompt').textContent = img.dataset.optimized_prompt;
472
+
473
+ modal.show();
474
+ }
475
+
476
+ document.querySelectorAll('.image-thumbnail').forEach(function (img) {
477
+ img.addEventListener('click', function () {
478
+ openImageModal(this);
479
+ });
480
+ });
481
+
482
+ // Funktion für die "Alle auswählen"-Checkbox
483
+ const selectAllCheckbox = document.getElementById('selectAll');
484
+ const itemCheckboxes = document.querySelectorAll('.select-item');
485
+ selectAllCheckbox.addEventListener('change', function () {
486
+ itemCheckboxes.forEach(checkbox => {
487
+ checkbox.checked = selectAllCheckbox.checked;
488
+ });
489
+ });
490
+
491
+ // Funktion zum Sammeln ausgewählter Bilder
492
+ function getSelectedImages() {
493
+ const selectedImages = [];
494
+ document.querySelectorAll('.select-item:checked').forEach(checkbox => {
495
+ const img = checkbox.closest('.card').querySelector('img');
496
+ if (img && img.dataset.filename) {
497
+ selectedImages.push(img.dataset.filename);
498
+ }
499
+ });
500
+ return selectedImages;
501
+ }
502
+
503
+ // Download der ausgewählten Bilder
504
+ document.getElementById('downloadSelected').addEventListener('click', async function () {
505
+ const selectedImages = getSelectedImages();
506
+ if (selectedImages.length === 0) {
507
+ alert('Keine Bilder ausgewählt.');
508
+ return;
509
+ }
510
+
511
+ let downloadType = 'single';
512
+ if (selectedImages.length > 1) {
513
+ const choice = confirm('Möchten Sie die Bilder als ZIP-Datei herunterladen? Klicken Sie "OK" für ZIP oder "Abbrechen" für Einzeldownloads.');
514
+ if (choice) {
515
+ downloadType = 'zip';
516
+ }
517
+ }
518
+
519
+ try {
520
+ if (downloadType === 'zip') {
521
+ const response = await fetch('/flux-pics', {
522
+ method: 'POST',
523
+ headers: { 'Content-Type': 'application/json' },
524
+ body: JSON.stringify({ selectedImages })
525
+ });
526
+
527
+ if (!response.ok) {
528
+ const errorText = await response.text();
529
+ console.error('Server Error:', errorText);
530
+ throw new Error(`HTTP error! status: ${response.status}`);
531
+ }
532
+
533
+ const blob = await response.blob();
534
+ const url = window.URL.createObjectURL(blob);
535
+ const a = document.createElement('a');
536
+ a.style.display = 'none';
537
+ a.href = url;
538
+ a.download = 'images.zip';
539
+ document.body.appendChild(a);
540
+ a.click();
541
+ window.URL.revokeObjectURL(url);
542
+ } else {
543
+ for (const filename of selectedImages) {
544
+ await downloadSingleImage(filename);
545
+ }
546
+ }
547
+ alert('Download erfolgreich abgeschlossen.');
548
+ } catch (error) {
549
+ console.error('Fehler beim Downloaden:', error);
550
+ alert('Ein Fehler ist aufgetreten: ' + error.message);
551
+ }
552
+ });
553
+
554
+ // Automatisches Aktualisieren der Bilder pro Seite
555
+ const itemsPerPageSelect = document.getElementById('itemsPerPageSelect');
556
+ if (itemsPerPageSelect) {
557
+ itemsPerPageSelect.addEventListener('change', function () {
558
+ const form = document.getElementById('generateForm');
559
+ if (form) {
560
+ form.submit();
561
+ }
562
+ });
563
+ }
564
+
565
+ // "Nach oben"-Button
566
+ const scrollTopBtn = document.getElementById('scrollTopBtn');
567
+ window.addEventListener('scroll', function () {
568
+ scrollTopBtn.style.display = window.scrollY > 300 ? 'block' : 'none';
569
+ });
570
+
571
+ scrollTopBtn.addEventListener('click', function () {
572
+ window.scrollTo({ top: 0, behavior: 'smooth' });
573
+ });
574
+ });
575
+ </script>
576
+
577
+ {% endblock %}
templates/backend.html ADDED
@@ -0,0 +1,581 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}Backend Verwaltung{% endblock %}
4
+
5
+ {% block head %}
6
+ <script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
7
+ <script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
8
+ <script src="https://unpkg.com/recharts/umd/Recharts.min.js"></script>
9
+ <link href="https://cdn.jsdelivr.net/npm/[email protected]/font/bootstrap-icons.css" rel="stylesheet">
10
+ <style>
11
+ .hoverable-row:hover {
12
+ background-color: rgba(0,0,0,0.05);
13
+ cursor: pointer;
14
+ }
15
+ .card {
16
+ transition: transform 0.2s;
17
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
18
+ }
19
+ .card:hover {
20
+ transform: translateY(-2px);
21
+ box-shadow: 0 4px 8px rgba(0,0,0,0.15);
22
+ }
23
+ .stat-card {
24
+ border-radius: 10px;
25
+ border: none;
26
+ background: linear-gradient(45deg, #3498db, #2980b9);
27
+ color: white;
28
+ }
29
+ .stat-icon {
30
+ font-size: 2rem;
31
+ margin-bottom: 1rem;
32
+ }
33
+ .stat-value {
34
+ font-size: 1.5rem;
35
+ font-weight: bold;
36
+ }
37
+ .stat-label {
38
+ font-size: 0.9rem;
39
+ opacity: 0.9;
40
+ }
41
+ .action-button {
42
+ transition: all 0.2s;
43
+ }
44
+ .action-button:hover {
45
+ transform: scale(1.05);
46
+ }
47
+ .toast-container {
48
+ z-index: 1051;
49
+ }
50
+ .chart-container {
51
+ position: relative;
52
+ margin: 20px 0;
53
+ height: 300px;
54
+ }
55
+ .loader {
56
+ position: absolute;
57
+ top: 50%;
58
+ left: 50%;
59
+ transform: translate(-50%, -50%);
60
+ }
61
+ </style>
62
+ {% endblock %}
63
+
64
+ {% block content %}
65
+ <form id="generateForm" class="needs-validation custom-bg p-3 rounded" novalidate>
66
+ <div class="container-fluid py-4">
67
+ <div class="d-flex justify-content-between align-items-center mb-4">
68
+ <div class="btn-group">
69
+ <button class="btn btn-primary" onclick="refreshData()">
70
+ <i class="bi bi-arrow-clockwise"></i> Aktualisieren
71
+ </button>
72
+ <button class="btn btn-secondary" onclick="toggleDarkMode()">
73
+ <i class="bi bi-moon"></i> Dark Mode
74
+ </button>
75
+ </div>
76
+ </div>
77
+
78
+ <!-- Quick Stats Row -->
79
+ <div class="row mb-4">
80
+ <div class="col-md-3">
81
+ <div class="card stat-card mb-3">
82
+ <div class="card-body text-center">
83
+ <i class="bi bi-images stat-icon"></i>
84
+ <div class="stat-value" id="totalImages">0</div>
85
+ <div class="stat-label">Gesamt Bilder</div>
86
+ </div>
87
+ </div>
88
+ </div>
89
+ <div class="col-md-3">
90
+ <div class="card stat-card mb-3">
91
+ <div class="card-body text-center">
92
+ <i class="bi bi-folder stat-icon"></i>
93
+ <div class="stat-value" id="totalAlbums">0</div>
94
+ <div class="stat-label">Alben</div>
95
+ </div>
96
+ </div>
97
+ </div>
98
+ <div class="col-md-3">
99
+ <div class="card stat-card mb-3">
100
+ <div class="card-body text-center">
101
+ <i class="bi bi-tags stat-icon"></i>
102
+ <div class="stat-value" id="totalCategories">0</div>
103
+ <div class="stat-label">Kategorien</div>
104
+ </div>
105
+ </div>
106
+ </div>
107
+ <div class="col-md-3">
108
+ <div class="card stat-card mb-3">
109
+ <div class="card-body text-center">
110
+ <i class="bi bi-hdd stat-icon"></i>
111
+ <div class="stat-value" id="storageUsage">0 MB</div>
112
+ <div class="stat-label">Speicherplatz</div>
113
+ </div>
114
+ </div>
115
+ </div>
116
+ </div>
117
+
118
+ <!-- Main Content -->
119
+ <div class="row">
120
+ <!-- Album Management -->
121
+ <div id="albumStats" class="col-12 col-lg-6 mb-4">
122
+ <div class="card h-100">
123
+ <div class="card-header bg-primary text-white d-flex justify-content-between align-items-center">
124
+ <h5 class="mb-0">Album Verwaltung</h5>
125
+ <button class="btn btn-light btn-sm" onclick="showNewAlbumModal()">
126
+ <i class="bi bi-plus-lg"></i> Neu
127
+ </button>
128
+ </div>
129
+ <div class="card-body">
130
+ <div class="table-responsive">
131
+ <table class="table table-hover" id="albumTable">
132
+ <thead>
133
+ <tr>
134
+ <th>
135
+ <input type="checkbox" class="form-check-input" id="selectAllAlbums">
136
+ </th>
137
+ <th>Name</th>
138
+ <th>Bilder</th>
139
+ <th>Erstellt</th>
140
+ <th>Aktionen</th>
141
+ </tr>
142
+ </thead>
143
+ <tr>
144
+ <td>
145
+ <div class="album-list d-flex flex-wrap">
146
+ {% for album in albums %}
147
+ <div class="album-item p-2">
148
+ {{ album[1] }}
149
+ </div>
150
+ {% endfor %}
151
+ </div>
152
+ </td>
153
+ </tr>
154
+ </table>
155
+ </div>
156
+ <div id="albumPagination" class="d-flex justify-content-between align-items-center mt-3">
157
+ <div class="d-flex align-items-center">
158
+ <select class="form-select me-2" id="albumPageSize">
159
+ <option value="10">10</option>
160
+ <option value="25">25</option>
161
+ <option value="50">50</option>
162
+ </select>
163
+ <span>Einträge pro Seite</span>
164
+ </div>
165
+ <nav>
166
+ <ul class="pagination mb-0">
167
+ <!-- Pagination will be inserted here -->
168
+ </ul>
169
+ </nav>
170
+ </div>
171
+ </div>
172
+ </div>
173
+ </div>
174
+
175
+ <!-- Category Management -->
176
+ <div id="categoryStats" class="col-12 col-lg-6 mb-4">
177
+ <div class="card h-100">
178
+ <div class="card-header bg-primary text-white d-flex justify-content-between align-items-center">
179
+ <h5 class="mb-0">Kategorie Verwaltung</h5>
180
+ <button class="btn btn-light btn-sm" onclick="showNewCategoryModal()">
181
+ <i class="bi bi-plus-lg"></i> Neu
182
+ </button>
183
+ </div>
184
+ <div class="card-body">
185
+ <div class="table-responsive">
186
+ <table class="table table-hover" id="categoryTable">
187
+ <thead>
188
+ <tr>
189
+ <th>
190
+ <input type="checkbox" class="form-check-input" id="selectAllCategories">
191
+ </th>
192
+ <th>Name</th>
193
+ <th>Bilder</th>
194
+ <th>Erstellt</th>
195
+ <th>Aktionen</th>
196
+ </tr>
197
+ </thead>
198
+ <tbody id="categoryList">
199
+ <!-- Category rows will be inserted here -->
200
+ </tbody>
201
+ </table>
202
+ </div>
203
+ <div id="categoryPagination" class="d-flex justify-content-between align-items-center mt-3">
204
+ <div class="d-flex align-items-center">
205
+ <select class="form-select me-2" id="categoryPageSize">
206
+ <option value="10">10</option>
207
+ <option value="25">25</option>
208
+ <option value="50">50</option>
209
+ </select>
210
+ <span>Einträge pro Seite</span>
211
+ </div>
212
+ <nav>
213
+ <ul class="pagination mb-0">
214
+ <!-- Pagination will be inserted here -->
215
+ </ul>
216
+ </nav>
217
+ </div>
218
+ </div>
219
+ </div>
220
+ </div>
221
+ </div>
222
+ </div>
223
+ </form>
224
+ <!-- Statistics Row -->
225
+ <!-- <div class="row">
226
+ <div class="col-12">
227
+ <div class="card">
228
+ <div class="card-header bg-primary text-white">
229
+ <h5 class="mb-0">Statistiken</h5>
230
+ </div>
231
+ <div class="card-body">
232
+ <div class="row">
233
+ <div class="col-md-6">
234
+ <div class="chart-container">
235
+ <canvas id="monthlyStats"></canvas>
236
+ <div class="loader" id="monthlyStatsLoader">
237
+ <div class="spinner-border text-primary" role="status">
238
+ <span class="visually-hidden">Laden...</span>
239
+ </div>
240
+ </div>
241
+ </div>
242
+ </div>
243
+ <div class="col-md-6">
244
+ <div class="chart-container">
245
+ <canvas id="categoryStats"></canvas>
246
+ <div class="loader" id="categoryStatsLoader">
247
+ <div class="spinner-border text-primary" role="status">
248
+ <span class="visually-hidden">Laden...</span>
249
+ </div>
250
+ </div>
251
+ </div>
252
+ </div>
253
+ </div>
254
+ <div class="row mt-4">
255
+ <div class="col-md-12">
256
+ <div class="chart-container">
257
+ <canvas id="storageStats"></canvas>
258
+ <div class="loader" id="storageStatsLoader">
259
+ <div class="spinner-border text-primary" role="status">
260
+ <span class="visually-hidden">Laden...</span>
261
+ </div>
262
+ </div>
263
+ </div>
264
+ </div>
265
+ </div>
266
+ </div>
267
+ </div>
268
+ </div>
269
+ </div>
270
+ </div> -->
271
+
272
+ <!-- Modals -->
273
+ <!-- New Album Modal -->
274
+ <div class="modal fade" id="newAlbumModal" tabindex="-1">
275
+ <div class="modal-dialog">
276
+ <div class="modal-content">
277
+ <div class="modal-header">
278
+ <h5 class="modal-title">Neues Album erstellen</h5>
279
+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
280
+ </div>
281
+ <div class="modal-body">
282
+ <form id="newAlbumForm">
283
+ <div class="mb-3">
284
+ <label for="albumName" class="form-label">Album Name</label>
285
+ <input type="text" class="form-control" id="albumName" required>
286
+ </div>
287
+ <div class="mb-3">
288
+ <label for="albumDescription" class="form-label">Beschreibung (optional)</label>
289
+ <textarea class="form-control" id="albumDescription" rows="3"></textarea>
290
+ </div>
291
+ </form>
292
+ </div>
293
+ <div class="modal-footer">
294
+ <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
295
+ <button type="button" class="btn btn-primary" onclick="createAlbum()">Erstellen</button>
296
+ </div>
297
+ </div>
298
+ </div>
299
+ </div>
300
+
301
+ <!-- New Category Modal -->
302
+ <div class="modal fade" id="newCategoryModal" tabindex="-1">
303
+ <div class="modal-dialog">
304
+ <div class="modal-content">
305
+ <div class="modal-header">
306
+ <h5 class="modal-title">Neue Kategorie erstellen</h5>
307
+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
308
+ </div>
309
+ <div class="modal-body">
310
+ <form id="newCategoryForm">
311
+ <div class="mb-3">
312
+ <label for="categoryName" class="form-label">Kategorie Name</label>
313
+ <input type="text" class="form-control" id="categoryName" required>
314
+ </div>
315
+ <div class="mb-3">
316
+ <label for="categoryDescription" class="form-label">Beschreibung (optional)</label>
317
+ <textarea class="form-control" id="categoryDescription" rows="3"></textarea>
318
+ </div>
319
+ </form>
320
+ </div>
321
+ <div class="modal-footer">
322
+ <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
323
+ <button type="button" class="btn btn-primary" onclick="createCategory()">Erstellen</button>
324
+ </div>
325
+ </div>
326
+ </div>
327
+ </div>
328
+
329
+ <!-- Delete Confirmation Modal -->
330
+ <div class="modal fade" id="deleteModal" tabindex="-1">
331
+ <div class="modal-dialog">
332
+ <div class="modal-content">
333
+ <div class="modal-header">
334
+ <h5 class="modal-title">Löschen bestätigen</h5>
335
+ <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
336
+ </div>
337
+ <div class="modal-body">
338
+ <p>Möchten Sie <span id="deleteItemName"></span> wirklich löschen?</p>
339
+ <p class="text-danger">Diese Aktion kann nicht rückgängig gemacht werden!</p>
340
+ </div>
341
+ <div class="modal-footer">
342
+ <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
343
+ <button type="button" class="btn btn-danger" onclick="confirmDelete()">Löschen</button>
344
+ </div>
345
+ </div>
346
+ </div>
347
+ </div>
348
+
349
+ <!-- Bulk Action Modal -->
350
+ <div class="modal fade" id="bulkActionModal" tabindex="-1">
351
+ <div class="modal-dialog">
352
+ <div class="modal-content">
353
+ <div class="modal-header">
354
+ <h5 class="modal-title">Massenbearbeitung</h5>
355
+ <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
356
+ </div>
357
+ <div class="modal-body">
358
+ <p>Ausgewählte Elemente: <span id="selectedCount">0</span></p>
359
+ <div class="list-group">
360
+ <button type="button" class="list-group-item list-group-item-action" onclick="bulkDelete()">
361
+ <i class="bi bi-trash"></i> Alle löschen
362
+ </button>
363
+ <button type="button" class="list-group-item list-group-item-action" onclick="mergeSelected()">
364
+ <i class="bi bi-arrow-join"></i> Zusammenführen
365
+ </button>
366
+ <button type="button" class="list-group-item list-group-item-action" onclick="exportSelected()">
367
+ <i class="bi bi-download"></i> Exportieren
368
+ </button>
369
+ </div>
370
+ </div>
371
+ <div class="modal-footer">
372
+ <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Schließen</button>
373
+ </div>
374
+ </div>
375
+ </div>
376
+ </div>
377
+
378
+ <!-- Toast Container -->
379
+ <!-- <div class="toast-container position-fixed bottom-0 end-0 p-3">
380
+ <div id="toast" class="toast" role="alert" aria-live="assertive" aria-atomic="true">
381
+ <div class="toast-header">
382
+ <i class="bi bi-info-circle me-2"></i>
383
+ <strong class="me-auto" id="toastTitle"></strong>
384
+ <button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button>
385
+ </div>
386
+ <div class="toast-body" id="toastMessage"></div>
387
+ </div>
388
+ </div> -->
389
+ {% endblock %}
390
+
391
+ {% block scripts %}
392
+ <!-- Chart.js für Statistiken -->
393
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
394
+
395
+ <!-- Backend Manager Script -->
396
+ <script src="/static/backend.js"></script>
397
+
398
+ <!-- React Components -->
399
+ <script type="module">
400
+ import AlbumManager from '/static/components/AlbumManager.js';
401
+ import CategoryManager from '/static/components/CategoryManager.js';
402
+ import StatsVisualization from '/static/components/StatsVisualization.js';
403
+
404
+ // React Components initialisieren
405
+ ReactDOM.createRoot(document.getElementById('albumManagerRoot')).render(
406
+ React.createElement(AlbumManager)
407
+ );
408
+
409
+ ReactDOM.createRoot(document.getElementById('categoryManagerRoot')).render(
410
+ React.createElement(CategoryManager)
411
+ );
412
+
413
+ ReactDOM.createRoot(document.getElementById('statsRoot')).render(
414
+ React.createElement(StatsVisualization)
415
+ );
416
+ </script>
417
+
418
+ <!-- Inline Script für zusätzliche Funktionalität -->
419
+ <script>
420
+ document.addEventListener('DOMContentLoaded', function() {
421
+ // Dark Mode Toggle
422
+ const darkModeToggle = document.getElementById('darkModeToggle');
423
+ if (darkModeToggle) {
424
+ darkModeToggle.addEventListener('click', function() {
425
+ document.body.classList.toggle('dark-mode');
426
+ localStorage.setItem('darkMode', document.body.classList.contains('dark-mode'));
427
+ });
428
+
429
+ // Dark Mode aus localStorage wiederherstellen
430
+ if (localStorage.getItem('darkMode') === 'true') {
431
+ document.body.classList.add('dark-mode');
432
+ }
433
+ }
434
+
435
+ // Toast Funktionalität
436
+ window.showToast = function(title, message, type = 'info') {
437
+ const toast = document.getElementById('toast');
438
+ const toastTitle = document.getElementById('toastTitle');
439
+ const toastMessage = document.getElementById('toastMessage');
440
+
441
+ toast.classList.remove('bg-success', 'bg-danger', 'bg-info');
442
+ toast.classList.add(`bg-${type}`);
443
+ toastTitle.textContent = title;
444
+ toastMessage.textContent = message;
445
+
446
+ const bsToast = new bootstrap.Toast(toast);
447
+ bsToast.show();
448
+ };
449
+
450
+ // Pagination Event Listener
451
+ document.querySelectorAll('.page-link').forEach(button => {
452
+ button.addEventListener('click', function(e) {
453
+ e.preventDefault();
454
+ const page = this.getAttribute('data-page');
455
+ loadPage(page);
456
+ });
457
+ });
458
+
459
+ // Items per Page Event Listener
460
+ document.querySelectorAll('.items-per-page').forEach(select => {
461
+ select.addEventListener('change', function() {
462
+ loadPage(1);
463
+ });
464
+ });
465
+
466
+ // Select All Checkboxes
467
+ document.querySelectorAll('.select-all').forEach(checkbox => {
468
+ checkbox.addEventListener('change', function() {
469
+ const itemCheckboxes = document.querySelectorAll(
470
+ `.item-checkbox[data-type="${this.dataset.type}"]`
471
+ );
472
+ itemCheckboxes.forEach(item => item.checked = this.checked);
473
+ updateBulkActionButton();
474
+ });
475
+ });
476
+
477
+ // Bulk Action Button Update
478
+ function updateBulkActionButton() {
479
+ const selectedCount = document.querySelectorAll('.item-checkbox:checked').length;
480
+ const bulkActionBtn = document.getElementById('bulkActionBtn');
481
+ if (bulkActionBtn) {
482
+ bulkActionBtn.disabled = selectedCount === 0;
483
+ document.getElementById('selectedCount').textContent = selectedCount;
484
+ }
485
+ }
486
+
487
+ // Initial Load
488
+ loadPage(1);
489
+ updateStatistics();
490
+ });
491
+
492
+ // Refresh Funktionen
493
+ async function refreshData() {
494
+ try {
495
+ await Promise.all([
496
+ loadPage(1),
497
+ updateStatistics()
498
+ ]);
499
+ showToast('Erfolg', 'Daten erfolgreich aktualisiert', 'success');
500
+ } catch (error) {
501
+ showToast('Fehler', 'Fehler beim Aktualisieren der Daten', 'danger');
502
+ }
503
+ }
504
+
505
+ // Statistik Aktualisierung
506
+ async function updateStatistics() {
507
+ try {
508
+ const response = await fetch('/backend/stats');
509
+ const stats = await response.json();
510
+
511
+ // Update Quick Stats
512
+ document.getElementById('totalImages').textContent = stats.total_images;
513
+ document.getElementById('totalAlbums').textContent = stats.albums.total;
514
+ document.getElementById('totalCategories').textContent = stats.categories.total;
515
+ document.getElementById('storageUsage').textContent =
516
+ `${Math.round(stats.storage_usage_mb * 100) / 100} MB`;
517
+
518
+ // Update Charts
519
+ updateCharts(stats);
520
+ } catch (error) {
521
+ console.error('Fehler beim Laden der Statistiken:', error);
522
+ }
523
+ }
524
+
525
+ // Chart Aktualisierung
526
+ function updateCharts(stats) {
527
+ updateMonthlyChart(stats.monthly);
528
+ updateCategoryChart(stats.categories);
529
+ updateStorageChart(stats.storage);
530
+ }
531
+
532
+ function createChart(ctx, config) {
533
+ if (window.charts && window.charts[ctx.id]) {
534
+ window.charts[ctx.id].destroy();
535
+ }
536
+ window.charts = window.charts || {};
537
+ window.charts[ctx.id] = new Chart(ctx, config);
538
+ }
539
+
540
+ // Export Funktion
541
+ async function exportSelected() {
542
+ const selectedIds = Array.from(document.querySelectorAll('.item-checkbox:checked'))
543
+ .map(cb => cb.dataset.id);
544
+
545
+ if (selectedIds.length === 0) {
546
+ showToast('Warnung', 'Keine Elemente ausgewählt', 'warning');
547
+ return;
548
+ }
549
+
550
+ try {
551
+ const response = await fetch('/backend/export', {
552
+ method: 'POST',
553
+ headers: { 'Content-Type': 'application/json' },
554
+ body: JSON.stringify({ ids: selectedIds })
555
+ });
556
+
557
+ if (!response.ok) throw new Error('Export fehlgeschlagen');
558
+
559
+ const blob = await response.blob();
560
+ const url = window.URL.createObjectURL(blob);
561
+ const a = document.createElement('a');
562
+ a.href = url;
563
+ a.download = 'export.csv';
564
+ document.body.appendChild(a);
565
+ a.click();
566
+ document.body.removeChild(a);
567
+ window.URL.revokeObjectURL(url);
568
+
569
+ showToast('Erfolg', 'Export erfolgreich', 'success');
570
+ } catch (error) {
571
+ showToast('Fehler', 'Export fehlgeschlagen', 'danger');
572
+ }
573
+ }
574
+
575
+ // Error Handler
576
+ function handleError(error) {
577
+ console.error('Fehler:', error);
578
+ showToast('Fehler', error.message || 'Ein Fehler ist aufgetreten', 'danger');
579
+ }
580
+ </script>
581
+ {% endblock %}
templates/banner.jpg ADDED
templates/base.html ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>{% block title %}Flux Image Generator{% endblock %}</title>
7
+ <link rel="icon" href="/static/favicon.ico" type="image/x-icon">
8
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css">
9
+ <link rel="stylesheet" href="/static/style.css">
10
+ <style>
11
+ .custom-bg {
12
+ background-color: rgba(248, 249, 250, 0.8); /* Leicht abgehobenes Weiß mit 80% Deckkraft */
13
+ }
14
+ .nav-buttons {
15
+ width: 100%;
16
+ }
17
+ .nav-buttons .btn {
18
+ flex-grow: 1;
19
+ text-align: center;
20
+ }
21
+ .nav-buttons .btn + .btn {
22
+ margin-left: 0;
23
+ }
24
+ </style>
25
+ {% block head %}
26
+ <!-- Additional head elements can be added here -->
27
+ {% endblock %}
28
+ </head>
29
+ <body>
30
+ <div class="container">
31
+ <!-- Bannerbild -->
32
+ <div class="banner mb-3">
33
+ <img src="/static/banner.jpg" alt="Flux Image Generator Banner" class="img-fluid w-100">
34
+ </div>
35
+
36
+ <!-- Navigation Buttons -->
37
+ <div class="nav-buttons d-flex flex-wrap justify-content-between gap-2 mb-3">
38
+ <a href="/" class="btn btn-primary flex-fill text-center">Home</a>
39
+ <a href="/archive" class="btn btn-secondary flex-fill text-center">Archiv</a>
40
+ <a href="/backend" class="btn btn-secondary flex-fill text-center">Backend</a>
41
+ </div>
42
+
43
+ {% block content %}
44
+ <!-- Main content will be injected here -->
45
+ {% endblock %}
46
+ </div>
47
+ <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
48
+ {% block scripts %}
49
+ <!-- Additional scripts can be added here -->
50
+ {% endblock %}
51
+ </body>
52
+ </html>
templates/favicon.ico ADDED
templates/index.html ADDED
@@ -0,0 +1,395 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}Flux Image Generator - Home{% endblock %}
4
+
5
+ {% block content %}
6
+ <!-- <h1>Flux Image Generator</h1> -->
7
+
8
+ <form id="generateForm" class="needs-validation custom-bg p-3 rounded" novalidate>
9
+ <!-- Prompt-Eingabe -->
10
+ <div class="mb-3">
11
+ <label for="prompt" class="form-label">Prompt:</label>
12
+ <textarea class="form-control" id="prompt" name="prompt" rows="4" required></textarea>
13
+ <div class="invalid-feedback">Bitte geben Sie einen Prompt ein.</div>
14
+ </div>
15
+ <!-- Fortschrittsanzeige -->
16
+ <div class="progress mb-3" id="progressContainer" style="display: none;">
17
+ <div id="progressBar" class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" style="width: 0%;" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div>
18
+ </div>
19
+
20
+ <!-- Fortschrittsnachricht -->
21
+ <div id="progressMessage" class="mb-3 blink-text" style="display: none;">
22
+ <p></h4>Generiere Bilder, bitte warten...</h4></p>
23
+ </div>
24
+ <!-- Album-Auswahl -->
25
+ <div class="mb-3">
26
+ <label for="album_id" class="form-label">Album:</label>
27
+ <input class="form-control" id="album_id" name="album_id" list="albums" placeholder="Wählen oder neues Album eingeben">
28
+ <datalist id="albums">
29
+ {% for album in albums %}
30
+ <option value="{{ album[1] }}">{{ album[0] }}</option>
31
+ {% endfor %}
32
+ </datalist>
33
+ </div>
34
+ <!-- Kategorie-Auswahl -->
35
+ <div class="mb-3">
36
+ <label for="category_id" class="form-label">Kategorie:</label>
37
+ <input class="form-control" id="category_id" name="category_id" list="categories" placeholder="Wählen oder neue Kategorie eingeben">
38
+ <datalist id="categories">
39
+ {% for category in categories %}
40
+ <option value="{{ category[1] }}">{{ category[0] }}</option>
41
+ {% endfor %}
42
+ </datalist>
43
+ </div>
44
+ <div class="btn-group d-flex flex-wrap">
45
+ <button type="button" class="btn btn-primary flex-fill mb-3" onclick="startGeneration()">ERSTELLEN</button>
46
+ <button type="button" class="btn btn-secondary flex-fill mb-3" onclick="optimizeOnly()">OPTIMIEREN</button>
47
+ <button type="button" class="btn btn-secondary flex-fill mb-3" onclick="copyPrompt()">ZURÜCKSETZEN</button>
48
+ </div>
49
+
50
+ <!--
51
+ Buttons zur Generierung und Optimierung
52
+ <button type="button" class="btn btn-primary mb-3" onclick="startGeneration()">Bild Generieren</button>
53
+ <button type="button" class="btn btn-secondary mb-3" onclick="optimizeOnly()">Nur Optimieren</button>
54
+ <button type="button" class="btn btn-secondary mb-3" onclick="copyPrompt()">Prompt Kopieren</button>
55
+ -->
56
+ <!-- Erweiterte Einstellungen -->
57
+ <div class="accordion" id="advancedSettingsAccordion">
58
+ <div class="accordion-item custom-bg">
59
+ <h2 class="accordion-header" id="headingOne">
60
+ <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseOne" aria-expanded="false" aria-controls="collapseOne">
61
+ Erweiterte Einstellungen
62
+ </button>
63
+ </h2>
64
+ <div id="collapseOne" class="accordion-collapse collapse" aria-labelledby="headingOne" data-bs-parent="#advancedSettingsAccordion">
65
+ <div class="accordion-body">
66
+ <div class="row g-3">
67
+ <div class="col-12 col-md-6">
68
+ <label for="num_outputs" class="form-label">Anzahl der Ausgaben:</label>
69
+ <select class="form-select" id="num_outputs" name="num_outputs">
70
+ <option value="1">1</option>
71
+ <option value="2">2</option>
72
+ <option value="3">3</option>
73
+ <option value="4">4</option>
74
+ </select>
75
+ </div>
76
+ <div class="col-12 col-md-6">
77
+ <label for="aspect_ratio" class="form-label">Seitenverhältnis:</label>
78
+ <select class="form-select" id="aspect_ratio" name="aspect_ratio">
79
+ <option value="1:1">1:1</option>
80
+ <option value="16:9">16:9</option>
81
+ <option value="21:9">21:9</option>
82
+ <option value="3:2">3:2</option>
83
+ <option value="2:3">2:3</option>
84
+ <option value="4:5">4:5</option>
85
+ <option value="5:4">5:4</option>
86
+ <option value="3:4">3:4</option>
87
+ <option value="4:3">4:3</option>
88
+ <option value="9:16">9:16</option>
89
+ <option value="9:21">9:21</option>
90
+ </select>
91
+ </div>
92
+ <div class="col-12 col-md-6">
93
+ <label for="output_format" class="form-label">Ausgabeformat:</label>
94
+ <select class="form-select" id="output_format" name="output_format">
95
+ <option value="png">PNG</option>
96
+ <option value="jpg">JPG</option>
97
+ <option value="webp">WEBP</option>
98
+ </select>
99
+ </div>
100
+ <div class="col-12 col-md-6">
101
+ <label for="guidance_scale" class="form-label">Guidance Scale:</label>
102
+ <input type="number" step="0.1" class="form-control" id="guidance_scale" name="guidance_scale" value="3.5">
103
+ </div>
104
+ <div class="col-12 col-md-6">
105
+ <label for="output_quality" class="form-label">Ausgabequalität:</label>
106
+ <input type="number" class="form-control" id="output_quality" name="output_quality" value="80">
107
+ </div>
108
+ <div class="col-12 col-md-6">
109
+ <label for="prompt_strength" class="form-label">Prompt Strength:</label>
110
+ <input type="number" step="0.1" class="form-control" id="prompt_strength" name="prompt_strength" value="0.8">
111
+ </div>
112
+ <div class="col-12 col-md-6">
113
+ <label for="num_inference_steps" class="form-label">Anzahl der Inference Steps:</label>
114
+ <input type="number" class="form-control" id="num_inference_steps" name="num_inference_steps" value="28">
115
+ </div>
116
+ <div class="col-12 col-md-6">
117
+ <label for="lora_scale" class="form-label">LoRA Scale:</label>
118
+ <input type="number" step="0.1" class="form-control" id="lora_scale" name="lora_scale" value="0.8">
119
+ </div>
120
+ <!-- <div class="col-12 col-md-6 d-flex align-items-center">
121
+ <div class="col-12 col-md-6">
122
+ <input class="form-check-input" type="checkbox" id="hf_lora_toggle" name="hf_lora_toggle" checked>
123
+ <label class="form-check-label" for="hf_lora_toggle">HF LoRA verwenden</label>
124
+ </div> -->
125
+
126
+ <div class="col-12 col-md-6">
127
+ <label class="form-check-label" for="hf_lora_toggle">HF LoRA verwenden:</label>
128
+ <input class="form-control" type="text" id="hf_lora_toggle" name="hf_lora_toggle" value="Scalino84/my-flux-face">
129
+ </div>
130
+ <div class="col-12 col-md-6">
131
+ <label class="form-check-label" for="agent">Mistral Agent verwenden:</label>
132
+ <input class="form-check-input" type="checkbox" id="agent" name="agent" checked>
133
+ </div>
134
+ <!-- </div> -->
135
+ </div>
136
+ </div>
137
+ </div>
138
+ </div>
139
+ </div>
140
+
141
+ <!-- Batch-Optimierung -->
142
+ <div class="accordion mt-3" id="batchOptimizationAccordion">
143
+ <div class="accordion-item custom-bg">
144
+ <h2 class="accordion-header" id="headingTwo">
145
+ <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseTwo" aria-expanded="false" aria-controls="collapseTwo">
146
+ Batch-Optimierung
147
+ </button>
148
+ </h2>
149
+ <div id="collapseTwo" class="accordion-collapse collapse" aria-labelledby="headingTwo" data-bs-parent="#batchOptimizationAccordion">
150
+ <div class="accordion-body">
151
+ <textarea id="batchPrompts" class="form-control" rows="10" placeholder="Mehrere Prompts eingeben, einer pro Zeile"></textarea>
152
+ <button type="button" class="btn btn-primary mt-2" onclick="batchOptimize()">Batch Optimieren</button>
153
+ </div>
154
+ </div>
155
+ <!-- Bildausgabebereich -->
156
+ <div id="output" class="mt-4 p-2 border rounded bg-light">
157
+ <!--<h5>Generierte Bilder:</h5>-->
158
+ <!-- Bilder werden hier dynamisch hinzugefügt -->
159
+ </div>
160
+ </div>
161
+ </div>
162
+ </form>
163
+
164
+ <!-- Ausgabebereich -->
165
+ <div id="output" class="mt-3"></div>
166
+ {% endblock %}
167
+
168
+ {% block scripts %}
169
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.12/cropper.min.js"></script>
170
+ <script>
171
+ // Initialisierung des Croppers
172
+ let cropper;
173
+ function startCropper(imageElement) {
174
+ if (cropper) {
175
+ cropper.destroy();
176
+ }
177
+ cropper = new Cropper(imageElement, {
178
+ aspectRatio: 1,
179
+ viewMode: 1,
180
+ autoCropArea: 1,
181
+ responsive: true,
182
+ });
183
+ }
184
+
185
+ // Funktion zur Bildgenerierung
186
+ function startGeneration() {
187
+ const form = document.getElementById('generateForm');
188
+ if (!form.checkValidity()) {
189
+ form.classList.add('was-validated');
190
+ return;
191
+ }
192
+
193
+ // Fortschrittsanzeige einblenden
194
+ const progressContainer = document.getElementById('progressContainer');
195
+ progressContainer.style.display = 'block';
196
+ const progressBarInner = document.getElementById('progressBar');
197
+ progressBarInner.style.width = '0%';
198
+ progressBarInner.setAttribute('aria-valuenow', 0);
199
+
200
+ const progressMessage = document.getElementById('progressMessage');
201
+ progressMessage.style.display = 'block';
202
+
203
+ const outputDiv = document.getElementById('output');
204
+ outputDiv.innerHTML = '';
205
+
206
+ const formData = new FormData(form);
207
+
208
+ // HF LoRA-Einstellung hinzufügen
209
+ const hfLoraInput = document.getElementById('hf_lora_toggle');
210
+ const hfLoraValue = hfLoraInput.value.trim();
211
+
212
+ if (hfLoraValue) {
213
+ formData.append('hf_lora', hfLoraValue);
214
+ } else {
215
+ formData.delete('hf_lora');
216
+ }
217
+
218
+ // Formulardaten in ein Objekt umwandeln
219
+ const formObject = Object.fromEntries(formData.entries());
220
+
221
+ // WebSocket-Verbindung öffnen
222
+ const socket = new WebSocket('ws://cf-lenovo:8000/ws');
223
+
224
+ socket.onopen = function (event) {
225
+ socket.send(JSON.stringify(formObject));
226
+ };
227
+
228
+ socket.onmessage = function (event) {
229
+ const data = JSON.parse(event.data);
230
+ if (data.optimized_prompt) {
231
+ document.getElementById('prompt').value = data.optimized_prompt;
232
+ }
233
+ if (data.progress !== undefined) {
234
+ progressBarInner.style.width = `${data.progress}%`;
235
+ progressBarInner.setAttribute('aria-valuenow', data.progress);
236
+ }
237
+ if (data.message) {
238
+ const message = document.createElement('div');
239
+ message.textContent = data.message;
240
+ outputDiv.appendChild(message);
241
+ }
242
+ if (data.generated_files) {
243
+ data.generated_files.forEach(file => {
244
+ const img = document.createElement('img');
245
+ img.src = file;
246
+ img.style.maxWidth = '100%';
247
+ outputDiv.appendChild(img);
248
+ const promptText = document.createElement('p');
249
+ promptText.textContent = `Prompt: ${formData.get('prompt')}`;
250
+ outputDiv.appendChild(promptText);
251
+ const copyButton = document.createElement('button');
252
+ copyButton.textContent = 'Prompt Kopieren';
253
+ copyButton.className = 'btn btn-secondary mt-2';
254
+ copyButton.onclick = function() { copyPrompt(formData.get('prompt')) };
255
+ outputDiv.appendChild(copyButton);
256
+ outputDiv.appendChild(document.createElement('br'));
257
+ });
258
+ // Fortschrittsanzeige ausblenden
259
+ progressContainer.style.display = 'none';
260
+ progressMessage.style.display = 'none';
261
+ }
262
+ };
263
+ socket.onerror = function (event) {
264
+ console.error('WebSocket error:', event);
265
+ // Fortschrittsanzeige ausblenden im Fehlerfall
266
+ progressContainer.style.display = 'none';
267
+ progressMessage.style.display = 'none';
268
+ };
269
+
270
+ socket.onclose = function (event) {
271
+ console.log('WebSocket connection closed:', event);
272
+ };
273
+ // Alle Buttons auf der Seite deaktivieren
274
+ const buttons = document.querySelectorAll('button');
275
+ buttons.forEach(button => {
276
+ button.disabled = true;
277
+ });
278
+ }
279
+
280
+ // Funktion zur Optimierung des Prompts
281
+ function optimizeOnly() {
282
+ const form = document.getElementById('generateForm');
283
+ if (!form.checkValidity()) {
284
+ form.classList.add('was-validated');
285
+ return;
286
+ }
287
+
288
+ const formData = new FormData(form);
289
+
290
+ // HF LoRA-Einstellung hinzufügen
291
+ if (document.getElementById('hf_lora_toggle').checked) {
292
+ formData.append('hf_lora', 'Scalino84/my-flux-face');
293
+ } else {
294
+ formData.delete('hf_lora');
295
+ }
296
+
297
+ // Formulardaten in ein Objekt umwandeln
298
+ const formObject = Object.fromEntries(formData.entries());
299
+ formObject.optimize_only = true;
300
+
301
+ // WebSocket-Verbindung öffnen
302
+ const socket = new WebSocket('ws://localhost:8000/ws');
303
+
304
+ socket.onopen = function (event) {
305
+ socket.send(JSON.stringify(formObject));
306
+ };
307
+
308
+ socket.onmessage = function (event) {
309
+ const data = JSON.parse(event.data);
310
+ if (data.optimized_prompt) {
311
+ document.getElementById('prompt').value = data.optimized_prompt;
312
+ }
313
+ };
314
+
315
+ socket.onerror = function (event) {
316
+ console.error('WebSocket error:', event);
317
+ };
318
+
319
+ socket.onclose = function (event) {
320
+ console.log('WebSocket connection closed:', event);
321
+ };
322
+ }
323
+
324
+ function batchOptimize() {
325
+ // Prompts aus dem Textbereich sammeln
326
+ const batchPrompts = document.getElementById('batchPrompts').value.split('\n').filter(prompt => prompt.trim() !== '');
327
+ if (batchPrompts.length === 0) {
328
+ alert("Bitte geben Sie mindestens einen Prompt ein.");
329
+ return;
330
+ }
331
+
332
+ // Formulardaten sammeln
333
+ const formData = new FormData(document.getElementById('generateForm'));
334
+ const prompts = batchPrompts.map(prompt => {
335
+ const data = Object.fromEntries(formData);
336
+ data.prompt = prompt;
337
+ return data;
338
+ });
339
+
340
+ // Add HF LoRA toggle value to each prompt
341
+ if (hf_lora_toggle.checked) {
342
+ formData.append('hf_lora', 'Scalino84/my-flux-face');
343
+ } else {
344
+ formData.delete('hf_lora');
345
+ }
346
+
347
+ // WebSocket-Verbindung öffnen
348
+ const socket = new WebSocket('ws://localhost:8000/ws');
349
+
350
+ socket.onopen = function () {
351
+ // Prompts an den Server senden
352
+ socket.send(JSON.stringify({ prompts }));
353
+ };
354
+
355
+ socket.onmessage = function (event) {
356
+ const data = JSON.parse(event.data);
357
+ if (data.optimized_prompt) {
358
+ document.getElementById('prompt').value = data.optimized_prompt;
359
+ }
360
+ if (data.generated_files) {
361
+ data.generated_files.forEach(file => {
362
+ const livePreviewImage = document.getElementById('livePreviewImage');
363
+ if (livePreviewImage) {
364
+ livePreviewImage.src = file;
365
+ } else {
366
+ const img = document.createElement('img');
367
+ img.id = 'livePreviewImage';
368
+ img.src = file;
369
+ img.style.maxWidth = '100%';
370
+ document.getElementById('output').appendChild(img);
371
+ }
372
+ });
373
+ }
374
+ };
375
+
376
+ socket.onerror = function (event) {
377
+ console.error('WebSocket error:', event);
378
+ alert('Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.');
379
+ };
380
+
381
+ socket.onclose = function (event) {
382
+ console.log('WebSocket connection closed:', event);
383
+ };
384
+ }
385
+
386
+ function copyPrompt() {
387
+ const promptText = document.getElementById('prompt').value;
388
+ navigator.clipboard.writeText(promptText).then(() => {
389
+ alert('Prompt in die Zwischenablage kopiert!');
390
+ }).catch(err => {
391
+ console.error('Fehler beim Kopieren des Prompts:', err);
392
+ });
393
+ }
394
+ </script>
395
+ {% endblock %}