Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src-tauri/src/commands/league/locator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,16 @@ pub fn default_lockfile_paths() -> Vec<PathBuf> {
async fn read_process_command_lines() -> Result<String, String> {
#[cfg(windows)]
{
// tasklist costs ~50ms; the CIM query costs seconds and stutters the
// whole app, so it only runs once the cheap probe sees the process.
let probe = crate::core::process::command("tasklist")
.args(["/FI", "IMAGENAME eq LeagueClientUx.exe", "/NH"])
.output()
.await
.map_err(|e| format!("failed to probe processes: {}", e))?;
if !String::from_utf8_lossy(&probe.stdout).contains("LeagueClientUx.exe") {
return Ok(String::new());
}
let output = crate::core::process::command("powershell")
.args([
"-NoProfile",
Expand Down
14 changes: 11 additions & 3 deletions src-tauri/src/commands/league/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@ async fn get_client() -> Result<LcuClient, String> {
// behind one network round trip, and a slow client stalls all of them.
let cached = { CACHED_CLIENT.lock().await.clone() };
if let Some(client) = cached {
if lcu_reachable(&client).await {
// The websocket task already proves the client is alive; probing it
// again would add a round-trip to every single command.
if ws::is_connected() || lcu_reachable(&client).await {
return Ok(client);
}
}
Expand All @@ -65,13 +67,19 @@ async fn get_client() -> Result<LcuClient, String> {
Ok(discovered)
}

fn http_client() -> Result<reqwest::Client, String> {
static HTTP_CLIENT: Lazy<Option<reqwest::Client>> = Lazy::new(|| {
reqwest::Client::builder()
.danger_accept_invalid_certs(true)
.timeout(std::time::Duration::from_secs(10))
.no_proxy()
.build()
.map_err(|e| e.to_string())
.ok()
});

fn http_client() -> Result<reqwest::Client, String> {
HTTP_CLIENT
.clone()
.ok_or_else(|| "http client init failed".to_string())
}

async fn lcu_reachable(client: &LcuClient) -> bool {
Expand Down
29 changes: 29 additions & 0 deletions src/components/league/AnalysisTab.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import { markedPlayers, winrateSquad } from "$lib/league-scouting";
import { availability, featureById, type Platform } from "./registry";
import { writeText } from "@tauri-apps/plugin-clipboard-manager";
import Skeleton from "./Skeleton.svelte";

let {
analysis,
Expand Down Expand Up @@ -160,13 +161,41 @@
</div>
{/if}
</section>
{:else if analysisLoading}
<section class="card" aria-busy="true">
<div class="card-head">
<h3>{$t("league.win_title")}</h3>
</div>
<Skeleton w="100%" h="14px" round="999px" />
<div class="skeleton-lines">
<Skeleton w="45%" h="12px" />
<Skeleton w="60%" h="11px" />
</div>
</section>
{:else}
<div class="guard-card">
<p>{analysisAvailability.available ? $t("league.win_unavailable") : $t(analysisAvailability.reasonKey)}</p>
<button class="button" onclick={onRefreshAnalysis} disabled={analysisLoading}>{$t("league.refresh")}</button>
</div>
{/if}

{#if (phase === "ChampSelect" || phase === "InProgress") && scoutPlayers.length === 0 && scoutLoading}
<section class="card" aria-busy="true">
<div class="card-head">
<h3>{$t("league.scout_title")}</h3>
</div>
<div class="skeleton-lines">
{#each Array(4) as _, i (i)}
<div class="scoreboard-row">
<Skeleton w="26px" h="26px" round="6px" />
<Skeleton w="150px" h="12px" />
<Skeleton w="80px" h="12px" />
</div>
{/each}
</div>
</section>
{/if}

{#if (phase === "ChampSelect" || phase === "InProgress") && scoutPlayers.length > 0}
<section class="card">
<div class="card-head">
Expand Down
32 changes: 30 additions & 2 deletions src/components/league/HistoryTab.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import { CDRAGON, assetUrl, type Champion } from "./shared";
import { filterByQueue, queuesInGames, summarise } from "$lib/league-history";
import { findTeam, teamBans, teamObjectives } from "$lib/league-match-detail";
import Skeleton from "./Skeleton.svelte";

let {
games,
Expand Down Expand Up @@ -171,7 +172,21 @@
<h3>{$t("league.history_title")}</h3>
<button class="button" onclick={onRefresh} disabled={loading}>{$t("league.refresh")}</button>
</div>
{#if games.length === 0}
{#if loading && games.length === 0}
<div class="game-list" aria-busy="true">
{#each Array(4) as _, i (i)}
<div class="game-row-skeleton">
<Skeleton w="34px" h="34px" round="6px" />
<div class="game-skeleton-info">
<Skeleton w="88px" h="13px" />
<Skeleton w="64px" h="11px" />
</div>
<Skeleton w="72px" h="13px" />
<Skeleton w="56px" h="11px" />
</div>
{/each}
</div>
{:else if games.length === 0}
<p class="empty-hint">{$t("league.history_empty")}</p>
{:else}
{#if availableQueues.length > 1}
Expand Down Expand Up @@ -219,7 +234,20 @@
</button>
{#if expandedGame === game.gameId}
{#if gameDetailLoading === game.gameId}
<p class="empty-hint">…</p>
<div class="scoreboard" aria-busy="true">
{#each Array(2) as _, ti (ti)}
<div class="scoreboard-team">
<Skeleton w="72px" h="12px" />
{#each Array(5) as _, si (si)}
<div class="scoreboard-row">
<Skeleton w="22px" h="22px" round="5px" />
<Skeleton w="120px" h="12px" />
<Skeleton w="52px" h="12px" />
</div>
{/each}
</div>
{/each}
</div>
{:else if gameDetails[game.gameId]}
<div class="scoreboard">
{#each scoreboardTeams(gameDetails[game.gameId]) as team (team.teamId)}
Expand Down
17 changes: 16 additions & 1 deletion src/components/league/OverviewTab.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@
import { invoke } from "@tauri-apps/api/core";
import { t } from "$lib/i18n";
import { CDRAGON, formatGameTime, type Champion, type RankedEntry, type LobbyQueue } from "./shared";
import Skeleton from "./Skeleton.svelte";

let {
summoner,
profileLoading = false,
ranked,
phase,
champSelect,
Expand All @@ -19,6 +21,7 @@
active,
}: {
summoner: any;
profileLoading?: boolean;
ranked: Record<string, RankedEntry>;
phase: string;
champSelect: any;
Expand Down Expand Up @@ -156,7 +159,19 @@
</script>

{#if active !== false}
{#if summoner}
{#if !summoner && profileLoading}
<section class="profile-card" aria-busy="true">
<Skeleton w="56px" h="56px" round="50%" />
<div class="profile-info">
<Skeleton w="150px" h="16px" />
<Skeleton w="76px" h="12px" />
</div>
<div class="ranked-chips">
<Skeleton w="118px" h="42px" round="8px" />
<Skeleton w="118px" h="42px" round="8px" />
</div>
</section>
{:else if summoner}
<section class="profile-card">
<img
class="profile-icon"
Expand Down
44 changes: 44 additions & 0 deletions src/components/league/Skeleton.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<script lang="ts">
let {
w = "100%",
h = "14px",
round = "6px",
}: {
w?: string;
h?: string;
round?: string;
} = $props();
</script>

<span class="skeleton" style={`width:${w};height:${h};border-radius:${round}`} aria-hidden="true"></span>

<style>
.skeleton {
display: inline-block;
flex-shrink: 0;
background: linear-gradient(
100deg,
var(--button) 40%,
var(--border) 50%,
var(--button) 60%
);
background-size: 200% 100%;
animation: skeleton-shimmer 1.4s ease-in-out infinite;
}

@media (prefers-reduced-motion: reduce) {
.skeleton {
animation: none;
background: var(--button);
}
}

@keyframes skeleton-shimmer {
from {
background-position: 200% 0;
}
to {
background-position: -100% 0;
}
}
</style>
2 changes: 2 additions & 0 deletions src/lib/style/macos-shell.css
Original file line number Diff line number Diff line change
Expand Up @@ -515,6 +515,7 @@
align-items: flex-start;
justify-content: center;
background: color-mix(in srgb, var(--surface) 92%, var(--bg));
border-radius: calc(var(--radius-md) - 1px) 0 0 calc(var(--radius-md) - 1px);
overflow: visible;
}

Expand Down Expand Up @@ -547,6 +548,7 @@
border-bottom: 1px solid var(--border);
padding: var(--space-3);
justify-content: flex-start;
border-radius: calc(var(--radius-md) - 1px) calc(var(--radius-md) - 1px) 0 0;
}

.mac-inspector {
Expand Down
31 changes: 30 additions & 1 deletion src/routes/league/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
let phase = $state<string>("");
let games = $state<any[]>([]);
let loadingHistory = $state(false);
let profileLoading = $state(false);
let liveTimer: ReturnType<typeof setInterval> | null = null;
let unlisteners: UnlistenFn[] = [];

Expand Down Expand Up @@ -345,17 +346,21 @@
}

async function loadProfile() {
profileLoading = true;
try {
summoner = await invoke<any>("league_summoner");
} catch {
summoner = null;
profileLoading = false;
return;
}
try {
const stats = await invoke<any>("league_ranked");
ranked = stats?.queueMap ?? {};
} catch {
ranked = {};
} finally {
profileLoading = false;
}
loadHistory();
}
Expand Down Expand Up @@ -497,7 +502,7 @@
timers, expanded games) survives switching, and the meta tab's
auto-rune effect keeps working from any tab. -->
<div class="tab-panel" class:active={tab === "overview"}>
<OverviewTab {summoner} {ranked} {phase} {champSelect} {liveGame} {lobby} {queues} {actionError} {champions} {championById} {championByAlias} onAction={action} active={tab === "overview"} />
<OverviewTab {summoner} {profileLoading} {ranked} {phase} {champSelect} {liveGame} {lobby} {queues} {actionError} {champions} {championById} {championByAlias} onAction={action} active={tab === "overview"} />
</div>
<div class="tab-panel" class:active={tab === "analysis"}>
<AnalysisTab {analysis} {analysisLoading} onRefreshAnalysis={loadAnalysis} {phase} {scoutPlayers} {scoutReports} {scoutLoading} onRefreshScouting={loadScouting} {championById} {notes} onSaveNote={saveNote} {timesSeenBefore} {platform} clientConnected={status.connected} active={tab === "analysis"} />
Expand Down Expand Up @@ -1861,6 +1866,30 @@
border-color: var(--accent);
}

.league-page :global(.game-row-skeleton) {
display: flex;
align-items: center;
gap: 12px;
padding: 9px 12px;
background: var(--surface);
border: 1px solid var(--border);
border-radius: calc(var(--border-radius) - 2px);
}

.league-page :global(.game-skeleton-info) {
display: flex;
flex-direction: column;
gap: 5px;
flex: 1;
}

.league-page :global(.skeleton-lines) {
display: flex;
flex-direction: column;
gap: 8px;
margin-top: 10px;
}

.league-page :global(.champ-icon) {
width: 34px;
height: 34px;
Expand Down
Loading