tauri-app: fix end-of-game stats reading, pass raw json file to front
All checks were successful
record-daemon / Build, check and test (push) Successful in 2m6s

This commit is contained in:
2026-03-26 23:42:31 +01:00
parent 0871703b11
commit b09f669e73
3 changed files with 30 additions and 180 deletions

View File

@@ -1,163 +1,6 @@
use chrono::{DateTime, Utc}; use serde_json::Value;
use serde::{Deserialize, Serialize};
use std::fs; use std::fs;
use std::path::PathBuf; use std::path::PathBuf;
use uuid::Uuid;
/// A timestamped event in the timeline.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TimestampedEvent {
/// Video timestamp (offset from recording start) as [seconds, nanos].
pub video_timestamp: (i64, i32),
/// Game timestamp (in-game time) as [seconds, nanos].
pub game_timestamp: Option<(i64, i32)>,
/// Real-world timestamp.
pub timestamp: DateTime<Utc>,
/// Event type name.
pub event_type: String,
/// Human-readable description.
pub description: String,
}
impl TimestampedEvent {
/// Get video timestamp in seconds.
pub fn video_timestamp_secs(&self) -> i64 {
self.video_timestamp.0
}
}
/// Final game statistics for the player.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GameFinalStats {
pub kills: u32,
pub deaths: u32,
pub assists: u32,
pub creep_score: u32,
pub gold_earned: u32,
pub damage_dealt: u64,
pub damage_taken: u64,
pub vision_score: f64,
pub game_duration: f64,
}
/// Rune page configuration.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RunePage {
pub primary_style_id: u32,
pub secondary_style_id: u32,
pub selected_perks: Vec<u32>,
#[serde(default)]
pub stat_modifiers: Vec<u32>,
pub name: Option<String>,
#[serde(default)]
pub current: bool,
}
/// Summoner spell information.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SummonerSpells {
pub spell1_id: u32,
pub spell2_id: u32,
pub spell1_name: Option<String>,
pub spell2_name: Option<String>,
}
/// Individual item information.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ItemInfo {
pub item_id: u32,
pub name: Option<String>,
pub slot: u32,
}
/// Item build at game end.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ItemBuild {
pub items: Vec<ItemInfo>,
pub trinket: Option<ItemInfo>,
}
/// Player identity information.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PlayerIdentityInfo {
pub puuid: String,
#[serde(default)]
pub summoner_name: String,
pub champion_name: Option<String>,
pub team: Option<u32>,
}
/// A timeline of events for a recording.
/// This matches the full JSON structure from record-daemon.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Timeline {
/// Recording ID.
pub recording_id: Uuid,
/// Recording start time.
pub start_time: DateTime<Utc>,
/// Recording end time.
pub end_time: Option<DateTime<Utc>>,
/// Total duration in seconds.
pub duration_secs: i64,
/// Events in the timeline.
pub events: Vec<TimestampedEvent>,
/// Champion played.
#[serde(default)]
pub champion: Option<String>,
/// Skin name.
#[serde(default)]
pub skin_name: Option<String>,
/// Queue type.
#[serde(default)]
pub queue_type: Option<String>,
/// Queue ID.
#[serde(default)]
pub queue_id: Option<u32>,
/// Game mode.
#[serde(default)]
pub game_mode: Option<String>,
/// Map name.
#[serde(default)]
pub map_name: Option<String>,
/// Summoner name.
#[serde(default)]
pub summoner_name: Option<String>,
/// Player's PUUID.
#[serde(default)]
pub puuid: Option<String>,
/// Team (100 = blue, 200 = red).
#[serde(default)]
pub team: Option<u32>,
/// Whether the game was won.
#[serde(default)]
pub victory: Option<bool>,
/// Final player stats.
#[serde(default)]
pub final_stats: Option<GameFinalStats>,
/// Game ID.
#[serde(default)]
pub game_id: Option<u64>,
/// Match ID.
#[serde(default)]
pub match_id: Option<String>,
/// Rune page at game start.
#[serde(default)]
pub runes: Option<RunePage>,
/// Summoner spells.
#[serde(default)]
pub summoner_spells: Option<SummonerSpells>,
/// Final item build at game end.
#[serde(default)]
pub final_items: Option<ItemBuild>,
/// All players in the game.
#[serde(default)]
pub all_players: Vec<PlayerIdentityInfo>,
}
/// Get the default output directory for recordings. /// Get the default output directory for recordings.
/// Uses the same directory structure as the record-daemon. /// Uses the same directory structure as the record-daemon.
@@ -174,8 +17,8 @@ fn get_timeline_dir() -> PathBuf {
.unwrap_or_else(|| PathBuf::from("./recordings/timelines")) .unwrap_or_else(|| PathBuf::from("./recordings/timelines"))
} }
/// Load all timelines from disk. /// Load all timelines from disk as raw JSON values.
fn load_timelines() -> Vec<Timeline> { fn load_timelines() -> Vec<Value> {
let timeline_dir = get_timeline_dir(); let timeline_dir = get_timeline_dir();
let mut timelines = Vec::new(); let mut timelines = Vec::new();
@@ -188,7 +31,7 @@ fn load_timelines() -> Vec<Timeline> {
let path = entry.path(); let path = entry.path();
if path.extension().map(|e| e == "json").unwrap_or(false) { if path.extension().map(|e| e == "json").unwrap_or(false) {
if let Ok(contents) = fs::read_to_string(&path) { if let Ok(contents) = fs::read_to_string(&path) {
if let Ok(timeline) = serde_json::from_str::<Timeline>(&contents) { if let Ok(timeline) = serde_json::from_str::<Value>(&contents) {
timelines.push(timeline); timelines.push(timeline);
} }
} }
@@ -197,25 +40,29 @@ fn load_timelines() -> Vec<Timeline> {
} }
// Sort by start time, newest first // Sort by start time, newest first
timelines.sort_by(|a, b| b.start_time.cmp(&a.start_time)); timelines.sort_by(|a, b| {
let a_time = a.get("start_time").and_then(|v| v.as_str()).unwrap_or("");
let b_time = b.get("start_time").and_then(|v| v.as_str()).unwrap_or("");
b_time.cmp(a_time)
});
timelines timelines
} }
/// Get game history - returns full timeline data for each game. /// Get game history - returns full timeline data for each game as raw JSON.
#[tauri::command] #[tauri::command]
fn get_game_history() -> Vec<Timeline> { fn get_game_history() -> Vec<Value> {
load_timelines() load_timelines()
} }
/// Get a specific timeline by recording ID. /// Get a specific timeline by recording ID as raw JSON.
#[tauri::command] #[tauri::command]
fn get_timeline(recording_id: String) -> Option<Timeline> { fn get_timeline(recording_id: String) -> Option<Value> {
let timeline_dir = get_timeline_dir(); let timeline_dir = get_timeline_dir();
let path = timeline_dir.join(format!("{}.json", recording_id)); let path = timeline_dir.join(format!("{}.json", recording_id));
if path.exists() { if path.exists() {
if let Ok(contents) = fs::read_to_string(&path) { if let Ok(contents) = fs::read_to_string(&path) {
return serde_json::from_str::<Timeline>(&contents).ok(); return serde_json::from_str::<Value>(&contents).ok();
} }
} }

View File

@@ -53,9 +53,9 @@ function closeDetail() {
function getLocalPlayer(stats: RawEndGameStats | null): EndGamePlayer | null { function getLocalPlayer(stats: RawEndGameStats | null): EndGamePlayer | null {
if (!stats) return null; if (!stats) return null;
// Try local_player field first // Try localPlayer field first (camelCase from API)
if (stats.local_player) { if (stats.localPlayer) {
return stats.local_player; return stats.localPlayer;
} }
// Try teams // Try teams
@@ -63,7 +63,7 @@ function getLocalPlayer(stats: RawEndGameStats | null): EndGamePlayer | null {
for (const team of stats.teams) { for (const team of stats.teams) {
if (team.players) { if (team.players) {
for (const player of team.players) { for (const player of team.players) {
if (player.is_local_player) { if (player.isLocalPlayer) {
return player; return player;
} }
} }

View File

@@ -180,30 +180,32 @@ export interface Timeline {
/** /**
* Raw end-of-game stats from the League Client API. * Raw end-of-game stats from the League Client API.
* This is the full response from the end-of-game stats endpoint. * This is the full response from the end-of-game stats endpoint.
* Note: Properties use camelCase to match the actual API response.
*/ */
export interface RawEndGameStats { export interface RawEndGameStats {
/** Game ID. */ /** Game ID. */
game_id?: number; gameId?: number;
/** Game length in seconds. */ /** Game length in seconds. */
game_length?: number; gameLength?: number;
/** Match ID. */ /** Match ID. */
match_id?: number; matchId?: number | null;
/** Game result. */ /** Game result. */
game_result?: string; gameResult?: string | null;
/** Local player data. */ /** Local player data. */
local_player?: EndGamePlayer; localPlayer?: EndGamePlayer;
/** Teams data. */ /** Teams data. */
teams?: EndGameTeam[]; teams?: EndGameTeam[];
/** Players (legacy format). */ /** Players (legacy format). */
players?: EndGamePlayer[]; players?: EndGamePlayer[] | null;
} }
/** /**
* Player in end-of-game stats. * Player in end-of-game stats.
* Note: Properties use camelCase to match the actual API response.
*/ */
export interface EndGamePlayer { export interface EndGamePlayer {
/** Whether this is the local player. */ /** Whether this is the local player. */
is_local_player?: boolean; isLocalPlayer?: boolean;
/** Player stats. */ /** Player stats. */
stats?: PlayerStats; stats?: PlayerStats;
/** Items (array of item IDs). */ /** Items (array of item IDs). */
@@ -212,12 +214,13 @@ export interface EndGamePlayer {
/** /**
* Team in end-of-game stats. * Team in end-of-game stats.
* Note: Properties use camelCase to match the actual API response.
*/ */
export interface EndGameTeam { export interface EndGameTeam {
/** Whether this is the player's team. */ /** Whether this is the player's team. */
is_player_team?: boolean; isPlayerTeam?: boolean;
/** Whether this is the winning team. */ /** Whether this is the winning team. */
is_winning_team?: boolean; isWinningTeam?: boolean;
/** Players on the team. */ /** Players on the team. */
players?: EndGamePlayer[]; players?: EndGamePlayer[];
} }