Merge pull request 'master' (#7) from master into dev

Reviewed-on: daniel/Diceghost-Liga-System#7
This commit is contained in:
Daniel Nagel 2026-03-08 17:20:27 +01:00
commit d4cd961d8a
9 changed files with 112 additions and 23 deletions

View File

@ -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"}
{"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"}

View File

@ -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
"""
@ -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]

View File

@ -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()
if app.storage.user.get('authenticated', False):
ui.card().classes("w-full")

View File

@ -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,7 +91,7 @@ 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:

View File

@ -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)

View File

@ -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?

View File

@ -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')
@ -125,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))
# ---------------------------

View File

@ -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()

View File

@ -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")