Last active
February 11, 2026 07:01
-
-
Save Ensamisten/b5bea164e8cf8503719c2acc765a9c15 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| {% 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 %} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| {% 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 %} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* ============================================================ | |
| 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