From d5336d180037c19c5fdeff294a6e93443a9a4efb Mon Sep 17 00:00:00 2001 From: Daniel Nagel Date: Mon, 9 Mar 2026 07:57:39 +0000 Subject: [PATCH] Bilder auf Hauptseite. Spielerstatistik kann jetzt gesamte Match historie in eingener Unterseite. --- data/data_api.py | 27 ++++++++++++ gui/gui_style.py | 3 ++ gui/main_gui.py | 53 +++++++++++++--------- gui/match_history_gui.py | 95 ++++++++++++++++++++++++++++++++++++++++ main.py | 7 ++- 5 files changed, 163 insertions(+), 22 deletions(-) create mode 100644 gui/match_history_gui.py diff --git a/data/data_api.py b/data/data_api.py index 101b151..3f88cb2 100644 --- a/data/data_api.py +++ b/data/data_api.py @@ -560,3 +560,30 @@ def get_submitted_matches(player_id): connection.close() return [dict(row) for row in rows] + + + + +def get_match_history_log(player_id): + """Holt ALLE Matches eines Spielers inklusive der MMR-Änderungen für das Log.""" + connection = sqlite3.connect(DB_PATH) + connection.row_factory = sqlite3.Row + cursor = connection.cursor() + + query = """ + SELECT m.played_at, sys.name AS gamesystem_name, + m.player1_id, p1.display_name AS p1_display, p1.discord_name AS p1_discord, m.score_player1, m.player1_mmr_change, + m.player2_id, p2.display_name AS p2_display, p2.discord_name AS p2_discord, m.score_player2, m.player2_mmr_change, + m.player2_check, m.match_is_counted + FROM matches m + JOIN gamesystems sys ON m.gamesystem_id = sys.id + JOIN players p1 ON m.player1_id = p1.id + JOIN players p2 ON m.player2_id = p2.id + WHERE m.player1_id = ? OR m.player2_id = ? + ORDER BY m.played_at DESC + """ + cursor.execute(query, (player_id, player_id)) + rows = cursor.fetchall() + connection.close() + + return [dict(row) for row in rows] diff --git a/gui/gui_style.py b/gui/gui_style.py index 11873a9..caccc84 100644 --- a/gui/gui_style.py +++ b/gui/gui_style.py @@ -1,5 +1,8 @@ from nicegui import ui +# Funktion die jede GUI Seite am Anfang aufrufen kann. Dann haben alle die gleichen Farbeinstellungen. Und, wenn man was ändert, +# muss man es nur hier ändern! + def apply_design(): ui.add_css('body { background-color: #18181b; }') diff --git a/gui/main_gui.py b/gui/main_gui.py index 202ab8b..13c827c 100644 --- a/gui/main_gui.py +++ b/gui/main_gui.py @@ -170,61 +170,72 @@ def setup_routes(admin_discord_id): placements = data_api.get_player_statistics(player_id) systems = data_api.get_gamesystem_data() - + + # Python-Trick: Wir wandeln die Spieler-Stats in ein "Wörterbuch" (Dictionary) um. + # So können wir blitzschnell über die System-ID nachschauen, ob er Stats hat. + my_stats = { p['gamesystem_id']: p for p in placements } + def click_join_league(p_id, sys_id): data_api.join_league(p_id, sys_id) ui.navigate.to("/") - # --- NEU: Wir machen die Funktion allgemein --- def toggle_visibility(row_a, row_b): row_a.visible = not row_a.visible row_b.visible = not row_b.visible with ui.element('div').classes("w-full grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 items-center"): - for place in placements: + + # LOGIK UMGEKEHRT: Wir schleifen über den Katalog der verfügbaren Systeme! + for sys in systems: + sys_id = sys['id'] + sys_name = sys['name'] + sys_card = ui.card().classes("h-60 w-full items-center justify-center transition-colors") with sys_card: - # Kopfzeile der Karte (Name & Beschreibung) - ui.label(text=place['gamesystem_name']).classes('text-xl font-bold') + # Kopfzeile + ui.label(text=sys_name).classes('text-xl font-bold') - if 'description' in place and place['description']: - ui.label(text=place['description']).classes('text-xs text-gray-400') + # --- BILD EINFÜGEN --- + # prüfen, ob die Spalte 'pictures' existiert und nicht leer ist + if 'pictures' in sys and sys['pictures']: + ui.image(f"/pictures/{sys['pictures']}").classes('w-24 h-24 object-contain m-2') + + if 'description' in sys and sys['description']: + ui.label(text=sys['description']).classes('text-xs text-gray-400 text-center') ui.space() - if place['mmr'] is None: + # Prüfen: Ist diese sys_id in den Stats des Spielers? UND hat er ein MMR? + if sys_id not in my_stats or my_stats[sys_id]['mmr'] is None: ui.label(text="Du bist noch nicht in dieser Liga.").classes("text-red-500 font-bold") - # 1. Wir legen die Reihen an und speichern sie in lokalen Variablen join_row = ui.row().classes('items-center gap-2') confirm_row = ui.row().classes('items-center gap-2') - confirm_row.visible = False # Standardmäßig unsichtbar + confirm_row.visible = False - # 2. Erste Reihe (Der "Beitreten" Button) with join_row: ui.button("Beitreten", on_click=lambda e, r1=join_row, r2=confirm_row: toggle_visibility(r1, r2)) - # 3. Zweite Reihe (Die Bestätigungs-Buttons) with confirm_row: - ui.button("Liga Beitreten", color="green", on_click=lambda e, p=player_id, s=place['gamesystem_id']: click_join_league(p, s)) - # Der Abbrechen Button kriegt den gleichen Toggle-Befehl wie oben: + ui.button("Liga Beitreten", color="green", on_click=lambda e, p=player_id, s=sys_id: click_join_league(p, s)) ui.button(icon='cancel', color='red', on_click=lambda e, r1=join_row, r2=confirm_row: toggle_visibility(r1, r2)).props('round dense') else: # Spieler IST in der Liga! - sys_card.classes("cursor-pointer hover:bg-zinc-800") - # Auch hier machen wir es mit dem 'e' absolut sicher: - sys_card.on('click', lambda e, name=place['gamesystem_name']: ui.navigate.to(f'/statistic/{name}')) + stat = my_stats[sys_id] # Wir holen uns seine Stats aus dem Wörterbuch + + sys_card.classes("cursor-pointer hover:bg-zinc-800") + sys_card.on('click', lambda e, name=sys_name: ui.navigate.to(f'/statistic/{name}')) - # Wir zeigen die Stats. with ui.row().classes('items-center gap-4'): - ui.label(text=f"MMR: {place['mmr']}") - ui.label(text=f"Spiele: {place['games_in_system']}") + ui.label(text=f"MMR: {stat['mmr']}").classes("text-lg font-bold text-blue-400") + ui.label(text=f"Spiele: {stat['games_in_system']}").classes("text-lg") + + # --------------------------- # Match Historie # --------------------------- - with ui.card().classes("w-full"): ui.label(text= "Meine letzten Spiele").classes("font-bold text-white text-xl") # 1. Daten aus der DB holen diff --git a/gui/match_history_gui.py b/gui/match_history_gui.py new file mode 100644 index 0000000..911be5b --- /dev/null +++ b/gui/match_history_gui.py @@ -0,0 +1,95 @@ +from nicegui import ui, app +from data import data_api +# Falls du eine gui_style.py für Farben/Header hast, hier importieren! +# from gui import gui_style + +def setup_routes(): + @ui.page('/matchhistory', dark=True) + def match_history_page(): + + gui_style.apply_design() + + # Sicherheits-Check: Ist der Nutzer eingeloggt? + if not app.storage.user.get('authenticated', False): + ui.label('Bitte logge dich ein.').classes('text-red-500 text-2xl m-4') + return + + player_id = app.storage.user.get('db_id') + + # Das Haupt-Layout der Seite + with ui.column().classes('w-full max-w-5xl mx-auto p-4'): + + # Kopfbereich mit Zurück-Button + with ui.row().classes('w-full items-center justify-between mb-6'): + ui.label("Komplette Match Historie").classes("text-3xl font-bold text-white") + ui.button("Zurück", icon="arrow_back", on_click=lambda: ui.navigate.to('/')).classes('bg-zinc-700 text-white') + + raw_matches = data_api.get_match_history_log(player_id) + table_rows = [] + + # Daten für die Tabelle aufbereiten + for i, match in enumerate(raw_matches): + # Bin ich P1 oder P2? + if match['player1_id'] == player_id: + opponent = f"{match['p2_display']} aka {match['p2_discord']}" + my_score = match['score_player1'] + opp_score = match['score_player2'] + my_mmr_change = match['player1_mmr_change'] + else: + opponent = f"{match['p1_display']} aka {match['p1_discord']}" + my_score = match['score_player2'] + opp_score = match['score_player1'] + my_mmr_change = match['player2_mmr_change'] + + # Ergebnis Text + if my_score > opp_score: + result = "Gewonnen" + elif my_score < opp_score: + result = "Verloren" + else: + result = "Unentschieden" + + # MMR Text schön formatieren + if match['match_is_counted'] == 0: + mmr_text = "Ausstehend" + elif my_mmr_change is None: + mmr_text = "0" + elif my_mmr_change > 0: + mmr_text = f"+{my_mmr_change}" + else: + mmr_text = str(my_mmr_change) + + table_rows.append({ + 'id': i, + 'date': str(match['played_at'])[:10], + 'system': match['gamesystem_name'], + 'opponent': opponent, + 'score': f"{my_score} : {opp_score}", + 'result': result, + 'mmr': mmr_text + }) + + # Spalten definieren + columns = [ + {'name': 'date', 'label': 'Datum', 'field': 'date', 'align': 'left'}, + {'name': 'system', 'label': 'System', 'field': 'system', 'align': 'left'}, + {'name': 'opponent', 'label': 'Gegner', 'field': 'opponent', 'align': 'left'}, + {'name': 'score', 'label': 'Punkte', 'field': 'score', 'align': 'center'}, + {'name': 'result', 'label': 'Ergebnis', 'field': 'result', 'align': 'left'}, + {'name': 'mmr', 'label': 'MMR Änderung', 'field': 'mmr', 'align': 'right'} + ] + + # Tabelle zeichnen + if len(table_rows) > 0: + history_table = ui.table(columns=columns, rows=table_rows, row_key='id').classes('w-full bg-zinc-900 text-white') + + # KLEINER TRICK: Wir färben die MMR-Spalte grün oder rot, je nachdem ob da ein "+" oder "-" steht! + history_table.add_slot('body-cell-mmr', ''' + + + {{ props.row.mmr }} + + + ''') + else: + ui.label("Keine Spiele gefunden.").classes("text-gray-400 italic") diff --git a/main.py b/main.py index 458de2b..516cd8b 100644 --- a/main.py +++ b/main.py @@ -2,13 +2,16 @@ import os from dotenv import load_dotenv from nicegui import ui -from gui import main_gui, match_gui, discord_login, league_statistic, admin_gui +from gui import main_gui, match_gui, discord_login, league_statistic, admin_gui, match_history_gui +from nicegui import app from data import database # 1. Lade die geheimen Variablen aus der .env Datei in den Speicher load_dotenv() +# Festschreiben des Bilder Ordnerns. +app.add_static_files('/pictures', 'gui/pictures') # 2. Variablen abrufen client_id = os.getenv("DISCORD_CLIENT_ID") @@ -23,6 +26,8 @@ discord_login.setup_login_routes() league_statistic.setup_routes() match_gui.setup_routes() admin_gui.setup_routes() +match_history_gui.setup_routes() + # 4. Wir starten die NiceGUI App ui.run(title="Westside Diceghost Liga", port=9000, storage_secret="ein_sehr_geheimes_passwort_fuer_die_cookies", favicon="gui/pictures/wsdg.png")