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
All checks were successful
record-daemon / Build, check and test (push) Successful in 2m6s
This commit is contained in:
@@ -1,163 +1,6 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use std::fs;
|
||||
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.
|
||||
/// 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"))
|
||||
}
|
||||
|
||||
/// Load all timelines from disk.
|
||||
fn load_timelines() -> Vec<Timeline> {
|
||||
/// Load all timelines from disk as raw JSON values.
|
||||
fn load_timelines() -> Vec<Value> {
|
||||
let timeline_dir = get_timeline_dir();
|
||||
let mut timelines = Vec::new();
|
||||
|
||||
@@ -188,7 +31,7 @@ fn load_timelines() -> Vec<Timeline> {
|
||||
let path = entry.path();
|
||||
if path.extension().map(|e| e == "json").unwrap_or(false) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -197,25 +40,29 @@ fn load_timelines() -> Vec<Timeline> {
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
/// 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]
|
||||
fn get_game_history() -> Vec<Timeline> {
|
||||
fn get_game_history() -> Vec<Value> {
|
||||
load_timelines()
|
||||
}
|
||||
|
||||
/// Get a specific timeline by recording ID.
|
||||
/// Get a specific timeline by recording ID as raw JSON.
|
||||
#[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 path = timeline_dir.join(format!("{}.json", recording_id));
|
||||
|
||||
if path.exists() {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -53,9 +53,9 @@ function closeDetail() {
|
||||
function getLocalPlayer(stats: RawEndGameStats | null): EndGamePlayer | null {
|
||||
if (!stats) return null;
|
||||
|
||||
// Try local_player field first
|
||||
if (stats.local_player) {
|
||||
return stats.local_player;
|
||||
// Try localPlayer field first (camelCase from API)
|
||||
if (stats.localPlayer) {
|
||||
return stats.localPlayer;
|
||||
}
|
||||
|
||||
// Try teams
|
||||
@@ -63,7 +63,7 @@ function getLocalPlayer(stats: RawEndGameStats | null): EndGamePlayer | null {
|
||||
for (const team of stats.teams) {
|
||||
if (team.players) {
|
||||
for (const player of team.players) {
|
||||
if (player.is_local_player) {
|
||||
if (player.isLocalPlayer) {
|
||||
return player;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,30 +180,32 @@ export interface Timeline {
|
||||
/**
|
||||
* Raw end-of-game stats from the League Client API.
|
||||
* 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 {
|
||||
/** Game ID. */
|
||||
game_id?: number;
|
||||
gameId?: number;
|
||||
/** Game length in seconds. */
|
||||
game_length?: number;
|
||||
gameLength?: number;
|
||||
/** Match ID. */
|
||||
match_id?: number;
|
||||
matchId?: number | null;
|
||||
/** Game result. */
|
||||
game_result?: string;
|
||||
gameResult?: string | null;
|
||||
/** Local player data. */
|
||||
local_player?: EndGamePlayer;
|
||||
localPlayer?: EndGamePlayer;
|
||||
/** Teams data. */
|
||||
teams?: EndGameTeam[];
|
||||
/** Players (legacy format). */
|
||||
players?: EndGamePlayer[];
|
||||
players?: EndGamePlayer[] | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Player in end-of-game stats.
|
||||
* Note: Properties use camelCase to match the actual API response.
|
||||
*/
|
||||
export interface EndGamePlayer {
|
||||
/** Whether this is the local player. */
|
||||
is_local_player?: boolean;
|
||||
isLocalPlayer?: boolean;
|
||||
/** Player stats. */
|
||||
stats?: PlayerStats;
|
||||
/** Items (array of item IDs). */
|
||||
@@ -212,12 +214,13 @@ export interface EndGamePlayer {
|
||||
|
||||
/**
|
||||
* Team in end-of-game stats.
|
||||
* Note: Properties use camelCase to match the actual API response.
|
||||
*/
|
||||
export interface EndGameTeam {
|
||||
/** Whether this is the player's team. */
|
||||
is_player_team?: boolean;
|
||||
isPlayerTeam?: boolean;
|
||||
/** Whether this is the winning team. */
|
||||
is_winning_team?: boolean;
|
||||
isWinningTeam?: boolean;
|
||||
/** Players on the team. */
|
||||
players?: EndGamePlayer[];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user