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