diff --git a/tauri-app/src-tauri/Cargo.lock b/tauri-app/src-tauri/Cargo.lock index 8739b70..8fb7ab0 100644 --- a/tauri-app/src-tauri/Cargo.lock +++ b/tauri-app/src-tauri/Cargo.lock @@ -715,10 +715,10 @@ dependencies = [ ] [[package]] -name = "dirs" +name = "directories" version = "5.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35" dependencies = [ "dirs-sys 0.4.1", ] @@ -3631,7 +3631,7 @@ dependencies = [ "anyhow", "bytes", "cookie", - "dirs 6.0.0", + "dirs", "dunce", "embed_plist", "getrandom 0.3.4", @@ -3678,7 +3678,7 @@ name = "tauri-app" version = "0.1.0" dependencies = [ "chrono", - "dirs 5.0.1", + "directories", "serde", "serde_json", "tauri", @@ -3695,7 +3695,7 @@ checksum = "4bbc990d1dbf57a8e1c7fa2327f2a614d8b757805603c1b9ba5c81bade09fd4d" dependencies = [ "anyhow", "cargo_toml", - "dirs 6.0.0", + "dirs", "glob", "heck 0.5.0", "json-patch", @@ -4219,7 +4219,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5e85aa143ceb072062fc4d6356c1b520a51d636e7bc8e77ec94be3608e5e80c" dependencies = [ "crossbeam-channel", - "dirs 6.0.0", + "dirs", "libappindicator", "muda", "objc2", @@ -5292,7 +5292,7 @@ dependencies = [ "block2", "cookie", "crossbeam-channel", - "dirs 6.0.0", + "dirs", "dom_query", "dpi", "dunce", diff --git a/tauri-app/src-tauri/Cargo.toml b/tauri-app/src-tauri/Cargo.toml index 0f88542..b5b749e 100644 --- a/tauri-app/src-tauri/Cargo.toml +++ b/tauri-app/src-tauri/Cargo.toml @@ -24,5 +24,5 @@ serde = { version = "1", features = ["derive"] } serde_json = "1" chrono = { version = "0.4", features = ["serde"] } uuid = { version = "1", features = ["v4", "serde"] } -dirs = "5" +directories = "5" diff --git a/tauri-app/src-tauri/src/lib.rs b/tauri-app/src-tauri/src/lib.rs index 3e341da..e662d51 100644 --- a/tauri-app/src-tauri/src/lib.rs +++ b/tauri-app/src-tauri/src/lib.rs @@ -7,10 +7,10 @@ use uuid::Uuid; /// A timestamped event in the timeline. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TimestampedEvent { - /// Video timestamp (offset from recording start) in seconds. - pub video_timestamp: i64, - /// Game timestamp (in-game time) in seconds. - pub game_timestamp: Option, + /// 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, /// Event type name. @@ -19,7 +19,81 @@ pub struct TimestampedEvent { 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, + #[serde(default)] + pub stat_modifiers: Vec, + pub name: Option, + #[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, + pub spell2_name: Option, +} + +/// Individual item information. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ItemInfo { + pub item_id: u32, + pub name: Option, + pub slot: u32, +} + +/// Item build at game end. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ItemBuild { + pub items: Vec, + pub trinket: Option, +} + +/// 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, + pub team: Option, +} + /// 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. @@ -32,39 +106,68 @@ pub struct Timeline { pub duration_secs: i64, /// Events in the timeline. pub events: Vec, -} - -/// Game history item for display in the UI. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct GameHistoryItem { - /// Recording ID. - pub id: String, - /// Game start time. - pub start_time: DateTime, - /// Game end time. - pub end_time: Option>, - /// Duration in seconds. - pub duration_secs: i64, - /// Formatted duration string (e.g., "32:15"). - pub duration_formatted: String, - /// Number of events. - pub event_count: usize, - /// Champion played (if available). + /// Champion played. + #[serde(default)] pub champion: Option, - /// Game result (if available). - pub result: Option, - /// Video file path. - pub video_path: Option, + /// Skin name. + #[serde(default)] + pub skin_name: Option, + /// Queue type. + #[serde(default)] + pub queue_type: Option, + /// Queue ID. + #[serde(default)] + pub queue_id: Option, + /// Game mode. + #[serde(default)] + pub game_mode: Option, + /// Map name. + #[serde(default)] + pub map_name: Option, + /// Summoner name. + #[serde(default)] + pub summoner_name: Option, + /// Player's PUUID. + #[serde(default)] + pub puuid: Option, + /// Team (100 = blue, 200 = red). + #[serde(default)] + pub team: Option, + /// Whether the game was won. + #[serde(default)] + pub victory: Option, + /// Final player stats. + #[serde(default)] + pub final_stats: Option, + /// Game ID. + #[serde(default)] + pub game_id: Option, + /// Match ID. + #[serde(default)] + pub match_id: Option, + /// Rune page at game start. + #[serde(default)] + pub runes: Option, + /// Summoner spells. + #[serde(default)] + pub summoner_spells: Option, + /// Final item build at game end. + #[serde(default)] + pub final_items: Option, + /// All players in the game. + #[serde(default)] + pub all_players: Vec, } /// Get the default output directory for recordings. +/// Uses the same directory structure as the record-daemon. fn get_default_output_dir() -> Option { - dirs::video_dir() - .map(|p| p.join("LeagueRecorder")) - .or_else(|| dirs::home_dir().map(|p| p.join("Videos").join("LeagueRecorder"))) + directories::ProjectDirs::from("com", "leaguerecorder", "record-daemon") + .map(|dirs| dirs.data_dir().join("recordings")) } /// Get the timeline storage directory. +/// Uses the same directory structure as the record-daemon. fn get_timeline_dir() -> PathBuf { get_default_output_dir() .map(|p| p.join("timelines")) @@ -98,105 +201,13 @@ fn load_timelines() -> Vec { timelines } -/// Format duration as MM:SS or HH:MM:SS. -fn format_duration(secs: i64) -> String { - let hours = secs / 3600; - let minutes = (secs % 3600) / 60; - let seconds = secs % 60; - - if hours > 0 { - format!("{:02}:{:02}:{:02}", hours, minutes, seconds) - } else { - format!("{:02}:{:02}", minutes, seconds) - } -} - -/// Extract game result from events. -fn extract_game_result(timeline: &Timeline) -> Option { - for event in &timeline.events { - if event.event_type == "GameEnd" { - // Check description for win/loss - if event.description.to_lowercase().contains("win") { - return Some("Victory".to_string()); - } else if event.description.to_lowercase().contains("loss") - || event.description.to_lowercase().contains("defeat") - { - return Some("Defeat".to_string()); - } - } - } - None -} - -/// Extract champion from events. -fn extract_champion(timeline: &Timeline) -> Option { - for event in &timeline.events { - if event.event_type == "GameStart" { - // Try to extract champion from description - let desc = &event.description; - if desc.contains("Playing as ") { - return Some(desc.replace("Playing as ", "").trim().to_string()); - } - } - } - None -} - -/// Extract video file path from timeline. -fn extract_video_path(timeline: &Timeline) -> Option { - // Look for video file in the recordings directory - let output_dir = get_default_output_dir()?; - let video_dir = output_dir.join("videos"); - - if video_dir.exists() { - if let Ok(entries) = fs::read_dir(&video_dir) { - for entry in entries.flatten() { - let path = entry.path(); - let file_stem = path.file_stem()?.to_string_lossy().to_string(); - if file_stem.contains(&timeline.recording_id.to_string()) { - return Some(path.to_string_lossy().to_string()); - } - } - } - } - - // Also check for files named with the recording ID - let recording_id = timeline.recording_id.to_string(); - if let Ok(entries) = fs::read_dir(&output_dir) { - for entry in entries.flatten() { - let path = entry.path(); - if path.is_file() { - let file_stem = path.file_stem()?.to_string_lossy().to_string(); - if file_stem.contains(&recording_id) { - return Some(path.to_string_lossy().to_string()); - } - } - } - } - - None -} - +/// Get game history - returns full timeline data for each game. #[tauri::command] -fn get_game_history() -> Vec { - let timelines = load_timelines(); - - timelines - .into_iter() - .map(|timeline| GameHistoryItem { - id: timeline.recording_id.to_string(), - start_time: timeline.start_time, - end_time: timeline.end_time, - duration_secs: timeline.duration_secs, - duration_formatted: format_duration(timeline.duration_secs), - event_count: timeline.events.len(), - champion: extract_champion(&timeline), - result: extract_game_result(&timeline), - video_path: extract_video_path(&timeline), - }) - .collect() +fn get_game_history() -> Vec { + load_timelines() } +/// Get a specific timeline by recording ID. #[tauri::command] fn get_timeline(recording_id: String) -> Option { let timeline_dir = get_timeline_dir(); @@ -211,6 +222,7 @@ fn get_timeline(recording_id: String) -> Option { None } +/// Get the recordings directory path. #[tauri::command] fn get_recordings_dir() -> String { get_default_output_dir() diff --git a/tauri-app/src/components/GameHistory.vue b/tauri-app/src/components/GameHistory.vue index 7fac7d1..8a0464a 100644 --- a/tauri-app/src/components/GameHistory.vue +++ b/tauri-app/src/components/GameHistory.vue @@ -1,8 +1,27 @@