From 84410a49b6571dd5d83806903758d06332edb3ca Mon Sep 17 00:00:00 2001 From: Daniel Nagel Date: Sun, 8 Mar 2026 09:26:30 +0100 Subject: [PATCH 1/3] favicon angepasst. --- main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.py b/main.py index 9e0b150..458de2b 100644 --- a/main.py +++ b/main.py @@ -25,4 +25,4 @@ match_gui.setup_routes() admin_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") +ui.run(title="Westside Diceghost Liga", port=9000, storage_secret="ein_sehr_geheimes_passwort_fuer_die_cookies", favicon="gui/pictures/wsdg.png") -- 2.43.0 From 3936a0b53b538834bfcf393d30ff762497cae7b6 Mon Sep 17 00:00:00 2001 From: Daniel Nagel Date: Sun, 8 Mar 2026 16:23:39 +0100 Subject: [PATCH 2/3] =?UTF-8?q?Randomizer=20Button=20f=C3=BCr=20den=20Disp?= =?UTF-8?q?layName.=20Seiten=20flashen=20nicht=20mehr=20wei=C3=9F=20beim?= =?UTF-8?q?=20Neu=20laden.=20Discord=20Login=20nur=20noch=20wenn=20man=20a?= =?UTF-8?q?uf=20dem=20Westside=20Diceghost=20Server=20"Diceghost"=20oder?= =?UTF-8?q?=20"Friend"=20ist.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...-47e8cea6-65ad-4725-abd9-23cf84587a03.json | 2 +- data/data_api.py | 18 ++++----- gui/admin_gui.py | 6 ++- gui/discord_login.py | 40 ++++++++++++++++--- gui/gui_style.py | 2 + gui/league_statistic.py | 2 +- gui/main_gui.py | 10 ++++- gui/match_gui.py | 2 +- 8 files changed, 60 insertions(+), 22 deletions(-) diff --git a/.nicegui/storage-user-47e8cea6-65ad-4725-abd9-23cf84587a03.json b/.nicegui/storage-user-47e8cea6-65ad-4725-abd9-23cf84587a03.json index 6b89cb8..90a427a 100644 --- a/.nicegui/storage-user-47e8cea6-65ad-4725-abd9-23cf84587a03.json +++ b/.nicegui/storage-user-47e8cea6-65ad-4725-abd9-23cf84587a03.json @@ -1 +1 @@ -{"authenticated":true,"discord_id":"277898241750859776","discord_name":"mrteels","db_id":2,"display_name":"r","discord_avatar_url":"https://cdn.discordapp.com/avatars/277898241750859776/7c3446bb51fafd72b1b4c21124b4994f.png"} \ No newline at end of file +{"authenticated":true,"discord_id":"277898241750859776","discord_name":"mrteels","db_id":2,"display_name":"Stolpernder Meta-Chaser","discord_avatar_url":"https://cdn.discordapp.com/avatars/277898241750859776/7c3446bb51fafd72b1b4c21124b4994f.png"} \ No newline at end of file diff --git a/data/data_api.py b/data/data_api.py index 771787c..dd2a4df 100644 --- a/data/data_api.py +++ b/data/data_api.py @@ -11,7 +11,12 @@ def change_display_name(player_id, new_name): connection.commit() connection.close() - +def generate_silly_name(): + adjectives = ["Verwirrter", "Blinder", "Heulender", "Zorniger", "Chaos", "Verzweifelter", "Schreiender", "Stolpernder", "Schwitzender"] + nouns = ["Grot", "Kultist", "Servitor", "Snotling", "Guardmen", "Würfellecker", "Regelvergesser", "Meta-Chaser", "Klebschnüffler"] + adj = random.choice(adjectives) + noun = random.choice(nouns) + return f"{adj} {noun}" def get_or_create_player(discord_id, discord_name, avatar_url): @@ -25,12 +30,6 @@ def get_or_create_player(discord_id, discord_name, avatar_url): if player is None: # Random Silly Name Generator für neue Spieler. Damit sie angeregt werden ihren richtigen Namen einzutragen. - def generate_silly_name(): - adjectives = ["Verwirrter", "Blinder", "Heulender", "Zorniger", "Chaos", "Verzweifelter", "Schreiender", "Stolpernder", "Schwitzender"] - nouns = ["Grot", "Kultist", "Servitor", "Snotling", "Guardmen", "Würfellecker", "Regelvergesser", "Meta-Chaser", "Klebschnüffler"] - adj = random.choice(adjectives) - noun = random.choice(nouns) - return f"{adj} {noun}" silly_name = generate_silly_name() cursor.execute("INSERT INTO players (discord_id, discord_name, display_name, discord_avatar_url) VALUES (?, ?, ?, ?)", (discord_id, discord_name, silly_name, avatar_url)) @@ -456,13 +455,14 @@ def get_leaderboard(system_name): connection.row_factory = sqlite3.Row cursor = connection.cursor() - # Wir holen ID, Namen, Discord und MMR, sortiert vom höchsten MMR zum niedrigsten + # WIR HABEN HIER EINE BEDINGUNG HINZUGEFÜGT: AND stat.games_in_system > 0 + # Dadurch filtert die Datenbank direkt auf dem Server schon alle "0-Spiele"-Accounts raus. query = """ SELECT p.id, p.display_name, p.discord_name, stat.mmr FROM players p JOIN player_game_statistic stat ON p.id = stat.player_id JOIN gamesystems sys ON stat.gamesystem_id = sys.id - WHERE sys.name = ? + WHERE sys.name = ? AND stat.games_in_system > 0 ORDER BY stat.mmr DESC """ diff --git a/gui/admin_gui.py b/gui/admin_gui.py index 4636cb9..7a30fa8 100644 --- a/gui/admin_gui.py +++ b/gui/admin_gui.py @@ -3,6 +3,8 @@ from data import database, data_api from gui import gui_style def setup_routes(): - @ui.page('/admin') + @ui.page('/admin', dark=True) def home_page(): - gui_style.apply_design() \ No newline at end of file + gui_style.apply_design() + if app.storage.user.get('authenticated', False): + ui.card().classes("w-full") \ No newline at end of file diff --git a/gui/discord_login.py b/gui/discord_login.py index 2ca768f..712e123 100644 --- a/gui/discord_login.py +++ b/gui/discord_login.py @@ -11,8 +11,9 @@ def get_auth_url(): redirect_uri = f"{app_url}/login/discord" encoded_redirect_uri = urllib.parse.quote(redirect_uri, safe="") - # NEU: scope=identify%20guilds fragt Profilbild UND Server ab - return f"https://discord.com/api/oauth2/authorize?client_id={client_id}&redirect_uri={encoded_redirect_uri}&response_type=code&scope=identify%20guilds" + # NEU: guilds.members.read erlaubt uns, die Rollen des Users in einem bestimmten Server abzufragen + return f"https://discord.com/api/oauth2/authorize?client_id={client_id}&redirect_uri={encoded_redirect_uri}&response_type=code&scope=identify%20guilds.members.read" + def setup_login_routes(): client_id = os.getenv("DISCORD_CLIENT_ID") @@ -20,7 +21,7 @@ def setup_login_routes(): app_url = os.getenv("APP_URL") redirect_uri = f"{app_url}/login/discord" - @ui.page('/login/discord') + @ui.page('/login/discord', dark=True) def discord_callback(code: str = None): if not code: ui.label('Fehler: Kein Code erhalten.').classes('text-red-500') @@ -39,8 +40,35 @@ def setup_login_routes(): if 'access_token' in token_json: access_token = token_json['access_token'] - user_headers = {'Authorization': f"Bearer {access_token}"} + + # 1. Die IDs aus der .env laden (Passe die Namen an, falls sie bei dir anders heißen!) + guild_id = os.getenv("DISCORD_SERVER_ID") + role_diceghosts = os.getenv("DISCORD_SERVER_DICEGHOST_ID") + role_friend = os.getenv("DISCORD_SERVER_FRIEND_ID") + + # 2. Prüfen: Ist der Nutzer überhaupt auf unserem Server? + # Wir fragen Discord gezielt nach dem Profil des Nutzers auf DEINEM Server. + member_response = requests.get(f'https://discord.com/api/users/@me/guilds/{guild_id}/member', headers=user_headers) + + # Ein HTTP Status-Code 200 bedeutet "OK". Alles andere (z.B. 404 Not Found) bedeutet: Er ist nicht auf dem Server! + if member_response.status_code != 200: + ui.label('Zugriff verweigert: Du bist nicht auf dem Diceghosts Server!').classes('text-red-500 text-xl font-bold p-4') + return # Bricht die Funktion hier ab. Kein Login! + + # 3. Prüfen: Hat er die richtige Rolle? + member_json = member_response.json() + # Wir holen die Liste aller Rollen des Nutzers. Wenn er keine hat, nehmen wir eine leere Liste [] + user_roles = member_json.get('roles', []) + + # Wir prüfen, ob mindestens eine der beiden Rollen-IDs in seiner Liste auftaucht + if role_diceghosts not in user_roles and role_friend not in user_roles: + ui.label('Zugriff verweigert: Du hast nicht die benötigte Rolle (Diceghosts oder Friend)!').classes('text-red-500 text-xl font-bold p-4') + return # Bricht die Funktion hier ab. Kein Login! + + # --- AB HIER: ZUGANG GEWÄHRT! --- + + # Jetzt laden wir noch seine allgemeinen Discord-Daten (für Name und Profilbild) user_response = requests.get('https://discord.com/api/users/@me', headers=user_headers) user_json = user_response.json() @@ -63,8 +91,8 @@ def setup_login_routes(): app.storage.user['discord_name'] = discord_name app.storage.user['db_id'] = player[0] app.storage.user['display_name'] = player[2] - app.storage.user['discord_avatar_url'] = player[3] # Bild speichern! + app.storage.user['discord_avatar_url'] = player[3] ui.navigate.to('/') else: - ui.label('Fehler beim Login!').classes('text-red-500') \ No newline at end of file + ui.label('Fehler beim Login!').classes('text-red-500') diff --git a/gui/gui_style.py b/gui/gui_style.py index 29f8b7b..11873a9 100644 --- a/gui/gui_style.py +++ b/gui/gui_style.py @@ -1,6 +1,8 @@ from nicegui import ui def apply_design(): + ui.add_css('body { background-color: #18181b; }') + # 1. Dark Mode aktivieren ui.dark_mode(True) diff --git a/gui/league_statistic.py b/gui/league_statistic.py index ca9856b..a5d5317 100644 --- a/gui/league_statistic.py +++ b/gui/league_statistic.py @@ -4,7 +4,7 @@ from data import data_api def setup_routes(): # 1. Die {}-Klammern definieren eine dynamische Variable in der URL - @ui.page('/statistic/{systemname}') + @ui.page('/statistic/{systemname}', dark=True) def gamesystem_statistic_page(systemname: str): # <--- WICHTIG: Hier fangen wir das Wort aus der URL auf! # Sicherheitscheck: Ist der User eingeloggt? diff --git a/gui/main_gui.py b/gui/main_gui.py index e4d48af..731118d 100644 --- a/gui/main_gui.py +++ b/gui/main_gui.py @@ -4,7 +4,7 @@ from gui import discord_login, gui_style from match_calculations import calc_match def setup_routes(admin_discord_id): - @ui.page('/') + @ui.page('/', dark=True) def home_page(): gui_style.apply_design() @@ -48,7 +48,7 @@ def setup_routes(admin_discord_id): ui.button(icon='edit', color='primary', on_click=toggle_edit_mode).props('round dense') # --- ANSICHT 2: Das Eingabefeld (startet unsichtbar!) --- - with ui.row().classes('items-center gap-2') as edit_row: + with ui.row().classes('items-center gap-5') as edit_row: edit_row.visible = False # Am Anfang verstecken def save_new_name(): @@ -65,8 +65,14 @@ def setup_routes(admin_discord_id): # Wenn nichts geändert wurde, einfach wieder einklappen toggle_edit_mode() + def generate_random_silly_name(): + silly_name = data_api.generate_silly_name() + name_input.value=silly_name + + name_input = ui.input('Neuer Name', value=display_name).on('keydown.enter', save_new_name) ui.button(icon='save', color='positive', on_click=save_new_name).props('round dense') + ui.button(icon='casino', on_click=generate_random_silly_name).props('round dense') ui.button(icon='close', color='negative', on_click=toggle_edit_mode).props('round dense') avatar = app.storage.user.get('avatar_url') diff --git a/gui/match_gui.py b/gui/match_gui.py index 65a4d7a..6413df8 100644 --- a/gui/match_gui.py +++ b/gui/match_gui.py @@ -6,7 +6,7 @@ from match_calculations import calc_match def setup_routes(): # 1. Die {}-Klammern definieren eine dynamische Variable in der URL - @ui.page('/add-match/{systemname}') + @ui.page('/add-match/{systemname}', dark=True) def match_form_page(systemname: str): # <--- WICHTIG: Hier fangen wir das Wort aus der URL auf! gui_style.apply_design() -- 2.43.0 From ad2089ec1ffaabf78300ee69361f701a4ca12bd0 Mon Sep 17 00:00:00 2001 From: Daniel Nagel Date: Sun, 8 Mar 2026 16:30:52 +0100 Subject: [PATCH 3/3] =?UTF-8?q?Spieler=20k=C3=B6nnen=20jetzt=20selbst=20ei?= =?UTF-8?q?ngetragene=20Spieler=20selber=20l=C3=B6schen=20solange=20der=20?= =?UTF-8?q?Gegner=20es=20noch=20nicht=20best=C3=A4tigt=20hat.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data/data_api.py | 24 ++++++++++++++++++++++++ gui/main_gui.py | 27 +++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/data/data_api.py b/data/data_api.py index dd2a4df..101b151 100644 --- a/data/data_api.py +++ b/data/data_api.py @@ -536,3 +536,27 @@ def set_match_counted(match_id): connection.commit() connection.close() + + +def get_submitted_matches(player_id): + """Holt alle offenen Matches, die der Spieler selbst eingetragen hat, aber vom Gegner noch nicht bestätigt wurden.""" + connection = sqlite3.connect(DB_PATH) + connection.row_factory = sqlite3.Row + cursor = connection.cursor() + + # ACHTUNG: Wir joinen hier p2 (player2), weil wir den Namen des Gegners anzeigen wollen! + query = """ + SELECT m.id AS match_id, m.score_player1, m.score_player2, m.played_at, + sys.name AS system_name, + p2.display_name AS p2_name + FROM matches m + JOIN gamesystems sys ON m.gamesystem_id = sys.id + JOIN players p2 ON m.player2_id = p2.id + WHERE m.player1_id = ? AND m.player2_check = 0 + """ + + cursor.execute(query, (player_id,)) + rows = cursor.fetchall() + connection.close() + + return [dict(row) for row in rows] diff --git a/gui/main_gui.py b/gui/main_gui.py index 731118d..202ab8b 100644 --- a/gui/main_gui.py +++ b/gui/main_gui.py @@ -131,6 +131,33 @@ def setup_routes(admin_discord_id): # BESTÄTIGEN und spiel berechnen lassen ui.button("Bestätigen", color="positive", icon="check", on_click=lambda e, m_id=match['match_id']: acccept_match(m_id)) + # --------------------------- + # --- Selbst eingetragene, offene Spiele --- + # --------------------------- + submitted_matches = data_api.get_submitted_matches(player_id) + + if len(submitted_matches) > 0: + # Eine etwas dezentere Karte (grau) + with ui.card().classes('w-full bg-zinc-800 border border-gray-600 mb-6'): + ui.label(f"Warten auf Gegner: Du hast {len(submitted_matches)} offene(s) Spiel(e) eingetragen.").classes('text-xl font-bold text-gray-300 mb-2') + + for match in submitted_matches: + + # Die Lösch-Funktion, die beim Klick ausgeführt wird + def retract_match(m_id): + data_api.delete_match(m_id) + ui.notify("Eingetragenes Spiel zurückgezogen!", color="warning") + ui.navigate.reload() + + # Für jedes Match machen wir eine kleine Reihe + with ui.row().classes('w-full items-center justify-between bg-zinc-900 p-3 rounded shadow-inner'): + + # Info-Text: Auf wen warten wir? + info_text = f"[{match['system_name']}] Warten auf Bestätigung von {match['p2_name']} ({match['score_player1']} : {match['score_player2']})" + ui.label(info_text).classes('text-lg text-gray-400') + + # Der Zurückziehen-Button (wieder mit unserem lambda m_id=... Trick!) + ui.button("Zurückziehen", color="warning", icon="delete", on_click=lambda e, m_id=match['match_id']: retract_match(m_id)) # --------------------------- -- 2.43.0