tauri-app: use patch version from recorded game; or latest
All checks were successful
record-daemon / Build, check and test (push) Successful in 2m11s

This commit is contained in:
2026-05-17 16:25:04 +02:00
parent 8138b542b7
commit f9ff272798
5 changed files with 164 additions and 21 deletions

View File

@@ -1,9 +1,10 @@
<script setup lang="ts">
import { ref } from "vue";
import { ref, onMounted } from "vue";
import GameHistory from "./components/GameHistory.vue";
import GameReview from "./components/GameReview.vue";
import Settings from "./components/Settings.vue";
import type { GameHistoryItem } from "./types/timeline";
import { fetchLatestDdragonVersion } from "./types/ddragon";
// Current view state
const currentView = ref<"history" | "review" | "settings">("history");
@@ -30,6 +31,11 @@ function openSettings() {
function closeSettings() {
currentView.value = "history";
}
// Initialize DDragon version on app mount
onMounted(() => {
fetchLatestDdragonVersion();
});
</script>
<template>

View File

@@ -33,13 +33,20 @@ import {
getLpChange,
formatLpDelta,
formatRank,
getGameVersion,
} from "../types/timeline";
import { getDdragonVersion } from "../types/ddragon";
// Helper to get video timestamp in seconds from tuple format
function getVideoTimestampSecs(event: TimestampedEvent): number {
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
type QueueFilter = "all" | "ranked_solo" | "ranked_flex" | "normals" | "aram" | "custom";
const activeFilter = ref<QueueFilter>("all");
@@ -203,10 +210,10 @@ onMounted(() => {
<div class="champion-section">
<div class="champion-image-wrapper">
<img
:src="getChampionImageUrl(getChampionName(game))"
:src="getChampionImageUrl(getChampionName(game), 'square', versionForGame(game))"
:alt="getChampionName(game) || 'Unknown Champion'"
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)">
{{ Math.min(18, Math.floor(getFinalStats(game)!.game_duration / 60)) }}
@@ -218,7 +225,7 @@ onMounted(() => {
<div class="spell-slot">
<img
v-if="getSummonerSpells(game)"
:src="getSummonerSpellUrl(getSummonerSpells(game)!.spell1Id)"
:src="getSummonerSpellUrl(getSummonerSpells(game)!.spell1Id, versionForGame(game))"
alt="Spell 1"
class="spell-image"
/>
@@ -227,7 +234,7 @@ onMounted(() => {
<div class="spell-slot">
<img
v-if="getSummonerSpells(game)"
:src="getSummonerSpellUrl(getSummonerSpells(game)!.spell2Id)"
:src="getSummonerSpellUrl(getSummonerSpells(game)!.spell2Id, versionForGame(game))"
alt="Spell 2"
class="spell-image"
/>
@@ -291,7 +298,7 @@ onMounted(() => {
<div class="item-slot" v-for="(item, idx) in getItemsArray(game).slice(0, 6)" :key="idx">
<img
v-if="item && item.itemId"
:src="getItemImageUrl(item.itemId)"
:src="getItemImageUrl(item.itemId, versionForGame(game))"
:alt="item.name || `Item ${item.itemId}`"
class="item-image"
/>
@@ -300,7 +307,7 @@ onMounted(() => {
<div class="item-slot trinket">
<img
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'"
class="item-image"
/>
@@ -323,7 +330,7 @@ onMounted(() => {
<div class="modal-header">
<div class="modal-champion">
<img
:src="getChampionImageUrl(getChampionName(selectedGame))"
:src="getChampionImageUrl(getChampionName(selectedGame), 'square', versionForGame(selectedGame))"
:alt="getChampionName(selectedGame) || 'Unknown Champion'"
class="modal-champion-image"
/>

View File

@@ -21,7 +21,9 @@ import {
computeHighlights,
DEFAULT_HIGHLIGHT_SETTINGS,
getHighlightTypeColor,
getGameVersion,
} from "../types/timeline";
import { getDdragonVersion } from "../types/ddragon";
// Props
const props = defineProps<{
@@ -33,6 +35,9 @@ const emit = defineEmits<{
(e: "back"): void;
}>();
// DDragon version: use the game's recorded version if available, otherwise latest
const gameVersion = computed(() => getGameVersion(props.game) || getDdragonVersion());
// Video state
const videoRef = ref<HTMLVideoElement | null>(null);
const videoPath = ref<string | null>(null);
@@ -657,7 +662,7 @@ onUnmounted(() => {
<div class="header-info">
<div class="header-champion">
<img
:src="getChampionImageUrl(getChampionName(game))"
:src="getChampionImageUrl(getChampionName(game), 'square', gameVersion)"
:alt="getChampionName(game) || 'Champion'"
class="header-champion-img"
/>
@@ -917,7 +922,7 @@ onUnmounted(() => {
<!-- Player details -->
<div v-if="selectedPlayer" class="selected-player-details">
<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-name-large">{{ selectedPlayer.championName }}</div>
<div class="player-riot-id">{{ formatPlayerName(selectedPlayer) }}</div>
@@ -965,7 +970,7 @@ onUnmounted(() => {
<div class="items-label">Items:</div>
<div class="items-row">
<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>
@@ -991,7 +996,7 @@ onUnmounted(() => {
:class="{ local: player.isLocalPlayer }"
>
<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>
</div>
<div class="team-kda">
@@ -1011,7 +1016,7 @@ onUnmounted(() => {
class="team-row enemy"
>
<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>
</div>
<div class="team-kda">

View 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;
}

View File

@@ -2,6 +2,8 @@
* TypeScript types for timeline data from the record-daemon.
*/
import { getDdragonVersion, gameVersionToDdragonVersion } from "./ddragon";
/**
* A timestamped event in the timeline.
* 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;
}
/**
* 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.
*/
@@ -552,20 +575,25 @@ export function formatNumber(num: number): string {
/**
* 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) {
// 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
const formattedName = formatChampionName(championName);
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.
* @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
// Reference: https://github.com/RiotGames/developer-relations/issues/478
const spellNames: Record<number, string> = {
@@ -618,17 +649,20 @@ export function getSummonerSpellUrl(spellId: number): string {
};
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.
* @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) {
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`;
}
/**