Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8788839589 | ||
| 7ef6de6aea | |||
| 8d8b0decd1 | |||
| 8cbe229c65 | |||
| f278591268 | |||
| 66a5e61129 | |||
| e43cc64cff | |||
| f0f6de827d | |||
| ac90850a94 | |||
|
|
86ab395fa8 | ||
|
|
0a45cb426f | ||
|
|
c29c1f31d7 | ||
|
|
5b320bac56 | ||
|
|
d2000dfe08 | ||
| a5c327213e | |||
|
|
fe518303c4 | ||
|
|
1cf43cd670 | ||
| 3d735b928e | |||
| b2323aabf5 | |||
|
|
ac8f3e4289 | ||
|
|
13ae6a24e8 | ||
|
|
356c2e944a | ||
|
|
e4ad5c5063 | ||
|
|
2997a77d0e | ||
| c04a681bec | |||
|
|
1b1031a5e7 | ||
|
|
e281160ec7 | ||
| 85e0b57833 | |||
| 2d2f1ac9ab |
4
.aiassistant/rules/first.md
Normal file
4
.aiassistant/rules/first.md
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
apply: always
|
||||
---
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -19,4 +19,5 @@ env/
|
|||
.vscode/
|
||||
.idea/
|
||||
|
||||
data_trans.py
|
||||
data_trans.py
|
||||
.aider*
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
Projekt und Repo werden nicht mehr geupdatet.
|
||||
|
||||
Werde das in ein Liga System für mehrere Clubs umwandeln. Das aktuelle System ist nur auf einen Club ausgelegt. Wer es als Grundlage verwenden will für was eigenes darf sich gerne bedienen.
|
||||
0
auth/__init__.py
Normal file
0
auth/__init__.py
Normal file
|
|
@ -1,3 +1,6 @@
|
|||
import os
|
||||
import requests
|
||||
from data import data_api
|
||||
|
||||
import os
|
||||
import urllib.parse
|
||||
|
|
@ -5,16 +8,57 @@ import requests
|
|||
from nicegui import ui, app
|
||||
from data import data_api
|
||||
|
||||
def process_discord_login(code: str):
|
||||
client_id = os.getenv("DISCORD_CLIENT_ID")
|
||||
client_secret = os.getenv("DISCORD_CLIENT_SECRET")
|
||||
app_url = os.getenv("APP_URL")
|
||||
|
||||
# Token austauschen
|
||||
token_response = requests.post('https://discord.com/api/oauth2/token', data={
|
||||
'client_id': client_id,
|
||||
'client_secret': client_secret,
|
||||
'grant_type': 'authorization_code',
|
||||
'code': code,
|
||||
'redirect_uri': f"{app_url}/login/discord"
|
||||
})
|
||||
|
||||
if token_response.status_code != 200:
|
||||
return None, "Fehler beim Token-Austausch."
|
||||
|
||||
access_token = token_response.json().get('access_token')
|
||||
headers = {'Authorization': f"Bearer {access_token}"}
|
||||
|
||||
# Rollen/Server prüfen
|
||||
guild_id = os.getenv("DISCORD_SERVER_ID")
|
||||
member_response = requests.get(f'https://discord.com/api/users/@me/guilds/{guild_id}/member', headers=headers)
|
||||
|
||||
if member_response.status_code != 200:
|
||||
return None, "Du bist nicht auf dem Diceghosts Server."
|
||||
|
||||
roles = member_response.json().get('roles', [])
|
||||
if os.getenv("DISCORD_SERVER_DICEGHOST_ID") not in roles and os.getenv("DISCORD_SERVER_FRIEND_ID") not in roles:
|
||||
return None, "Du hast nicht die benötigte Rolle."
|
||||
|
||||
# User Daten holen
|
||||
user_json = requests.get('https://discord.com/api/users/@me', headers=headers).json()
|
||||
discord_id = user_json['id']
|
||||
avatar = f"https://cdn.discordapp.com/avatars/{discord_id}/{user_json['avatar']}.png" if user_json.get('avatar') else None
|
||||
|
||||
player = data_api.get_or_create_player(discord_id, user_json['username'], avatar)
|
||||
|
||||
return (player, discord_id, user_json['username'], avatar), None
|
||||
|
||||
|
||||
def get_auth_url():
|
||||
client_id = os.getenv("DISCORD_CLIENT_ID")
|
||||
app_url = os.getenv("APP_URL")
|
||||
redirect_uri = f"{app_url}/login/discord"
|
||||
encoded_redirect_uri = urllib.parse.quote(redirect_uri, safe="")
|
||||
|
||||
# NEU: guilds.members.read erlaubt uns, die Rollen des Users in einem bestimmten Server abzufragen
|
||||
# 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")
|
||||
client_secret = os.getenv("DISCORD_CLIENT_SECRET")
|
||||
|
|
@ -103,3 +147,5 @@ def setup_login_routes():
|
|||
ui.navigate.to('/')
|
||||
else:
|
||||
ui.label('Fehler beim Login!').classes('text-red-500')
|
||||
|
||||
"""
|
||||
39
auth/providers/base.py
Normal file
39
auth/providers/base.py
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Optional
|
||||
|
||||
|
||||
@dataclass
|
||||
class UserInfo:
|
||||
"""Genormte Nutzerdaten — egal ob von Discord, Google oder sonstwem.
|
||||
Jeder Provider liefert dieses Format, damit der Rest der App
|
||||
nicht wissen muss, woher die Daten kommen."""
|
||||
provider: str # 'discord', 'google', ...
|
||||
provider_user_id: str # eindeutige ID beim Provider
|
||||
username: str # z. B. Discord-Username
|
||||
display_name: str # Anzeigename in deiner App
|
||||
avatar_url: Optional[str] = None
|
||||
email: Optional[str] = None
|
||||
raw: dict = field(default_factory=dict) # Original-Payload des Providers
|
||||
|
||||
|
||||
class AuthProvider(ABC):
|
||||
"""Abstrakte Basisklasse für einen OAuth-Provider (Strategy Pattern).
|
||||
|
||||
Das ist der 'Steckplatz'. Jeder konkrete Provider muss diese
|
||||
drei Dinge liefern. Wie er das intern macht, ist ihm überlassen.
|
||||
"""
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def name(self) -> str:
|
||||
"""Kurzname des Providers, z. B. 'discord'."""
|
||||
|
||||
@abstractmethod
|
||||
def get_auth_url(self) -> str:
|
||||
"""Baut die URL, zu der der User weitergeleitet wird, um zu autorisieren."""
|
||||
|
||||
@abstractmethod
|
||||
def process_callback(self, code: str) -> UserInfo:
|
||||
"""Tauscht den Auth-Code gegen Tokens und holt die Nutzerdaten.
|
||||
Gibt ein genormtes UserInfo zurück oder wirft einen Fehler."""
|
||||
333
data/data_api.py
333
data/data_api.py
|
|
@ -1,6 +1,6 @@
|
|||
import random
|
||||
from wood import logger
|
||||
|
||||
from pymysql.cursors import DictCursor
|
||||
from data.database import db_connection
|
||||
|
||||
|
||||
|
|
@ -9,7 +9,7 @@ from data.database import db_connection
|
|||
# -----------------------------------------------------
|
||||
|
||||
def validate_user_session(db_id, discord_id):
|
||||
"""Prüft, ob das Cookie noch zur aktuellen Datenbank passt."""
|
||||
"""Checks if the cookie still matches the current database."""
|
||||
connection = db_connection()
|
||||
cursor = connection.cursor()
|
||||
|
||||
|
|
@ -23,7 +23,7 @@ def validate_user_session(db_id, discord_id):
|
|||
return False
|
||||
|
||||
if str(result[0]) != str(discord_id):
|
||||
logger.log(f"Player with false coockies logged in! {discord_id} doesnt belong to {db_id}")
|
||||
logger.log(f"Player with false cookies logged in! {discord_id} does not belong to {db_id}")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
|
@ -41,16 +41,16 @@ def change_display_name(player_id, new_name):
|
|||
|
||||
|
||||
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"]
|
||||
adjectives = ["Confused", "Blind", "Howling", "Angry", "Chaos",
|
||||
"Desperate", "Shouting", "Stumbling", "Sweating"]
|
||||
nouns = ["Grot", "Cultist", "Servitor", "Snotling", "Guardmen",
|
||||
"DiceLover", "RuleBreaker", "MetaChaser", "SlimeSniffer"]
|
||||
return f"{random.choice(adjectives)} {random.choice(nouns)}"
|
||||
|
||||
|
||||
def get_or_create_player(discord_id, discord_name, avatar_url):
|
||||
connection = db_connection()
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
cursor = connection.cursor(cursor=DictCursor)
|
||||
|
||||
cursor.execute(
|
||||
"SELECT id, discord_name, display_name, discord_avatar_url FROM players WHERE discord_id = %s",
|
||||
|
|
@ -92,13 +92,13 @@ def get_or_create_player(discord_id, discord_name, avatar_url):
|
|||
|
||||
|
||||
# -----------------------------------------------------
|
||||
# Player-Listen
|
||||
# Player Lists
|
||||
# -----------------------------------------------------
|
||||
|
||||
def get_all_players():
|
||||
"""Alle Spieler – Basisdaten."""
|
||||
"""All players – basic data."""
|
||||
connection = db_connection()
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
cursor = connection.cursor(cursor=DictCursor)
|
||||
|
||||
cursor.execute(
|
||||
"SELECT id, display_name, discord_name, discord_avatar_url FROM players "
|
||||
|
|
@ -113,7 +113,7 @@ def get_all_players():
|
|||
|
||||
def get_all_players_from_system(system_name):
|
||||
connection = db_connection()
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
cursor = connection.cursor(cursor=DictCursor)
|
||||
|
||||
query = """
|
||||
SELECT DISTINCT
|
||||
|
|
@ -153,7 +153,7 @@ def get_gamesystem_id_by_name(system_name):
|
|||
|
||||
def get_player_statistics(player_id):
|
||||
connection = db_connection()
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
cursor = connection.cursor(cursor=DictCursor)
|
||||
|
||||
query = """
|
||||
SELECT
|
||||
|
|
@ -182,71 +182,121 @@ def get_player_statistics(player_id):
|
|||
return rows
|
||||
|
||||
|
||||
def get_player_statistic(player_id, system_id):
|
||||
connection = db_connection()
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
|
||||
query = """
|
||||
SELECT
|
||||
sys.id AS gamesystem_id,
|
||||
sys.name AS gamesystem_name,
|
||||
sys.description,
|
||||
stat.mmr,
|
||||
stat.games_in_system,
|
||||
stat.points,
|
||||
stat.avv_points,
|
||||
stat.last_played,
|
||||
stat.win_rate,
|
||||
stat.trend,
|
||||
stat.tyrann_id,
|
||||
stat.pushover_id,
|
||||
stat.nemesis_id
|
||||
FROM gamesystems sys
|
||||
LEFT JOIN player_game_statistic stat
|
||||
ON sys.id = stat.gamesystem_id AND stat.player_id = %s
|
||||
WHERE sys.id = %s
|
||||
def get_player_statistic(player_id, system_id, season_id=None):
|
||||
"""
|
||||
- If season_id is provided: Data for this season.
|
||||
- If season_id is None: Aggregated total statistics.
|
||||
"""
|
||||
connection = db_connection()
|
||||
cursor = connection.cursor(cursor=DictCursor)
|
||||
|
||||
if season_id:
|
||||
query = """
|
||||
SELECT
|
||||
sys.name AS gamesystem_name,
|
||||
stat.mmr, stat.games_in_system, stat.points,
|
||||
stat.avv_points, stat.last_played, stat.win_rate,
|
||||
stat.trend, stat.tyrann_id, stat.pushover_id, stat.nemesis_id,
|
||||
(
|
||||
SELECT COUNT(*) + 1
|
||||
FROM player_game_statistic rank_stat
|
||||
WHERE rank_stat.gamesystem_id = %s
|
||||
AND rank_stat.season_id = %s
|
||||
AND rank_stat.games_in_system > 0
|
||||
AND rank_stat.mmr > COALESCE(stat.mmr, 0)
|
||||
) AS rank
|
||||
FROM gamesystems sys
|
||||
LEFT JOIN player_game_statistic stat
|
||||
ON sys.id = stat.gamesystem_id
|
||||
AND stat.player_id = %s
|
||||
AND stat.season_id = %s
|
||||
WHERE sys.id = %s
|
||||
"""
|
||||
cursor.execute(query, (system_id, season_id, player_id, season_id, system_id))
|
||||
else:
|
||||
query = """
|
||||
SELECT
|
||||
sys.name AS gamesystem_name,
|
||||
SUM(stat.mmr) / COUNT(stat.season_id) AS mmr, -- Average MMR over seasons
|
||||
SUM(stat.games_in_system) AS games_in_system,
|
||||
SUM(stat.points) AS points,
|
||||
SUM(stat.points) / SUM(stat.games_in_system) AS avv_points,
|
||||
MAX(stat.last_played) AS last_played,
|
||||
NULL AS rank
|
||||
FROM gamesystems sys
|
||||
LEFT JOIN player_game_statistic stat
|
||||
ON sys.id = stat.gamesystem_id
|
||||
AND stat.player_id = %s
|
||||
WHERE sys.id = %s
|
||||
GROUP BY sys.id
|
||||
"""
|
||||
cursor.execute(query, (player_id, system_id))
|
||||
|
||||
cursor.execute(query, (player_id, system_id))
|
||||
row = cursor.fetchone()
|
||||
|
||||
cursor.close()
|
||||
connection.close()
|
||||
|
||||
# Sicherstellen dass rank nie None ist wenn season_id gesetzt
|
||||
if row and season_id and row.get('rank') is None:
|
||||
row['rank'] = 1
|
||||
|
||||
return row
|
||||
|
||||
|
||||
def get_player_rank(player_id, system_id):
|
||||
"""Holt den Rang eines Spielers in einem System basierend auf MMR (absteigend)."""
|
||||
def get_seasons_by_system(system_id):
|
||||
|
||||
"""Retrieves all seasons of a game system, sorted by number descending."""
|
||||
connection = db_connection()
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
cursor = connection.cursor(cursor=DictCursor)
|
||||
|
||||
query = """
|
||||
SELECT id, name, season_number
|
||||
FROM seasons
|
||||
WHERE gamesystem_id = %s
|
||||
ORDER BY season_number DESC
|
||||
"""
|
||||
|
||||
cursor.execute(query, (system_id,))
|
||||
seasons = cursor.fetchall()
|
||||
|
||||
cursor.close()
|
||||
connection.close()
|
||||
return seasons
|
||||
|
||||
|
||||
def get_player_rank(player_id, system_id, season_id):
|
||||
"""Retrieves the rank of a player in a system for a specific season."""
|
||||
connection = db_connection()
|
||||
cursor = connection.cursor(cursor=DictCursor)
|
||||
|
||||
query = """
|
||||
SELECT COUNT(*) + 1 AS rank
|
||||
FROM player_game_statistic stat
|
||||
WHERE stat.gamesystem_id = %s
|
||||
AND stat.season_id = %s
|
||||
AND stat.games_in_system > 0
|
||||
AND stat.mmr > (
|
||||
SELECT stat2.mmr
|
||||
FROM player_game_statistic stat2
|
||||
WHERE stat2.player_id = %s
|
||||
AND stat2.gamesystem_id = %s
|
||||
AND stat2.season_id = %s
|
||||
)
|
||||
"""
|
||||
cursor.execute(query, (system_id, player_id, system_id))
|
||||
# Parameters: gamesystem, season, player, gamesystem, season
|
||||
cursor.execute(query, (system_id, season_id, player_id, system_id, season_id))
|
||||
row = cursor.fetchone()
|
||||
|
||||
cursor.close()
|
||||
connection.close()
|
||||
|
||||
if row and row['rank'] is not None:
|
||||
return row['rank']
|
||||
return None
|
||||
return row['rank'] if row and row['rank'] is not None else 1
|
||||
|
||||
|
||||
|
||||
def join_league(player_id, gamesystem_id):
|
||||
try:
|
||||
# 1. Die richtige Season für dieses System holen
|
||||
# 1. Get the correct season for this system
|
||||
season_id = get_current_season_id(gamesystem_id)
|
||||
|
||||
connection = db_connection()
|
||||
|
|
@ -260,22 +310,22 @@ def join_league(player_id, gamesystem_id):
|
|||
cursor.execute(query, (player_id, gamesystem_id, season_id))
|
||||
connection.commit()
|
||||
|
||||
logger.log(f"Erfolg: Spieler {player_id} joined System {gamesystem_id} in Season {season_id}")
|
||||
logger.log(f"Success: Player {player_id} joined System {gamesystem_id} in Season {season_id}")
|
||||
|
||||
except Exception as e:
|
||||
logger.log(f"FEHLER beim Beitreten: {e}")
|
||||
logger.log(f"ERROR joining league: {e}")
|
||||
raise
|
||||
finally:
|
||||
# Hier ist ein kleiner Check, ob die connection überhaupt existiert
|
||||
# Here is a small check if the connection actually exists
|
||||
if 'connection' in locals() and connection:
|
||||
cursor.close()
|
||||
connection.close()
|
||||
|
||||
|
||||
|
||||
def get_current_season_id(gamesystem_id):
|
||||
def get_current_season_id(system_id):
|
||||
connection = db_connection()
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
cursor = connection.cursor(cursor=DictCursor)
|
||||
|
||||
query = """
|
||||
SELECT id FROM seasons
|
||||
|
|
@ -283,7 +333,7 @@ def get_current_season_id(gamesystem_id):
|
|||
ORDER BY start_date DESC
|
||||
LIMIT 1
|
||||
"""
|
||||
cursor.execute(query, (gamesystem_id,))
|
||||
cursor.execute(query, (system_id,))
|
||||
result = cursor.fetchone()
|
||||
|
||||
cursor.close()
|
||||
|
|
@ -292,18 +342,17 @@ def get_current_season_id(gamesystem_id):
|
|||
if result:
|
||||
return result['id']
|
||||
else:
|
||||
# Hier wissen wir jetzt genau, welches Spielsystem keine Saison hat
|
||||
raise RuntimeError(f"Keine Saison für {gamesystem_id} gefunden!")
|
||||
|
||||
# Here we know exactly which game system has no season
|
||||
raise RuntimeError(f"No season found for {gamesystem_id}!")
|
||||
|
||||
|
||||
# -----------------------------------------------------
|
||||
# Matches: Lesen
|
||||
# Matches: Reading
|
||||
# -----------------------------------------------------
|
||||
|
||||
def get_recent_matches_for_player(player_id):
|
||||
connection = db_connection()
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
cursor = connection.cursor(cursor=DictCursor)
|
||||
|
||||
query = """
|
||||
SELECT
|
||||
|
|
@ -335,36 +384,44 @@ def get_recent_matches_for_player(player_id):
|
|||
|
||||
|
||||
def add_new_match(system_name, player1_id, player2_id, score_p1, score_p2):
|
||||
connection = db_connection()
|
||||
cursor = connection.cursor()
|
||||
connection = None
|
||||
try:
|
||||
connection = db_connection()
|
||||
cursor = connection.cursor()
|
||||
|
||||
cursor.execute("SELECT id FROM gamesystems WHERE name = %s", (system_name,))
|
||||
sys_row = cursor.fetchone()
|
||||
cursor.execute("SELECT id FROM gamesystems WHERE name = %s", (system_name,))
|
||||
sys_row = cursor.fetchone()
|
||||
|
||||
if not sys_row:
|
||||
cursor.close()
|
||||
connection.close()
|
||||
return None
|
||||
if not sys_row:
|
||||
raise ValueError(f"Game system '{system_name}' not found.")
|
||||
|
||||
sys_id = sys_row[0]
|
||||
sys_id = sys_row[0]
|
||||
|
||||
query = """
|
||||
INSERT INTO matches (gamesystem_id, player1_id, player2_id, score_player1, score_player2)
|
||||
VALUES (%s, %s, %s, %s, %s)
|
||||
"""
|
||||
cursor.execute(query, (sys_id, player1_id, player2_id, score_p1, score_p2))
|
||||
new_match_id = cursor.lastrowid
|
||||
query = """
|
||||
INSERT INTO matches (gamesystem_id, player1_id, player2_id, score_player1, score_player2)
|
||||
VALUES (%s, %s, %s, %s, %s)
|
||||
"""
|
||||
cursor.execute(query, (sys_id, player1_id, player2_id, score_p1, score_p2))
|
||||
new_match_id = cursor.lastrowid
|
||||
|
||||
logger.log(
|
||||
f"{get_player_name(player1_id)}:({score_p1}) posted Match. "
|
||||
f"System: {system_name}, {get_player_name(player2_id)}:({score_p2})"
|
||||
)
|
||||
logger.log(
|
||||
f"{get_player_name(player1_id)}:({score_p1}) posted Match. "
|
||||
f"System: {system_name}, {get_player_name(player2_id)}:({score_p2})"
|
||||
)
|
||||
|
||||
connection.commit()
|
||||
cursor.close()
|
||||
connection.close()
|
||||
connection.commit()
|
||||
return new_match_id
|
||||
|
||||
except Exception as e:
|
||||
if connection:
|
||||
connection.rollback()
|
||||
raise # Propagate the exception
|
||||
|
||||
finally:
|
||||
if connection:
|
||||
cursor.close()
|
||||
connection.close()
|
||||
|
||||
return new_match_id
|
||||
|
||||
|
||||
def save_calculated_match(calc_results: dict):
|
||||
|
|
@ -382,7 +439,7 @@ def save_calculated_match(calc_results: dict):
|
|||
)
|
||||
row = cursor.fetchone()
|
||||
if not row:
|
||||
raise ValueError(f"Match ID {match_id} nicht in der Datenbank gefunden.")
|
||||
raise ValueError(f"Match ID {match_id} not found in the database.")
|
||||
|
||||
player1_id, player2_id, gamesystem_id = row
|
||||
|
||||
|
|
@ -438,7 +495,7 @@ def save_calculated_match(calc_results: dict):
|
|||
|
||||
except Exception as e:
|
||||
connection.rollback()
|
||||
logger.log(f"KRITISCHER FEHLER beim Speichern des Matches: {e}")
|
||||
logger.log(f"CRITICAL ERROR saving the match: {e}")
|
||||
return False
|
||||
|
||||
finally:
|
||||
|
|
@ -447,12 +504,12 @@ def save_calculated_match(calc_results: dict):
|
|||
|
||||
|
||||
# -----------------------------------------------------
|
||||
# Get Data Funktionen
|
||||
# Get Data Functions
|
||||
# -----------------------------------------------------
|
||||
|
||||
def get_gamesystem_data(system_id):
|
||||
connection = db_connection()
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
cursor = connection.cursor(cursor=DictCursor)
|
||||
|
||||
cursor.execute("SELECT * FROM gamesystems WHERE id = %s", (system_id,))
|
||||
row = cursor.fetchone()
|
||||
|
|
@ -464,7 +521,7 @@ def get_gamesystem_data(system_id):
|
|||
|
||||
def get_gamesystems_data():
|
||||
connection = db_connection()
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
cursor = connection.cursor(cursor=DictCursor)
|
||||
|
||||
cursor.execute("SELECT * FROM gamesystems")
|
||||
rows = cursor.fetchall()
|
||||
|
|
@ -476,19 +533,22 @@ def get_gamesystems_data():
|
|||
|
||||
|
||||
def get_leaderboard(system_name):
|
||||
"""Holt alle Spieler eines Systems sortiert nach MMR für die Rangliste."""
|
||||
"""Retrieves all players of a system sorted by MMR for the leaderboard (current season only)."""
|
||||
connection = db_connection()
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
cursor = connection.cursor(cursor=DictCursor)
|
||||
|
||||
query = query = """
|
||||
query = """
|
||||
SELECT p.id, p.display_name, p.discord_name, p.discord_id, p.discord_avatar_url,
|
||||
stat.mmr, stat.games_in_system, stat.wins, stat.loss, stat.avv_points
|
||||
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 = %s AND stat.games_in_system > 0
|
||||
JOIN seasons s ON stat.season_id = s.id
|
||||
WHERE sys.name = %s
|
||||
AND s.end_date IS NULL
|
||||
AND stat.games_in_system > 0
|
||||
ORDER BY stat.mmr DESC
|
||||
"""
|
||||
"""
|
||||
cursor.execute(query, (system_name,))
|
||||
rows = cursor.fetchall()
|
||||
|
||||
|
|
@ -498,9 +558,9 @@ def get_leaderboard(system_name):
|
|||
|
||||
|
||||
def get_match_by_id(match_id: int) -> dict | None:
|
||||
"""Gibt alle Match-Daten inkl. Gamesystem-Name als Dict zurück."""
|
||||
"""Returns all match data including game system name as a dictionary."""
|
||||
connection = db_connection()
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
cursor = connection.cursor(cursor=DictCursor)
|
||||
|
||||
try:
|
||||
cursor.execute("""
|
||||
|
|
@ -521,7 +581,7 @@ def get_match_by_id(match_id: int) -> dict | None:
|
|||
return cursor.fetchone()
|
||||
|
||||
except Exception as e:
|
||||
logger.log(f"Fehler beim Laden des Matches: {e}")
|
||||
logger.log(f"Error loading the match: {e}")
|
||||
return None
|
||||
|
||||
finally:
|
||||
|
|
@ -530,9 +590,9 @@ def get_match_by_id(match_id: int) -> dict | None:
|
|||
|
||||
|
||||
def get_player_name(player_id):
|
||||
"""Gibt den Namen eines Spielers im Format 'Anzeigename (Discordname)' zurück."""
|
||||
"""Returns the name of a player in the format 'Display Name (Discord Name)'."""
|
||||
if player_id is None:
|
||||
return "Unbekannter Spieler"
|
||||
return "Unknown Player"
|
||||
|
||||
connection = db_connection()
|
||||
cursor = connection.cursor()
|
||||
|
|
@ -545,12 +605,12 @@ def get_player_name(player_id):
|
|||
|
||||
if row:
|
||||
return f"{row[0]} ({row[1]})"
|
||||
return "Gelöschter Spieler"
|
||||
return "Deleted Player"
|
||||
|
||||
|
||||
def get_system_name(sys_id):
|
||||
def get_system_name_by_id(sys_id):
|
||||
if sys_id is None:
|
||||
return "Unbekanntes System"
|
||||
return "Unknown System"
|
||||
|
||||
connection = db_connection()
|
||||
cursor = connection.cursor()
|
||||
|
|
@ -561,15 +621,15 @@ def get_system_name(sys_id):
|
|||
cursor.close()
|
||||
connection.close()
|
||||
|
||||
return row[0] if row else "Gelöschtes System"
|
||||
return row[0] if row else "Deleted System"
|
||||
|
||||
|
||||
def get_days_since_last_system_game(player_id, gamesystem_id):
|
||||
"""Gibt zurück, wie viele Tage das letzte Spiel in einem bestimmten System her ist."""
|
||||
"""Returns how many days the last game in a specific system was."""
|
||||
connection = db_connection()
|
||||
cursor = connection.cursor()
|
||||
|
||||
# MariaDB: DATEDIFF statt julianday()
|
||||
# MariaDB: DATEDIFF instead of julianday()
|
||||
query = """
|
||||
SELECT DATEDIFF(NOW(), last_played)
|
||||
FROM player_game_statistic
|
||||
|
|
@ -587,33 +647,51 @@ def get_days_since_last_system_game(player_id, gamesystem_id):
|
|||
|
||||
|
||||
# -----------------------------------------------------
|
||||
# Matches Bestätigen, Löschen, Berechnen, ...
|
||||
# Matches Confirming, Deleting, Calculating, ...
|
||||
# -----------------------------------------------------
|
||||
|
||||
def get_unconfirmed_matches(player_id):
|
||||
"""Holt alle offenen Matches, die der Spieler noch bestätigen muss."""
|
||||
def get_open_matches(player_id):
|
||||
"""Retrieves all open matches (player2_check = 0)."""
|
||||
connection = db_connection()
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
cursor = connection.cursor(cursor=DictCursor)
|
||||
|
||||
query = """
|
||||
SELECT m.id AS match_id, m.score_player1, m.score_player2, m.played_at,
|
||||
SELECT m.id AS match_id,
|
||||
m.score_player1, m.score_player2,
|
||||
m.played_at,
|
||||
sys.name AS system_name,
|
||||
p1.display_name AS p1_name
|
||||
p1.display_name AS player1_name,
|
||||
p2.display_name AS player2_name,
|
||||
CASE WHEN m.player1_id = %s THEN 1 ELSE 0 END AS is_submitter
|
||||
FROM matches m
|
||||
JOIN gamesystems sys ON m.gamesystem_id = sys.id
|
||||
JOIN players p1 ON m.player1_id = p1.id
|
||||
WHERE m.player2_id = %s AND m.player2_check = 0
|
||||
LEFT JOIN gamesystems sys ON m.gamesystem_id = sys.id
|
||||
LEFT JOIN players p1 ON m.player1_id = p1.id
|
||||
LEFT JOIN players p2 ON m.player2_id = p2.id
|
||||
WHERE m.player2_check = 0
|
||||
AND (m.player1_id = %s OR m.player2_id = %s)
|
||||
"""
|
||||
cursor.execute(query, (player_id,))
|
||||
cursor.execute(query, (player_id, player_id, player_id))
|
||||
rows = cursor.fetchall()
|
||||
|
||||
cursor.close()
|
||||
connection.close()
|
||||
return rows
|
||||
|
||||
submitted = []
|
||||
to_confirm = []
|
||||
|
||||
for row in rows:
|
||||
if row.pop('is_submitter'):
|
||||
submitted.append(row)
|
||||
else:
|
||||
to_confirm.append(row)
|
||||
|
||||
return {"submitted": submitted, "to_confirm": to_confirm}
|
||||
|
||||
|
||||
|
||||
|
||||
def delete_match(match_id, player_id):
|
||||
"""Löscht ein Match anhand seiner ID komplett aus der Datenbank."""
|
||||
"""Deletes a match completely from the database by its ID."""
|
||||
connection = db_connection()
|
||||
cursor = connection.cursor()
|
||||
|
||||
|
|
@ -630,7 +708,7 @@ def confirm_match(match_id):
|
|||
cursor = connection.cursor()
|
||||
|
||||
cursor.execute("UPDATE matches SET player2_check = 1 WHERE id = %s", (match_id,))
|
||||
logger.log(f"Match mit ID{match_id} von Player2 bestätigt.")
|
||||
logger.log(f"Match with ID{match_id} confirmed by Player2.")
|
||||
connection.commit()
|
||||
|
||||
cursor.close()
|
||||
|
|
@ -638,7 +716,7 @@ def confirm_match(match_id):
|
|||
|
||||
|
||||
def set_match_counted(match_id):
|
||||
"""Setzt den Haken (1), dass das Match erfolgreich in die MMR eingeflossen ist."""
|
||||
"""Sets the flag (1) that the match was successfully incorporated into the MMR."""
|
||||
connection = db_connection()
|
||||
cursor = connection.cursor()
|
||||
|
||||
|
|
@ -649,32 +727,11 @@ def set_match_counted(match_id):
|
|||
connection.close()
|
||||
|
||||
|
||||
def get_submitted_matches(player_id):
|
||||
"""Holt alle offenen Matches, die der Spieler selbst eingetragen hat."""
|
||||
connection = db_connection()
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
|
||||
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 = %s AND m.player2_check = 0
|
||||
"""
|
||||
cursor.execute(query, (player_id,))
|
||||
rows = cursor.fetchall()
|
||||
|
||||
cursor.close()
|
||||
connection.close()
|
||||
return rows
|
||||
|
||||
|
||||
def get_match_history_log(player_id):
|
||||
"""Holt ALLE Matches eines Spielers inkl. MMR-Änderungen und Faktoren."""
|
||||
"""Retrieves ALL matches of a player including MMR changes and factors."""
|
||||
connection = db_connection()
|
||||
cursor = connection.cursor(dictionary=True)
|
||||
cursor = connection.cursor(cursor=DictCursor)
|
||||
|
||||
query = """
|
||||
SELECT m.played_at, sys.name AS gamesystem_name,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import mariadb
|
||||
import pymysql
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
|
||||
|
|
@ -11,18 +11,16 @@ def db_connection():
|
|||
# Prefix für die Umgebungsvariablen wählen
|
||||
prefix = "DB_" if is_prod else "DB_TEST_"
|
||||
|
||||
|
||||
try:
|
||||
connection = mariadb.connect(
|
||||
connection = pymysql.connect(
|
||||
host=os.getenv("DB_HOST", "localhost"),
|
||||
port=int(os.getenv("DB_PORT", 3306)),
|
||||
user=os.getenv(f"{prefix}USER"),
|
||||
password=os.getenv("DB_PASSWORD"),
|
||||
password=os.getenv(f"DB_PASSWORD"),
|
||||
database=os.getenv(f"{prefix}NAME")
|
||||
)
|
||||
return connection
|
||||
if is_prod:
|
||||
print("Verbindung mit Test DB")
|
||||
|
||||
except mariadb.Error as e:
|
||||
return connection
|
||||
|
||||
except pymysql.Error as e:
|
||||
raise RuntimeError(f"Fehler beim Verbinden mit {'Prod' if is_prod else 'Test'}-DB: {e}")
|
||||
|
|
|
|||
18
gui/__init__.py
Normal file
18
gui/__init__.py
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# gui/__init__.py
|
||||
import importlib
|
||||
import pkgutil
|
||||
import gui
|
||||
from gui import _templates
|
||||
from wood.logger import log as Log
|
||||
|
||||
|
||||
def setup_all_routes():
|
||||
"""Scannt automatisch alle Dateien in 'gui' und führt setup_routes() aus."""
|
||||
# Liste aller Dateien in diesem Ordner (außer __init__.py selbst)
|
||||
for loader, module_name, is_pkg in pkgutil.iter_modules(gui.__path__):
|
||||
# Wir laden das Modul dynamisch
|
||||
module = importlib.import_module(f'gui.{module_name}')
|
||||
|
||||
# Prüfen, ob das Modul eine Funktion 'setup_routes' hat
|
||||
if hasattr(module, 'setup_routes'):
|
||||
module.setup_routes()
|
||||
10
gui/_templates/__init__.py
Normal file
10
gui/_templates/__init__.py
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
from .header import DesktopHeader, MobileHeader
|
||||
from .leaderboard import DesktopLeaderboard, MobileLeaderboard
|
||||
from .player_card import DesktopPlayerCard, MobilePlayerCard
|
||||
from .gamesystems import DesktopGamesystems, MobileGamesystems
|
||||
from .match_form import DesktopMatchForm, MobileMatchForm
|
||||
from .opengames import DesktopUncofirmedGames, MobileUncofirmedGames
|
||||
from .stats_1 import DesktopStats_1, MobileStats_1
|
||||
from .seasonselector import DesktopSeasonSelector, MobileSeasonSelector
|
||||
from .player_match_history import DesktopMatchHistory, MobileMatchHistory
|
||||
from .profile_menue import DesktopProfileMenu, MobileProfileMenu
|
||||
44
gui/_templates/gamesystems.py
Normal file
44
gui/_templates/gamesystems.py
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
from nicegui import ui, app
|
||||
from gui.common import *
|
||||
|
||||
class Gamesystems:
|
||||
def __init__(self, game_systems:dict):
|
||||
self.game_systems = game_systems
|
||||
self.build_content()
|
||||
|
||||
def build_content(self):
|
||||
# Will be overwritte by Childs
|
||||
pass
|
||||
|
||||
|
||||
class MobileGamesystems(Gamesystems):
|
||||
def build_content(self):
|
||||
with ui.card().classes("w-full"):
|
||||
for g in self.game_systems:
|
||||
mobile_gamecard(g)
|
||||
|
||||
|
||||
|
||||
class DesktopGamesystems(Gamesystems):
|
||||
def build_content(self):
|
||||
with ui.card().classes("w-full"):
|
||||
with ui.row().classes("w-full flex-wrap justify-center gap-4 p-4"):
|
||||
for g in self.game_systems:
|
||||
desktop_gamecard(g)
|
||||
|
||||
|
||||
|
||||
def desktop_gamecard(game):
|
||||
with ui.link(target=f"/stats/{game['name']}").classes("no-underline"):
|
||||
with ui.card().classes("w-120 h-60 items-center"):
|
||||
ui.label(text=game["name"]).classes("text-3xl text-bold text-center")
|
||||
ui.image(source=f"pictures/{game['picture']}").classes("w-80")
|
||||
ui.label(game["description"]).classes("text-italic text-center")
|
||||
|
||||
|
||||
def mobile_gamecard(game):
|
||||
with ui.link(target=f"/stats/{game['name']}").classes("no-underline"):
|
||||
with ui.card().classes("w-full items-center"):
|
||||
ui.label(text=game["name"]).classes("text-lg text-bold text-center")
|
||||
ui.image(source=f"pictures/{game['picture']}").classes("w-40")
|
||||
ui.label(game["description"]).classes("text-italic text-center")
|
||||
75
gui/_templates/header.py
Normal file
75
gui/_templates/header.py
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
from gui.common import *
|
||||
|
||||
class BaseHeader:
|
||||
def __init__(self, title: str, request: Request, tab_names: list[str] | None = None):
|
||||
self.request = request # Store centrally
|
||||
self.is_logged_in = app.storage.user.get('authenticated', False)
|
||||
self.tab_names = tab_names or []
|
||||
self.tabs = None # set in build_content
|
||||
|
||||
with ui.header().classes('bg-[#15151B] items-center p-4 shadow-lg'):
|
||||
self.build_content(title)
|
||||
|
||||
def logout(self):
|
||||
app.storage.user.clear()
|
||||
ui.navigate.to('/')
|
||||
|
||||
def build_content(self):
|
||||
# Will be overwritten by Childs
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class MobileHeader(BaseHeader):
|
||||
def build_content(self, title):
|
||||
# Make the main container a column
|
||||
# So the title is at the top and the tabs take up the full width below
|
||||
with ui.column().classes('w-full items-center gap-0'):
|
||||
|
||||
# Row for header elements (Logo/Back, Title, Profile)
|
||||
with ui.row().classes('w-full items-center px-6'):
|
||||
# 1. Left
|
||||
if self.request.url.path != '/':
|
||||
ui.button(icon='arrow_back', on_click=lambda: ui.navigate.to('/'))
|
||||
else:
|
||||
ui.image(source="/pictures/wsdg.png").classes("w-16")
|
||||
|
||||
# Spacer
|
||||
ui.space()
|
||||
|
||||
# 3. Right
|
||||
if self.request.url.path == '/':
|
||||
ui.button(text="Profile", on_click=lambda: ui.notify("Menu"))
|
||||
else:
|
||||
ui.label(text=title).classes("flex-grow text-center text-bold text-xl text-white px-2")
|
||||
|
||||
# Tabs separately below the header block
|
||||
if self.tab_names:
|
||||
with ui.tabs().props('inline-label active-color=white indicator-color=white scrollable') \
|
||||
.classes('text-white w-full') as self.tabs:
|
||||
for name in self.tab_names:
|
||||
ui.tab(name, label=name).classes('text-bold text-xs')
|
||||
|
||||
|
||||
class DesktopHeader(BaseHeader):
|
||||
def build_content(self, title):
|
||||
# Header as a row with 'items-end', so everything slides down
|
||||
with ui.row().classes('w-full items-end px-6 py-4 gap-6'):
|
||||
# 1. Block: Logo & Back (Left)
|
||||
with ui.column().classes('gap-6'):
|
||||
ui.image(source="/pictures/wsdg.png").classes("w-30")
|
||||
if self.request.url.path != '/':
|
||||
ui.button(icon='arrow_back', text="back", on_click=lambda: ui.navigate.to('/')).classes('text-lg')
|
||||
|
||||
# 2. Block: Title & Tabs (Middle)
|
||||
# 'flex-grow' makes this block take up the space in the middle
|
||||
with ui.column().classes('flex-grow items-center gap-0'):
|
||||
ui.label(text=title).classes("text-bold text-2xl text-white")
|
||||
if self.tab_names:
|
||||
with ui.tabs().props('inline-label active-color=white indicator-color=white') \
|
||||
.classes('text-white') as self.tabs:
|
||||
for name in self.tab_names:
|
||||
ui.tab(name, label=name).classes('text-bold')
|
||||
|
||||
# 3. Block: Profile & Logout (Right)
|
||||
with ui.row().classes('gap-6'):
|
||||
templates.DesktopProfileMenu()
|
||||
54
gui/_templates/leaderboard.py
Normal file
54
gui/_templates/leaderboard.py
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
from gui.common import *
|
||||
from gui._templates.player_card import PlayerCard, DesktopPlayerCard, MobilePlayerCard
|
||||
|
||||
|
||||
class Leaderboard:
|
||||
def __init__(self, system_name: str):
|
||||
leaderboard_data = data_api.get_leaderboard(system_name)
|
||||
self.build_content(leaderboard_data)
|
||||
|
||||
def build_content(self, leaderboard_data):
|
||||
pass
|
||||
|
||||
|
||||
class DesktopLeaderboard(Leaderboard):
|
||||
def build_content(self, leaderboard_data):
|
||||
with ui.column().classes('w-full gap-2') as self.container:
|
||||
for index, player in enumerate(leaderboard_data):
|
||||
with ui.row().classes('w-full items-center gap-10 flex-nowrap'):
|
||||
ui.label(f'#{index + 1}').classes('text-2xl font-bold w-10 text-center shrink-0')
|
||||
DesktopPlayerCard(
|
||||
display_name=player['display_name'],
|
||||
discord_name=player['discord_name'],
|
||||
mmr=player['mmr'],
|
||||
discord_avatar_url=player.get('discord_avatar_url'),
|
||||
discord_id=player.get('discord_id'),
|
||||
games_in_system=player.get('games_in_system', 0),
|
||||
wins=player.get('wins', 0),
|
||||
loss=player.get('loss', 0),
|
||||
avv_points=player.get('avv_points', 0)
|
||||
)
|
||||
|
||||
|
||||
class MobileLeaderboard(Leaderboard):
|
||||
def build_content(self, leaderboard_data):
|
||||
with ui.column().classes('w-full gap-1') as self.container:
|
||||
for index, player in enumerate(leaderboard_data):
|
||||
# Row mit fester Breite für das Rang-Label, damit es nicht abschneidet
|
||||
with ui.row().classes('w-full items-center gap-2 flex-nowrap'):
|
||||
# w-10 statt w-6 gibt der zweistelligen Zahl genug Platz
|
||||
ui.label(f'#{index + 1}').classes('font-bold w-8 text-center shrink-0')
|
||||
|
||||
# Flex-1 sorgt dafür, dass die Card den restlichen Platz einnimmt
|
||||
with ui.column().classes('w-full'):
|
||||
MobilePlayerCard(
|
||||
display_name=player['display_name'],
|
||||
discord_name=player['discord_name'],
|
||||
mmr=player['mmr'],
|
||||
discord_avatar_url=player.get('discord_avatar_url'),
|
||||
discord_id=player.get('discord_id'),
|
||||
games_in_system=player.get('games_in_system', 0),
|
||||
wins=player.get('wins', 0),
|
||||
loss=player.get('loss', 0),
|
||||
avv_points=player.get('avv_points', 0)
|
||||
)
|
||||
153
gui/_templates/match_form.py
Normal file
153
gui/_templates/match_form.py
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
from gui.common import *
|
||||
|
||||
|
||||
class MatchForm:
|
||||
def __init__(self, system_name: str, player1_id: int, active_players: dict, min_points: int, max_points: int):
|
||||
self.system_name = system_name
|
||||
self.player1_id = player1_id
|
||||
self.active_players = active_players
|
||||
self.min_points = min_points
|
||||
self.max_points = max_points
|
||||
self.build_content()
|
||||
|
||||
def build_content(self):
|
||||
pass
|
||||
|
||||
def get_select_options(active_players, player1_id):
|
||||
return {
|
||||
p["player_id"]: f"{p['display_name']} aka {p['discord_name']}"
|
||||
for p in active_players
|
||||
if p["player_id"] != player1_id
|
||||
}
|
||||
|
||||
class DesktopMatchForm(MatchForm):
|
||||
def __init__(self, **kwargs): # packt alle Parameter in ein Dict
|
||||
super().__init__(**kwargs) # entpackt und gibt sie an Parent
|
||||
|
||||
def build_content(self):
|
||||
with ui.card().classes("w-full items-center"):
|
||||
ui.label(f"Neues Spiel in {self.system_name} eintragen").classes("text-center w-full text-bold text-4xl")
|
||||
ui.space()
|
||||
with ui.row().classes('w-full items-center gap-2 flex-nowrap'):
|
||||
with ui.column().classes("w-full items-center"):
|
||||
ui.label("Deine Punkte:").classes("text-bold text-2xl")
|
||||
score_p1 = ui.number(value=10, precision=0, min=self.min_points, max=self.max_points).classes("w-80")
|
||||
score_p1_slider = ui.slider(min=self.min_points, max=self.max_points, value=10, step=1).classes("w-80")
|
||||
|
||||
score_p1.on("update:model-value", lambda: setattr(score_p1_slider, "value", score_p1.value))
|
||||
score_p1_slider.on("update:model-value", lambda: setattr(score_p1, "value", score_p1_slider.value))
|
||||
ui.select(options=[], label=" ").classes("w-80 opacity-0")
|
||||
|
||||
ui.space()
|
||||
with ui.column().classes("w-full items-center"):
|
||||
ui.label("Gegner:").classes("text-bold text-2xl")
|
||||
player2_id = ui.select(
|
||||
options=get_select_options(self.active_players, self.player1_id),
|
||||
label="Gegner Name"
|
||||
).classes("w-80")
|
||||
score_p2 = ui.number(value=10, precision=0, min=self.min_points, max=self.max_points).classes("w-80")
|
||||
score_p2_slider = ui.slider(min=self.min_points, max=self.max_points, value=10, step=1).classes("w-80")
|
||||
|
||||
score_p2.on("update:model-value", lambda: setattr(score_p2_slider, "value", score_p2.value))
|
||||
score_p2_slider.on("update:model-value", lambda: setattr(score_p2, "value", score_p2_slider.value))
|
||||
|
||||
def submit():
|
||||
# Validierung: Prüfen, ob ein Gegner gewählt wurde
|
||||
if player2_id.value is None:
|
||||
ui.notify("Bitte einen Gegner auswählen!", type="warning")
|
||||
return
|
||||
|
||||
# DB-Eintrag
|
||||
match_id = input_match_to_database(self.system_name, self.player1_id, player2_id.value, score_p1.value, score_p2.value)
|
||||
|
||||
if match_id:
|
||||
ui.notify(f"Spiel erfolgreich gespeichert (ID: {match_id})", type="positive")
|
||||
# Felder zurücksetzen
|
||||
reset()
|
||||
else:
|
||||
ui.notify("Fehler beim Speichern in die Datenbank", type="negative")
|
||||
|
||||
def reset():
|
||||
score_p1.value = 10
|
||||
score_p2.value = 10
|
||||
score_p1_slider.value = 10
|
||||
score_p2_slider.value = 10
|
||||
player2_id.value = None
|
||||
|
||||
ui.space()
|
||||
with ui.row().classes('w-full justify-center gap-40'):
|
||||
ui.button(text="Löschen", on_click=reset).classes("bg-red-500")
|
||||
ui.button(text="Absenden", on_click=submit).props("color=positive")
|
||||
|
||||
|
||||
class MobileMatchForm(MatchForm):
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def build_content(self):
|
||||
with ui.card().classes("w-full items-center px-3 py-3"):
|
||||
# Row 1: Title
|
||||
ui.label("Spiel eintragen:").classes("text-center w-full font-bold text-lg mb-2")
|
||||
|
||||
# Row 2: "Deine Punkte:" + input
|
||||
with ui.row().classes("w-full justify-center items-center gap-3"):
|
||||
ui.label("Deine Punkte:").classes("font-bold text-sm w-32 text-right")
|
||||
p1_pts = ui.number(value=10, precision=0, min=self.min_points, max=self.max_points).classes("w-24") \
|
||||
.props('input-class="text-center" type="number"')
|
||||
|
||||
# Row 3: Opponent dropdown + pts input (aligned under row 2)
|
||||
with ui.row().classes("w-full justify-center items-center gap-3"):
|
||||
p2_id = ui.select(
|
||||
options=get_select_options(self.active_players, self.player1_id),
|
||||
label="Gegner"
|
||||
).classes("w-32")
|
||||
p2_pts = ui.number(value=10, precision=0, min=self.min_points, max=self.max_points).classes("w-24") \
|
||||
.props('input-class="text-center" type="number"')
|
||||
|
||||
def submit():
|
||||
if p2_id.value is None:
|
||||
ui.notify("Bitte Gegner wählen!", type="warning")
|
||||
return
|
||||
|
||||
match = input_match_to_database(
|
||||
self.system_name,
|
||||
self.player1_id,
|
||||
p2_id.value,
|
||||
p1_pts.value,
|
||||
p2_pts.value
|
||||
)
|
||||
|
||||
if match:
|
||||
ui.notify("Spiel gespeichert!", type="positive")
|
||||
reset()
|
||||
else:
|
||||
ui.notify("Fehler beim Speichern!", type="negative")
|
||||
|
||||
def reset():
|
||||
p1_pts.value = 10
|
||||
p2_pts.value = 10
|
||||
p2_id.value = None
|
||||
|
||||
# Row 4: Buttons
|
||||
with ui.row().classes("w-full justify-center gap-6 mt-2"):
|
||||
ui.button("Reset", on_click=reset).classes("bg-red-500 text-sm")
|
||||
ui.button("Absenden", on_click=submit).props("color=positive").classes("text-sm")
|
||||
|
||||
|
||||
|
||||
def input_match_to_database(system_name, player1_id, player2_id, score_p1, score_p2):
|
||||
try:
|
||||
match_id = data_api.add_new_match(system_name, player1_id, player2_id, score_p1, score_p2)
|
||||
ui.notify(f"Spiel erfolgreich gespeichert (ID: {match_id})", type="positive")
|
||||
return match_id
|
||||
|
||||
except ValueError as e:
|
||||
# Logikfehler (z.B. System nicht gefunden)
|
||||
ui.notify(f"Fehler: {e}", type="negative")
|
||||
Log(f"Error:{e}")
|
||||
|
||||
|
||||
except Exception as e:
|
||||
# DB-Fehler, Verbindungsfehler, etc.
|
||||
ui.notify(f"Datenbankfehler: {e}", type="negative")
|
||||
Log(f"Error:{e}")
|
||||
115
gui/_templates/opengames.py
Normal file
115
gui/_templates/opengames.py
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
from gui.common import *
|
||||
from match_calculations.calc_match import calculate_match
|
||||
|
||||
class UnconfirmedGames():
|
||||
def __init__(self, open_games :dict, player_id :int):
|
||||
self.open_games = open_games
|
||||
self.player_id = player_id
|
||||
self.build_content()
|
||||
|
||||
def build_content(self):
|
||||
pass
|
||||
|
||||
class DesktopUncofirmedGames(UnconfirmedGames):
|
||||
# Bestätigungs für offene Spiele --- Der "Marian Balken !!!1!11!"
|
||||
def build_content(self):
|
||||
with ui.card().classes("w-full"):
|
||||
ui.label(text="Deine Offenen Spiele:").classes("text-3xl text-bold")
|
||||
for s in self.open_games["submitted"]:
|
||||
with ui.card().classes('w-full bg-zinc-800 border border-gray-600 mb-6'):
|
||||
with ui.row().classes("w-full items-center"):
|
||||
with ui.column().classes("flex-1 items-center"):
|
||||
ui.label(f"{s['system_name']}").classes("text-2xl text-bold")
|
||||
ui.label(f"am {s['played_at']}").classes("text-sm text-bold")
|
||||
with ui.column().classes("flex-1 items-center"):
|
||||
ui.label(
|
||||
f"Du ({s['score_player1']}) --vs.-- "
|
||||
f"{s['player2_name']} ({s['score_player2']})"
|
||||
).classes("text-2xl text-bold text-center")
|
||||
ui.label("Dein Gegner muss das Spiel noch bestätigen.").classes("text-center")
|
||||
with ui.column().classes("flex-1 items-center"):
|
||||
ui.button(
|
||||
icon='delete', color='negative',
|
||||
on_click=lambda e, m_id=s['match_id']: delete_match(m_id, self.player_id)
|
||||
).props('round')
|
||||
|
||||
|
||||
for c in self.open_games["to_confirm"]:
|
||||
with ui.card().classes('w-full bg-red-900/80 border-2 border-red-500 mb-6'):
|
||||
with ui.row().classes("w-full items-center"):
|
||||
with ui.column().classes("flex-1 items-center"):
|
||||
ui.label(f"{s['system_name']}").classes("text-2xl text-bold")
|
||||
ui.label(f"am {s['played_at']}").classes("text-sm text-bold")
|
||||
with ui.column().classes("flex-1 items-center"):
|
||||
ui.label(
|
||||
f"Du ({s['score_player1']}) --vs.-- "
|
||||
f"{s['player2_name']} ({s['score_player2']})"
|
||||
).classes("text-2xl text-bold text-center")
|
||||
ui.label("Wenn das Ergebnis stimmt, bestätige das Spiel damit es berechnet wird.").classes("text-center")
|
||||
with ui.column().classes("flex-1 items-center"):
|
||||
ui.button(
|
||||
icon='thumb_up', color='positive',
|
||||
on_click=lambda e, m_id=s['match_id']: confirm_match(m_id, self.player_id)
|
||||
).props('round')
|
||||
ui.button(
|
||||
icon='delete', color='negative',
|
||||
on_click=lambda e, m_id=s['match_id']: delete_match(m_id, self.player_id)
|
||||
).props('round')
|
||||
|
||||
|
||||
class MobileUncofirmedGames(UnconfirmedGames):
|
||||
def build_content(self):
|
||||
with ui.card().classes("w-full"):
|
||||
ui.label(text="Deine Offenen Spiele:").classes("text-xl text-bold")
|
||||
|
||||
for s in self.open_games["submitted"]:
|
||||
name = (s['system_name'][:18] + '…') if len(s['system_name']) > 20 else s['system_name']
|
||||
date = str(s['played_at'])[:10]
|
||||
with ui.card().classes('w-full bg-zinc-800 border border-gray-600 mb-2 py-1'):
|
||||
with ui.row().classes("w-full justify-between items-center"):
|
||||
ui.label(name).classes("text-sm font-bold")
|
||||
ui.label(date).classes("text-xs text-gray-400")
|
||||
with ui.row().classes("w-full justify-between items-center"):
|
||||
ui.label(
|
||||
f"Du ({s['score_player1']}) vs. {s['player2_name']} ({s['score_player2']})"
|
||||
).classes("text-xs font-bold flex-1 text-center")
|
||||
ui.button(
|
||||
icon='delete', color='negative',
|
||||
on_click=lambda e, m_id=s['match_id']: delete_match(m_id, self.player_id)
|
||||
).props('round dense')
|
||||
|
||||
for c in self.open_games["to_confirm"]:
|
||||
name = (c['system_name'][:18] + '…') if len(c['system_name']) > 20 else c['system_name']
|
||||
date = str(c['played_at'])[:10]
|
||||
with ui.card().classes('w-full bg-red-900/80 border-2 border-red-500 mb-2 py-1'):
|
||||
with ui.row().classes("w-full justify-between items-center"):
|
||||
ui.label(name).classes("text-sm font-bold")
|
||||
ui.label(date).classes("text-xs text-gray-400")
|
||||
with ui.row().classes("w-full justify-between items-center"):
|
||||
ui.label(
|
||||
f"Du ({c['score_player1']}) vs. {c['player2_name']} ({c['score_player2']})"
|
||||
).classes("text-xs font-bold flex-1 text-center")
|
||||
with ui.row().classes("gap-2 items-center"):
|
||||
ui.button(
|
||||
icon='delete', color='negative',
|
||||
on_click=lambda e, m_id=c['match_id']: delete_match(m_id, self.player_id)
|
||||
).props('round dense')
|
||||
ui.button(
|
||||
icon='thumb_up', color='positive',
|
||||
on_click=lambda e, m_id=c['match_id']: confirm_match(m_id, self.player_id)
|
||||
).props('round dense')
|
||||
|
||||
|
||||
|
||||
def confirm_match(match_id :int, player_id :int):
|
||||
data_api.confirm_match(match_id)
|
||||
calculate_match(match_id)
|
||||
ui.navigate.reload() # Lädt die Seite neu, um die Karte zu aktualisieren
|
||||
ui.notify("Spiel bestätigt. Berechnung wird durchgeführt.", color="positiv")
|
||||
|
||||
|
||||
def delete_match(match_id :int, player_id :int):
|
||||
data_api.delete_match(match_id, player_id)
|
||||
ui.navigate.reload() # Lädt die Seite neu, um die Karte zu aktualisieren
|
||||
ui.notify("Spiel abgelehnt und gelöscht!", color="warning")
|
||||
|
||||
105
gui/_templates/player_card.py
Normal file
105
gui/_templates/player_card.py
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
from gui.common import *
|
||||
|
||||
|
||||
class PlayerCard:
|
||||
def __init__(self, display_name: str, discord_name: str, mmr: int,
|
||||
discord_avatar_url: str = None, discord_id: str = None,
|
||||
games_in_system: int = 0, wins: int = 0, loss: int = 0, avv_points: int = 0):
|
||||
self.display_name = display_name
|
||||
self.discord_name = discord_name
|
||||
self.mmr = mmr
|
||||
self.discord_avatar_url = discord_avatar_url
|
||||
self.discord_id = discord_id
|
||||
self.games_in_system = games_in_system
|
||||
self.wins = wins
|
||||
self.loss = loss
|
||||
self.avv_points = avv_points
|
||||
self.build_content()
|
||||
|
||||
def build_content(self):
|
||||
pass
|
||||
|
||||
def _discord_content(self, avatar_url, display_name, discord_name, short_len=15):
|
||||
short_name = display_name[:short_len] + '...' if len(display_name) > short_len else display_name
|
||||
short_discord = discord_name[:short_len] + '...' if len(discord_name) > short_len else discord_name
|
||||
return avatar_url, short_name, short_discord
|
||||
|
||||
class DesktopPlayerCard(PlayerCard):
|
||||
def build_content(self):
|
||||
avatar_url, short_name, short_discord = self._discord_content(
|
||||
self.discord_avatar_url, self.display_name, self.discord_name, short_len=25
|
||||
)
|
||||
|
||||
with ui.card().classes("shrink-0 grow-0 no-shadow bg-zinc-900 w-[750px] h-[190px] overflow-hidden p-4"):
|
||||
# REIHE 1: Discord Card Links, Achievements Rechts
|
||||
with ui.row().classes('w-full items-center justify-between flex-nowrap mb-4'):
|
||||
if self.discord_id:
|
||||
with ui.link(target=f'https://discord.com/users/{self.discord_id}', new_tab=True).classes('no-underline'):
|
||||
self._discord_block(avatar_url, short_name, short_discord, desktop=True)
|
||||
else:
|
||||
self._discord_block(avatar_url, short_name, short_discord, desktop=True)
|
||||
|
||||
# Achievments
|
||||
with ui.row().classes('items-center gap-3 shrink-0'):
|
||||
ui.image('gui/pictures/achievments/AchievementIcon_4_1Sieg.png').classes('w-15')
|
||||
ui.image('gui/pictures/achievments/AchievementIcon_4_1Sieg.png').classes('w-15')
|
||||
ui.image('gui/pictures/achievments/AchievementIcon_4_1Sieg.png').classes('w-15')
|
||||
|
||||
# REIHE 2: Stats
|
||||
with ui.row().classes('w-full items-center gap-10'):
|
||||
ui.label(f'{self.mmr} MMR').classes('font-bold text-lg')
|
||||
ui.label(f'{self.games_in_system} Games').classes('font-bold text-lg')
|
||||
ui.label(f'{self.wins} Wins').classes('font-bold text-lg')
|
||||
ui.label(f'{self.loss} Losses').classes('font-bold text-lg')
|
||||
ui.label(f'{self.avv_points} Ø pts per Game').classes('font-bold text-lg')
|
||||
|
||||
|
||||
def _discord_block(self, avatar_url, short_name, short_discord, desktop=True):
|
||||
with ui.card().classes("w-110 bg-indigo-950 no-shadow"):
|
||||
with ui.row().classes('items-center gap-3 flex-nowrap w-full'):
|
||||
if avatar_url:
|
||||
ui.image(avatar_url).classes('w-20 h-20 rounded-full border-red-900 border-2 shrink-0')
|
||||
with ui.column().classes('gap-0 min-w-0 flex-1'):
|
||||
ui.label(short_name).classes('text-2xl font-bold whitespace-nowrap truncate')
|
||||
ui.label(short_discord).classes('text-sm opacity-60 whitespace-nowrap truncate')
|
||||
|
||||
|
||||
class MobilePlayerCard(PlayerCard):
|
||||
def build_content(self):
|
||||
avatar_url, short_name, short_discord = self._discord_content(
|
||||
self.discord_avatar_url, self.display_name, self.discord_name, short_len=12
|
||||
)
|
||||
|
||||
with ui.card().classes("w-full no-shadow bg-zinc-900 min-w-0 overflow-hidden"):
|
||||
# Flex-Container für alles
|
||||
with ui.row().classes('items-center gap-6 flex-nowrap w-full justify-between'):
|
||||
|
||||
# 2. DISCORD CARD
|
||||
if self.discord_id:
|
||||
# w-full hier entfernen, damit der Link nur so breit ist wie die Karte
|
||||
with ui.link(target=f'https://discord.com/users/{self.discord_id}', new_tab=True).classes('no-underline'):
|
||||
self._discord_block(avatar_url, short_name, short_discord)
|
||||
else:
|
||||
self._discord_block(avatar_url, short_name, short_discord)
|
||||
|
||||
# 1. ACHIEVEMENTS
|
||||
with ui.column().classes('items-center gap-1 shrink-0'):
|
||||
ui.image('gui/pictures/achievments/AchievementIcon_4_1Sieg.png').classes('w-5')
|
||||
ui.image('gui/pictures/achievments/AchievementIcon_4_1Sieg.png').classes('w-5')
|
||||
ui.image('gui/pictures/achievments/AchievementIcon_4_1Sieg.png').classes('w-5')
|
||||
|
||||
# 3. Stats
|
||||
with ui.row().classes('w-full justify-center gap-4 mt-2'):
|
||||
ui.label(f'{self.mmr} MMR').classes('text-xs')
|
||||
ui.label(f'{self.games_in_system} Spiele').classes('text-xs')
|
||||
ui.label(f'{self.avv_points} Ø pts.').classes('text-xs')
|
||||
|
||||
|
||||
def _discord_block(self, avatar_url, short_name, short_discord, desktop=False):
|
||||
with ui.card().classes("w-fit min-w-[180px] bg-indigo-950 no-shadow"):
|
||||
with ui.row().classes('items-center gap-3 min-w-0'):
|
||||
if avatar_url:
|
||||
ui.image(avatar_url).classes('w-9 h-9 rounded-full border-red-900 border-2 shrink-0')
|
||||
with ui.column().classes('gap-0 min-w-0'):
|
||||
ui.label(short_name).classes('text-sm font-bold whitespace-nowrap truncate')
|
||||
ui.label(short_discord).classes('text-xs opacity-60 whitespace-nowrap truncate')
|
||||
246
gui/_templates/player_match_history.py
Normal file
246
gui/_templates/player_match_history.py
Normal file
|
|
@ -0,0 +1,246 @@
|
|||
from gui.common import *
|
||||
from data import data_api
|
||||
|
||||
|
||||
# ─── Hilfsfunktion ─────────────────────────────────────────────
|
||||
def fmt_signed(value, pending):
|
||||
"""Formatiert einen Zahlenwert mit explizitem Vorzeichen.
|
||||
|
||||
- Bei pending=True: 'Ausstehend' (Match noch nicht gewertet)
|
||||
- Positiv (+5.2): '+5.2' (Pluszeichen wird explizit dazu)
|
||||
- Negativ (-3.1): '-3.1' (Minuszeichen ist schon im Wert)
|
||||
- None: '0'
|
||||
"""
|
||||
if pending:
|
||||
return 'Ausstehend'
|
||||
if value is None:
|
||||
return '0'
|
||||
if value >= 0:
|
||||
return f'+{value}'
|
||||
return f'{value}'
|
||||
|
||||
|
||||
# ─── StatRow: Eine einzelne Match-Card ─────────────────────────
|
||||
class StatRow:
|
||||
"""Eine einzelne Karten-Zeile für ein Match.
|
||||
|
||||
Wird mit einem Daten-Dictionary gefüttert und baut
|
||||
entweder eine Desktop- oder Mobile-Ansicht.
|
||||
"""
|
||||
|
||||
def __init__(self, data: dict, compact: bool = False):
|
||||
"""
|
||||
Args:
|
||||
data: Dictionary mit den Match-Daten
|
||||
compact: True = Mobile-Ansicht (weniger Details)
|
||||
"""
|
||||
self.data = data
|
||||
self.compact = compact
|
||||
self.build()
|
||||
|
||||
def build(self):
|
||||
if self.compact:
|
||||
self._build_mobile()
|
||||
else:
|
||||
self._build_desktop()
|
||||
|
||||
# --- Hilfsmethoden für Farben ---
|
||||
def _color_for_value(self, value_str: str) -> str:
|
||||
"""Gibt CSS-Klassen für MMR-Werte zurück (grün/rot/grau)."""
|
||||
if value_str == 'Ausstehend':
|
||||
return 'text-gray-400 italic'
|
||||
if value_str.startswith('+'):
|
||||
return 'text-green-500 font-bold'
|
||||
if value_str.startswith('-'):
|
||||
return 'text-red-500 font-bold'
|
||||
return 'text-white'
|
||||
|
||||
def _result_color(self, result: str) -> str:
|
||||
"""Gibt CSS-Klassen für Gewonnen/Verloren zurück."""
|
||||
if result == 'Gewonnen':
|
||||
return 'text-green-500 font-bold'
|
||||
if result == 'Verloren':
|
||||
return 'text-red-500 font-bold'
|
||||
return 'text-yellow-500 font-bold'
|
||||
|
||||
# --- Desktop-Ansicht ---
|
||||
def _build_desktop(self):
|
||||
"""Desktop: Alle Felder nebeneinander in einer Card."""
|
||||
with ui.card().classes('w-full bg-zinc-900 border border-zinc-700'):
|
||||
with ui.row().classes('w-full items-center no-wrap'):
|
||||
|
||||
# Datum
|
||||
ui.label(self.data['date']).classes('w-24 text-gray-300')
|
||||
|
||||
# Gegner Name (mit eigenem Score des Gegners)
|
||||
ui.label(self.data['opponent']).classes('flex-grow text-white')
|
||||
|
||||
# Spielsystem
|
||||
ui.label(self.data['system']).classes('w-28 text-gray-300')
|
||||
|
||||
# Ergebnis (Gewonnen / Verloren)
|
||||
ui.label(self.data['result']).classes(
|
||||
f'w-24 {self._result_color(self.data["result"])}'
|
||||
)
|
||||
|
||||
# Elo Faktor
|
||||
ui.label(self.data['elo']).classes(
|
||||
f'w-20 text-right {self._color_for_value(self.data["elo"])}'
|
||||
)
|
||||
|
||||
# Rost Faktor
|
||||
ui.label(self.data['rust']).classes(
|
||||
f'w-20 text-right {self._color_for_value(self.data["rust"])}'
|
||||
)
|
||||
|
||||
# Basis MMR
|
||||
ui.label(str(self.data['basis'])).classes(
|
||||
'w-20 text-right text-gray-300'
|
||||
)
|
||||
|
||||
# Khorne
|
||||
ui.label(self.data['khorne']).classes(
|
||||
f'w-16 text-right {self._color_for_value(self.data["khorne"])}'
|
||||
)
|
||||
|
||||
# Tzeentch
|
||||
ui.label(self.data['tzeentch']).classes(
|
||||
f'w-20 text-right {self._color_for_value(self.data["tzeentch"])}'
|
||||
)
|
||||
|
||||
# Slaanesh
|
||||
ui.label(self.data['slaanesh']).classes(
|
||||
f'w-20 text-right {self._color_for_value(self.data["slaanesh"])}'
|
||||
)
|
||||
|
||||
# MMR Gesamt
|
||||
ui.label(self.data['mmr']).classes(
|
||||
f'w-24 text-right {self._color_for_value(self.data["mmr"])}'
|
||||
)
|
||||
|
||||
# --- Mobile-Ansicht ---
|
||||
def _build_mobile(self):
|
||||
"""Mobile: Kompakte Ansicht, weniger Details."""
|
||||
with ui.card().classes('w-full bg-zinc-900 border border-zinc-700'):
|
||||
with ui.column().classes('w-full gap-1'):
|
||||
|
||||
# Zeile 1: Datum + Spielsystem + Ergebnis
|
||||
with ui.row().classes('w-full items-center justify-between'):
|
||||
ui.label(self.data['date']).classes('text-gray-400 text-sm')
|
||||
ui.label(self.data['system']).classes('text-gray-400 text-sm')
|
||||
ui.label(self.data['result']).classes(
|
||||
f'text-sm {self._result_color(self.data["result"])}'
|
||||
)
|
||||
|
||||
# Zeile 2: Gegner Name
|
||||
ui.label(self.data['opponent']).classes('text-white text-sm')
|
||||
|
||||
# Zeile 3: MMR Gesamt (die wichtigste Zahl)
|
||||
with ui.row().classes('items-center gap-2'):
|
||||
ui.label('MMR:').classes('text-gray-400 text-sm')
|
||||
ui.label(self.data['mmr']).classes(
|
||||
f'text-sm {self._color_for_value(self.data["mmr"])}'
|
||||
)
|
||||
|
||||
# Detail-Button (Platzhalter für später)
|
||||
ui.button('Details', on_click=lambda: None).props('flat dense')
|
||||
|
||||
|
||||
# ─── MatchHistory: Basisklasse ─────────────────────────────────
|
||||
class MatchHistory:
|
||||
"""Basisklasse — extrahiert die Match-Daten aus der DB."""
|
||||
|
||||
def __init__(self, player_id: int):
|
||||
self.player_id = player_id
|
||||
self.build_content()
|
||||
|
||||
def build_content(self):
|
||||
pass
|
||||
|
||||
def _extract_match_data(self, match: dict, index: int) -> dict:
|
||||
"""Extrahiert ein Match-Dictionary in ein sauberes Daten-Dict.
|
||||
|
||||
Diese Methode wird von Desktop und Mobile GEMEINSAM genutzt,
|
||||
damit die Extraktions-Logik nicht doppelt geschrieben wird.
|
||||
"""
|
||||
is_player1 = match['player1_id'] == self.player_id
|
||||
pending = match['match_is_counted'] == 0
|
||||
|
||||
# ── Je nachdem ob wir player1 oder player2 sind ──
|
||||
if is_player1:
|
||||
opponent = f"{match['p2_display']} aka {match['p2_discord']}"
|
||||
my_score = match['score_player1']
|
||||
opp_score = match['score_player2']
|
||||
mmr_base = match["player1_base_change"]
|
||||
mmr_change = match['player1_mmr_change']
|
||||
khorne = match['player1_khorne']
|
||||
tzeentch = match['player1_tzeentch']
|
||||
slaanesh = match['player1_slaanesh']
|
||||
else:
|
||||
opponent = f"{match['p1_display']} aka {match['p1_discord']}"
|
||||
my_score = match['score_player2']
|
||||
opp_score = match['score_player1']
|
||||
mmr_base = match["player2_base_change"]
|
||||
mmr_change = match['player2_mmr_change']
|
||||
khorne = match['player2_khorne']
|
||||
tzeentch = match['player2_tzeentch']
|
||||
slaanesh = match['player2_slaanesh']
|
||||
|
||||
# ── Ergebnis bestimmen ──
|
||||
if my_score > opp_score:
|
||||
result = "Gewonnen"
|
||||
elif my_score < opp_score:
|
||||
result = "Verloren"
|
||||
else:
|
||||
result = "Unentschieden"
|
||||
|
||||
elo_factor = match['elo_factor']
|
||||
rust_factor = match['rust_factor']
|
||||
|
||||
# ── Sauberes Dictionary zurückgeben ──
|
||||
return {
|
||||
'id': index,
|
||||
'date': str(match['played_at'])[:10],
|
||||
'system': match['gamesystem_name'],
|
||||
'score': f"{my_score}:{opp_score}",
|
||||
'opponent': f"{opponent} ({opp_score})",
|
||||
'result': result,
|
||||
'basis': mmr_base,
|
||||
'elo': fmt_signed(round(elo_factor, 2) if elo_factor is not None else None, pending),
|
||||
'rust': fmt_signed(round(rust_factor, 2) if rust_factor is not None else None, pending),
|
||||
'khorne': fmt_signed(khorne, pending),
|
||||
'tzeentch': fmt_signed(tzeentch, pending),
|
||||
'slaanesh': fmt_signed(slaanesh, pending),
|
||||
'mmr': fmt_signed(mmr_change, pending),
|
||||
'pending': pending,
|
||||
}
|
||||
|
||||
|
||||
# ─── DesktopMatchHistory ───────────────────────────────────────
|
||||
class DesktopMatchHistory(MatchHistory):
|
||||
def build_content(self):
|
||||
raw_matches = data_api.get_match_history_log(self.player_id)
|
||||
|
||||
if not raw_matches:
|
||||
ui.label("Keine Spiele gefunden.").classes("text-gray-400 italic")
|
||||
return
|
||||
|
||||
with ui.column().classes('w-full gap-2'):
|
||||
for i, match in enumerate(raw_matches):
|
||||
data = self._extract_match_data(match, i)
|
||||
StatRow(data, compact=False)
|
||||
|
||||
|
||||
# ─── MobileMatchHistory ────────────────────────────────────────
|
||||
class MobileMatchHistory(MatchHistory):
|
||||
def build_content(self):
|
||||
raw_matches = data_api.get_match_history_log(self.player_id)
|
||||
|
||||
if not raw_matches:
|
||||
ui.label("Keine Spiele gefunden.").classes("text-gray-400 italic")
|
||||
return
|
||||
|
||||
with ui.column().classes('w-full gap-2'):
|
||||
for i, match in enumerate(raw_matches):
|
||||
data = self._extract_match_data(match, i)
|
||||
StatRow(data, compact=True)
|
||||
79
gui/_templates/profile_menue.py
Normal file
79
gui/_templates/profile_menue.py
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
from gui.common import *
|
||||
from data import data_api
|
||||
|
||||
|
||||
class ProfileMenu:
|
||||
"""Base template for the profile popup menu in the header.
|
||||
|
||||
Shows the Discord avatar as a clickable button. On click a popup
|
||||
menu (Quasar QMenu) opens where menu items can be added.
|
||||
"""
|
||||
def __init__(self, logout_callback=None):
|
||||
# Daten aus dem Session-Cache lesen
|
||||
self.discord_avatar_url = app.storage.user.get('discord_avatar_url')
|
||||
self.discord_name = app.storage.user.get('discord_name')
|
||||
self.discord_id = app.storage.user.get('discord_id')
|
||||
self.is_logged_in = bool(app.storage.user.get('authenticated', False))
|
||||
self.logout_callback = logout_callback
|
||||
|
||||
self.menu = None
|
||||
self.build_content()
|
||||
|
||||
def build_content(self):
|
||||
pass
|
||||
|
||||
def add_item(self, text, on_click=None, auto_close=True):
|
||||
"""Fügt einen Menü-Eintrag nachträglich hinzu.
|
||||
Kann nach der Konstruktion aufgerufen werden, um dynamisch
|
||||
Buttons einzufügen."""
|
||||
with self.menu:
|
||||
ui.menu_item(text, on_click, auto_close=auto_close)
|
||||
|
||||
def add_separator(self):
|
||||
"""Fügt eine Trennlinie zwischen Menü-Einträgen ein."""
|
||||
with self.menu:
|
||||
ui.separator()
|
||||
|
||||
|
||||
class DesktopProfileMenu(ProfileMenu):
|
||||
def build_content(self):
|
||||
with ui.row().classes('items-center gap-3 shrink-0'):
|
||||
# Optional: Name links vom Avatar (Desktop hat Platz)
|
||||
if self.discord_name:
|
||||
ui.label(self.discord_name).classes('text-sm opacity-80')
|
||||
|
||||
# Avatar als klickbarer Button -> Menu öffnet bei Klick
|
||||
with ui.button().props('flat round dense'):
|
||||
if self.discord_avatar_url:
|
||||
ui.image(self.discord_avatar_url).classes('w-10 h-10 rounded-full')
|
||||
else:
|
||||
ui.icon('account_circle').classes('text-3xl')
|
||||
|
||||
# Popup-Menu (INNERHALB des Buttons -> verankert automatisch)
|
||||
with ui.menu() as self.menu:
|
||||
self._build_menu_items()
|
||||
|
||||
def _build_menu_items(self):
|
||||
ui.menu_item('Profile', lambda: ui.navigate.to('/profile'))
|
||||
ui.menu_item('Settings', lambda: ui.navigate.to('/settings'))
|
||||
ui.separator()
|
||||
if self.is_logged_in:
|
||||
ui.menu_item('Logout', self.logout_callback)
|
||||
|
||||
|
||||
class MobileProfileMenu(ProfileMenu):
|
||||
def build_content(self):
|
||||
with ui.button().props('flat round dense'):
|
||||
if self.discord_avatar_url:
|
||||
ui.image(self.discord_avatar_url).classes('w-8 h-8 rounded-full')
|
||||
else:
|
||||
ui.icon('account_circle').classes('text-2xl')
|
||||
|
||||
with ui.menu() as self.menu:
|
||||
self._build_menu_items()
|
||||
|
||||
def _build_menu_items(self):
|
||||
ui.menu_item('Profile', lambda: ui.navigate.to('/profile'))
|
||||
ui.separator()
|
||||
if self.is_logged_in:
|
||||
ui.menu_item('Logout', self.logout_callback)
|
||||
61
gui/_templates/seasonselector.py
Normal file
61
gui/_templates/seasonselector.py
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
from gui.common import *
|
||||
|
||||
|
||||
class SeasonSelector():
|
||||
def __init__(self, seasons: dict, current_season: int, on_change=None): # ← on_change hinzufügen
|
||||
self.seasons = seasons
|
||||
self.current_season = current_season
|
||||
self.on_change = on_change
|
||||
self.build_content()
|
||||
|
||||
def build_content(self):
|
||||
pass
|
||||
|
||||
|
||||
class DesktopSeasonSelector(SeasonSelector):
|
||||
def build_content(self):
|
||||
seasons = sorted(self.seasons, key=lambda s: s['season_number'])
|
||||
|
||||
if not seasons:
|
||||
return
|
||||
|
||||
self.season_index = len(seasons) - 1
|
||||
self.seasons = seasons
|
||||
|
||||
with ui.card().classes("w-full"):
|
||||
with ui.row().classes("w-full justify-center items-center gap-15"):
|
||||
self.btn_prev = ui.button(text="<", on_click=lambda: self._navigate(-1))
|
||||
self.season_label = (
|
||||
ui.label(text="")
|
||||
.classes("text-bold text-3xl text-center")
|
||||
.style("min-width: 400px;") # ← feste Breite, anpassen nach Bedarf
|
||||
)
|
||||
self.btn_next = ui.button(text=">", on_click=lambda: self._navigate(1))
|
||||
|
||||
self._update_display()
|
||||
|
||||
def _navigate(self, direction: int):
|
||||
self.season_index = max(0, min(len(self.seasons) - 1, self.season_index + direction))
|
||||
self._update_display()
|
||||
|
||||
def _update_display(self):
|
||||
current = self.seasons[self.season_index]
|
||||
self.season_label.set_text(current['name'])
|
||||
self.current_season_id = current['id']
|
||||
self.btn_prev.set_enabled(self.season_index > 0)
|
||||
self.btn_next.set_enabled(self.season_index < len(self.seasons) - 1)
|
||||
|
||||
if self.on_change: # ← das fehlt!
|
||||
self.on_change(self.current_season_id)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class MobileSeasonSelector(SeasonSelector):
|
||||
def build_content(self):
|
||||
with ui.card().classes("W-full"):
|
||||
pass
|
||||
|
||||
|
||||
|
||||
93
gui/_templates/stats_1.py
Normal file
93
gui/_templates/stats_1.py
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
from gui.common import *
|
||||
from data import data_api
|
||||
|
||||
|
||||
class Stats_1():
|
||||
def __init__(self, player_stats: dict):
|
||||
self.player_stats = player_stats
|
||||
self.build_content() # ← kein self als Argument!
|
||||
|
||||
def build_content(self):
|
||||
pass
|
||||
|
||||
|
||||
class DesktopStats_1(Stats_1):
|
||||
def build_content(self):
|
||||
if not self.player_stats:
|
||||
ui.label("Keine Statistiken für diese Season.").classes("text-gray-400")
|
||||
return
|
||||
|
||||
s = self.player_stats
|
||||
|
||||
with ui.card().classes("w-full"):
|
||||
ui.label(f"{s['gamesystem_name']}").classes("text-2xl text-bold mb-4")
|
||||
|
||||
# CSS Grid: Rang links (row-span 2), Stats rechts in 2 Zeilen
|
||||
with ui.element('div').style(
|
||||
"display: grid;"
|
||||
"grid-template-columns: 10rem 1fr;"
|
||||
"grid-template-rows: 1fr 1fr;"
|
||||
"gap: 1rem;"
|
||||
"width: 100%;"
|
||||
):
|
||||
# --- Rang Card (links, überspannt beide Zeilen) ---
|
||||
with ui.card().style(
|
||||
"grid-column: 1;"
|
||||
"grid-row: 1 / 3;"
|
||||
"display: flex;"
|
||||
"flex-direction: column;"
|
||||
"align-items: center;"
|
||||
"justify-content: center;"
|
||||
):
|
||||
ui.label(str(s['rank'])).classes("text-5xl text-bold")
|
||||
ui.label("Rang").classes("text-gray-400 text-lg")
|
||||
|
||||
# --- Zeile 1 ---
|
||||
with ui.element('div').style(
|
||||
"grid-column: 2;"
|
||||
"grid-row: 1;"
|
||||
"display: flex;"
|
||||
"gap: 1rem;"
|
||||
):
|
||||
with ui.card().classes("flex-1 items-center justify-center"):
|
||||
ui.label(str(s['mmr'])).classes("text-3xl text-bold")
|
||||
ui.label("MMR").classes("text-gray-400 text-sm")
|
||||
|
||||
with ui.card().classes("flex-1 items-center justify-center"):
|
||||
ui.label(str(s['games_in_system'])).classes("text-3xl text-bold")
|
||||
ui.label("Spiele").classes("text-gray-400 text-sm")
|
||||
|
||||
with ui.card().classes("flex-1 items-center justify-center"):
|
||||
ui.label(str(s['points'])).classes("text-3xl text-bold")
|
||||
ui.label("Punkte").classes("text-gray-400 text-sm")
|
||||
|
||||
# --- Zeile 2 ---
|
||||
with ui.element('div').style(
|
||||
"grid-column: 2;"
|
||||
"grid-row: 2;"
|
||||
"display: flex;"
|
||||
"gap: 1rem;"
|
||||
):
|
||||
with ui.card().classes("flex-1 items-center justify-center"):
|
||||
ui.label(str(s['avv_points'])).classes("text-3xl text-bold")
|
||||
ui.label("Ø Punkte/Spiel").classes("text-gray-400 text-sm")
|
||||
|
||||
if 'win_rate' in s and s['win_rate'] is not None:
|
||||
with ui.card().classes("flex-1 items-center justify-center"):
|
||||
ui.label(f"{s['win_rate']}%").classes("text-3xl text-bold")
|
||||
ui.label("Win Rate").classes("text-gray-400 text-sm")
|
||||
|
||||
if s.get('last_played'):
|
||||
with ui.card().classes("flex-1 items-center justify-center"):
|
||||
ui.label(s['last_played'].strftime("%d.%m.%Y")).classes("text-3xl text-bold")
|
||||
ui.label("Zuletzt gespielt").classes("text-gray-400 text-sm")
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class MobileStats_1(Stats_1):
|
||||
def build_content(self):
|
||||
pass
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
from nicegui import ui, app
|
||||
from data import database, data_api
|
||||
from gui import gui_style
|
||||
from wood import logger
|
||||
from gui import main_gui
|
||||
|
||||
def setup_routes():
|
||||
@ui.page('/admin', dark=True)
|
||||
def admin_page():
|
||||
gui_style.apply_design()
|
||||
|
||||
with ui.header().classes('items-center justify-between bg-zinc-900 p-4 shadow-lg'):
|
||||
ui.button("Zurück", on_click= lambda: ui.navigate.to("/") )
|
||||
|
||||
if app.storage.user.get('authenticated', False):
|
||||
with ui.row().classes("w-full"):
|
||||
ui.button(text= "test", on_click=lambda: data_api.create_random_dummy_match(2))
|
||||
ui.button(icon="refresh", on_click= lambda: ui.navigate.to("/admin") )
|
||||
with ui.card().classes("w-full"):
|
||||
ui.label("System Audit Log").classes('text-2xl font-bold text-white mb-4')
|
||||
|
||||
# Daten abrufen
|
||||
log_data = logger.get_full_log()
|
||||
|
||||
# Tabelle aufbauen
|
||||
columns = [
|
||||
{'name': 'time', 'label': 'Time', 'field': 'timestamp', 'align': 'left', 'sortable': True},
|
||||
{'name': 'file', 'label': 'Datei', 'field': 'file', 'align': 'left'},
|
||||
{'name': 'source', 'label': 'Quelle', 'field': 'source', 'align': 'left'},
|
||||
{'name': 'details', 'label': 'Details', 'field': 'details', 'align': 'left'}
|
||||
]
|
||||
|
||||
if len(log_data) > 0:
|
||||
# Millisekunden vom Zeitstempel abschneiden [:19]
|
||||
for row in log_data:
|
||||
row['timestamp'] = str(row['timestamp'])[:19]
|
||||
ui.table(columns=columns, rows=log_data, row_key='id').classes('w-full bg-zinc-900 text-gray-300')
|
||||
else:
|
||||
ui.label("Das Logbuch ist leer.").classes('text-gray-500 italic')
|
||||
26
gui/base_page.py
Normal file
26
gui/base_page.py
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
# gui/base_page.py
|
||||
from nicegui import ui, app
|
||||
from starlette.requests import Request
|
||||
|
||||
def apply_design():
|
||||
ui.add_css('body { background-color: #171721; }')
|
||||
ui.dark_mode(True)
|
||||
ui.colors(primary='#B80B0B', secondary='#FF3333', accent='#2078D4',
|
||||
positive='#188C42', negative='#E31919', info='#939393', warning='#f59e0b')
|
||||
|
||||
def is_mobile(request: Request) -> bool:
|
||||
user_agent = request.headers.get("user-agent", "").lower()
|
||||
return any(k in user_agent for k in ["mobile", "android", "iphone", "ipod", "blackberry", "windows phone"])
|
||||
|
||||
def base_page(request: Request, title: str):
|
||||
apply_design()
|
||||
|
||||
# Zentrale Login-Prüfung
|
||||
if not app.storage.user.get('authenticated', False):
|
||||
# Was wird angezeigt wenn ein User NICHT eingeloggt ist und versucht eine Seite aufzurufen.
|
||||
apply_design()
|
||||
ui.label('Access Denied. Please Login').classes('text-red-500')
|
||||
ui.button('Login', on_click=lambda: ui.navigate.to('/login'))
|
||||
return False
|
||||
|
||||
return True
|
||||
10
gui/common.py
Normal file
10
gui/common.py
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# gui/common.py
|
||||
from gui.base_page import base_page, is_mobile
|
||||
from gui import _templates as templates
|
||||
from gui._templates import *
|
||||
|
||||
# Wenn du noch mehr hast, was jede Seite braucht, pack es hier rein:
|
||||
from nicegui import ui, app
|
||||
from starlette.requests import Request
|
||||
from wood.logger import log as Log
|
||||
from data import data_api
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
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: #171721; }')
|
||||
# 1. Dark Mode aktivieren
|
||||
ui.dark_mode(True)
|
||||
ui.colors(
|
||||
primary='#B80B0B', # Hauptfarbe (z.B. für Standard-Buttons) Wenn keine Farbe angegeben wird, ist es diese Farbe
|
||||
secondary='#FF3333', # Zweitfarbe
|
||||
accent='#2078D4', # Akzentfarbe
|
||||
positive='#188C42', # Farbe für Erfolg (Grün)
|
||||
negative='#E31919', # Farbe für Fehler/Abbruch (Rot)
|
||||
info='#939393', # Info-Farbe
|
||||
warning='#f59e0b', # Warn-Farbe (Orange)
|
||||
# Farben für Texte
|
||||
normaltext="#F7F7F7",
|
||||
accenttext="#2078D4",
|
||||
infotext="#A8A8A8"
|
||||
)
|
||||
|
||||
|
|
@ -1,8 +1,7 @@
|
|||
import json
|
||||
from pathlib import Path
|
||||
from nicegui import ui
|
||||
from gui.common import *
|
||||
from fastapi import Request
|
||||
from gui import gui_style
|
||||
|
||||
# JSON laden - liegt laut Screenshot direkt in gui/
|
||||
_JSON_PATH = Path(__file__).resolve().parent / 'imprint.json'
|
||||
|
|
@ -32,7 +31,6 @@ def _get_locale_options() -> list[str]:
|
|||
def setup_routes():
|
||||
@ui.page('/info', dark=True)
|
||||
def info_page(request: Request):
|
||||
gui_style.apply_design()
|
||||
|
||||
lang = _get_locale(request)
|
||||
locale_data = imprint_data['locales'][lang]
|
||||
|
|
|
|||
|
|
@ -1,91 +1,97 @@
|
|||
from nicegui import ui, app
|
||||
from gui import gui_style
|
||||
from gui.common import *
|
||||
|
||||
from data import data_api
|
||||
from gui.info_text import info_system
|
||||
from gui.templates.leaderboard import Leaderboard
|
||||
|
||||
def setup_routes():
|
||||
@ui.page('/statistic/{system_name}', dark=True)
|
||||
def gamesystem_statistic_page(system_name: str):
|
||||
|
||||
if not app.storage.user.get('authenticated', False):
|
||||
ui.navigate.to('/')
|
||||
@ui.page('/stats/{system_name}', dark=True)
|
||||
def gamesystem_statistic_page(request: Request, system_name: str):
|
||||
# 1. Wrapper aufrufen (Login-Check & Styling)
|
||||
if not base_page(request, f"{system_name} Stats"):
|
||||
return
|
||||
|
||||
gui_style.apply_design()
|
||||
|
||||
player_id = app.storage.user.get('db_id')
|
||||
# 2. Daten laden (Das bleibt in der Seite)
|
||||
player_id = app.storage.user.get('player_id')
|
||||
system_id = data_api.get_gamesystem_id_by_name(system_name)
|
||||
print(player_id, system_id)
|
||||
|
||||
player_stats = data_api.get_player_statistic(player_id, system_id)
|
||||
active_players = data_api.get_all_players_from_system(system_name)
|
||||
gamesystem_data = data_api.get_gamesystem_data(system_id)
|
||||
|
||||
# Header
|
||||
with ui.header().classes('items-center justify-between bg-zinc-900 p-4 shadow-lg'):
|
||||
ui.button(icon="arrow_back", on_click=lambda: ui.navigate.to('/')).props("round")
|
||||
ui.button("Spiel eintragen", on_click=lambda: ui.navigate.to(f'/add-match/{system_name}'))
|
||||
seasons = data_api.get_seasons_by_system(system_id)
|
||||
current_season = data_api.get_current_season_id(system_id)
|
||||
player_stats = data_api.get_player_statistic(player_id, system_id, current_season)
|
||||
|
||||
# Die Tabs werden aus dieser Liste hier generiert mit den Namen die hier angegeben sind. Sie können auch abgefragt werden.
|
||||
tab_names = ['Übersicht', 'Rangliste', "Spiel eintragen"]
|
||||
|
||||
with ui.column().classes('w-full items-center justify-center mt-10'):
|
||||
ui.label(f'Deine Statistik in {system_name}').classes('text-3xl justify-center font-bold text-normaltext')
|
||||
# 3. Desktop OR Mobile
|
||||
def show_desktop():
|
||||
if not base_page(request, f"{system_name} Stats"):
|
||||
return
|
||||
header = templates.DesktopHeader(f"Stats für {system_name}", tab_names=tab_names, request=request)
|
||||
|
||||
# --- BLOCK 1 (MMR & Rang | Rangliste) ---
|
||||
with ui.element('div').classes("w-full grid grid-cols-1 lg:grid-cols-3 gap-4 mt-4"):
|
||||
with ui.column().classes("w-full gap-4"):
|
||||
with ui.card().classes("w-full items-center justify-center text-center"):
|
||||
with ui.row().classes("w-full items-center text-center"):
|
||||
ui.label("MMR Punkte: ").classes('justify-center text-2xl font-bold text-normaltext')
|
||||
ui.space()
|
||||
info_system.create_info_button("mmr_info")
|
||||
ui.label(str(player_stats["mmr"])).classes('text-4xl font-bold text-accent')
|
||||
# Content-Bereich: Tab-Panels
|
||||
with ui.tab_panels(header.tabs, value=tab_names[0]).classes('w-full mt-4'):
|
||||
for name in tab_names:
|
||||
with ui.tab_panel(name):
|
||||
match name:
|
||||
case 'Übersicht':
|
||||
stats_container = None
|
||||
def on_season_change(season_id: int):
|
||||
if stats_container is None: # ← Callback feuert zu früh → ignorieren
|
||||
return
|
||||
stats_container.clear()
|
||||
with stats_container:
|
||||
fresh_stats = data_api.get_player_statistic(player_id, system_id, season_id)
|
||||
templates.DesktopStats_1(fresh_stats)
|
||||
|
||||
with ui.card().classes("w-full items-center justify-center text-center"):
|
||||
with ui.row().classes("w-full items-center text-center"):
|
||||
ui.label("Rang: ").classes('justify-center text-2xl font-bold text-normaltext')
|
||||
ui.space()
|
||||
info_system.create_info_button("rang_info")
|
||||
ui.label(str(data_api.get_player_rank(player_id,system_id))).classes('text-4xl font-bold text-blue-100')
|
||||
# 1. Season Selector (oben)
|
||||
templates.DesktopSeasonSelector(
|
||||
seasons,
|
||||
current_season,
|
||||
on_change=on_season_change
|
||||
)
|
||||
|
||||
|
||||
with ui.card().classes("w-full lg:col-span-2 max-h-[50vh] overflow-y-auto"):
|
||||
ui.label("Liga Rangliste").classes("text-xl font-bold text-white mb-2 top-0 bg-zinc-900 z-10")
|
||||
Leaderboard(system_name)
|
||||
# 2. Stats Container (unten)
|
||||
stats_container = ui.column().classes("w-full")
|
||||
|
||||
# --- BLOCK 2 (5 Karten) ---
|
||||
with ui.card().classes("w-full"):
|
||||
with ui.element('div').classes("w-full grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"):
|
||||
# 3. Initial befüllen
|
||||
with stats_container:
|
||||
templates.DesktopStats_1(player_stats)
|
||||
|
||||
with ui.card().classes("items-center justify-center text-center"):
|
||||
ui.label("Spiele: ").classes('text-2xl font-bold')
|
||||
ui.label(str(player_stats["games_in_system"])).classes('text-4xl font-bold text-blue-100')
|
||||
case 'Rangliste':
|
||||
templates.DesktopLeaderboard(system_name)
|
||||
|
||||
case 'Spiel eintragen':
|
||||
templates.DesktopMatchForm(
|
||||
system_name= system_name,
|
||||
player1_id= player_id,
|
||||
active_players= active_players,
|
||||
min_points= gamesystem_data["min_score"],
|
||||
max_points= gamesystem_data["max_score"])
|
||||
|
||||
with ui.card().classes("items-center justify-center text-center"):
|
||||
ui.label("Ø Punkte pro Spiel: ").classes('text-2xl font-bold')
|
||||
ui.label(str(player_stats["avv_points"])).classes('text-4xl font-bold text-blue-100')
|
||||
def show_mobile():
|
||||
if not base_page(request, f"{system_name} Stats"):
|
||||
return
|
||||
header = templates.MobileHeader(f"Stats für {system_name}", tab_names=tab_names, request=request)
|
||||
|
||||
with ui.card().classes("items-center justify-center text-center"):
|
||||
ui.label("Win-Rate: ").classes('text-2xl font-bold')
|
||||
ui.label("-").classes('text-4xl font-bold text-blue-100')
|
||||
with ui.tab_panels(header.tabs, value=tab_names[0]).classes('w-full mt-4'):
|
||||
for name in tab_names:
|
||||
with ui.tab_panel(name):
|
||||
match name:
|
||||
case 'Übersicht':
|
||||
templates.DesktopSeasonSelector(seasons, current_season)
|
||||
templates.MobileStats_1(player_stats)
|
||||
case 'Rangliste':
|
||||
templates.MobileLeaderboard(system_name)
|
||||
case 'Spiel eintragen':
|
||||
templates.MobileMatchForm(
|
||||
system_name= system_name,
|
||||
player1_id= player_id,
|
||||
active_players= active_players,
|
||||
min_points= gamesystem_data["min_score"],
|
||||
max_points= gamesystem_data["max_score"])
|
||||
|
||||
with ui.card().classes("items-center justify-center text-center"):
|
||||
ui.label("Letztes Spiel am: ").classes('text-2xl font-bold')
|
||||
ui.label(str(player_stats["last_played"])).classes('text-4xl font-bold text-blue-100')
|
||||
|
||||
with ui.card().classes("items-center justify-center text-center"):
|
||||
ui.label("Win-Streak: ").classes('text-2xl font-bold')
|
||||
ui.label("-").classes('text-4xl font-bold text-blue-100')
|
||||
|
||||
# --- BLOCK 3 (3 Karten) ---
|
||||
with ui.card().classes("w-full"):
|
||||
with ui.element('div').classes("w-full grid grid-cols-1 md:grid-cols-3 lg:grid-cols-3 gap-4"):
|
||||
|
||||
with ui.card().classes("items-center justify-center text-center"):
|
||||
ui.label("Dein Nemesis: ").classes('text-2xl font-bold')
|
||||
ui.label(str(player_stats["nemesis_id"])).classes('text-4xl font-bold text-blue-100')
|
||||
|
||||
with ui.card().classes("items-center justify-center text-center"):
|
||||
ui.label("Meisten Spiele mit: ").classes('text-2xl font-bold')
|
||||
ui.label(str(player_stats["nemesis_id"])).classes('text-4xl font-bold text-blue-100')
|
||||
|
||||
with ui.card().classes("items-center justify-center text-center"):
|
||||
ui.label("Dein 'Prügelknabe': ").classes('text-2xl font-bold')
|
||||
ui.label(str(player_stats["pushover_id"])).classes('text-4xl font-bold text-blue-100')
|
||||
# Weiche entscheiden
|
||||
if is_mobile(request):
|
||||
show_mobile()
|
||||
else:
|
||||
show_desktop()
|
||||
|
|
|
|||
|
|
@ -1,24 +0,0 @@
|
|||
from nicegui import ui, app
|
||||
from gui import gui_style
|
||||
from data import data_api
|
||||
from gui.info_text import info_system
|
||||
|
||||
from gui.templates.leaderboard import Leaderboard
|
||||
|
||||
|
||||
|
||||
def setup_routes():
|
||||
@ui.page('/statistic/visitor/{systemname}', dark=True)
|
||||
def gamesystem_statistic_visitor_page(systemname: str):
|
||||
if not app.storage.user.get('authenticated', False):
|
||||
ui.navigate.to('/')
|
||||
return
|
||||
|
||||
gui_style.apply_design()
|
||||
|
||||
with ui.header().classes('items-center justify-between bg-zinc-900 p-4 shadow-lg'):
|
||||
ui.button(icon="arrow_back", on_click=lambda: ui.navigate.to('/')).props("round")
|
||||
|
||||
with ui.column().classes('w-full items-center justify-center mt-10'):
|
||||
ui.label(f'Die aktuelle Statistik in {systemname}').classes('text-3xl justify-center font-bold text-normaltext')
|
||||
Leaderboard(systemname)
|
||||
26
gui/login_gui.py
Normal file
26
gui/login_gui.py
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
from gui.common import *
|
||||
|
||||
from auth import discord_auth # Für get_auth_url()
|
||||
|
||||
def setup_routes():
|
||||
@ui.page('/login')
|
||||
def login_page():
|
||||
ui.label("Login Bereich").classes("text-h3")
|
||||
ui.button("Login mit Discord", on_click=lambda: ui.navigate.to(discord_auth.get_auth_url()))
|
||||
|
||||
@ui.page('/login/discord')
|
||||
async def discord_callback(code: str = None):
|
||||
if not code:
|
||||
ui.label("Kein Code erhalten.").classes("text-red-500")
|
||||
return
|
||||
|
||||
user_info, error = discord_auth.process_discord_login(code)
|
||||
|
||||
if error:
|
||||
ui.label(error).classes("text-red-500")
|
||||
else:
|
||||
player, d_id, d_name, avatar = user_info
|
||||
app.storage.user['authenticated'] = True
|
||||
app.storage.user["player_id"] = player["id"]
|
||||
app.storage.user["display_name"] = player["display_name"]
|
||||
ui.navigate.to('/')
|
||||
326
gui/main_gui.py
326
gui/main_gui.py
|
|
@ -1,300 +1,46 @@
|
|||
from nicegui import ui, app
|
||||
from data import database, data_api
|
||||
from gui import discord_login, gui_style
|
||||
from match_calculations import calc_match
|
||||
from gui.info_text import info_system
|
||||
# gui/league_statistic_gui.py
|
||||
from gui.common import *
|
||||
from data import data_api
|
||||
|
||||
def setup_routes(admin_discord_id):
|
||||
def setup_routes():
|
||||
@ui.page('/', dark=True)
|
||||
def home_page():
|
||||
gui_style.apply_design()
|
||||
def main_page(request: Request):
|
||||
# 1. Wrapper aufrufen (Login-Check & Styling)
|
||||
if not base_page(request, title=""):
|
||||
return
|
||||
|
||||
# --- SICHERHEITS-CHECK (Hassan, DER TÜRSTEHER) ---
|
||||
if app.storage.user.get('authenticated', False):
|
||||
db_id = app.storage.user.get('db_id')
|
||||
discord_id = app.storage.user.get('discord_id')
|
||||
# 2. Daten laden
|
||||
player_id = app.storage.user.get('player_id')
|
||||
game_systems = data_api.get_gamesystems_data()
|
||||
open_games = data_api.get_open_matches(player_id)
|
||||
|
||||
# Fehlt die Discord-ID (altes Cookie) ODER sagt die Datenbank, dass da was nicht stimmt?
|
||||
if not discord_id or not data_api.validate_user_session(db_id, discord_id):
|
||||
app.storage.user.clear()
|
||||
ui.notify("Deine Sitzung ist ungültig oder abgelaufen. Bitte neu einloggen!", color="negative")
|
||||
|
||||
ui.navigate.reload()
|
||||
return
|
||||
if not open_games["submitted"] and not open_games["to_confirm"]:
|
||||
are_games_open = False
|
||||
else:
|
||||
are_games_open = True
|
||||
|
||||
|
||||
# 3. Dektop OR Mobile
|
||||
def show_desktop():
|
||||
templates.DesktopHeader("",request=request)
|
||||
if are_games_open:
|
||||
templates.DesktopUncofirmedGames(open_games, player_id)
|
||||
templates.DesktopGamesystems(game_systems)
|
||||
ui.button('Spieler Statistik', on_click=lambda: ui.navigate.to("/player_match_history"))
|
||||
|
||||
|
||||
# ---------------------------
|
||||
# --- NAVIGATIONSLEISTE (HEADER)
|
||||
# ---------------------------
|
||||
def show_mobile():
|
||||
templates.MobileHeader("",request=request)
|
||||
if are_games_open:
|
||||
templates.MobileUncofirmedGames(open_games, player_id)
|
||||
templates.MobileGamesystems(game_systems)
|
||||
ui.button('Spieler Statistik', on_click=lambda: ui.navigate.to("/player_match_history"))
|
||||
|
||||
with ui.header(fixed=False).classes('items-center justify-between bg-zinc-900 shadow-lg'):
|
||||
|
||||
# --- LINKE SEITE ---
|
||||
# Vereinslogo und den Titel in einer eigenen Reihe (Reihe 1)
|
||||
with ui.row().classes('items-center'):
|
||||
ui.image("gui/pictures/wsdg.png").classes('w-12 h-12 md:w-25 md:h-25 rounded-full')
|
||||
ui.label('Westside Diceghost Liga').classes('text-lg md:text-3xl font-bold text-normaltext')
|
||||
|
||||
# --- MITTE ---
|
||||
if app.storage.user.get('authenticated', False):
|
||||
discord_id = app.storage.user.get("discord_id")
|
||||
if discord_id == admin_discord_id:
|
||||
ui.button(icon="hardware", on_click=lambda: ui.navigate.to('/admin')).classes('w-2 h-2')
|
||||
|
||||
# --- RECHTE SEITE ---
|
||||
if app.storage.user.get('authenticated', False):
|
||||
with ui.row().classes('items-center gap-4'):
|
||||
ui.image(app.storage.user.get('discord_avatar_url')).classes('w-10 h-10 md:w-25 md:h-25 rounded-full border-1 border-red-900')
|
||||
discord_name = app.storage.user.get('discord_name')
|
||||
display_name = app.storage.user.get('display_name')
|
||||
player_id = app.storage.user.get('db_id')
|
||||
|
||||
# 1. kleine Funktion, die zwischen Text und Eingabe hin- und herschaltet
|
||||
def toggle_edit_mode():
|
||||
display_row.visible = not display_row.visible
|
||||
edit_row.visible = not edit_row.visible
|
||||
edit_button.visible = not edit_button.visible
|
||||
|
||||
# --- ANSICHT 1: Der normale Text mit Edit-Button ---
|
||||
with ui.column().classes('items-center gap-0') as display_row:
|
||||
with ui.column():
|
||||
ui.label(display_name).classes('md:text-3xl font-bold text-normaltext')
|
||||
with ui.row().classes("items-center justify-between"):
|
||||
ui.label("'aka'").classes('md:text-lg text-xs text-italic text-infotext')
|
||||
ui.label(discord_name).classes('text-sm md:text-xl text-bold text-infotext')
|
||||
edit_button = ui.button(icon='edit', color='accent', on_click=toggle_edit_mode).props('round dense')
|
||||
|
||||
# --- ANSICHT 2: Das Eingabefeld (startet unsichtbar!) ---
|
||||
with ui.row().classes('items-center gap-5') as edit_row:
|
||||
edit_row.visible = False # Am Anfang verstecken
|
||||
def save_new_name():
|
||||
new_name = name_input.value
|
||||
# Nur speichern, wenn ein Name drinsteht und er anders ist als vorher
|
||||
if new_name and new_name != display_name:
|
||||
print("save")
|
||||
data_api.change_display_name(player_id, new_name) # Deine DB Funktion
|
||||
app.storage.user['display_name'] = new_name
|
||||
ui.notify('Name gespeichert!', color='positive')
|
||||
ui.navigate.reload()
|
||||
else:
|
||||
# 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', color="accent", 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')
|
||||
if avatar:
|
||||
ui.image(avatar).classes('w-5 h-5 rounded-full border-2 border-red-500')
|
||||
|
||||
def logout():
|
||||
app.storage.user.clear()
|
||||
ui.navigate.to('/')
|
||||
|
||||
ui.button(icon="logout", on_click=logout).props('round dense size=lg')
|
||||
|
||||
else:
|
||||
auth_url = discord_login.get_auth_url()
|
||||
ui.button('Login with Discord', on_click=lambda: ui.navigate.to(auth_url))
|
||||
|
||||
|
||||
|
||||
# ---------------------------
|
||||
# --- Match Bestätigung ---
|
||||
# ---------------------------
|
||||
# Bestätigungs für offene Spiele --- Der "Marian Balken !!!1!11!"
|
||||
if app.storage.user.get('authenticated', False):
|
||||
unconfirmed_matches = data_api.get_unconfirmed_matches(player_id)
|
||||
|
||||
if len(unconfirmed_matches) > 0:
|
||||
# Eine auffällige, rote Karte über die volle Breite
|
||||
with ui.card().classes('w-full bg-red-900/80 border-2 border-red-500 mb-6'):
|
||||
ui.label(f"Aktion erforderlich: Du hast {len(unconfirmed_matches)} offen(e) Spiel(e)!").classes('text-2xl font-bold text-normaltext mb-2')
|
||||
|
||||
for match in unconfirmed_matches:
|
||||
|
||||
# Button Funktionen. Akzeptieren oder Rejecten. Der Reject Button löscht das Match aus der DB.
|
||||
def reject_match(m_id):
|
||||
data_api.delete_match(m_id, player_id)
|
||||
ui.notify("Spiel abgelehnt und gelöscht!", color="warning")
|
||||
ui.navigate.reload() # Lädt die Seite neu, um die Karte zu aktualisieren
|
||||
|
||||
def acccept_match(m_id):
|
||||
ui.notify("Spiel akzeptiert. Wird Berechnet.")
|
||||
ui.navigate.reload() # Lädt die Seite neu, um die Karte zu aktualisieren
|
||||
data_api.confirm_match(m_id)
|
||||
calc_match.calculate_match(m_id)
|
||||
|
||||
with ui.row().classes('w-full items-center justify-between bg-zinc-900 p-3 rounded shadow-inner'):
|
||||
info_text = f"{match['system_name']} - {match["played_at"]} "
|
||||
detail_text = f"{match['p1_name']} behauptet: {match['p1_name']} ({match['score_player1']}) vs. Du ({match['score_player2']})"
|
||||
ui.label(info_text).classes('text-bold text-lg text-normaltext')
|
||||
ui.label(detail_text).classes('text-bold text-normaltext')
|
||||
|
||||
# Die Buttons (Funktion machen wir im nächsten Schritt!)
|
||||
with ui.row().classes('gap-2'):
|
||||
# ABLEHNEN und Spiel löschen
|
||||
ui.button(color="negative", icon="close", on_click=lambda e, m_id=match['match_id']: reject_match(m_id))
|
||||
ui.space()
|
||||
# BESTÄTIGEN und spiel berechnen lassen
|
||||
ui.button(color="positive", icon="check", on_click=lambda e, m_id=match['match_id']: acccept_match(m_id))
|
||||
ui.label("Bestätigen wenn die Angaben stimmen, ablehnen wenn sich ein Fehler eingeschlichen hat.")
|
||||
|
||||
# ---------------------------
|
||||
# --- 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).").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, player_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']} - {match["played_at"]} "
|
||||
detail_text = f"Du ({match["score_player1"]}) vs. {match["p2_name"]}({match["score_player2"]})"
|
||||
ui.label(info_text).classes('text-bold text-lg text-normaltext')
|
||||
ui.label(detail_text).classes('text-bold text-normaltext')
|
||||
ui.button(color="warning", icon="delete", on_click=lambda e, m_id=match['match_id']: retract_match(m_id))
|
||||
ui.label("Dein Gegner muss das Match noch bestätigen. Wenn du einen Fehler gemacht hast, kannst du es löschen.")
|
||||
|
||||
# ---------------------------
|
||||
# --- Spielsysteme ---
|
||||
# ---------------------------
|
||||
if app.storage.user.get('authenticated', False):
|
||||
with ui.card().classes("w-full items-center"):
|
||||
with ui.row():
|
||||
ui.label(text="Meine Ligaplätze").classes("font-bold text-white text-xl text-normaltext")
|
||||
info_system.create_info_button("league_info")
|
||||
|
||||
placements = data_api.get_player_statistics(player_id)
|
||||
systems = data_api.get_gamesystems_data()
|
||||
|
||||
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("/")
|
||||
|
||||
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 sys in systems:
|
||||
sys_id = sys['id']
|
||||
sys_name = sys['name']
|
||||
sys_logo = sys["picture"]
|
||||
sys_description = sys['description']
|
||||
|
||||
sys_card = ui.card().classes("h-60 w-full items-center justify-center transition-colors")
|
||||
|
||||
with sys_card:
|
||||
# Wir erstellen einen Container für den "klickbaren" Teil
|
||||
clickable_content = ui.column().classes("w-full h-full items-center")
|
||||
|
||||
with clickable_content:
|
||||
ui.label(text=sys_name).classes('text-xl font-bold text-center text-normaltext')
|
||||
if sys_logo:
|
||||
ui.image(f"/pictures/{sys_logo}").classes("w-60")
|
||||
if sys_description:
|
||||
ui.label(text=sys['description']).classes('text-xs text-gray-400 text-center mt-2 text-infotext')
|
||||
|
||||
# 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")
|
||||
|
||||
clickable_content.classes("cursor-pointer hover:bg-zinc-800")
|
||||
clickable_content.on('click', lambda e, name=sys_name: ui.navigate.to(f'/statistic/visitor/{name}'))
|
||||
|
||||
join_row = ui.row().classes('items-center gap-2')
|
||||
confirm_row = ui.row().classes('items-center gap-2')
|
||||
confirm_row.visible = False
|
||||
|
||||
with join_row:
|
||||
ui.button("Beitreten", on_click=lambda e, r1=join_row, r2=confirm_row: toggle_visibility(r1, r2))
|
||||
|
||||
with confirm_row:
|
||||
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
|
||||
stat = my_stats[sys_id]
|
||||
|
||||
# JETZT machen wir nur den Container klickbar, nicht die ganze Karte!
|
||||
clickable_content.classes("cursor-pointer hover:bg-zinc-800")
|
||||
clickable_content.on('click', lambda e, name=sys_name: ui.navigate.to(f'/statistic/{name}'))
|
||||
|
||||
with ui.row().classes('items-center gap-4'):
|
||||
ui.label(text=f"MMR: {stat['mmr']}").classes("text-lg font-bold text-accenttext")
|
||||
ui.label(text=f"Spiele: {stat['games_in_system']}").classes("text-lg font-bold text-accenttext")
|
||||
|
||||
# ---------------------------
|
||||
# Match Historie
|
||||
# ---------------------------
|
||||
with ui.card().classes("w-full items-center"):
|
||||
ui.label(text= "Meine letzten Spiele").classes("font-bold text-normaltext text-xl")
|
||||
# 1. Daten aus der DB holen ABER per [:5] hart auf die neuesten 5 Listen-Einträge abschneiden!
|
||||
raw_matches = data_api.get_recent_matches_for_player(player_id)[:5]
|
||||
|
||||
# 2. Daten für die Tabelle aufbereiten (Bleibt exakt gleich wie bei dir)
|
||||
table_rows = []
|
||||
for match in raw_matches:
|
||||
if match['player1_id'] == player_id:
|
||||
opponent_name = f"{match['p2_display']} aka {match['p2_discord']}"
|
||||
my_score = match['score_player1']
|
||||
opp_score = match['score_player2']
|
||||
else:
|
||||
opponent_name = f"{match['p1_display']} aka {match['p1_discord']}"
|
||||
my_score = match['score_player2']
|
||||
opp_score = match['score_player1']
|
||||
|
||||
if my_score > opp_score:
|
||||
result_text = "Gewonnen"
|
||||
elif my_score < opp_score:
|
||||
result_text = "Verloren"
|
||||
else:
|
||||
result_text = "Unentschieden"
|
||||
|
||||
date_clean = str(match['played_at'])[:10]
|
||||
|
||||
table_rows.append({
|
||||
'date': date_clean,
|
||||
'system': match['gamesystem_name'],
|
||||
'opponent': opponent_name,
|
||||
'result': f"{result_text} ({my_score} : {opp_score})"
|
||||
})
|
||||
|
||||
table_columns = [
|
||||
{'name': 'date', 'label': 'Gespielt am', 'field': 'date', 'align': 'left'},
|
||||
{'name': 'system', 'label': 'System', 'field': 'system', 'align': 'left'},
|
||||
{'name': 'opponent', 'label': 'Gegner', 'field': 'opponent', 'align': 'left'},
|
||||
{'name': 'result', 'label': 'Ergebnis', 'field': 'result', 'align': 'left'}
|
||||
]
|
||||
|
||||
# 4. Die Tabelle zeichnen und den NEUEN BUTTON hinzufügen
|
||||
if len(table_rows) > 0:
|
||||
ui.table(columns=table_columns, rows=table_rows, row_key='date').classes('w-full bg-zinc-900 text-white')
|
||||
|
||||
# NEU: Der Button, der zur großen Log-Seite führt
|
||||
ui.button("Komplette Historie & Log anzeigen", icon="history", on_click=lambda: ui.navigate.to('/matchhistory')).classes('mt-4 bg-zinc-700 text-white hover:bg-zinc-600')
|
||||
else:
|
||||
ui.label("Noch keine Spiele absolviert.").classes("text-infotext italic")
|
||||
|
||||
with ui.footer(fixed=False).classes('items-center justify-between bg-zinc-900 shadow-lg'):
|
||||
ui.button(icon="description", on_click=lambda: ui.navigate.to('/info'))
|
||||
# Desktop oder Mobile?
|
||||
if is_mobile(request):
|
||||
show_mobile()
|
||||
else:
|
||||
show_desktop()
|
||||
|
|
|
|||
|
|
@ -1,91 +0,0 @@
|
|||
from nicegui import ui, app
|
||||
from gui import gui_style
|
||||
from data import data_api
|
||||
from match_calculations import calc_match
|
||||
from gui.info_text import info_system
|
||||
|
||||
def setup_routes():
|
||||
# 1. Die {}-Klammern definieren eine dynamische Variable in der URL
|
||||
@ui.page('/add-match/{system_name}', dark=True)
|
||||
def match_form_page(system_name: str): # <-- Hier wird der Name des Spielsystems gefiltert.
|
||||
gui_style.apply_design()
|
||||
|
||||
# --- SICHERHEITS-CHECK ---
|
||||
if not app.storage.user.get('authenticated', False):
|
||||
ui.label('Access Denied. Please log in first.').classes('text-red-500')
|
||||
ui.button('Back to Home', on_click=lambda: ui.navigate.to('/'))
|
||||
return
|
||||
|
||||
# --- Eingabeformular ---
|
||||
with ui.card().classes('w-full max-w-md mx-auto items-center mt-10 p-6 shadow-xl'):
|
||||
system_id = data_api.get_gamesystem_id_by_name(system_name)
|
||||
system_data = data_api.get_gamesystem_data(system_id)
|
||||
min_score = system_data["min_score"]
|
||||
max_score = system_data["max_score"]
|
||||
|
||||
# Text-Center hinzugefügt, falls der Systemname sehr lang ist und auf dem Handy umbricht
|
||||
ui.label(f'Neues Spiel für {system_name} eintragen').classes('text-2xl font-bold text-center mb-6')
|
||||
|
||||
with ui.card().classes('w-full max-w-md mx-auto items-center mt-10 p-6'):
|
||||
ui.label("Meine Punkte:").classes('text-xl font-bold w-full text-left')
|
||||
with ui.column().classes("w-full items-center gap-6"):
|
||||
# 1. Daten aus der DB holen
|
||||
raw_players = data_api.get_all_players_from_system(system_name)
|
||||
my_id = app.storage.user.get('db_id')
|
||||
|
||||
with ui.column().classes("items-center justify-between"):
|
||||
p1_points = ui.number(placeholder="Punkte eingeben",
|
||||
min=system_data["min_score"],
|
||||
max=system_data["max_score"],
|
||||
precision=0
|
||||
).classes("w-35").props("clearable")
|
||||
|
||||
with ui.card().classes('w-full max-w-md mx-auto items-center mt-10 p-6'):
|
||||
dropdown_options = {}
|
||||
for p in raw_players:
|
||||
if p['player_id'] == my_id:
|
||||
continue
|
||||
dropdown_options[p['player_id']] = f"{p['display_name']} 'aka' {p['discord_name']}"
|
||||
|
||||
opponent_select = ui.select(options=dropdown_options, label='Gegner auswählen').classes('w-full')
|
||||
|
||||
with ui.column().classes("items-center justify-between"):
|
||||
p2_points = ui.number(placeholder="Punkte eingeben",
|
||||
min=system_data["min_score"],
|
||||
max=system_data["max_score"],
|
||||
precision=0
|
||||
).classes("w-35").props("clearable")
|
||||
|
||||
|
||||
|
||||
# Das Match in die Datenbank eintragen lassen.
|
||||
def input_match_to_database():
|
||||
p2_id = opponent_select.value
|
||||
score_p1 = p1_points.value
|
||||
score_p2 = p2_points.value
|
||||
|
||||
|
||||
# Check der Eingegebenen Daten.
|
||||
# Fehler wenn kein Spieler oder keine Punkte eingegeben werden. Das Punkte Eingabefeld fängt mit min_score und max_score ab,
|
||||
# ob für das Spielsystem legale Punkte eingetragen wurden.
|
||||
if p2_id is None:
|
||||
ui.notify("Bitte wähle zuerst einen Gegner aus!", color="red", position="top")
|
||||
return
|
||||
|
||||
if score_p1 is None:
|
||||
ui.notify("Ungültige Punkteeingabe Spieler 1!", color="red", position="top")
|
||||
return
|
||||
|
||||
if score_p2 is None:
|
||||
ui.notify("Ungültige Punkteeingabe Spieler 2!", color="red", position="top")
|
||||
return
|
||||
|
||||
match_id = data_api.add_new_match(system_name, my_id, p2_id, score_p1, score_p2)
|
||||
|
||||
ui.navigate.to(f'/statistic/{system_name}')
|
||||
|
||||
# Buttons ganz unten in einer Reihe
|
||||
with ui.row().classes("w-full items-center justify-between mt-8"):
|
||||
ui.button(icon="close", on_click=lambda: ui.navigate.to(f'/statistic/{system_name}')).classes("w-10 h-8 rounded-full")
|
||||
info_system.create_info_button("match_form_info")
|
||||
ui.button(text="Absenden", color="positive", on_click=lambda: input_match_to_database())
|
||||
|
|
@ -1,125 +1,27 @@
|
|||
from nicegui import ui, app
|
||||
from gui.common import *
|
||||
from data import data_api
|
||||
from gui import gui_style
|
||||
from gui.info_text.info_system import create_info_button
|
||||
|
||||
|
||||
def setup_routes():
|
||||
@ui.page('/matchhistory', dark=True)
|
||||
def match_history_page():
|
||||
|
||||
gui_style.apply_design()
|
||||
|
||||
if not app.storage.user.get('authenticated', False):
|
||||
ui.label('Bitte logge dich ein.').classes('text-red-500 text-2xl m-4')
|
||||
@ui.page('/player_match_history', dark=True)
|
||||
def player_match_history_page(request: Request):
|
||||
# 1. Wrapper aufrufen (Login-Check & Styling)
|
||||
if not base_page(request, "Match History"):
|
||||
return
|
||||
|
||||
player_id = app.storage.user.get('db_id')
|
||||
# 2. Daten laden
|
||||
player_id = app.storage.user.get('player_id')
|
||||
|
||||
with ui.column().classes('w-full mx-auto p-4'):
|
||||
# 3. Dektop OR Mobile
|
||||
def show_desktop():
|
||||
templates.DesktopHeader("Match History", request=request)
|
||||
templates.DesktopMatchHistory(player_id)
|
||||
|
||||
with ui.row().classes('w-full items-center justify-between mb-6'):
|
||||
ui.label("Komplette Match Historie").classes("text-3xl font-bold text-white")
|
||||
create_info_button("mmr_calc")
|
||||
ui.button("Zurück", icon="arrow_back", on_click=lambda: ui.navigate.to('/')).classes('bg-zinc-700 text-white')
|
||||
def show_mobile():
|
||||
templates.MobileHeader("Match History", request=request)
|
||||
templates.MobileMatchHistory(player_id)
|
||||
|
||||
raw_matches = data_api.get_match_history_log(player_id)
|
||||
table_rows = []
|
||||
|
||||
def fmt_signed(val, pending=False):
|
||||
"""Formatiert einen Integer-Wert mit Vorzeichen oder gibt Sondertexte zurück."""
|
||||
if pending:
|
||||
return "Ausstehend"
|
||||
if val is None:
|
||||
return "0"
|
||||
if val > 0:
|
||||
return f"+{val}"
|
||||
return str(val)
|
||||
|
||||
for i, match in enumerate(raw_matches):
|
||||
is_player1 = match['player1_id'] == player_id
|
||||
pending = match['match_is_counted'] == 0
|
||||
|
||||
if is_player1:
|
||||
opponent = f"{match['p2_display']} aka {match['p2_discord']}"
|
||||
my_score = match['score_player1']
|
||||
opp_score = match['score_player2']
|
||||
mmr_base = match["player1_base_change"]
|
||||
mmr_change = match['player1_mmr_change']
|
||||
khorne = match['player1_khorne']
|
||||
tzeentch = match['player1_tzeentch']
|
||||
slaanesh = match['player1_slaanesh']
|
||||
else:
|
||||
opponent = f"{match['p1_display']} aka {match['p1_discord']}"
|
||||
my_score = match['score_player2']
|
||||
opp_score = match['score_player1']
|
||||
mmr_base = match["player2_base_change"]
|
||||
mmr_change = match['player2_mmr_change']
|
||||
khorne = match['player2_khorne']
|
||||
tzeentch = match['player2_tzeentch']
|
||||
slaanesh = match['player2_slaanesh']
|
||||
|
||||
if my_score > opp_score:
|
||||
result = "Gewonnen"
|
||||
elif my_score < opp_score:
|
||||
result = "Verloren"
|
||||
else:
|
||||
result = "Unentschieden"
|
||||
|
||||
elo_factor = match['elo_factor']
|
||||
rust_factor = match['rust_factor']
|
||||
|
||||
table_rows.append({
|
||||
'id': i,
|
||||
'date': str(match['played_at'])[:10],
|
||||
'system': match['gamesystem_name'],
|
||||
'score': str(my_score),
|
||||
'opponent': (f"{opponent} ({opp_score})"),
|
||||
"basis" : mmr_base,
|
||||
'elo': fmt_signed(round(elo_factor, 2) if elo_factor is not None else None, pending),
|
||||
'rust': fmt_signed(round(rust_factor, 2) if rust_factor is not None else None, pending),
|
||||
'khorne': fmt_signed(khorne, pending),
|
||||
'tzeentch': fmt_signed(tzeentch, pending),
|
||||
'slaanesh': fmt_signed(slaanesh, pending),
|
||||
'mmr': fmt_signed(mmr_change, pending),
|
||||
})
|
||||
|
||||
columns = [
|
||||
{'name': 'date', 'label': 'Datum', 'field': 'date', 'align': 'left'},
|
||||
{'name': 'system', 'label': 'System', 'field': 'system', 'align': 'left'},
|
||||
{'name': 'score', 'label': 'Punkte', 'field': 'score', 'align': 'center'},
|
||||
{'name': 'opponent', 'label': 'Gegner (Pkt.)', 'field': 'opponent', 'align': 'left'},
|
||||
{'name': 'elo', 'label': 'Elo Faktor', 'field': 'elo', 'align': 'right'},
|
||||
{'name': 'basisMMR', 'label': 'Basis MMR', 'field': 'basis', 'align': 'right'},
|
||||
{'name': 'rust', 'label': 'Rost Faktor', 'field': 'rust', 'align': 'right'},
|
||||
{'name': 'khorne', 'label': 'Khorne', 'field': 'khorne', 'align': 'right'},
|
||||
{'name': 'tzeentch', 'label': 'Tzeentch', 'field': 'tzeentch', 'align': 'right'},
|
||||
{'name': 'slaanesh', 'label': 'Slaanesh', 'field': 'slaanesh', 'align': 'right'},
|
||||
{'name': 'mmr', 'label': 'MMR GESAMT', 'field': 'mmr', 'align': 'right'},
|
||||
|
||||
]
|
||||
|
||||
# Shared slot template for colored signed values
|
||||
colored_slot = '''
|
||||
<q-td :props="props">
|
||||
<span :class="{
|
||||
'text-green-500 font-bold': props.row[props.col.field].startsWith('+'),
|
||||
'text-red-500 font-bold': props.row[props.col.field].startsWith('-'),
|
||||
'text-gray-400 italic': props.row[props.col.field] === 'Ausstehend'
|
||||
}">
|
||||
{{ props.row[props.col.field] }}
|
||||
</span>
|
||||
</q-td>
|
||||
'''
|
||||
|
||||
if table_rows:
|
||||
history_table = ui.table(
|
||||
columns=columns,
|
||||
rows=table_rows,
|
||||
row_key='id'
|
||||
).classes('w-full bg-zinc-900 text-white')
|
||||
|
||||
for col in ['elo', 'rust', 'khorne', 'tzeentch', 'slaanesh', 'mmr']:
|
||||
history_table.add_slot(f'body-cell-{col}', colored_slot)
|
||||
else:
|
||||
ui.label("Keine Spiele gefunden.").classes("text-gray-400 italic")
|
||||
# Desktop oder Mobile?
|
||||
if is_mobile(request):
|
||||
show_mobile()
|
||||
else:
|
||||
show_desktop()
|
||||
|
|
|
|||
8
gui/profile_gui.py
Normal file
8
gui/profile_gui.py
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
from gui.common import *
|
||||
|
||||
from data import data_api
|
||||
|
||||
def setup_routes():
|
||||
@ui.page('/profile/{player_id}', dark=True)
|
||||
def gamesystem_statistic_page(request: Request, player_id: int):
|
||||
pass
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
from nicegui import ui
|
||||
from data import data_api
|
||||
from gui.templates.player_card import PlayerCard
|
||||
|
||||
class Leaderboard:
|
||||
def __init__(self, system_name: str):
|
||||
# Daten holen
|
||||
leaderboard_data = data_api.get_leaderboard(system_name)
|
||||
|
||||
# Container
|
||||
with ui.column().classes('w-full gap-2') as self.container:
|
||||
for index, player in enumerate(leaderboard_data):
|
||||
PlayerCard(
|
||||
rank=index + 1,
|
||||
display_name=player['display_name'],
|
||||
discord_name=player['discord_name'],
|
||||
mmr=player['mmr'],
|
||||
discord_avatar_url=player.get('discord_avatar_url'),
|
||||
discord_id=player.get('discord_id'),
|
||||
games_in_system=player.get('games_in_system', 0),
|
||||
wins=player.get('wins', 0),
|
||||
loss=player.get('loss', 0),
|
||||
avv_points=player.get('avv_points', 0)
|
||||
)
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
from nicegui import ui
|
||||
|
||||
class PlayerCard:
|
||||
def __init__(self, rank: int, display_name: str, discord_name: str, mmr: int,
|
||||
discord_avatar_url: str = None, discord_id: str = None,
|
||||
games_in_system: int = 0, wins: int = 0, loss: int = 0, avv_points: int = 0):
|
||||
|
||||
# Rang links, Card rechts
|
||||
with ui.row().classes('w-full items-center gap-1 flex-nowrap'):
|
||||
ui.label(f'#{rank}').classes('text-2xl font-bold w-10 text-center shrink-0')
|
||||
|
||||
# Player Card
|
||||
with ui.card().classes("flex-1 no-shadow bg-zinc-900 min-w-0 overflow-hidden"):
|
||||
# Discord Block - klickbar
|
||||
if discord_id:
|
||||
with ui.link(target=f'https://discord.com/users/{discord_id}', new_tab=True).classes('no-underline w-full'):
|
||||
self._discord_content(discord_avatar_url, display_name, discord_name)
|
||||
else:
|
||||
self._discord_content(discord_avatar_url, display_name, discord_name)
|
||||
|
||||
# Stats und Achievments
|
||||
with ui.row().classes('w-full items-center flex-nowrap'):
|
||||
with ui.row().classes('flex-2 items-center gap-2 md:gap-3 md:justify-center shrink min-w-0'):
|
||||
ui.label(f'{mmr} MMR').classes('font-bold md:text-lg')
|
||||
with ui.row().classes('flex-1 justify-end items-center gap-1 md:gap-3 shrink-0'):
|
||||
ui.image('gui/pictures/achievments/AchievementIcon_4_1Sieg.png').classes('w-5 md:w-15')
|
||||
ui.image('gui/pictures/achievments/AchievementIcon_4_1Sieg.png').classes('w-5 md:w-15')
|
||||
ui.image('gui/pictures/achievments/AchievementIcon_4_1Sieg.png').classes('w-5 md:w-15')
|
||||
|
||||
|
||||
def _discord_content(self, avatar_url, display_name, discord_name):
|
||||
short_name = display_name[:15] + '...' if len(display_name) > 15 else display_name
|
||||
short_discord = discord_name[:15] + '...' if len(discord_name) > 15 else discord_name
|
||||
|
||||
with ui.card().classes("w-full md:w-90 bg-indigo-950 no-shadow"):
|
||||
with ui.row().classes('items-center gap-3 min-w-0'):
|
||||
if avatar_url:
|
||||
ui.image(avatar_url).classes('w-10 h-10 md:w-20 md:h-20 rounded-full border-red-900 border-2 shrink-0')
|
||||
with ui.column().classes('gap-0 min-w-0'):
|
||||
ui.label(short_name).classes('text-sm md:text-2xl font-bold whitespace-nowrap truncate')
|
||||
ui.label(short_discord).classes('text-xs md:text-sm opacity-60 whitespace-nowrap truncate')
|
||||
35
main.py
35
main.py
|
|
@ -1,43 +1,36 @@
|
|||
import os
|
||||
import schedule
|
||||
import schedule
|
||||
from dotenv import load_dotenv
|
||||
from nicegui import ui, app
|
||||
from gui import setup_all_routes
|
||||
|
||||
from data import database
|
||||
from gui import main_gui, match_gui, discord_login, league_statistic_gui, admin_gui, match_history_gui, imprint_gui, league_statistic_visitor_gui
|
||||
from wood import logger
|
||||
from gui.info_text import info_system
|
||||
from utils import season_scheduler
|
||||
from starlette.requests import Request
|
||||
from wood import logger
|
||||
|
||||
# 1. Lade die geheimen Variablen aus der .env Datei in den Speicher
|
||||
# 1. Load the secret variables from the .env file into memory
|
||||
load_dotenv()
|
||||
|
||||
# Festschreiben des Ordners für die Bilder (Darf hier bleiben!)
|
||||
# Fixed directory for the pictures (must remain here!)
|
||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
PICTURE_DIR = os.path.join(BASE_DIR, "gui", "pictures")
|
||||
|
||||
# Fester Pfad für den Webserver
|
||||
# Fixed path for the web server
|
||||
app.add_static_files('/pictures', PICTURE_DIR)
|
||||
|
||||
# 2. Variablen abrufen
|
||||
# 2. Retrieve variables
|
||||
client_id = os.getenv("DISCORD_CLIENT_ID")
|
||||
client_secret = os.getenv("DISCORD_CLIENT_SECRET")
|
||||
admin_discord_id = os.getenv("ADMIN")
|
||||
url = os.getenv("APP_URL")
|
||||
|
||||
|
||||
logger.setup_log_db()
|
||||
|
||||
# 3. Seitenrouten aufbauen
|
||||
main_gui.setup_routes(admin_discord_id)
|
||||
discord_login.setup_login_routes()
|
||||
league_statistic_gui.setup_routes()
|
||||
match_gui.setup_routes()
|
||||
admin_gui.setup_routes()
|
||||
match_history_gui.setup_routes()
|
||||
imprint_gui.setup_routes()
|
||||
league_statistic_visitor_gui.setup_routes()
|
||||
# 3. Build page routes
|
||||
setup_all_routes()
|
||||
|
||||
# Timer der schedule regelmäßig prüft
|
||||
# Timer of schedule regularly checks
|
||||
@app.on_startup
|
||||
def setup_scheduler():
|
||||
import threading
|
||||
|
|
@ -46,7 +39,5 @@ def setup_scheduler():
|
|||
thread = threading.Thread(target=run_scheduler, daemon=True)
|
||||
thread.start()
|
||||
|
||||
|
||||
|
||||
# 4. Starten der App Runtime auf dem Server.
|
||||
# 4. Start the App Runtime on the server.
|
||||
ui.run(title="Westside Diceghost Liga", port=9000, storage_secret="EIN_super-geheimes_Pa$$wort#!", favicon="gui/pictures/wsdg.png")
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ from wood import logger
|
|||
import sqlite3
|
||||
|
||||
|
||||
def check_player_achievments(player_id):
|
||||
def check_player_achievments(player_id :int, system_id :int):
|
||||
# Schauen ob der Spieler sich nach dem Match (function wird aufgerufen NACHDEM ein Match berechnet wurde) sich ein Achievment verdient hat.
|
||||
game_stats = data_api.get_player_statistic(player_id, system_id)
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ from wood import logger
|
|||
from match_calculations.achievments import achievments
|
||||
|
||||
|
||||
point_inflation = 1 # => entspricht % 1 == 100% 0.5 == 50%
|
||||
point_inflation = 1 # => entspricht %
|
||||
K_FACTOR = 40 # Die "Border" (Maximalpunkte) die ein Sieg gibt.
|
||||
|
||||
# Mach die DB abfrage für die Relevanten Daten. Von hier aus werden die "Aufgaben" und Daten dann an die kleineren Berechnungs Funktionen verteilt.
|
||||
|
|
@ -79,23 +79,23 @@ def calculate_match (match_id):
|
|||
"looser_id" : looser_id, # <-- String-Key für save_calculated_match
|
||||
|
||||
winner_id: { # <-- Variable als Key (z.B. 42: {...})
|
||||
"base" : w_base,
|
||||
"khorne" : w_khorne,
|
||||
"slaanesh" : slaanesh,
|
||||
"tzeentch" : tzeentch,
|
||||
"total" : int(w_base * rust_factor + w_khorne + slaanesh + tzeentch)
|
||||
"base" : w_base,
|
||||
"khorne" : w_khorne,
|
||||
"slaanesh" : slaanesh,
|
||||
"tzeentch" : tzeentch,
|
||||
"total" : int((w_base + w_khorne + slaanesh + tzeentch) * rust_factor),
|
||||
},
|
||||
looser_id: { # <-- Variable als Key (z.B. 7: {...})
|
||||
"base" : -l_base,
|
||||
"khorne" : l_khorne,
|
||||
"slaanesh" : -slaanesh,
|
||||
"tzeentch" : -tzeentch,
|
||||
"total" : int(-l_base * rust_factor + l_khorne - slaanesh - tzeentch)
|
||||
"base" : -l_base,
|
||||
"khorne" : l_khorne,
|
||||
"slaanesh" : -slaanesh,
|
||||
"tzeentch" : -tzeentch,
|
||||
"total" : int((-l_base + l_khorne - slaanesh - tzeentch) * rust_factor),
|
||||
}
|
||||
}
|
||||
|
||||
logger.log(f"...Winner: Base({w_base} × {rust_factor}) + Khorne({w_khorne}) + Slaanesh({slaanesh}) + Tzeentch({tzeentch}) = {total}")
|
||||
logger.log(f"...Loser: Base(-{l_base} × {rust_factor}) + Khorne({l_khorne}) - Slaanesh({slaanesh}) - Tzeentch({tzeentch}) = {total}")
|
||||
logger.log(f"Match{match_id}: Winner {data_api.get_player_name(winner_id)}: Base {w_base} + Khorne({w_khorne}) + Slaanesh({slaanesh}) + Tzeentch({tzeentch}) = {calc_results[winner_id]["total"]}")
|
||||
logger.log(f"Match{match_id}: Looser {data_api.get_player_name(looser_id)}: -Base({l_base}) + Khorne({l_khorne}) - Slaanesh({slaanesh}) - Tzeentch({tzeentch}) = {calc_results[looser_id]["total"]}")
|
||||
data_api.save_calculated_match(calc_results)
|
||||
achievments.check_player_achievments(winner_id, system_id)
|
||||
achievments.check_player_achievments(looser_id, system_id)
|
||||
|
|
|
|||
|
|
@ -37,17 +37,18 @@ def calc_rust_factor(winner_id, looser_id, gamesystem_id):
|
|||
w_days = data_api.get_days_since_last_system_game(winner_id, gamesystem_id)
|
||||
l_days = data_api.get_days_since_last_system_game(looser_id, gamesystem_id)
|
||||
|
||||
# Der größere der beiden Werte wird verwendet.
|
||||
# Der größeren der beiden Werte wird verwendet.
|
||||
days_ago = max(w_days, l_days)
|
||||
|
||||
"""Berechnet den Dämpfungsfaktor basierend auf den vergangen Tagen."""
|
||||
if days_ago <= 30:
|
||||
return 1.0 # Volle Punkte
|
||||
elif days_ago > 90:
|
||||
return 0.4 # Maximal eingerostet (40% der Punkteänderung)
|
||||
return 0.1 # Maximal eingerostet (nur 10% der Punkteänderung)
|
||||
else:
|
||||
# Lineare Rampe von 0.9 (bei Tag 31) runter auf 0.4 (bei Tag 90)
|
||||
factor = 0.9 - 0.4 * ((days_ago - 30) / 60)
|
||||
# Lineare Rampe von 0.8 (bei Tag 31) runter auf 0.1 (bei Tag 90)
|
||||
# Formel: Startwert - (Differenz * Prozentualer Weg)
|
||||
factor = 0.8 - 0.7 * ((days_ago - 30) / 60)
|
||||
return round(factor, 2)
|
||||
|
||||
|
||||
|
|
|
|||
125
match_history_gui_OLD.py
Normal file
125
match_history_gui_OLD.py
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
from nicegui import ui, app
|
||||
from data import data_api
|
||||
from gui import gui_style
|
||||
from gui.info_text.info_system import create_info_button
|
||||
|
||||
|
||||
def setup_routes():
|
||||
@ui.page('/matchhistory', dark=True)
|
||||
def match_history_page():
|
||||
|
||||
gui_style.apply_design()
|
||||
|
||||
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')
|
||||
|
||||
with ui.column().classes('w-full mx-auto p-4'):
|
||||
|
||||
with ui.row().classes('w-full items-center justify-between mb-6'):
|
||||
ui.label("Komplette Match Historie").classes("text-3xl font-bold text-white")
|
||||
create_info_button("mmr_calc")
|
||||
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 = []
|
||||
|
||||
def fmt_signed(val, pending=False):
|
||||
"""Formatiert einen Integer-Wert mit Vorzeichen oder gibt Sondertexte zurück."""
|
||||
if pending:
|
||||
return "Ausstehend"
|
||||
if val is None:
|
||||
return "0"
|
||||
if val > 0:
|
||||
return f"+{val}"
|
||||
return str(val)
|
||||
|
||||
for i, match in enumerate(raw_matches):
|
||||
is_player1 = match['player1_id'] == player_id
|
||||
pending = match['match_is_counted'] == 0
|
||||
|
||||
if is_player1:
|
||||
opponent = f"{match['p2_display']} aka {match['p2_discord']}"
|
||||
my_score = match['score_player1']
|
||||
opp_score = match['score_player2']
|
||||
mmr_base = match["player1_base_change"]
|
||||
mmr_change = match['player1_mmr_change']
|
||||
khorne = match['player1_khorne']
|
||||
tzeentch = match['player1_tzeentch']
|
||||
slaanesh = match['player1_slaanesh']
|
||||
else:
|
||||
opponent = f"{match['p1_display']} aka {match['p1_discord']}"
|
||||
my_score = match['score_player2']
|
||||
opp_score = match['score_player1']
|
||||
mmr_base = match["player2_base_change"]
|
||||
mmr_change = match['player2_mmr_change']
|
||||
khorne = match['player2_khorne']
|
||||
tzeentch = match['player2_tzeentch']
|
||||
slaanesh = match['player2_slaanesh']
|
||||
|
||||
if my_score > opp_score:
|
||||
result = "Gewonnen"
|
||||
elif my_score < opp_score:
|
||||
result = "Verloren"
|
||||
else:
|
||||
result = "Unentschieden"
|
||||
|
||||
elo_factor = match['elo_factor']
|
||||
rust_factor = match['rust_factor']
|
||||
|
||||
table_rows.append({
|
||||
'id': i,
|
||||
'date': str(match['played_at'])[:10],
|
||||
'system': match['gamesystem_name'],
|
||||
'score': str(my_score),
|
||||
'opponent': (f"{opponent} ({opp_score})"),
|
||||
"basis" : mmr_base,
|
||||
'elo': fmt_signed(round(elo_factor, 2) if elo_factor is not None else None, pending),
|
||||
'rust': fmt_signed(round(rust_factor, 2) if rust_factor is not None else None, pending),
|
||||
'khorne': fmt_signed(khorne, pending),
|
||||
'tzeentch': fmt_signed(tzeentch, pending),
|
||||
'slaanesh': fmt_signed(slaanesh, pending),
|
||||
'mmr': fmt_signed(mmr_change, pending),
|
||||
})
|
||||
|
||||
columns = [
|
||||
{'name': 'date', 'label': 'Datum', 'field': 'date', 'align': 'left'},
|
||||
{'name': 'system', 'label': 'System', 'field': 'system', 'align': 'left'},
|
||||
{'name': 'score', 'label': 'Punkte', 'field': 'score', 'align': 'center'},
|
||||
{'name': 'opponent', 'label': 'Gegner (Pkt.)', 'field': 'opponent', 'align': 'left'},
|
||||
{'name': 'elo', 'label': 'Elo Faktor', 'field': 'elo', 'align': 'right'},
|
||||
{'name': 'basisMMR', 'label': 'Basis MMR', 'field': 'basis', 'align': 'right'},
|
||||
{'name': 'rust', 'label': 'Rost Faktor', 'field': 'rust', 'align': 'right'},
|
||||
{'name': 'khorne', 'label': 'Khorne', 'field': 'khorne', 'align': 'right'},
|
||||
{'name': 'tzeentch', 'label': 'Tzeentch', 'field': 'tzeentch', 'align': 'right'},
|
||||
{'name': 'slaanesh', 'label': 'Slaanesh', 'field': 'slaanesh', 'align': 'right'},
|
||||
{'name': 'mmr', 'label': 'MMR GESAMT', 'field': 'mmr', 'align': 'right'},
|
||||
|
||||
]
|
||||
|
||||
# Shared slot template for colored signed values
|
||||
colored_slot = '''
|
||||
<q-td :props="props">
|
||||
<span :class="{
|
||||
'text-green-500 font-bold': props.row[props.col.field].startsWith('+'),
|
||||
'text-red-500 font-bold': props.row[props.col.field].startsWith('-'),
|
||||
'text-gray-400 italic': props.row[props.col.field] === 'Ausstehend'
|
||||
}">
|
||||
{{ props.row[props.col.field] }}
|
||||
</span>
|
||||
</q-td>
|
||||
'''
|
||||
|
||||
if table_rows:
|
||||
history_table = ui.table(
|
||||
columns=columns,
|
||||
rows=table_rows,
|
||||
row_key='id'
|
||||
).classes('w-full bg-zinc-900 text-white')
|
||||
|
||||
for col in ['elo', 'rust', 'khorne', 'tzeentch', 'slaanesh', 'mmr']:
|
||||
history_table.add_slot(f'body-cell-{col}', colored_slot)
|
||||
else:
|
||||
ui.label("Keine Spiele gefunden.").classes("text-gray-400 italic")
|
||||
56
requirements.txt
Normal file
56
requirements.txt
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
aiofiles==25.1.0
|
||||
aiohappyeyeballs==2.6.2
|
||||
aiohttp==3.14.1
|
||||
aiosignal==1.4.0
|
||||
annotated-doc==0.0.4
|
||||
annotated-types==0.7.0
|
||||
anyio==4.14.0
|
||||
attrs==26.1.0
|
||||
bidict==0.23.1
|
||||
certifi==2026.6.17
|
||||
charset-normalizer==3.4.7
|
||||
click==8.4.1
|
||||
docutils==0.23
|
||||
fastapi==0.138.0
|
||||
frozenlist==1.8.0
|
||||
h11==0.16.0
|
||||
httpcore==1.0.9
|
||||
httptools==0.8.0
|
||||
httpx==0.28.1
|
||||
idna==3.18
|
||||
ifaddr==0.2.0
|
||||
itsdangerous==2.2.0
|
||||
Jinja2==3.1.6
|
||||
lxml==6.1.1
|
||||
lxml_html_clean==0.4.5
|
||||
markdown2==2.5.5
|
||||
MarkupSafe==3.0.3
|
||||
multidict==6.7.1
|
||||
nicegui==3.13.0
|
||||
orjson==3.11.9
|
||||
packaging==26.2
|
||||
propcache==0.5.2
|
||||
pydantic==2.13.4
|
||||
pydantic_core==2.46.4
|
||||
Pygments==2.20.0
|
||||
PyMySQL==1.2.0
|
||||
python-dotenv==1.2.2
|
||||
python-engineio==4.13.2
|
||||
python-multipart==0.0.32
|
||||
python-socketio==5.16.3
|
||||
PyYAML==6.0.3
|
||||
requests==2.34.2
|
||||
schedule==1.2.2
|
||||
simple-websocket==1.1.0
|
||||
starlette==1.3.1
|
||||
tinycss2==1.5.1
|
||||
typing-inspection==0.4.2
|
||||
typing_extensions==4.15.0
|
||||
urllib3==2.7.0
|
||||
uvicorn==0.49.0
|
||||
uvloop==0.22.1
|
||||
watchfiles==1.2.0
|
||||
webencodings==0.5.1
|
||||
websockets==16.0
|
||||
wsproto==1.3.2
|
||||
yarl==1.24.2
|
||||
12
setup_env.sh
12
setup_env.sh
|
|
@ -56,12 +56,7 @@ echo "✅ Umgebung eingerichtet! Installiere Pakete ..."
|
|||
pip install --upgrade pip
|
||||
|
||||
# 6. Python-Pakete installieren
|
||||
pip install \
|
||||
nicegui \
|
||||
requests \
|
||||
python-dotenv \
|
||||
pymysql \
|
||||
schedule
|
||||
pip install -r requirements.txt
|
||||
|
||||
echo "✅ Pakete installiert. Starte Liga Service ..."
|
||||
|
||||
|
|
@ -72,11 +67,6 @@ if [ "$SERVICE_EXISTS" = true ]; then
|
|||
if sudo systemctl start liga; then
|
||||
echo "✅ Liga Service erfolgreich gestartet. Alles bereit!"
|
||||
else
|
||||
# Service konnte nicht gestartet werden – mögliche Ursachen:
|
||||
# - Konfigurationsfehler in der .service Datei
|
||||
# - Python-Fehler beim Start (Import-Error, Syntax-Error etc.)
|
||||
# - Port bereits belegt
|
||||
# - Fehlende Berechtigungen
|
||||
echo "❌ FEHLER: Liga Service konnte nicht gestartet werden!"
|
||||
echo " Prüfe mit: sudo systemctl status liga"
|
||||
echo " Prüfe mit: sudo journalctl -u liga --no-pager -n 50"
|
||||
|
|
|
|||
|
|
@ -1,26 +1,88 @@
|
|||
import schedule
|
||||
import time
|
||||
from datetime import date
|
||||
from data import database
|
||||
|
||||
from data import database, data_api
|
||||
from wood import logger
|
||||
|
||||
def check_new_seasons():
|
||||
print("Season mach brrrrr")
|
||||
today = date.today()
|
||||
|
||||
conn = database.db_connection()
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
|
||||
# 1. Prüfen, ob für heute ein Start geplant ist
|
||||
cursor.execute(
|
||||
"SELECT id, gamesystem_id FROM season_schedule "
|
||||
"WHERE start_month = %s AND start_day = %s",
|
||||
(today.month, today.day)
|
||||
)
|
||||
triggers = cursor.fetchall()
|
||||
print(triggers)
|
||||
|
||||
print(f"[Season Check] {today} – {len(triggers)} Treffer gefunden")
|
||||
for trigger in triggers:
|
||||
gamesystem_id = trigger['gamesystem_id']
|
||||
|
||||
# 2. Prüfen, ob heute schon eine Season für das System erstellt wurde
|
||||
cursor.execute(
|
||||
"SELECT id FROM seasons WHERE gamesystem_id = %s AND start_date = %s",
|
||||
(gamesystem_id, today)
|
||||
)
|
||||
if cursor.fetchone():
|
||||
continue
|
||||
|
||||
# 3. Alte Season beenden (end_date = gestern)
|
||||
cursor.execute(
|
||||
"UPDATE seasons SET end_date = DATE_SUB(CURDATE(), INTERVAL 1 DAY) "
|
||||
"WHERE gamesystem_id = %s AND end_date IS NULL",
|
||||
(gamesystem_id,)
|
||||
)
|
||||
|
||||
# 4. Neue Season anlegen
|
||||
cursor.execute(
|
||||
"SELECT MAX(season_number) as last_num FROM seasons WHERE gamesystem_id = %s",
|
||||
(gamesystem_id,)
|
||||
)
|
||||
result = cursor.fetchone()
|
||||
last_num = result['last_num'] if result['last_num'] else 0
|
||||
new_season_number = last_num + 1
|
||||
|
||||
cursor.execute(
|
||||
"INSERT INTO seasons (gamesystem_id, name, start_date, season_number) "
|
||||
"VALUES (%s, %s, %s, %s)",
|
||||
(gamesystem_id, f"{data_api.get_system_name_by_id(gamesystem_id)} - Season{new_season_number}", today, new_season_number)
|
||||
)
|
||||
new_season_id = cursor.lastrowid
|
||||
|
||||
logger.log(f"new Season for {data_api.get_system_name_by_id(gamesystem_id)}, Season ID:{new_season_id}")
|
||||
|
||||
# 5. AUTO-JOIN: Spieler der alten Season in die neue Season kopieren
|
||||
cursor.execute(
|
||||
"SELECT id FROM seasons WHERE gamesystem_id = %s AND id != %s ORDER BY id DESC LIMIT 1",
|
||||
(gamesystem_id, new_season_id)
|
||||
)
|
||||
old_season = cursor.fetchone()
|
||||
|
||||
if old_season:
|
||||
old_season_id = old_season['id']
|
||||
|
||||
# Der Auto-Join Befehl
|
||||
# Wir nehmen player_id und gamesystem_id aus der alten Season
|
||||
# und setzen die neue season_id ein.
|
||||
cursor.execute("""
|
||||
INSERT INTO player_game_statistic (player_id, gamesystem_id, season_id, wins, loss, draws, games_in_system)
|
||||
SELECT player_id, gamesystem_id, %s, 0, 0, 0, 0
|
||||
FROM player_game_statistic
|
||||
WHERE season_id = %s
|
||||
""", (new_season_id, old_season_id))
|
||||
|
||||
logger.log(f"Player copied from SeasonID {old_season_id} to SeasonID {new_season_id}")
|
||||
|
||||
conn.commit()
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
|
||||
|
||||
|
||||
def run_scheduler():
|
||||
"""Läuft in einem Hintergrund-Thread."""
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user