tauri-app: display game history with stats
This commit is contained in:
14
tauri-app/src-tauri/Cargo.lock
generated
14
tauri-app/src-tauri/Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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<i64>,
|
||||
/// 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.
|
||||
@@ -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<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.
|
||||
@@ -32,39 +106,68 @@ pub struct Timeline {
|
||||
pub duration_secs: i64,
|
||||
/// Events in the timeline.
|
||||
pub events: Vec<TimestampedEvent>,
|
||||
}
|
||||
|
||||
/// 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<Utc>,
|
||||
/// Game end time.
|
||||
pub end_time: Option<DateTime<Utc>>,
|
||||
/// 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<String>,
|
||||
/// Game result (if available).
|
||||
pub result: Option<String>,
|
||||
/// Video file path.
|
||||
pub video_path: 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.
|
||||
fn get_default_output_dir() -> Option<PathBuf> {
|
||||
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<Timeline> {
|
||||
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<String> {
|
||||
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<String> {
|
||||
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<String> {
|
||||
// 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<GameHistoryItem> {
|
||||
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<Timeline> {
|
||||
load_timelines()
|
||||
}
|
||||
|
||||
/// Get a specific timeline by recording ID.
|
||||
#[tauri::command]
|
||||
fn get_timeline(recording_id: String) -> Option<Timeline> {
|
||||
let timeline_dir = get_timeline_dir();
|
||||
@@ -211,6 +222,7 @@ fn get_timeline(recording_id: String) -> Option<Timeline> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Get the recordings directory path.
|
||||
#[tauri::command]
|
||||
fn get_recordings_dir() -> String {
|
||||
get_default_output_dir()
|
||||
|
||||
Reference in New Issue
Block a user