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">
|
||||
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>
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
@@ -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">
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
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`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user