Skip to content

Instantly share code, notes, and snippets.

@Ensamisten
Last active February 11, 2026 07:01
Show Gist options
  • Select an option

  • Save Ensamisten/b5bea164e8cf8503719c2acc765a9c15 to your computer and use it in GitHub Desktop.

Select an option

Save Ensamisten/b5bea164e8cf8503719c2acc765a9c15 to your computer and use it in GitHub Desktop.
import os
import json
import base64
import datetime
import requests
import urllib.parse
from flask import Flask, render_template, request, redirect, jsonify, make_response
from urllib3.exceptions import InsecureRequestWarning
# Disable SSL warnings for LCU
requests.packages.urllib3.disable_warnings(category=InsecureRequestWarning)
app = Flask(__name__)
DD_BASE = "https://ddragon.leagueoflegends.com"
ACCOUNTS_DIR = os.path.join(os.path.dirname(__file__), "accounts")
os.makedirs(ACCOUNTS_DIR, exist_ok=True)
# Riot API config (optional, not used for rank now)
RIOT_API_KEY = os.environ.get("RIOT_API_KEY")
RIOT_REGION = "euw1"
# ---------------------------
# Data Dragon version
# ---------------------------
def get_latest_version():
try:
r = requests.get(f"{DD_BASE}/api/versions.json")
versions = r.json()
if isinstance(versions, list) and versions:
return versions[0]
except Exception:
pass
return "14.10.1"
DD_VERSION = get_latest_version()
# ---------------------------
# LCU helpers
# ---------------------------
def read_lockfile():
path = r"C:\Riot Games\League of Legends\lockfile"
if not os.path.exists(path):
return None, None
with open(path, "r") as f:
parts = f.read().split(":")
return parts[2], parts[3] # port, password
def lcu_request(endpoint):
port, password = read_lockfile()
if not port:
return None
auth = base64.b64encode(f"riot:{password}".encode()).decode()
url = f"https://127.0.0.1:{port}{endpoint}"
headers = {
"Authorization": f"Basic {auth}",
"Accept": "application/json",
"Content-Type": "application/json"
}
try:
r = requests.get(url, headers=headers, verify=False)
if r.status_code in (401, 403):
print("LCU auth failed:", r.status_code, endpoint)
return None
if r.status_code == 204:
return None
return r.json()
except Exception as e:
print("LCU request error:", e)
return None
# ---------------------------
# Live Client Data (in-game stats)
# ---------------------------
def live_request(path):
url = f"https://127.0.0.1:2999{path}"
try:
r = requests.get(url, timeout=0.5, verify=False)
if r.status_code != 200:
return None
return r.json()
except Exception:
return None
def get_current_user():
return lcu_request("/lol-summoner/v1/current-summoner")
def get_current_action(cell_id, actions):
"""
Returns:
{
"type": "pick" / "ban",
"status": "active" / "locked",
"order": int
}
"""
order = 1
for group in actions:
for action in group:
if action.get("actorCellId") == cell_id:
return {
"type": action.get("type"),
"status": "locked" if action.get("completed") else "active",
"order": order
}
order += 1
return None
# =========================
# ROLE + PICK ORDER HELPERS
# =========================
ROLE_ORDER = ["TOP", "JUNGLE", "MIDDLE", "BOTTOM", "UTILITY"]
def build_pick_order_map(actions):
"""
Returns a dict: cellId -> pickOrder (1..N) for pick actions.
"""
order = 1
result = {}
for group in actions:
for action in group:
if action.get("type") == "pick":
cell_id = action.get("actorCellId")
if cell_id is not None and cell_id not in result:
result[cell_id] = order
order += 1
return result
def hybrid_role_for_player(player, pick_order_map):
"""
Hybrid role:
1) Use assignedPosition if present.
2) Otherwise infer from pick order.
"""
assigned = player.get("assignedPosition")
if assigned:
return assigned.upper()
cell_id = player.get("cellId")
pick_order = pick_order_map.get(cell_id)
if pick_order is None:
return None
idx = pick_order - 1
if 0 <= idx < len(ROLE_ORDER):
return ROLE_ORDER[idx]
return None
# ---------------------------
# Rank from LCU (per current client)
# ---------------------------
def fetch_rank_from_lcu():
stats = lcu_request("/lol-ranked/v1/current-ranked-stats")
if not stats:
return "Unranked"
for q in stats.get("queues", []):
if q.get("queueType") == "RANKED_SOLO_5x5":
tier = q.get("tier", "").title()
division = q.get("division", "")
lp = q.get("leaguePoints", 0)
if tier:
return f"{tier} {division} ({lp} LP)"
return "Unranked"
# ---------------------------
# Champion + Skin Data
# ---------------------------
def get_champions_dd():
url = f"{DD_BASE}/cdn/{DD_VERSION}/data/en_US/champion.json"
return requests.get(url).json()["data"]
def get_champion_detail_dd(champion_id):
url = f"{DD_BASE}/cdn/{DD_VERSION}/data/en_US/champion/{champion_id}.json"
return list(requests.get(url).json()["data"].values())[0]
# ---------------------------
# Account storage
# ---------------------------
def account_file_path(puuid):
return os.path.join(ACCOUNTS_DIR, f"{puuid}.json")
def load_all_accounts():
accounts = []
for fname in os.listdir(ACCOUNTS_DIR):
if fname.endswith(".json"):
try:
with open(os.path.join(ACCOUNTS_DIR, fname), "r", encoding="utf-8") as f:
accounts.append(json.load(f))
except Exception:
continue
accounts.sort(key=lambda a: a.get("username", "").lower())
return accounts
def load_account_by_puuid(puuid):
fpath = account_file_path(puuid)
if os.path.exists(fpath):
with open(fpath, "r", encoding="utf-8") as f:
return json.load(f)
return None
def save_account(account_data):
with open(account_file_path(account_data["puuid"]), "w", encoding="utf-8") as f:
json.dump(account_data, f, indent=2)
def build_account_from_lcu(current_user):
summoner_id = current_user["summonerId"]
puuid = current_user["puuid"]
# Owned champions
owned_champs = lcu_request(f"/lol-champions/v1/inventories/{summoner_id}/champions")
owned = []
if owned_champs:
for c in owned_champs:
if c.get("ownership", {}).get("owned", False):
owned.append(int(c["id"]))
# Owned skins
owned_skins = []
skins = lcu_request(f"/lol-champions/v1/inventories/{summoner_id}/skins-minimal")
if skins:
for s in skins:
if s.get("ownership", {}).get("owned", False):
owned_skins.append(int(s["id"]))
# Loot skins
loot_skins = []
loot = lcu_request("/lol-loot/v1/player-loot")
if loot:
for item in loot:
if item.get("displayCategories") == "SKIN":
try:
loot_skins.append(int(item["itemId"]))
except Exception:
pass
# Riot ID username
username = current_user.get("gameName", "").strip()
tag = current_user.get("tagLine", "").strip()
if username:
if tag:
username = f"{username}#{tag}"
else:
username = f"Account-{puuid[:6]}"
# Rank snapshot (saved per account)
rank = fetch_rank_from_lcu()
return {
"username": username,
"icon": current_user.get("profileIconId", 0),
"level": current_user.get("summonerLevel", 0),
"puuid": puuid,
"summonerId": summoner_id,
"owned_champions": owned,
"owned_skins": owned_skins,
"loot_skins": loot_skins,
"rank_soloq": rank,
"last_updated": datetime.datetime.utcnow().isoformat() + "Z"
}
def champ_icon_url(champion_name):
# Example: "Urgot" → https://ddragon.leagueoflegends.com/cdn/14.10.1/img/champion/Urgot.png
return f"{DD_BASE}/cdn/{DD_VERSION}/img/champion/{champion_name}.png"
def skin_splash_url(champion_name, selected_skin_id):
"""
Riot skinId format: championId * 1000 + skinIndex
We only need skinIndex for splash art.
"""
if not champion_name:
return None
if selected_skin_id is None:
skin_index = 0
else:
skin_index = selected_skin_id % 1000
# Full splash (client uses 308x560, you can crop via CSS to 120x50)
return f"{DD_BASE}/cdn/img/champion/splash/{champion_name}_{skin_index}.jpg"
# =========================
# CHAMP SELECT PLAYER BUILD
# =========================
def build_player(p, key_to_name, actions, pick_order_map):
summoner_id = p.get("summonerId")
champion_id = p.get("championId")
hover_id = p.get("championPickIntent")
selected_skin_id = p.get("selectedSkinId")
# Fallback hover from active pick action if championPickIntent is missing
if not hover_id and actions:
cell_id = p.get("cellId")
for group in actions:
for act in group:
if (
act.get("actorCellId") == cell_id
and act.get("type") == "pick"
and not act.get("completed", False)
and act.get("championId")
):
hover_id = act.get("championId")
break
# Champion name
champ_name = key_to_name.get(champion_id) if champion_id else None
hover_name = key_to_name.get(hover_id) if hover_id else None
# Icons
champ_icon = (
f"{DD_BASE}/cdn/{DD_VERSION}/img/champion/{champ_name}.png"
if champ_name else None
)
hover_icon = (
f"{DD_BASE}/cdn/{DD_VERSION}/img/champion/{hover_name}.png"
if hover_name else None
)
# Splash (for picked skin)
splash = skin_splash_url(champ_name, selected_skin_id)
# Hover object with placeholder when nothing is hovered
if hover_id and hover_name:
hover_obj = {
"id": hover_id,
"name": hover_name,
"icon": hover_icon,
"isPlaceholder": False,
}
else:
hover_obj = {
"id": None,
"name": None,
"icon": None,
"isPlaceholder": True,
}
# Summoner info
summoner = {}
ranked = {}
mastery = []
champ_mastery = None
if summoner_id and summoner_id != 0:
summoner = lcu_request(f"/lol-summoner/v1/summoners/{summoner_id}") or {}
ranked = lcu_request(f"/lol-ranked/v1/ranked-stats/{summoner_id}") or {}
raw_mastery = lcu_request(
f"/lol-collections/v1/inventories/{summoner_id}/champion-mastery"
)
if isinstance(raw_mastery, list):
mastery = raw_mastery
champ_mastery = next(
(m for m in mastery if m.get("championId") == champion_id), None
)
# Action + role
action = get_current_action(p.get("cellId"), actions)
hybrid_role = hybrid_role_for_player(p, pick_order_map)
return {
"summonerName": summoner.get("displayName", "Unknown"),
"championId": champion_id,
"championName": champ_name,
"championIcon": champ_icon,
"hoverChampionId": hover_id,
"hoverChampionIcon": hover_icon,
"hoverChampion": hover_obj, # unified hover object with placeholder
"skinSplash": splash,
"rankQueues": ranked.get("queues", []),
"mastery": champ_mastery,
"spell1": p.get("spell1Id"),
"spell2": p.get("spell2Id"),
"role": hybrid_role, # HYBRID ROLE HERE
"rawAssignedPosition": p.get("assignedPosition"),
"action": action,
"cellId": p.get("cellId"),
"team": p.get("teamId"),
}
# ---------------------------
# Routes
# ---------------------------
@app.route("/", methods=["GET"])
def index():
active_puuid = request.args.get("puuid", "").strip()
search = request.args.get("search", "").strip().lower()
saved_accounts = load_all_accounts()
active_account = load_account_by_puuid(active_puuid) if active_puuid else None
current_user = get_current_user()
# Auto-repair username if old JSON had empty or fallback name
if active_account and (not active_account.get("username") or active_account["username"].startswith("Account-")):
if current_user:
new_name = current_user.get("gameName", "").strip()
tag = current_user.get("tagLine", "").strip()
if new_name:
if tag:
new_name = f"{new_name}#{tag}"
active_account["username"] = new_name
save_account(active_account)
rank_soloq = active_account.get("rank_soloq", "Unranked") if active_account else None
champions_view = []
if active_account:
champs_dd = get_champions_dd()
owned_ids = set(active_account["owned_champions"])
for key, champ in champs_dd.items():
champ_id = int(champ["key"])
if champ_id not in owned_ids:
continue
if search and search not in champ["name"].lower():
continue
champions_view.append({
"id": champ["id"],
"name": champ["name"],
"title": champ["title"],
"icon": f"{DD_BASE}/cdn/{DD_VERSION}/img/champion/{champ['image']['full']}"
})
champions_view.sort(key=lambda c: c["name"])
return render_template(
"index.html",
saved_accounts=saved_accounts,
active_account=active_account,
champions=champions_view,
search=search,
dd_version=DD_VERSION,
dd_base=DD_BASE,
current_user=current_user,
rank_soloq=rank_soloq
)
@app.route("/create_from_current", methods=["POST"])
def create_from_current():
current = get_current_user()
if not current:
return "Riot Client not running."
acc = build_account_from_lcu(current)
save_account(acc)
return redirect(f"/?puuid={acc['puuid']}")
@app.route("/refresh", methods=["POST"])
def refresh_account():
puuid = request.form.get("puuid")
current = get_current_user()
if not current:
return "Riot Client not running."
updated = build_account_from_lcu(current)
save_account(updated)
return redirect(f"/?puuid={puuid}")
@app.route("/delete", methods=["POST"])
def delete_account():
puuid = request.form.get("puuid")
fpath = account_file_path(puuid)
if os.path.exists(fpath):
os.remove(fpath)
return redirect("/")
@app.route("/champion/<champion_id>")
def champion_skins(champion_id):
puuid = request.args.get("puuid", "")
account = load_account_by_puuid(puuid)
if not account:
return redirect("/")
owned_skin_ids = set(account["owned_skins"]) | set(account["loot_skins"])
champ = get_champion_detail_dd(champion_id)
champ_name = champ["name"]
owned_skins = []
unowned_skins = []
for skin in champ["skins"]:
skin_id = int(skin["id"])
num = skin["num"]
data = {
"id": skin_id,
"name": skin["name"],
"splash": f"{DD_BASE}/cdn/img/champion/splash/{champion_id}_{num}.jpg"
}
if skin_id in owned_skin_ids:
owned_skins.append(data)
else:
unowned_skins.append(data)
return render_template(
"skins.html",
champion_name=champ_name,
champion_id=champion_id,
owned_skins=owned_skins,
unowned_skins=unowned_skins,
account=account
)
def nocache(view):
def no_cache(*args, **kwargs):
response = make_response(view(*args, **kwargs))
response.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0"
response.headers["Pragma"] = "no-cache"
response.headers["Expires"] = "0"
return response
no_cache.__name__ = view.__name__
return no_cache
# ---------------- Real-time CS tracking ----------------
REALTIME_CS = {}
LAST_EVENT_ID = -1
def update_realtime_cs():
global LAST_EVENT_ID, REALTIME_CS
events = live_request("/liveclientdata/eventdata")
if not events:
return
for ev in events.get("Events", []):
eid = ev.get("EventID", -1)
# Skip already processed events
if eid <= LAST_EVENT_ID:
continue
LAST_EVENT_ID = eid
# Track minion kills
if ev.get("EventName") == "MinionKill":
killer = ev.get("KillerName")
if killer:
REALTIME_CS[killer] = REALTIME_CS.get(killer, 0) + 1
@app.route("/api/current-game")
@nocache
def api_current_game():
# Update real-time CS from events
update_realtime_cs()
# Gameflow phase
phase = lcu_request("/lol-gameflow/v1/gameflow-phase")
in_game = (phase == "InProgress")
if not in_game:
return jsonify({"in_game": False, "phase": phase})
# Active player (to highlight self)
active = live_request("/liveclientdata/activeplayer")
active_name = None
if active:
active_name = active.get("summonerName")
# Player list
all_data = live_request("/liveclientdata/allgamedata")
if not all_data:
return jsonify({"in_game": True, "loading": True})
players = all_data.get("allPlayers", [])
if not players:
return jsonify({"in_game": True, "loading": True})
blue_team = []
red_team = []
for p in players:
scores = p.get("scores", {})
items = p.get("items", [])
# Official CS from Riot (batched)
official_cs = scores.get("creepScore", 0)
# Real-time CS from events
rt_cs = REALTIME_CS.get(p.get("summonerName"), 0)
# Final CS
final_cs = official_cs + rt_cs
entry = {
"name": p.get("summonerName", "Unknown"),
"champion": p.get("championName", "Unknown"),
"champ_icon": champ_icon_url(p.get("championName", "Unknown")),
"team": p.get("team", "ORDER"),
"level": p.get("level", 1),
"k": scores.get("kills", 0),
"d": scores.get("deaths", 0),
"a": scores.get("assists", 0),
"cs": final_cs,
"items": [i["itemID"] for i in items],
"is_self": (p.get("summonerName") == active_name)
}
if p.get("team") == "ORDER":
blue_team.append(entry)
else:
red_team.append(entry)
# Game time
game_data = all_data.get("gameData", {})
game_time = int(game_data.get("gameTime", 0))
return jsonify({
"in_game": True,
"loading": False,
"game_time": game_time,
"blue_team": blue_team,
"red_team": red_team
})
@app.route("/current-game")
def current_game():
return render_template("current_game.html")
@app.route("/api/gameflow")
def api_gameflow():
phase = lcu_request("/lol-gameflow/v1/gameflow-phase")
if not phase:
return {"in_game": False, "phase": "None"}
return {
"in_game": phase == "InProgress",
"phase": phase
}
# ---------------------------
# Unified Champ Select Route
# ---------------------------
@app.route("/champ-select")
@nocache
def champ_select():
"""
Single route:
- If client requests JSON (Accept: application/json), return champ-select data.
- Otherwise, render the HTML page.
"""
accept = request.headers.get("Accept", "")
# If not asking for JSON, just serve the page
if "application/json" not in accept:
return render_template("champ_select.html", dd_version=DD_VERSION)
# JSON mode: build champ-select payload
session = lcu_request("/lol-champ-select/v1/session")
if not session:
return jsonify({"in_champ_select": False})
actions = session.get("actions", [])
my_team = session.get("myTeam", [])
their_team = session.get("theirTeam", [])
bans = session.get("bans", {})
# Data Dragon mapping
champs_dd = get_champions_dd()
key_to_name = {int(c["key"]): c["id"] for c in champs_dd.values()}
# Pick order map for hybrid roles
pick_order_map = build_pick_order_map(actions)
# Build teams
my_team_built = [build_player(p, key_to_name, actions, pick_order_map) for p in my_team]
their_team_built = [build_player(p, key_to_name, actions, pick_order_map) for p in their_team]
# Bans with names/icons
def build_ban_list(ban_ids):
result = []
for cid in ban_ids or []:
name = key_to_name.get(cid)
icon = f"{DD_BASE}/cdn/{DD_VERSION}/img/champion/{name}.png" if name else None
result.append({
"championId": cid,
"championName": name,
"championIcon": icon
})
return result
my_bans = build_ban_list(bans.get("myTeamBans"))
their_bans = build_ban_list(bans.get("theirTeamBans"))
payload = {
"in_champ_select": True,
"my_team": my_team_built,
"their_team": their_team_built,
"my_bans": my_bans,
"their_bans": their_bans,
"actions": actions,
}
return jsonify(payload)
if __name__ == "__main__":
app.run(debug=True)
{% extends "base.html" %}
{% block content %}
<div class="cs-wrapper">
<h2 class="cs-title">CHAMPION SELECT</h2>
<div id="cs-status" class="cs-status"></div>
<div class="cs-teams">
<!-- MY TEAM -->
<div class="cs-team">
<div class="cs-team-header">YOUR TEAM</div>
<div id="my-team" class="cs-team-list"></div>
</div>
<!-- ENEMY TEAM -->
<div class="cs-team">
<div class="cs-team-header">ENEMY TEAM</div>
<div id="their-team" class="cs-team-list"></div>
</div>
</div>
</div>
<script>
async function loadChampSelect() {
const res = await fetch("/api/champ-select");
const data = await res.json();
const status = document.getElementById("cs-status");
if (!data.in_champ_select) {
status.textContent = "Not currently in champion select.";
document.getElementById("my-team").innerHTML = "";
document.getElementById("their-team").innerHTML = "";
return;
}
status.textContent = "";
renderTeam("my-team", data.my_team);
renderTeam("their-team", data.their_team);
}
function renderTeam(targetId, team) {
const target = document.getElementById(targetId);
target.innerHTML = "";
team.forEach(p => {
const row = document.createElement("div");
row.className = "cs-row";
// Determine status text
let statusText = "";
if (p.action) {
if (p.action.status === "active") {
if (p.action.type === "pick") statusText = `Picking (Order ${p.action.order})`;
if (p.action.type === "ban") statusText = `Banning (Order ${p.action.order})`;
} else if (p.action.status === "locked") {
if (p.action.type === "pick") statusText = "Locked In";
if (p.action.type === "ban") statusText = "Banned";
}
}
// Hovering champion
let hoverHTML = "";
if (!p.championIcon && p.hoverChampionIcon) {
hoverHTML = `<img src="${p.hoverChampionIcon}" class="cs-champ-icon" style="opacity:0.5;">`;
}
row.innerHTML = `
<div class="cs-champ">
${p.championIcon ? `<img src="${p.championIcon}" class="cs-champ-icon">` : hoverHTML}
</div>
<div class="cs-info">
<div class="cs-name">${p.summonerName}</div>
<div class="cs-sub">
${renderRank(p.rankQueues)} • ${renderMastery(p.mastery)}<br>
<span style="color:#c8aa6e">${statusText}</span>
</div>
</div>
<div class="cs-role">${renderRole(p.role)}</div>
<div class="cs-spells">
${renderSpell(p.spell1)}
${renderSpell(p.spell2)}
</div>
`;
target.appendChild(row);
});
}
function renderRank(queues) {
if (!queues || !queues.length) return "Unranked";
const solo = queues.find(q => q.queueType === "RANKED_SOLO_5x5");
if (!solo) return "Unranked";
return `${solo.tier} ${solo.division} (${solo.leaguePoints} LP)`;
}
function renderMastery(m) {
if (!m) return "Mastery 0";
return `Mastery ${m.championLevel} (${m.championPoints} pts)`;
}
function renderSpell(id) {
if (!id) return "";
const spellMap = {
1: "SummonerBoost",
3: "SummonerExhaust",
4: "SummonerFlash",
6: "SummonerHaste",
7: "SummonerHeal",
11: "SummonerSmite",
12: "SummonerTeleport",
13: "SummonerMana",
14: "SummonerDot",
21: "SummonerBarrier"
};
const name = spellMap[id];
if (!name) return "";
return `<img class="cs-spell" src="https://ddragon.leagueoflegends.com/cdn/{{ dd_version }}/img/spell/${name}.png">`;
}
function renderRole(role) {
if (!role) return "";
const icons = {
top: "🛡️",
jungle: "🌿",
middle: "🔥",
bottom: "🏹",
utility: "💛"
};
return icons[role.toLowerCase()] || "";
}
loadChampSelect();
setInterval(loadChampSelect, 1500);
</script>
{% endblock %}
{% extends "base.html" %}
{% block content %}
<h2>Saved Accounts</h2>
{% if current_user %}
<div class="logged-in-box">
Logged in as: <strong>{{ current_user.displayName }}</strong>
</div>
{% else %}
<div class="logged-in-box">
<em>Riot Client not detected</em>
</div>
{% endif %}
<div id="ingame-banner" class="ingame-banner" style="display:none;">
<span>You are currently in a game</span>
<a href="/current-game" class="ingame-button">View Live Game</a>
</div>
<div class="account-grid">
{% for acc in saved_accounts %}
<a class="account-card {% if active_account and acc.puuid == active_account.puuid %}active-account{% endif %}"
href="/?puuid={{ acc.puuid }}">
<img src="https://ddragon.leagueoflegends.com/cdn/{{ dd_version }}/img/profileicon/{{ acc.icon }}.png"
width="64" height="64">
<div class="acc-name">{{ acc.username }}</div>
<div class="acc-level">Level {{ acc.level }}</div>
<div class="acc-rank">{{ rank_soloq if active_account and acc.puuid == active_account.puuid else "" }}</div>
</a>
{% endfor %}
</div>
<hr>
<h3>Load Account</h3>
<form method="post" action="/create_from_current">
<button type="submit">Use Current Riot Account</button>
</form>
{% if active_account %}
<hr>
<div class="account-actions">
<form method="post" action="/refresh">
<input type="hidden" name="puuid" value="{{ active_account.puuid }}">
<button class="refresh-btn">Refresh Account</button>
</form>
<form method="post" action="/delete">
<input type="hidden" name="puuid" value="{{ active_account.puuid }}">
<button class="delete-btn">Delete Account</button>
</form>
</div>
<h2>{{ active_account.username }} — Owned Champions</h2>
<form method="get" action="/">
<input type="hidden" name="puuid" value="{{ active_account.puuid }}">
<input type="text" name="search" placeholder="Search champions..." value="{{ search }}" class="lol-search-input"> <button type="submit">Search</button>
</form>
<h2 class="lol-section-title">CHAMPIONS <span class="lol-section-sub">{{ champions|length }} owned</span></h2>
<div class="lol-champion-grid">
{% for champ in champions %}
<a class="lol-champ-card" href="/champion/{{ champ.id }}?puuid={{ active_account.puuid }}">
<div class="lol-champ-icon-wrap">
<img src="{{ champ.icon }}" class="lol-champ-icon">
<div class="lol-champ-border"></div>
</div>
<div class="lol-champ-name">{{ champ.name }}</div>
</a>
{% endfor %}
</div>
<script>
async function checkGameflow() {
try {
const res = await fetch("/api/gameflow");
const data = await res.json();
const banner = document.getElementById("ingame-banner");
if (data.in_game) {
banner.style.display = "flex";
} else {
banner.style.display = "none";
}
} catch (e) {
console.log("Gameflow check failed");
}
}
// Check every 2 seconds
checkGameflow();
setInterval(checkGameflow, 2000);
</script>
{% endif %}
{% endblock %}
/* ============================================================
ACCOUNT GRID
============================================================ */
.account-grid {
display: flex;
flex-wrap: wrap;
gap: 16px;
margin-bottom: 20px;
}
.account-card-wrapper {
display: flex;
flex-direction: column;
gap: 6px;
}
.account-card {
display: flex;
flex-direction: column;
align-items: center;
text-decoration: none;
background: #1e1e2f;
padding: 10px;
border-radius: 8px;
color: #fff;
width: 160px;
border: 1px solid #333;
transition: 0.15s ease;
}
.account-card:hover {
border-color: #2d7dff;
}
.account-card.active-account {
border: 2px solid #2d7dff;
background-color: #1a1f35;
transform: scale(1.03);
}
.account-card-actions {
display: flex;
gap: 6px;
justify-content: center;
}
.logged-in-box {
display: flex;
align-items: center;
gap: 12px;
background: #1e1e2f;
padding: 12px 16px;
border-radius: 8px;
margin-bottom: 20px;
color: #fff;
border: 1px solid #333;
}
.logged-in-box img {
width: 48px;
height: 48px;
border-radius: 6px;
}
.logged-in-box.no-client {
background: #3a1e1e;
border-color: #662;
}
.refresh-btn,
.delete-btn {
padding: 4px 8px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 12px;
color: white;
}
.refresh-btn {
background: #2d7dff;
}
.refresh-btn:hover {
background: #1a5ed8;
}
.delete-btn {
background: #d9534f;
}
.delete-btn:hover {
background: #b52b27;
}
/* ============================================================
CHAMPION GRID (COLLECTION PAGE)
============================================================ */
.lol-champion-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
gap: 12px;
margin-top: 16px;
}
.lol-champ-card {
text-decoration: none;
color: #f0e6d2;
display: flex;
flex-direction: column;
align-items: center;
cursor: pointer;
transition: transform 0.15s ease;
}
.lol-champ-card:hover {
transform: translateY(-2px);
}
.lol-champ-icon-wrap {
position: relative;
width: 88px;
height: 88px;
border-radius: 6px;
overflow: hidden;
background: #050816;
}
.lol-champ-icon {
width: 100%;
height: 100%;
object-fit: cover;
filter: saturate(1.1);
}
.lol-champ-border {
position: absolute;
inset: 0;
border-radius: 6px;
border: 1px solid rgba(255, 255, 255, 0.06);
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.8) inset;
transition: border-color 0.15s, box-shadow 0.15s;
}
.lol-champ-card:hover .lol-champ-border {
border-color: #c8aa6e;
box-shadow: 0 0 8px rgba(200, 170, 110, 0.5);
}
.lol-champ-name {
margin-top: 6px;
font-size: 12px;
text-align: center;
color: #f0e6d2;
text-shadow: 0 0 4px rgba(0,0,0,0.8);
}
.lol-search-input {
background: rgba(5, 8, 16, 0.9);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 3px;
padding: 6px 10px;
color: #f0e6d2;
font-size: 12px;
min-width: 220px;
}
.lol-search-input::placeholder {
color: #7f7f7f;
}
.lol-section-title {
font-size: 14px;
letter-spacing: 2px;
color: #c8aa6e;
margin-bottom: 8px;
}
.lol-section-sub {
font-size: 12px;
color: #a0a0a0;
margin-left: 8px;
}
/* ============================================================
SKIN GRID
============================================================ */
.skin-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
gap: 16px;
padding: 10px 0;
}
.skin-card {
background: #0f1320;
border-radius: 6px;
overflow: hidden;
text-align: center;
border: 1px solid #1b2236;
transition: transform 0.15s ease, box-shadow 0.15s ease, border-color 0.15s ease;
}
.skin-card:hover {
transform: scale(1.06);
box-shadow: 0 0 12px rgba(0, 140, 255, 0.6);
border-color: #2d7dff;
}
.skin-thumb {
width: 100%;
aspect-ratio: 1 / 1;
background-size: cover;
background-position: center top;
border-bottom: 1px solid #1b2236;
}
.skin-name {
padding: 6px;
font-size: 13px;
font-weight: 600;
color: #e6e6e6;
text-shadow: 0 0 4px rgba(0,0,0,0.8);
}
.skin-card.owned {
background-color: #111827;
}
.skin-card.unowned {
opacity: 0.55;
}
.skin-card.unowned:hover {
opacity: 0.9;
}
/* ============================================================
CURRENT GAME UI
============================================================ */
.cg-status {
margin-bottom: 10px;
color: #cbd5f5;
font-size: 14px;
}
.cg-container {
background: #050814;
border-radius: 8px;
padding: 12px;
border: 1px solid #1b2236;
}
.cg-header {
display: flex;
justify-content: flex-end;
margin-bottom: 10px;
}
.cg-time {
font-size: 18px;
font-weight: 700;
color: #e6e6e6;
}
.cg-teams {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
.cg-team h3 {
margin-bottom: 6px;
font-size: 14px;
color: #cbd5f5;
}
.cg-team-grid {
display: flex;
flex-direction: column;
gap: 6px;
}
.cg-player-card {
background: #0f1320;
border-radius: 6px;
padding: 6px 8px;
border: 1px solid #1b2236;
display: flex;
flex-direction: column;
transition: background 0.15s ease, border-color 0.15s ease;
}
.cg-player-card.cg-self {
border-color: #2d7dff;
background: #111a33;
}
.cg-player-top {
display: flex;
align-items: center;
gap: 8px;
}
.cg-champ-icon {
width: 32px;
height: 32px;
border-radius: 4px;
overflow: hidden;
flex-shrink: 0;
}
.cg-champ-img {
width: 100%;
height: 100%;
object-fit: cover;
}
.cg-player-info {
display: flex;
flex-direction: column;
}
.cg-name {
font-size: 13px;
font-weight: 600;
color: #e6e6e6;
}
.cg-sub {
font-size: 11px;
color: #a5b0d0;
}
.ingame-banner {
background: linear-gradient(90deg, #0a1224, #0f1a33);
border: 1px solid #2d7dff;
padding: 12px 16px;
border-radius: 6px;
margin-bottom: 16px;
display: flex;
justify-content: space-between;
align-items: center;
color: #d7e0ff;
font-size: 15px;
font-weight: 600;
}
.ingame-button {
background: #2d7dff;
padding: 6px 12px;
border-radius: 4px;
color: white;
text-decoration: none;
font-weight: 600;
transition: background 0.15s ease;
}
.ingame-button:hover {
background: #1b5ed6;
}
/* ============================================================
CHAMP SELECT UI
============================================================ */
.cs-wrapper {
padding: 20px;
color: #f0e6d2;
}
.cs-title {
font-size: 22px;
color: #c8aa6e;
margin-bottom: 20px;
letter-spacing: 2px;
}
.cs-status {
font-size: 14px;
color: #a0a0a0;
margin-bottom: 12px;
}
.cs-teams {
display: flex;
gap: 20px;
}
.cs-team {
flex: 1;
}
.cs-team-header {
font-size: 14px;
color: #c8aa6e;
margin-bottom: 10px;
letter-spacing: 1px;
}
.cs-team-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.cs-row {
display: flex;
align-items: center;
background: rgba(5, 8, 16, 0.85);
border: 1px solid rgba(255,255,255,0.05);
padding: 8px;
border-radius: 6px;
}
.cs-champ-icon {
width: 48px;
height: 48px;
border-radius: 4px;
overflow: hidden;
}
.cs-champ-icon img {
width: 100%;
height: 100%;
object-fit: cover;
}
.cs-info {
margin-left: 10px;
flex: 1;
}
.cs-name {
font-size: 14px;
}
.cs-sub {
font-size: 11px;
color: #a0a0a0;
}
.cs-role {
width: 40px;
text-align: center;
font-size: 20px;
}
.cs-spells {
display: flex;
gap: 6px;
}
.cs-spell {
width: 28px;
height: 28px;
border-radius: 4px;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment