tauri-app: use fields from recorded raw league data
All checks were successful
record-daemon / Build, check and test (push) Successful in 2m6s

This commit is contained in:
2026-03-27 13:42:37 +01:00
parent d67d52fa86
commit 52f8be7caa
2 changed files with 218 additions and 134 deletions

View File

@@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted } from "vue"; import { ref, onMounted } from "vue";
import { invoke } from "@tauri-apps/api/core"; import { invoke } from "@tauri-apps/api/core";
import type { GameHistoryItem, TimestampedEvent, ItemInfo, RawEndGameStats, EndGamePlayer } from "../types/timeline"; import type { GameHistoryItem, TimestampedEvent, ItemInfo } from "../types/timeline";
import { import {
getGameResult, getGameResult,
formatDuration, formatDuration,
@@ -16,6 +16,15 @@ import {
formatGameStartTime, formatGameStartTime,
getSummonerSpellUrl, getSummonerSpellUrl,
getItemImageUrl, getItemImageUrl,
getChampionName,
getSummonerName,
getQueueType,
getQueueId,
getGameMode,
getMapName,
getFinalStats,
getSummonerSpells,
getItems,
} from "../types/timeline"; } from "../types/timeline";
// Helper to get video timestamp in seconds from tuple format // Helper to get video timestamp in seconds from tuple format
@@ -49,58 +58,9 @@ function closeDetail() {
selectedGame.value = null; selectedGame.value = null;
} }
// Helper to find local player from raw end game stats
function getLocalPlayer(stats: RawEndGameStats | null): EndGamePlayer | null {
if (!stats) return null;
// Try localPlayer field first (camelCase from API)
if (stats.localPlayer) {
return stats.localPlayer;
}
// Try teams
if (stats.teams) {
for (const team of stats.teams) {
if (team.players) {
for (const player of team.players) {
if (player.isLocalPlayer) {
return player;
}
}
}
}
}
// Try legacy players array
if (stats.players && stats.players.length > 0) {
return stats.players[0];
}
return null;
}
// Helper to get items array for display (6 slots + trinket) // Helper to get items array for display (6 slots + trinket)
// Items are now stored as raw item IDs in raw_end_game_stats
function getItemsArray(game: GameHistoryItem): (ItemInfo | null)[] { function getItemsArray(game: GameHistoryItem): (ItemInfo | null)[] {
const result: (ItemInfo | null)[] = [null, null, null, null, null, null, null]; return getItems(game);
const localPlayer = getLocalPlayer(game.raw_end_game_stats);
if (localPlayer && localPlayer.items) {
// Items are stored as an array of item IDs (up to 7 items: 6 main + 1 trinket)
for (let i = 0; i < Math.min(localPlayer.items.length, 7); i++) {
const itemId = localPlayer.items[i];
if (itemId && itemId > 0) {
// Slot 6 is trinket, slots 0-5 are main items
result[i] = {
itemId: itemId,
name: null,
slot: i
};
}
}
}
return result;
} }
onMounted(() => { onMounted(() => {
@@ -161,13 +121,13 @@ onMounted(() => {
<div class="champion-section"> <div class="champion-section">
<div class="champion-image-wrapper"> <div class="champion-image-wrapper">
<img <img
:src="getChampionImageUrl(game.champion)" :src="getChampionImageUrl(getChampionName(game))"
:alt="game.champion || 'Unknown Champion'" :alt="getChampionName(game) || 'Unknown Champion'"
class="champion-image" class="champion-image"
@error="($event.target as HTMLImageElement).src = getChampionImageUrl(null)" @error="($event.target as HTMLImageElement).src = getChampionImageUrl(null)"
/> />
<div class="champion-level" v-if="game.final_stats"> <div class="champion-level" v-if="getFinalStats(game)">
{{ Math.min(18, Math.floor(game.final_stats.game_duration / 60)) }} {{ Math.min(18, Math.floor(getFinalStats(game)!.game_duration / 60)) }}
</div> </div>
</div> </div>
@@ -175,18 +135,18 @@ onMounted(() => {
<div class="summoner-spells"> <div class="summoner-spells">
<div class="spell-slot"> <div class="spell-slot">
<img <img
v-if="game.summoner_spells" v-if="getSummonerSpells(game)"
:src="getSummonerSpellUrl(game.summoner_spells.spell1Id)" :src="getSummonerSpellUrl(getSummonerSpells(game)!.spell1Id)"
:alt="game.summoner_spells.spell1Name || 'Spell 1'" :alt="getSummonerSpells(game)!.spell1Name || 'Spell 1'"
class="spell-image" class="spell-image"
/> />
<div v-else class="spell-placeholder"></div> <div v-else class="spell-placeholder"></div>
</div> </div>
<div class="spell-slot"> <div class="spell-slot">
<img <img
v-if="game.summoner_spells" v-if="getSummonerSpells(game)"
:src="getSummonerSpellUrl(game.summoner_spells.spell2Id)" :src="getSummonerSpellUrl(getSummonerSpells(game)!.spell2Id)"
:alt="game.summoner_spells.spell2Name || 'Spell 2'" :alt="getSummonerSpells(game)!.spell2Name || 'Spell 2'"
class="spell-image" class="spell-image"
/> />
<div v-else class="spell-placeholder"></div> <div v-else class="spell-placeholder"></div>
@@ -200,7 +160,7 @@ onMounted(() => {
<!-- Left: Queue Type & Time --> <!-- Left: Queue Type & Time -->
<div class="game-info-left"> <div class="game-info-left">
<div class="queue-type"> <div class="queue-type">
{{ getQueueDisplayName(game.queue_type, game.queue_id) }} {{ getQueueDisplayName(getQueueType(game), getQueueId(game)) }}
</div> </div>
<div class="game-duration"> <div class="game-duration">
{{ formatDuration(game.duration_secs) }} {{ formatDuration(game.duration_secs) }}
@@ -211,12 +171,12 @@ onMounted(() => {
</div> </div>
<!-- Right: KDA Score --> <!-- Right: KDA Score -->
<div class="kda-section" v-if="game.final_stats"> <div class="kda-section" v-if="getFinalStats(game)">
<span class="kda-values"> <span class="kda-values">
{{ formatKDA(game.final_stats) }} {{ formatKDA(getFinalStats(game)) }}
</span> </span>
<span class="kda-ratio" :class="{ perfect: game.final_stats.deaths === 0 }"> <span class="kda-ratio" :class="{ perfect: getFinalStats(game)!.deaths === 0 }">
{{ calculateKDA(game.final_stats) }} KDA {{ calculateKDA(getFinalStats(game)) }} KDA
</span> </span>
</div> </div>
<div class="kda-section" v-else> <div class="kda-section" v-else>
@@ -227,15 +187,15 @@ onMounted(() => {
</div> </div>
<!-- Right: Key Stats Column + Items --> <!-- Right: Key Stats Column + Items -->
<div class="stats-section" v-if="game.final_stats"> <div class="stats-section" v-if="getFinalStats(game)">
<!-- Key Stats Column --> <!-- Key Stats Column -->
<div class="key-stats-column"> <div class="key-stats-column">
<div class="key-stat"> <div class="key-stat">
<span class="key-stat-value">{{ formatCSPerMin(game.final_stats) }}</span> <span class="key-stat-value">{{ formatCSPerMin(getFinalStats(game)) }}</span>
<span class="key-stat-label">CS/m</span> <span class="key-stat-label">CS/m</span>
</div> </div>
<div class="key-stat"> <div class="key-stat">
<span class="key-stat-value">{{ formatGoldPerMin(game.final_stats) }}</span> <span class="key-stat-value">{{ formatGoldPerMin(getFinalStats(game)) }}</span>
<span class="key-stat-label">Gold/m</span> <span class="key-stat-label">Gold/m</span>
</div> </div>
</div> </div>
@@ -277,14 +237,14 @@ onMounted(() => {
<div class="modal-header"> <div class="modal-header">
<div class="modal-champion"> <div class="modal-champion">
<img <img
:src="getChampionImageUrl(selectedGame.champion)" :src="getChampionImageUrl(getChampionName(selectedGame))"
:alt="selectedGame.champion || 'Unknown Champion'" :alt="getChampionName(selectedGame) || 'Unknown Champion'"
class="modal-champion-image" class="modal-champion-image"
/> />
<div class="modal-champion-info"> <div class="modal-champion-info">
<h2>{{ selectedGame.champion || 'Unknown Champion' }}</h2> <h2>{{ getChampionName(selectedGame) || 'Unknown Champion' }}</h2>
<div class="modal-queue"> <div class="modal-queue">
{{ getQueueDisplayName(selectedGame.queue_type, selectedGame.queue_id) }} {{ getQueueDisplayName(getQueueType(selectedGame), getQueueId(selectedGame)) }}
</div> </div>
</div> </div>
</div> </div>
@@ -306,66 +266,62 @@ onMounted(() => {
<span class="detail-label">Started:</span> <span class="detail-label">Started:</span>
<span class="detail-value">{{ formatGameStartTime(selectedGame.start_time) }}</span> <span class="detail-value">{{ formatGameStartTime(selectedGame.start_time) }}</span>
</div> </div>
<div class="detail-row" v-if="selectedGame.game_mode"> <div class="detail-row" v-if="getGameMode(selectedGame)">
<span class="detail-label">Game Mode:</span> <span class="detail-label">Game Mode:</span>
<span class="detail-value">{{ selectedGame.game_mode }}</span> <span class="detail-value">{{ getGameMode(selectedGame) }}</span>
</div> </div>
<div class="detail-row" v-if="selectedGame.map_name"> <div class="detail-row" v-if="getMapName(selectedGame)">
<span class="detail-label">Map:</span> <span class="detail-label">Map:</span>
<span class="detail-value">{{ selectedGame.map_name }}</span> <span class="detail-value">{{ getMapName(selectedGame) }}</span>
</div> </div>
<div class="detail-row" v-if="selectedGame.summoner_name"> <div class="detail-row" v-if="getSummonerName(selectedGame)">
<span class="detail-label">Summoner:</span> <span class="detail-label">Summoner:</span>
<span class="detail-value">{{ selectedGame.summoner_name }}</span> <span class="detail-value">{{ getSummonerName(selectedGame) }}</span>
</div>
<div class="detail-row" v-if="selectedGame.team">
<span class="detail-label">Team:</span>
<span class="detail-value">{{ selectedGame.team === 100 ? 'Blue' : 'Red' }}</span>
</div> </div>
</div> </div>
</div> </div>
<!-- Stats --> <!-- Stats -->
<div class="modal-section" v-if="selectedGame.final_stats"> <div class="modal-section" v-if="getFinalStats(selectedGame)">
<h3>Performance</h3> <h3>Performance</h3>
<div class="stats-highlight"> <div class="stats-highlight">
<div class="stat-highlight-item"> <div class="stat-highlight-item">
<span class="stat-highlight-value">{{ selectedGame.final_stats.kills }}</span> <span class="stat-highlight-value">{{ getFinalStats(selectedGame)!.kills }}</span>
<span class="stat-highlight-label">Kills</span> <span class="stat-highlight-label">Kills</span>
</div> </div>
<div class="stat-highlight-item"> <div class="stat-highlight-item">
<span class="stat-highlight-value">{{ selectedGame.final_stats.deaths }}</span> <span class="stat-highlight-value">{{ getFinalStats(selectedGame)!.deaths }}</span>
<span class="stat-highlight-label">Deaths</span> <span class="stat-highlight-label">Deaths</span>
</div> </div>
<div class="stat-highlight-item"> <div class="stat-highlight-item">
<span class="stat-highlight-value">{{ selectedGame.final_stats.assists }}</span> <span class="stat-highlight-value">{{ getFinalStats(selectedGame)!.assists }}</span>
<span class="stat-highlight-label">Assists</span> <span class="stat-highlight-label">Assists</span>
</div> </div>
</div> </div>
<div class="detail-grid"> <div class="detail-grid">
<div class="detail-row"> <div class="detail-row">
<span class="detail-label">KDA Ratio:</span> <span class="detail-label">KDA Ratio:</span>
<span class="detail-value">{{ calculateKDA(selectedGame.final_stats) }}</span> <span class="detail-value">{{ calculateKDA(getFinalStats(selectedGame)) }}</span>
</div> </div>
<div class="detail-row"> <div class="detail-row">
<span class="detail-label">Creep Score:</span> <span class="detail-label">Creep Score:</span>
<span class="detail-value">{{ selectedGame.final_stats.creep_score }} ({{ formatCSPerMin(selectedGame.final_stats) }}/min)</span> <span class="detail-value">{{ getFinalStats(selectedGame)!.creep_score }} ({{ formatCSPerMin(getFinalStats(selectedGame)) }}/min)</span>
</div> </div>
<div class="detail-row"> <div class="detail-row">
<span class="detail-label">Gold Earned:</span> <span class="detail-label">Gold Earned:</span>
<span class="detail-value">{{ formatNumber(selectedGame.final_stats.gold_earned) }} ({{ formatGoldPerMin(selectedGame.final_stats) }}/min)</span> <span class="detail-value">{{ formatNumber(getFinalStats(selectedGame)!.gold_earned) }} ({{ formatGoldPerMin(getFinalStats(selectedGame)) }}/min)</span>
</div> </div>
<div class="detail-row"> <div class="detail-row">
<span class="detail-label">Damage Dealt:</span> <span class="detail-label">Damage Dealt:</span>
<span class="detail-value">{{ formatNumber(selectedGame.final_stats.damage_dealt) }}</span> <span class="detail-value">{{ formatNumber(getFinalStats(selectedGame)!.damage_dealt) }}</span>
</div> </div>
<div class="detail-row"> <div class="detail-row">
<span class="detail-label">Damage Taken:</span> <span class="detail-label">Damage Taken:</span>
<span class="detail-value">{{ formatNumber(selectedGame.final_stats.damage_taken) }}</span> <span class="detail-value">{{ formatNumber(getFinalStats(selectedGame)!.damage_taken) }}</span>
</div> </div>
<div class="detail-row"> <div class="detail-row">
<span class="detail-label">Vision Score:</span> <span class="detail-label">Vision Score:</span>
<span class="detail-value">{{ selectedGame.final_stats.vision_score.toFixed(1) }}</span> <span class="detail-value">{{ getFinalStats(selectedGame)!.vision_score.toFixed(1) }}</span>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -119,6 +119,7 @@ export type GameResult = 'Victory' | 'Defeat' | 'Terminated';
/** /**
* A timeline of events for a recording. * A timeline of events for a recording.
* This is the main data structure returned by the backend. * This is the main data structure returned by the backend.
* Stores raw API responses for maximum flexibility.
*/ */
export interface Timeline { export interface Timeline {
/** Recording ID (UUID). */ /** Recording ID (UUID). */
@@ -132,49 +133,23 @@ export interface Timeline {
/** Events in the timeline. */ /** Events in the timeline. */
events: TimestampedEvent[]; events: TimestampedEvent[];
// Player information
/** Champion played. */
champion: string | null;
/** Skin name. */
skin_name: string | null;
/** Player's summoner name. */
summoner_name: string | null;
/** Player's PUUID. */
puuid: string | null;
/** Team (100 = blue, 200 = red). */
team: number | null;
// Game information // Game information
/** Queue type (ranked, normal, aram, etc.). */
queue_type: string | null;
/** Queue ID. */
queue_id: number | null;
/** Game mode. */
game_mode: string | null;
/** Map name. */
map_name: string | null;
/** Game ID. */ /** Game ID. */
game_id: number | null; game_id: number | null;
/** Match ID. */
match_id: string | null;
// Result // Raw API responses - frontend can parse these as needed
/** Whether the game was won. */ /** Raw session data from `/lol-gameflow/v1/session`. */
victory: boolean | null; raw_session: Record<string, unknown> | null;
/** Final player stats. */ /** Raw summoner data from `/lol-summoner/v1/current-summoner`. */
final_stats: GameFinalStats | null; raw_summoner: Record<string, unknown> | null;
/** Raw champion select data from `/lol-champ-select/v1/session`. */
// Player metadata (runes, summoner spells, items) raw_champion_select: Record<string, unknown> | null;
/** Rune page at game start. */ /** Raw rune page data from `/lol-perks/v1/currentpage`. */
runes: RunePage | null; raw_rune_page: Record<string, unknown> | null;
/** Summoner spells. */ /** Raw live client data from `/liveclientdata/allgamedata`. */
summoner_spells: SummonerSpells | null; raw_live_client_data: Record<string, unknown> | null;
/** Raw end-of-game stats JSON from the API. */ /** Raw end-of-game stats from `/lol-end-of-game/v1/eog-stats-block`. */
raw_end_game_stats: RawEndGameStats | null; raw_end_game_stats: Record<string, unknown> | null;
// All players in the game (puuid to summoner name mapping)
/** All players in the game. */
all_players: PlayerIdentityInfo[];
} }
/** /**
@@ -257,13 +232,166 @@ export type GameHistoryItem = Timeline;
/** /**
* Get the game result as a display string. * Get the game result as a display string.
* Extracts from raw_end_game_stats.
*/ */
export function getGameResult(game: GameHistoryItem): GameResult { export function getGameResult(game: GameHistoryItem): GameResult {
if (game.victory === true) return 'Victory'; // Try to extract victory from raw_end_game_stats
if (game.victory === false) return 'Defeat'; const stats = game.raw_end_game_stats as RawEndGameStats | null;
if (stats?.localPlayer?.stats?.WIN === 1) return 'Victory';
if (stats?.localPlayer?.stats?.WIN === 0) return 'Defeat';
return 'Terminated'; return 'Terminated';
} }
/**
* Extract champion name from raw API data.
*/
export function getChampionName(game: GameHistoryItem): string | null {
// Try raw_end_game_stats first (most reliable)
const endStats = game.raw_end_game_stats as { localPlayer?: { championName?: string } } | null;
if (endStats?.localPlayer?.championName) {
return endStats.localPlayer.championName;
}
// Try raw_live_client_data
const liveData = game.raw_live_client_data as { activePlayer?: { championName?: string } } | null;
if (liveData?.activePlayer?.championName) {
return liveData.activePlayer.championName;
}
return null;
}
/**
* Extract summoner name from raw API data.
*/
export function getSummonerName(game: GameHistoryItem): string | null {
// Try raw_end_game_stats first (most reliable for Riot ID)
const endStats = game.raw_end_game_stats as { localPlayer?: { riotIdGameName?: string; riotIdTagLine?: string } } | null;
if (endStats?.localPlayer?.riotIdGameName) {
const tagLine = endStats.localPlayer.riotIdTagLine;
return tagLine ? `${endStats.localPlayer.riotIdGameName}#${tagLine}` : endStats.localPlayer.riotIdGameName;
}
// Try raw_summoner
const summoner = game.raw_summoner as { displayName?: string; gameName?: string; tagLine?: string; name?: string } | null;
if (summoner?.gameName) {
const tagLine = summoner.tagLine;
return tagLine ? `${summoner.gameName}#${tagLine}` : summoner.gameName;
}
return summoner?.displayName || summoner?.name || null;
}
/**
* Extract queue type from raw session data.
*/
export function getQueueType(game: GameHistoryItem): string | null {
const session = game.raw_session as { gameData?: { queue?: { name?: string } } } | null;
return session?.gameData?.queue?.name || null;
}
/**
* Extract queue ID from raw session data.
*/
export function getQueueId(game: GameHistoryItem): number | null {
const session = game.raw_session as { gameData?: { queue?: { id?: number } } } | null;
return session?.gameData?.queue?.id || null;
}
/**
* Extract game mode from raw session data.
*/
export function getGameMode(game: GameHistoryItem): string | null {
const session = game.raw_session as { gameData?: { queue?: { gameMode?: string } } } | null;
return session?.gameData?.queue?.gameMode || null;
}
/**
* Extract map name from raw session data.
*/
export function getMapName(game: GameHistoryItem): string | null {
const session = game.raw_session as { map?: { name?: string } } | null;
return session?.map?.name || null;
}
/**
* Extract final stats from raw end game stats.
*/
export function getFinalStats(game: GameHistoryItem): GameFinalStats | null {
const stats = game.raw_end_game_stats as RawEndGameStats | null;
const player = stats?.localPlayer;
const playerStats = player?.stats;
if (!playerStats) return null;
return {
kills: playerStats.CHAMPIONS_KILLED || 0,
deaths: playerStats.NUM_DEATHS || 0,
assists: playerStats.ASSISTS || 0,
creep_score: playerStats.MINIONS_KILLED || 0,
gold_earned: playerStats.GOLD_EARNED || 0,
damage_dealt: playerStats.TOTAL_DAMAGE_DEALT_TO_CHAMPIONS || 0,
damage_taken: playerStats.TOTAL_DAMAGE_TAKEN || 0,
vision_score: playerStats.VISION_SCORE || 0,
game_duration: stats?.gameLength || 0,
};
}
/**
* Extract summoner spells from raw live client data.
*/
export function getSummonerSpells(game: GameHistoryItem): SummonerSpells | null {
const liveData = game.raw_live_client_data as {
activePlayer?: {
summonerSpells?: {
summonerSpellOne?: { spellId?: number; displayName?: string };
summonerSpellTwo?: { spellId?: number; displayName?: string };
spell1Id?: number;
spell2Id?: number;
}
}
} | null;
const spells = liveData?.activePlayer?.summonerSpells;
if (!spells) return null;
const spell1Id = spells.summonerSpellOne?.spellId || spells.spell1Id || 0;
const spell2Id = spells.summonerSpellTwo?.spellId || spells.spell2Id || 0;
if (spell1Id === 0 && spell2Id === 0) return null;
return {
spell1Id,
spell2Id,
spell1Name: spells.summonerSpellOne?.displayName || null,
spell2Name: spells.summonerSpellTwo?.displayName || null,
};
}
/**
* Extract items from raw end game stats.
*/
export function getItems(game: GameHistoryItem): (ItemInfo | null)[] {
const result: (ItemInfo | null)[] = [null, null, null, null, null, null, null];
const stats = game.raw_end_game_stats as RawEndGameStats | null;
const items = stats?.localPlayer?.items;
if (items) {
for (let i = 0; i < Math.min(items.length, 7); i++) {
const itemId = items[i];
if (itemId && itemId > 0) {
result[i] = {
itemId,
name: null,
slot: i,
};
}
}
}
return result;
}
/** /**
* Get the result color class. * Get the result color class.
*/ */