tauri-app: use patch version from recorded game; or latest
All checks were successful
record-daemon / Build, check and test (push) Successful in 2m11s
All checks were successful
record-daemon / Build, check and test (push) Successful in 2m11s
This commit is contained in:
@@ -1,9 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from "vue";
|
import { ref, onMounted } from "vue";
|
||||||
import GameHistory from "./components/GameHistory.vue";
|
import GameHistory from "./components/GameHistory.vue";
|
||||||
import GameReview from "./components/GameReview.vue";
|
import GameReview from "./components/GameReview.vue";
|
||||||
import Settings from "./components/Settings.vue";
|
import Settings from "./components/Settings.vue";
|
||||||
import type { GameHistoryItem } from "./types/timeline";
|
import type { GameHistoryItem } from "./types/timeline";
|
||||||
|
import { fetchLatestDdragonVersion } from "./types/ddragon";
|
||||||
|
|
||||||
// Current view state
|
// Current view state
|
||||||
const currentView = ref<"history" | "review" | "settings">("history");
|
const currentView = ref<"history" | "review" | "settings">("history");
|
||||||
@@ -30,6 +31,11 @@ function openSettings() {
|
|||||||
function closeSettings() {
|
function closeSettings() {
|
||||||
currentView.value = "history";
|
currentView.value = "history";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize DDragon version on app mount
|
||||||
|
onMounted(() => {
|
||||||
|
fetchLatestDdragonVersion();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -33,13 +33,20 @@ import {
|
|||||||
getLpChange,
|
getLpChange,
|
||||||
formatLpDelta,
|
formatLpDelta,
|
||||||
formatRank,
|
formatRank,
|
||||||
|
getGameVersion,
|
||||||
} from "../types/timeline";
|
} from "../types/timeline";
|
||||||
|
import { getDdragonVersion } from "../types/ddragon";
|
||||||
|
|
||||||
// Helper to get video timestamp in seconds from tuple format
|
// Helper to get video timestamp in seconds from tuple format
|
||||||
function getVideoTimestampSecs(event: TimestampedEvent): number {
|
function getVideoTimestampSecs(event: TimestampedEvent): number {
|
||||||
return event.video_timestamp[0];
|
return event.video_timestamp[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Resolve the DDragon version for a game: use the game's recorded version, or fall back to the latest. */
|
||||||
|
function versionForGame(game: GameHistoryItem): string {
|
||||||
|
return getGameVersion(game) || getDdragonVersion();
|
||||||
|
}
|
||||||
|
|
||||||
// Queue filter types
|
// Queue filter types
|
||||||
type QueueFilter = "all" | "ranked_solo" | "ranked_flex" | "normals" | "aram" | "custom";
|
type QueueFilter = "all" | "ranked_solo" | "ranked_flex" | "normals" | "aram" | "custom";
|
||||||
const activeFilter = ref<QueueFilter>("all");
|
const activeFilter = ref<QueueFilter>("all");
|
||||||
@@ -203,10 +210,10 @@ onMounted(() => {
|
|||||||
<div class="champion-section">
|
<div class="champion-section">
|
||||||
<div class="champion-image-wrapper">
|
<div class="champion-image-wrapper">
|
||||||
<img
|
<img
|
||||||
:src="getChampionImageUrl(getChampionName(game))"
|
:src="getChampionImageUrl(getChampionName(game), 'square', versionForGame(game))"
|
||||||
:alt="getChampionName(game) || '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, 'square', versionForGame(game))"
|
||||||
/>
|
/>
|
||||||
<div class="champion-level" v-if="getFinalStats(game)">
|
<div class="champion-level" v-if="getFinalStats(game)">
|
||||||
{{ Math.min(18, Math.floor(getFinalStats(game)!.game_duration / 60)) }}
|
{{ Math.min(18, Math.floor(getFinalStats(game)!.game_duration / 60)) }}
|
||||||
@@ -218,7 +225,7 @@ onMounted(() => {
|
|||||||
<div class="spell-slot">
|
<div class="spell-slot">
|
||||||
<img
|
<img
|
||||||
v-if="getSummonerSpells(game)"
|
v-if="getSummonerSpells(game)"
|
||||||
:src="getSummonerSpellUrl(getSummonerSpells(game)!.spell1Id)"
|
:src="getSummonerSpellUrl(getSummonerSpells(game)!.spell1Id, versionForGame(game))"
|
||||||
alt="Spell 1"
|
alt="Spell 1"
|
||||||
class="spell-image"
|
class="spell-image"
|
||||||
/>
|
/>
|
||||||
@@ -227,7 +234,7 @@ onMounted(() => {
|
|||||||
<div class="spell-slot">
|
<div class="spell-slot">
|
||||||
<img
|
<img
|
||||||
v-if="getSummonerSpells(game)"
|
v-if="getSummonerSpells(game)"
|
||||||
:src="getSummonerSpellUrl(getSummonerSpells(game)!.spell2Id)"
|
:src="getSummonerSpellUrl(getSummonerSpells(game)!.spell2Id, versionForGame(game))"
|
||||||
alt="Spell 2"
|
alt="Spell 2"
|
||||||
class="spell-image"
|
class="spell-image"
|
||||||
/>
|
/>
|
||||||
@@ -291,7 +298,7 @@ onMounted(() => {
|
|||||||
<div class="item-slot" v-for="(item, idx) in getItemsArray(game).slice(0, 6)" :key="idx">
|
<div class="item-slot" v-for="(item, idx) in getItemsArray(game).slice(0, 6)" :key="idx">
|
||||||
<img
|
<img
|
||||||
v-if="item && item.itemId"
|
v-if="item && item.itemId"
|
||||||
:src="getItemImageUrl(item.itemId)"
|
:src="getItemImageUrl(item.itemId, versionForGame(game))"
|
||||||
:alt="item.name || `Item ${item.itemId}`"
|
:alt="item.name || `Item ${item.itemId}`"
|
||||||
class="item-image"
|
class="item-image"
|
||||||
/>
|
/>
|
||||||
@@ -300,7 +307,7 @@ onMounted(() => {
|
|||||||
<div class="item-slot trinket">
|
<div class="item-slot trinket">
|
||||||
<img
|
<img
|
||||||
v-if="getItemsArray(game)[6]?.itemId"
|
v-if="getItemsArray(game)[6]?.itemId"
|
||||||
:src="getItemImageUrl(getItemsArray(game)[6]!.itemId)"
|
:src="getItemImageUrl(getItemsArray(game)[6]!.itemId, versionForGame(game))"
|
||||||
:alt="getItemsArray(game)[6]?.name || 'Trinket'"
|
:alt="getItemsArray(game)[6]?.name || 'Trinket'"
|
||||||
class="item-image"
|
class="item-image"
|
||||||
/>
|
/>
|
||||||
@@ -323,7 +330,7 @@ onMounted(() => {
|
|||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<div class="modal-champion">
|
<div class="modal-champion">
|
||||||
<img
|
<img
|
||||||
:src="getChampionImageUrl(getChampionName(selectedGame))"
|
:src="getChampionImageUrl(getChampionName(selectedGame), 'square', versionForGame(selectedGame))"
|
||||||
:alt="getChampionName(selectedGame) || 'Unknown Champion'"
|
:alt="getChampionName(selectedGame) || 'Unknown Champion'"
|
||||||
class="modal-champion-image"
|
class="modal-champion-image"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -21,7 +21,9 @@ import {
|
|||||||
computeHighlights,
|
computeHighlights,
|
||||||
DEFAULT_HIGHLIGHT_SETTINGS,
|
DEFAULT_HIGHLIGHT_SETTINGS,
|
||||||
getHighlightTypeColor,
|
getHighlightTypeColor,
|
||||||
|
getGameVersion,
|
||||||
} from "../types/timeline";
|
} from "../types/timeline";
|
||||||
|
import { getDdragonVersion } from "../types/ddragon";
|
||||||
|
|
||||||
// Props
|
// Props
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@@ -33,6 +35,9 @@ const emit = defineEmits<{
|
|||||||
(e: "back"): void;
|
(e: "back"): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
// DDragon version: use the game's recorded version if available, otherwise latest
|
||||||
|
const gameVersion = computed(() => getGameVersion(props.game) || getDdragonVersion());
|
||||||
|
|
||||||
// Video state
|
// Video state
|
||||||
const videoRef = ref<HTMLVideoElement | null>(null);
|
const videoRef = ref<HTMLVideoElement | null>(null);
|
||||||
const videoPath = ref<string | null>(null);
|
const videoPath = ref<string | null>(null);
|
||||||
@@ -657,7 +662,7 @@ onUnmounted(() => {
|
|||||||
<div class="header-info">
|
<div class="header-info">
|
||||||
<div class="header-champion">
|
<div class="header-champion">
|
||||||
<img
|
<img
|
||||||
:src="getChampionImageUrl(getChampionName(game))"
|
:src="getChampionImageUrl(getChampionName(game), 'square', gameVersion)"
|
||||||
:alt="getChampionName(game) || 'Champion'"
|
:alt="getChampionName(game) || 'Champion'"
|
||||||
class="header-champion-img"
|
class="header-champion-img"
|
||||||
/>
|
/>
|
||||||
@@ -917,7 +922,7 @@ onUnmounted(() => {
|
|||||||
<!-- Player details -->
|
<!-- Player details -->
|
||||||
<div v-if="selectedPlayer" class="selected-player-details">
|
<div v-if="selectedPlayer" class="selected-player-details">
|
||||||
<div class="player-header">
|
<div class="player-header">
|
||||||
<img :src="getChampionImageUrl(selectedPlayer.championName)" class="player-champion-img" />
|
<img :src="getChampionImageUrl(selectedPlayer.championName, 'square', gameVersion)" class="player-champion-img" />
|
||||||
<div class="player-info">
|
<div class="player-info">
|
||||||
<div class="player-name-large">{{ selectedPlayer.championName }}</div>
|
<div class="player-name-large">{{ selectedPlayer.championName }}</div>
|
||||||
<div class="player-riot-id">{{ formatPlayerName(selectedPlayer) }}</div>
|
<div class="player-riot-id">{{ formatPlayerName(selectedPlayer) }}</div>
|
||||||
@@ -965,7 +970,7 @@ onUnmounted(() => {
|
|||||||
<div class="items-label">Items:</div>
|
<div class="items-label">Items:</div>
|
||||||
<div class="items-row">
|
<div class="items-row">
|
||||||
<div v-for="(itemId, idx) in selectedPlayer.items.slice(0, 6)" :key="idx" class="item-slot">
|
<div v-for="(itemId, idx) in selectedPlayer.items.slice(0, 6)" :key="idx" class="item-slot">
|
||||||
<img v-if="itemId" :src="getItemImageUrl(itemId)" :alt="`Item ${itemId}`" />
|
<img v-if="itemId" :src="getItemImageUrl(itemId, gameVersion)" :alt="`Item ${itemId}`" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -991,7 +996,7 @@ onUnmounted(() => {
|
|||||||
:class="{ local: player.isLocalPlayer }"
|
:class="{ local: player.isLocalPlayer }"
|
||||||
>
|
>
|
||||||
<div class="team-champion">
|
<div class="team-champion">
|
||||||
<img :src="getChampionImageUrl(player.championName)" :alt="player.championName" />
|
<img :src="getChampionImageUrl(player.championName, 'square', gameVersion)" :alt="player.championName" />
|
||||||
<span>{{ player.championName }}</span>
|
<span>{{ player.championName }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="team-kda">
|
<div class="team-kda">
|
||||||
@@ -1011,7 +1016,7 @@ onUnmounted(() => {
|
|||||||
class="team-row enemy"
|
class="team-row enemy"
|
||||||
>
|
>
|
||||||
<div class="team-champion">
|
<div class="team-champion">
|
||||||
<img :src="getChampionImageUrl(player.championName)" :alt="player.championName" />
|
<img :src="getChampionImageUrl(player.championName, 'square', gameVersion)" :alt="player.championName" />
|
||||||
<span>{{ player.championName }}</span>
|
<span>{{ player.championName }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="team-kda">
|
<div class="team-kda">
|
||||||
|
|||||||
91
tauri-app/src/types/ddragon.ts
Normal file
91
tauri-app/src/types/ddragon.ts
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
/**
|
||||||
|
* DDragon version resolver for Data Dragon asset URLs.
|
||||||
|
*
|
||||||
|
* Resolves the appropriate patch version with the following priority:
|
||||||
|
* 1. Game-specific version (from recorded timeline data)
|
||||||
|
* 2. Latest version fetched from the DDragon API
|
||||||
|
* 3. Hardcoded fallback version
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ref } from "vue";
|
||||||
|
|
||||||
|
/** DDragon versions API endpoint. */
|
||||||
|
const DDRAGON_VERSIONS_URL = "https://ddragon.leagueoflegends.com/api/versions.json";
|
||||||
|
|
||||||
|
/** Fallback version used when the DDragon API is unreachable. */
|
||||||
|
const FALLBACK_VERSION = "16.10.1";
|
||||||
|
|
||||||
|
/** Latest version from DDragon API (reactive — updates trigger Vue re-renders). */
|
||||||
|
export const ddragonVersion = ref<string>(FALLBACK_VERSION);
|
||||||
|
|
||||||
|
/** Whether the version has been fetched at least once. */
|
||||||
|
let versionInitialized = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch the latest game version from the DDragon API.
|
||||||
|
* Results are cached after the first call — subsequent calls return immediately.
|
||||||
|
* Safe to call multiple times (deduplicated).
|
||||||
|
*/
|
||||||
|
export async function fetchLatestDdragonVersion(): Promise<string> {
|
||||||
|
if (versionInitialized) {
|
||||||
|
return ddragonVersion.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(DDRAGON_VERSIONS_URL);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP ${response.status}`);
|
||||||
|
}
|
||||||
|
const versions: string[] = await response.json();
|
||||||
|
if (versions.length > 0) {
|
||||||
|
ddragonVersion.value = versions[0];
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn("[ddragon] Failed to fetch versions, using fallback:", err);
|
||||||
|
} finally {
|
||||||
|
versionInitialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ddragonVersion.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current DDragon version (synchronous).
|
||||||
|
* Returns the cached latest version, or the fallback if not yet fetched.
|
||||||
|
*/
|
||||||
|
export function getDdragonVersion(): string {
|
||||||
|
return ddragonVersion.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a League game version string to DDragon format.
|
||||||
|
*
|
||||||
|
* The League Client reports versions like "16.10.1" or "16.10.1.1234".
|
||||||
|
* DDragon uses the first three components (e.g., "16.10.1").
|
||||||
|
*
|
||||||
|
* @returns The DDragon-formatted version string, or null if the input is invalid.
|
||||||
|
*/
|
||||||
|
export function gameVersionToDdragonVersion(gameVersion: string | null | undefined): string | null {
|
||||||
|
if (!gameVersion || typeof gameVersion !== "string") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const trimmed = gameVersion.trim();
|
||||||
|
if (!trimmed) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// DDragon versions are in the format "XX.YY.ZZ" (e.g., "16.10.1")
|
||||||
|
// The game client may report "XX.YY.ZZ.WWWW" — we take the first 3 parts
|
||||||
|
const parts = trimmed.split(".");
|
||||||
|
if (parts.length >= 3) {
|
||||||
|
return `${parts[0]}.${parts[1]}.${parts[2]}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Two-part version — unlikely but handle gracefully
|
||||||
|
if (parts.length === 2) {
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@@ -2,6 +2,8 @@
|
|||||||
* TypeScript types for timeline data from the record-daemon.
|
* TypeScript types for timeline data from the record-daemon.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { getDdragonVersion, gameVersionToDdragonVersion } from "./ddragon";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A timestamped event in the timeline.
|
* A timestamped event in the timeline.
|
||||||
* Video timestamp is serialized as [seconds, nanos] tuple from Rust.
|
* Video timestamp is serialized as [seconds, nanos] tuple from Rust.
|
||||||
@@ -340,6 +342,27 @@ export function getMapName(game: GameHistoryItem): string | null {
|
|||||||
return session?.map?.name || null;
|
return session?.map?.name || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract the game version from raw session data.
|
||||||
|
* The League Client reports versions like "16.10.1" or "16.10.1.1234".
|
||||||
|
* Returns null if no version is available in the recording.
|
||||||
|
*/
|
||||||
|
export function getGameVersion(game: GameHistoryItem): string | null {
|
||||||
|
// Try raw_session.gameData.gameVersion
|
||||||
|
const session = game.raw_session as { gameData?: { gameVersion?: string } } | null;
|
||||||
|
if (session?.gameData?.gameVersion) {
|
||||||
|
return gameVersionToDdragonVersion(session.gameData.gameVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try raw_end_game_stats.gameVersion
|
||||||
|
const stats = game.raw_end_game_stats as { gameVersion?: string } | null;
|
||||||
|
if (stats?.gameVersion) {
|
||||||
|
return gameVersionToDdragonVersion(stats.gameVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extract final stats from raw end game stats.
|
* Extract final stats from raw end game stats.
|
||||||
*/
|
*/
|
||||||
@@ -552,20 +575,25 @@ export function formatNumber(num: number): string {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get champion image URL from Data Dragon.
|
* Get champion image URL from Data Dragon.
|
||||||
|
* @param championName - Champion name (e.g. "Kai'Sa").
|
||||||
|
* @param size - Image size: 'square' for icon, 'loading' for splash.
|
||||||
|
* @param version - Optional DDragon patch version (e.g. "16.10.1").
|
||||||
|
* Falls back to the latest DDragon version if not provided.
|
||||||
*/
|
*/
|
||||||
export function getChampionImageUrl(championName: string | null, size: 'square' | 'loading' = 'square'): string {
|
export function getChampionImageUrl(championName: string | null, size: 'square' | 'loading' = 'square', version?: string): string {
|
||||||
|
const v = version || getDdragonVersion();
|
||||||
if (!championName) {
|
if (!championName) {
|
||||||
// Return placeholder for unknown champion
|
// Return placeholder for unknown champion
|
||||||
return 'https://ddragon.leagueoflegends.com/cdn/16.10.1/img/champion/Aatrox.png';
|
return `https://ddragon.leagueoflegends.com/cdn/${v}/img/champion/Aatrox.png`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Data Dragon uses specific champion name formatting
|
// Data Dragon uses specific champion name formatting
|
||||||
const formattedName = formatChampionName(championName);
|
const formattedName = formatChampionName(championName);
|
||||||
|
|
||||||
if (size === 'loading') {
|
if (size === 'loading') {
|
||||||
return `https://ddragon.leagueoflegends.com/cdn/16.10.1/img/champion/${formattedName}_0.jpg`;
|
return `https://ddragon.leagueoflegends.com/cdn/${v}/img/champion/${formattedName}_0.jpg`;
|
||||||
}
|
}
|
||||||
return `https://ddragon.leagueoflegends.com/cdn/16.10.1/img/champion/${formattedName}.png`;
|
return `https://ddragon.leagueoflegends.com/cdn/${v}/img/champion/${formattedName}.png`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -595,8 +623,11 @@ function formatChampionName(name: string): string {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get summoner spell image URL from Data Dragon.
|
* Get summoner spell image URL from Data Dragon.
|
||||||
|
* @param spellId - Summoner spell ID.
|
||||||
|
* @param version - Optional DDragon patch version. Falls back to latest.
|
||||||
*/
|
*/
|
||||||
export function getSummonerSpellUrl(spellId: number): string {
|
export function getSummonerSpellUrl(spellId: number, version?: string): string {
|
||||||
|
const v = version || getDdragonVersion();
|
||||||
// Map common spell IDs to names
|
// Map common spell IDs to names
|
||||||
// Reference: https://github.com/RiotGames/developer-relations/issues/478
|
// Reference: https://github.com/RiotGames/developer-relations/issues/478
|
||||||
const spellNames: Record<number, string> = {
|
const spellNames: Record<number, string> = {
|
||||||
@@ -618,17 +649,20 @@ export function getSummonerSpellUrl(spellId: number): string {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const spellName = spellNames[spellId] || 'SummonerFlash';
|
const spellName = spellNames[spellId] || 'SummonerFlash';
|
||||||
return `https://ddragon.leagueoflegends.com/cdn/16.6.1/img/spell/${spellName}.png`;
|
return `https://ddragon.leagueoflegends.com/cdn/${v}/img/spell/${spellName}.png`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get item image URL from Data Dragon.
|
* Get item image URL from Data Dragon.
|
||||||
|
* @param itemId - Item ID.
|
||||||
|
* @param version - Optional DDragon patch version. Falls back to latest.
|
||||||
*/
|
*/
|
||||||
export function getItemImageUrl(itemId: number): string {
|
export function getItemImageUrl(itemId: number, version?: string): string {
|
||||||
if (itemId === 0 || itemId === null) {
|
if (itemId === 0 || itemId === null) {
|
||||||
return ''; // Empty item slot
|
return ''; // Empty item slot
|
||||||
}
|
}
|
||||||
return `https://ddragon.leagueoflegends.com/cdn/16.6.1/img/item/${itemId}.png`;
|
const v = version || getDdragonVersion();
|
||||||
|
return `https://ddragon.leagueoflegends.com/cdn/${v}/img/item/${itemId}.png`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user