From 7ce502d4cf94369711fd57120a68e04497c553a7 Mon Sep 17 00:00:00 2001 From: SunnyWang0 Date: Mon, 13 Oct 2025 18:10:02 -0700 Subject: [PATCH 01/24] initial commit --- src/app/chat/UserTag.vue | 63 ++++- .../modules/eloward/EloWardModule.vue | 110 +++++++++ src/site/twitch.tv/modules/eloward/README.md | 129 ++++++++++ .../eloward/components/EloWardBadge.vue | 72 ++++++ .../eloward/components/EloWardTooltip.vue | 87 +++++++ .../eloward/composables/useEloWardRanks.ts | 232 ++++++++++++++++++ .../eloward/composables/useGameDetection.ts | 50 ++++ 7 files changed, 742 insertions(+), 1 deletion(-) create mode 100644 src/site/twitch.tv/modules/eloward/EloWardModule.vue create mode 100644 src/site/twitch.tv/modules/eloward/README.md create mode 100644 src/site/twitch.tv/modules/eloward/components/EloWardBadge.vue create mode 100644 src/site/twitch.tv/modules/eloward/components/EloWardTooltip.vue create mode 100644 src/site/twitch.tv/modules/eloward/composables/useEloWardRanks.ts create mode 100644 src/site/twitch.tv/modules/eloward/composables/useGameDetection.ts diff --git a/src/app/chat/UserTag.vue b/src/app/chat/UserTag.vue index 64dd74b3..c07f1279 100644 --- a/src/app/chat/UserTag.vue +++ b/src/app/chat/UserTag.vue @@ -8,10 +8,17 @@ + + + ([]); const twitchBadgeSets = toRef(properties, "twitchBadgeSets"); const mentionStyle = useConfig("chat.colored_mentions"); +// EloWard integration +const elowardRanks = useEloWardRanks(); +const gameDetection = useGameDetection(); +const elowardBadge = ref(null); +const elowardEnabled = useConfig("eloward.enabled"); +const elowardLeagueOnly = useConfig("eloward.league_only"); + const tagRef = ref(); const showUserCard = ref(false); const cardHandle = ref(); @@ -183,6 +201,49 @@ const stop = watch( }, { immediate: true }, ); + +// EloWard rank badge logic +let fetchTimeout: number | null = null; +const fetchEloWardBadge = () => { + if (fetchTimeout) { + clearTimeout(fetchTimeout); + } + + fetchTimeout = setTimeout(async () => { + if (!elowardEnabled.value) { + elowardBadge.value = null; + return; + } + + // Check if we should only show on League streams + if (elowardLeagueOnly.value && !gameDetection.isLeagueStream.value) { + elowardBadge.value = null; + return; + } + + try { + const rankData = await elowardRanks.fetchRankData(props.user.username); + elowardBadge.value = rankData ? elowardRanks.getRankBadge(rankData) : null; + } catch (error) { + if (process.env.NODE_ENV === 'development') { + console.error("[EloWard] Failed to fetch rank badge:", error); + } + elowardBadge.value = null; + } + }, 100); // 100ms debounce +}; + +// Watch for EloWard settings changes +watch([elowardEnabled, elowardLeagueOnly], () => { + fetchEloWardBadge(); +}, { immediate: true }); + +// Watch for game changes +watch(() => gameDetection.isLeagueStream.value, () => { + if (elowardLeagueOnly.value) { + fetchEloWardBadge(); + } +}); diff --git a/src/site/twitch.tv/modules/eloward/components/EloWardTooltip.vue b/src/site/twitch.tv/modules/eloward/components/EloWardTooltip.vue new file mode 100644 index 00000000..5544d809 --- /dev/null +++ b/src/site/twitch.tv/modules/eloward/components/EloWardTooltip.vue @@ -0,0 +1,87 @@ + + + + + diff --git a/src/site/twitch.tv/modules/eloward/composables/useEloWardRanks.ts b/src/site/twitch.tv/modules/eloward/composables/useEloWardRanks.ts new file mode 100644 index 00000000..e396b3a3 --- /dev/null +++ b/src/site/twitch.tv/modules/eloward/composables/useEloWardRanks.ts @@ -0,0 +1,232 @@ +import { computed, watch } from "vue"; +import { useConfig } from "@/composable/useSettings"; + +const API_BASE_URL = "https://api.eloward.com/ranks/lol"; +const CDN_BASE_URL = "https://cdn.eloward.com/ranks"; +const DEFAULT_CACHE_DURATION = 15 * 60 * 1000; // 15 minutes +const MAX_CACHE_SIZE = 1000; +const HIGH_RANKS = new Set(["MASTER", "GRANDMASTER", "CHALLENGER"]); + +interface CacheEntry { + data: EloWardRankData; + timestamp: number; + accessCount: number; +} + +class LRUCache { + private cache = new Map(); + private enabled = false; + private cacheDuration = DEFAULT_CACHE_DURATION; + + setEnabled(enabled: boolean) { + this.enabled = enabled; + if (!enabled) this.clear(); + } + + setCacheDuration(duration: number) { + this.cacheDuration = duration; + } + + get(username: string): EloWardRankData | null { + if (!this.enabled) return null; + + const key = username.toLowerCase(); + const cached = this.cache.get(key); + if (!cached) return null; + + // Check if cache is expired + if (Date.now() - cached.timestamp > this.cacheDuration) { + this.cache.delete(key); + return null; + } + + // Update access count for LRU + cached.accessCount++; + return cached.data; + } + + set(username: string, data: EloWardRankData) { + if (!this.enabled) return; + + // Evict least recently used entries if cache is full + if (this.cache.size >= MAX_CACHE_SIZE) { + let oldestKey = ""; + let oldestAccess = Infinity; + + for (const [key, entry] of this.cache) { + if (entry.accessCount < oldestAccess) { + oldestKey = key; + oldestAccess = entry.accessCount; + } + } + + if (oldestKey) this.cache.delete(oldestKey); + } + + this.cache.set(username.toLowerCase(), { + data, + timestamp: Date.now(), + accessCount: 1 + }); + } + + clear() { + this.cache.clear(); + } + + has(username: string): boolean { + return this.get(username) !== null; + } +} + +const rankCache = new LRUCache(); +const pendingRequests = new Map>(); + +export interface EloWardRankData { + tier: string; + division: string; + leaguePoints: number; + summonerName: string; + region: string; + animate_badge: boolean; +} + +export interface EloWardBadge { + id: string; + tier: string; + division: string; + leaguePoints: number; + summonerName: string; + region: string; + imageUrl: string; + tooltip: string; + animate: boolean; +} + +export function useEloWardRanks() { + const enabled = useConfig("eloward.enabled"); + const leagueOnly = useConfig("eloward.league_only"); + const animatedBadges = useConfig("eloward.animated_badges"); + const showTooltips = useConfig("eloward.show_tooltips"); + const cacheDuration = useConfig("eloward.cache_duration"); + + // Update cache duration when setting changes + watch(cacheDuration, (newDuration) => { + rankCache.setCacheDuration(newDuration * 60 * 1000); + }); + + async function fetchRankData(username: string): Promise { + if (!enabled.value) return null; + + // Check cache first + const cached = rankCache.get(username); + if (cached) return cached; + + // Check for pending request to avoid duplicate API calls + const pendingKey = username.toLowerCase(); + if (pendingRequests.has(pendingKey)) { + return pendingRequests.get(pendingKey)!; + } + + const requestPromise = (async () => { + try { + const response = await fetch(`${API_BASE_URL}/${username.toLowerCase()}`); + + if (response.status === 404) return null; + if (!response.ok) throw new Error(`HTTP ${response.status}`); + + const data = await response.json(); + const rankData: EloWardRankData = { + tier: data.rank_tier, + division: data.rank_division, + leaguePoints: data.lp, + summonerName: data.riot_id, + region: data.region, + animate_badge: data.animate_badge || false + }; + + // Cache the result + rankCache.set(username, rankData); + return rankData; + } catch (error) { + // Only log errors in development + if (process.env.NODE_ENV === 'development') { + console.error(`[EloWard] Failed to fetch rank data for ${username}:`, error); + } + return null; + } finally { + // Clean up pending request + pendingRequests.delete(pendingKey); + } + })(); + + pendingRequests.set(pendingKey, requestPromise); + return requestPromise; + } + + function getRankBadge(rankData: EloWardRankData): EloWardBadge { + const tier = rankData.tier.toUpperCase(); + const division = rankData.division || ""; + const lp = rankData.leaguePoints || 0; + + // Determine if badge should be animated + const shouldAnimate = animatedBadges.value && + rankData.animate_badge && + HIGH_RANKS.has(tier); + + // Get badge image URL + const imageUrl = `${CDN_BASE_URL}/${tier.toLowerCase()}.${shouldAnimate ? 'gif' : 'png'}`; + + // Create tooltip text + const tooltip = formatRankTooltip(rankData); + + return { + id: `eloward-${tier}-${division}`, + tier, + division, + leaguePoints: lp, + summonerName: rankData.summonerName, + region: rankData.region, + imageUrl, + tooltip, + animate: shouldAnimate + }; + } + + function formatRankTooltip(rankData: EloWardRankData): string { + const { tier, division, leaguePoints, summonerName, region } = rankData; + + let tooltip = tier; + if (division) tooltip += ` ${division}`; + if (leaguePoints > 0) tooltip += ` - ${leaguePoints} LP`; + if (summonerName) tooltip += `\n${summonerName}`; + if (region) tooltip += `\n${region}`; + + return tooltip; + } + + function enable() { + rankCache.setEnabled(true); + } + + function disable() { + rankCache.setEnabled(false); + } + + function clearCache() { + rankCache.clear(); + pendingRequests.clear(); + } + + return { + isEnabled: enabled, + isLeagueOnly: leagueOnly, + useAnimatedBadges: animatedBadges, + showRankTooltips: showTooltips, + fetchRankData, + getRankBadge, + enable, + disable, + clearCache + }; +} diff --git a/src/site/twitch.tv/modules/eloward/composables/useGameDetection.ts b/src/site/twitch.tv/modules/eloward/composables/useGameDetection.ts new file mode 100644 index 00000000..98b8f59f --- /dev/null +++ b/src/site/twitch.tv/modules/eloward/composables/useGameDetection.ts @@ -0,0 +1,50 @@ +import { ref, computed } from "vue"; + +const LEAGUE_OF_LEGENDS_ID = "21779"; +const LEAGUE_OF_LEGENDS_NAME = "League of Legends"; + +interface GameInfo { + displayName?: string; + name?: string; + title?: string; + id?: string; + gameId?: string; +} + +export function useGameDetection() { + const currentGame = ref(""); + const currentGameId = ref(""); + + const isLeagueStream = computed(() => { + return currentGameId.value === LEAGUE_OF_LEGENDS_ID || + currentGame.value === LEAGUE_OF_LEGENDS_NAME || + currentGame.value.toLowerCase().includes("league of legends"); + }); + + function updateStreamInfo(streamData: any) { + if (!streamData) return; + + // Try to get game information from various possible sources + const gameInfo: GameInfo = streamData.game || streamData.gameInfo || streamData.category; + + if (gameInfo) { + currentGame.value = gameInfo.displayName || gameInfo.name || gameInfo.title || ""; + currentGameId.value = gameInfo.id || gameInfo.gameId || ""; + } + } + + function setGameInfo(gameName: string, gameId?: string) { + currentGame.value = gameName; + if (gameId) { + currentGameId.value = gameId; + } + } + + return { + currentGame, + currentGameId, + isLeagueStream, + updateStreamInfo, + setGameInfo + }; +} From ea5d40f181137fc5e6403ae736c09fbde23460c7 Mon Sep 17 00:00:00 2001 From: SunnyWang0 Date: Mon, 13 Oct 2025 19:05:16 -0700 Subject: [PATCH 02/24] new changes --- src/app/chat/UserTag.vue | 60 ++- .../modules/eloward/EloWardModule.vue | 71 ++-- .../eloward/components/EloWardBadge.vue | 187 ++++++++-- .../eloward/components/EloWardTooltip.vue | 166 ++++++--- .../eloward/composables/useEloWardRanks.ts | 352 +++++++++++------- .../eloward/composables/useGameDetection.ts | 58 ++- 6 files changed, 561 insertions(+), 333 deletions(-) diff --git a/src/app/chat/UserTag.vue b/src/app/chat/UserTag.vue index c07f1279..c90d8e6f 100644 --- a/src/app/chat/UserTag.vue +++ b/src/app/chat/UserTag.vue @@ -202,46 +202,44 @@ const stop = watch( { immediate: true }, ); -// EloWard rank badge logic -let fetchTimeout: number | null = null; -const fetchEloWardBadge = () => { - if (fetchTimeout) { - clearTimeout(fetchTimeout); +// EloWard rank badge logic - fetch immediately without debounce for faster display +const fetchEloWardBadge = async () => { + // Check if EloWard is enabled + if (!elowardEnabled.value) { + elowardBadge.value = null; + return; } - fetchTimeout = setTimeout(async () => { - if (!elowardEnabled.value) { - elowardBadge.value = null; - return; - } - - // Check if we should only show on League streams - if (elowardLeagueOnly.value && !gameDetection.isLeagueStream.value) { - elowardBadge.value = null; - return; - } - - try { - const rankData = await elowardRanks.fetchRankData(props.user.username); - elowardBadge.value = rankData ? elowardRanks.getRankBadge(rankData) : null; - } catch (error) { - if (process.env.NODE_ENV === 'development') { - console.error("[EloWard] Failed to fetch rank badge:", error); - } - elowardBadge.value = null; - } - }, 100); // 100ms debounce + // Check if we should only show on League streams + if (elowardLeagueOnly.value && !gameDetection.isLeagueStream.value) { + elowardBadge.value = null; + return; + } + + // Fetch rank data (already cached and deduplicated in the composable) + const rankData = await elowardRanks.fetchRankData(props.user.username); + elowardBadge.value = rankData ? elowardRanks.getRankBadge(rankData) : null; }; -// Watch for EloWard settings changes +// Initial fetch on mount +watchEffect(() => { + fetchEloWardBadge(); +}); + +// Watch for settings changes watch([elowardEnabled, elowardLeagueOnly], () => { fetchEloWardBadge(); -}, { immediate: true }); +}); // Watch for game changes -watch(() => gameDetection.isLeagueStream.value, () => { +watch(() => gameDetection.isLeagueStream.value, (isLeague) => { if (elowardLeagueOnly.value) { - fetchEloWardBadge(); + // Fetch badge if now on League stream, clear if not + if (isLeague) { + fetchEloWardBadge(); + } else { + elowardBadge.value = null; + } } }); diff --git a/src/site/twitch.tv/modules/eloward/EloWardModule.vue b/src/site/twitch.tv/modules/eloward/EloWardModule.vue index e4e95552..eb8fc963 100644 --- a/src/site/twitch.tv/modules/eloward/EloWardModule.vue +++ b/src/site/twitch.tv/modules/eloward/EloWardModule.vue @@ -1,12 +1,10 @@ - + diff --git a/src/site/twitch.tv/modules/eloward/components/EloWardBadge.vue b/src/site/twitch.tv/modules/eloward/components/EloWardBadge.vue index 1b215673..d277d067 100644 --- a/src/site/twitch.tv/modules/eloward/components/EloWardBadge.vue +++ b/src/site/twitch.tv/modules/eloward/components/EloWardBadge.vue @@ -1,20 +1,24 @@ + \ No newline at end of file diff --git a/src/site/twitch.tv/modules/eloward/components/EloWardTooltip.vue b/src/site/twitch.tv/modules/eloward/components/EloWardTooltip.vue index 5544d809..c9a3a340 100644 --- a/src/site/twitch.tv/modules/eloward/components/EloWardTooltip.vue +++ b/src/site/twitch.tv/modules/eloward/components/EloWardTooltip.vue @@ -1,18 +1,28 @@ + \ No newline at end of file diff --git a/src/site/twitch.tv/modules/eloward/composables/useEloWardRanks.ts b/src/site/twitch.tv/modules/eloward/composables/useEloWardRanks.ts index e396b3a3..41248a83 100644 --- a/src/site/twitch.tv/modules/eloward/composables/useEloWardRanks.ts +++ b/src/site/twitch.tv/modules/eloward/composables/useEloWardRanks.ts @@ -1,72 +1,62 @@ -import { computed, watch } from "vue"; +import { ref } from "vue"; import { useConfig } from "@/composable/useSettings"; -const API_BASE_URL = "https://api.eloward.com/ranks/lol"; -const CDN_BASE_URL = "https://cdn.eloward.com/ranks"; -const DEFAULT_CACHE_DURATION = 15 * 60 * 1000; // 15 minutes -const MAX_CACHE_SIZE = 1000; -const HIGH_RANKS = new Set(["MASTER", "GRANDMASTER", "CHALLENGER"]); +const API_BASE_URL = "https://eloward-ranks.unleashai.workers.dev/api/ranks/lol"; +const CDN_BASE_URL = "https://eloward-cdn.unleashai.workers.dev/lol"; +const DEFAULT_CACHE_DURATION = 60 * 60 * 1000; // 1 hour for positive cache +const NEGATIVE_CACHE_DURATION = 15 * 60 * 1000; // 15 minutes for negative cache +const MAX_CACHE_SIZE = 500; interface CacheEntry { - data: EloWardRankData; + data: EloWardRankData | null; // null for negative cache timestamp: number; - accessCount: number; } class LRUCache { - private cache = new Map(); - private enabled = false; - private cacheDuration = DEFAULT_CACHE_DURATION; + private cache: Map; + private maxSize: number; - setEnabled(enabled: boolean) { - this.enabled = enabled; - if (!enabled) this.clear(); + constructor(maxSize = 500) { + this.cache = new Map(); + this.maxSize = maxSize; } - setCacheDuration(duration: number) { - this.cacheDuration = duration; - } - - get(username: string): EloWardRankData | null { - if (!this.enabled) return null; - + get(username: string): EloWardRankData | null | undefined { const key = username.toLowerCase(); - const cached = this.cache.get(key); - if (!cached) return null; - + const entry = this.cache.get(key); + + if (!entry) return undefined; // Not in cache + // Check if cache is expired - if (Date.now() - cached.timestamp > this.cacheDuration) { + const now = Date.now(); + const cacheDuration = entry.data === null + ? NEGATIVE_CACHE_DURATION // Shorter duration for negative cache + : DEFAULT_CACHE_DURATION; + + if (now - entry.timestamp > cacheDuration) { this.cache.delete(key); - return null; + return undefined; } - - // Update access count for LRU - cached.accessCount++; - return cached.data; + + // Move to end (most recently used) + this.cache.delete(key); + this.cache.set(key, entry); + + return entry.data; // Can be null (negative cache) or data } - set(username: string, data: EloWardRankData) { - if (!this.enabled) return; - - // Evict least recently used entries if cache is full - if (this.cache.size >= MAX_CACHE_SIZE) { - let oldestKey = ""; - let oldestAccess = Infinity; - - for (const [key, entry] of this.cache) { - if (entry.accessCount < oldestAccess) { - oldestKey = key; - oldestAccess = entry.accessCount; - } - } - - if (oldestKey) this.cache.delete(oldestKey); + set(username: string, data: EloWardRankData | null) { + const key = username.toLowerCase(); + + // Remove oldest entry if at max size + if (this.cache.size >= this.maxSize && !this.cache.has(key)) { + const firstKey = this.cache.keys().next().value; + if (firstKey) this.cache.delete(firstKey); } - - this.cache.set(username.toLowerCase(), { + + this.cache.set(key, { data, - timestamp: Date.now(), - accessCount: 1 + timestamp: Date.now() }); } @@ -74,143 +64,241 @@ class LRUCache { this.cache.clear(); } - has(username: string): boolean { - return this.get(username) !== null; + size(): number { + return this.cache.size; } } -const rankCache = new LRUCache(); +// Global cache instance +const rankCache = new LRUCache(MAX_CACHE_SIZE); + +// Track pending requests to avoid duplicate API calls const pendingRequests = new Map>(); +// Valid rank tiers +const RANK_TIERS = new Set(['iron', 'bronze', 'silver', 'gold', 'platinum', 'emerald', 'diamond', 'master', 'grandmaster', 'challenger', 'unranked']); +const HIGH_RANKS = new Set(['MASTER', 'GRANDMASTER', 'CHALLENGER']); + export interface EloWardRankData { tier: string; - division: string; - leaguePoints: number; - summonerName: string; - region: string; - animate_badge: boolean; + division?: string; + leaguePoints?: number; + summonerName?: string; + region?: string; + animate_badge?: boolean; } export interface EloWardBadge { id: string; tier: string; - division: string; - leaguePoints: number; - summonerName: string; - region: string; + division?: string; imageUrl: string; - tooltip: string; - animate: boolean; + animated: boolean; + summonerName?: string; + region?: string; + leaguePoints?: number; } export function useEloWardRanks() { const enabled = useConfig("eloward.enabled"); - const leagueOnly = useConfig("eloward.league_only"); const animatedBadges = useConfig("eloward.animated_badges"); const showTooltips = useConfig("eloward.show_tooltips"); - const cacheDuration = useConfig("eloward.cache_duration"); - // Update cache duration when setting changes - watch(cacheDuration, (newDuration) => { - rankCache.setCacheDuration(newDuration * 60 * 1000); - }); + const isLoading = ref(false); + /** + * Fetch rank data for a username with caching + */ async function fetchRankData(username: string): Promise { - if (!enabled.value) return null; + if (!enabled.value || !username) return null; - // Check cache first - const cached = rankCache.get(username); - if (cached) return cached; + const normalizedUsername = username.toLowerCase(); + + // Check cache first (returns undefined if not cached, null if negative cached, or data) + const cached = rankCache.get(normalizedUsername); + if (cached !== undefined) { + return cached; // Return cached result (can be null for negative cache) + } - // Check for pending request to avoid duplicate API calls - const pendingKey = username.toLowerCase(); - if (pendingRequests.has(pendingKey)) { - return pendingRequests.get(pendingKey)!; + // Check if there's already a pending request for this user + if (pendingRequests.has(normalizedUsername)) { + return pendingRequests.get(normalizedUsername)!; } + // Create new request const requestPromise = (async () => { try { - const response = await fetch(`${API_BASE_URL}/${username.toLowerCase()}`); + isLoading.value = true; + const response = await fetch(`${API_BASE_URL}/${normalizedUsername}`, { + method: 'GET', + headers: { + 'Accept': 'application/json', + }, + signal: AbortSignal.timeout(5000) // 5 second timeout + }); + + if (response.status === 404) { + // User not found - cache negative result + rankCache.set(normalizedUsername, null); + return null; + } + + if (!response.ok) { + // Don't cache other errors - they might be temporary + return null; + } - if (response.status === 404) return null; - if (!response.ok) throw new Error(`HTTP ${response.status}`); - const data = await response.json(); + + // Validate the response + if (!data.rank_tier || !RANK_TIERS.has(data.rank_tier.toLowerCase())) { + // Invalid data - cache as negative + rankCache.set(normalizedUsername, null); + return null; + } + const rankData: EloWardRankData = { tier: data.rank_tier, division: data.rank_division, leaguePoints: data.lp, summonerName: data.riot_id, region: data.region, - animate_badge: data.animate_badge || false + animate_badge: data.animate_badge === true }; - - // Cache the result - rankCache.set(username, rankData); + + // Cache successful result + rankCache.set(normalizedUsername, rankData); return rankData; } catch (error) { - // Only log errors in development - if (process.env.NODE_ENV === 'development') { - console.error(`[EloWard] Failed to fetch rank data for ${username}:`, error); - } + // Network or parsing error - don't cache return null; } finally { + isLoading.value = false; // Clean up pending request - pendingRequests.delete(pendingKey); + pendingRequests.delete(normalizedUsername); } })(); - pendingRequests.set(pendingKey, requestPromise); + pendingRequests.set(normalizedUsername, requestPromise); return requestPromise; } - function getRankBadge(rankData: EloWardRankData): EloWardBadge { - const tier = rankData.tier.toUpperCase(); - const division = rankData.division || ""; - const lp = rankData.leaguePoints || 0; + /** + * Get badge data from rank data + */ + function getRankBadge(rankData: EloWardRankData): EloWardBadge | null { + if (!rankData?.tier) return null; - // Determine if badge should be animated + const tier = rankData.tier.toLowerCase(); + if (!RANK_TIERS.has(tier)) return null; + + const tierUpper = rankData.tier.toUpperCase(); const shouldAnimate = animatedBadges.value && - rankData.animate_badge && - HIGH_RANKS.has(tier); - - // Get badge image URL - const imageUrl = `${CDN_BASE_URL}/${tier.toLowerCase()}.${shouldAnimate ? 'gif' : 'png'}`; + rankData.animate_badge === true && + HIGH_RANKS.has(tierUpper); + + const extension = shouldAnimate ? '.webp' : '.png'; + const suffix = shouldAnimate ? '_premium' : ''; + const imageUrl = `${CDN_BASE_URL}/${tier}${suffix}${extension}`; - // Create tooltip text - const tooltip = formatRankTooltip(rankData); - return { - id: `eloward-${tier}-${division}`, - tier, - division, - leaguePoints: lp, + id: `eloward-${tier}${rankData.division ? `-${rankData.division}` : ''}`, + tier: tierUpper, + division: rankData.division, + imageUrl, + animated: shouldAnimate, summonerName: rankData.summonerName, region: rankData.region, - imageUrl, - tooltip, - animate: shouldAnimate + leaguePoints: rankData.leaguePoints }; } - function formatRankTooltip(rankData: EloWardRankData): string { - const { tier, division, leaguePoints, summonerName, region } = rankData; + /** + * Format rank text for display + */ + function formatRankText(rankData: EloWardRankData): string { + if (!rankData?.tier) return 'UNRANKED'; + + const tierUpper = rankData.tier.toUpperCase(); + if (tierUpper === 'UNRANKED') return 'UNRANKED'; - let tooltip = tier; - if (division) tooltip += ` ${division}`; - if (leaguePoints > 0) tooltip += ` - ${leaguePoints} LP`; - if (summonerName) tooltip += `\n${summonerName}`; - if (region) tooltip += `\n${region}`; + let rankText = tierUpper; - return tooltip; + if (rankData.division && !HIGH_RANKS.has(tierUpper)) { + rankText += ` ${rankData.division}`; + } + + if (rankData.leaguePoints !== undefined && rankData.leaguePoints !== null) { + rankText += ` - ${rankData.leaguePoints} LP`; + } + + return rankText; } - function enable() { - rankCache.setEnabled(true); + /** + * Get region display name + */ + function getRegionDisplay(region?: string): string { + if (!region) return ''; + + const regionMap: Record = { + 'na1': 'NA', + 'euw1': 'EUW', + 'eun1': 'EUNE', + 'kr': 'KR', + 'br1': 'BR', + 'jp1': 'JP', + 'la1': 'LAN', + 'la2': 'LAS', + 'oc1': 'OCE', + 'tr1': 'TR', + 'ru': 'RU', + 'ph2': 'PH', + 'sg2': 'SG', + 'th2': 'TH', + 'tw2': 'TW', + 'vn2': 'VN', + 'me1': 'ME', + 'sea': 'SEA' + }; + + return regionMap[region.toLowerCase()] || region.toUpperCase(); } - function disable() { - rankCache.setEnabled(false); + /** + * Build OP.GG URL for a player + */ + function getOpGGUrl(rankData: EloWardRankData): string | null { + if (!rankData?.summonerName || !rankData?.region) return null; + + const regionMapping: Record = { + 'na1': 'na', + 'euw1': 'euw', + 'eun1': 'eune', + 'kr': 'kr', + 'br1': 'br', + 'jp1': 'jp', + 'la1': 'lan', + 'la2': 'las', + 'oc1': 'oce', + 'tr1': 'tr', + 'ru': 'ru', + 'ph2': 'ph', + 'sg2': 'sg', + 'th2': 'th', + 'tw2': 'tw', + 'vn2': 'vn', + 'me1': 'me' + }; + + const opGGRegion = regionMapping[rankData.region.toLowerCase()]; + if (!opGGRegion) return null; + + const [summonerName, tagLine] = rankData.summonerName.split('#'); + const encodedName = encodeURIComponent(summonerName); + const tag = tagLine || rankData.region.toUpperCase(); + + return `https://op.gg/lol/summoners/${opGGRegion}/${encodedName}-${tag}`; } function clearCache() { @@ -219,14 +307,14 @@ export function useEloWardRanks() { } return { - isEnabled: enabled, - isLeagueOnly: leagueOnly, - useAnimatedBadges: animatedBadges, - showRankTooltips: showTooltips, fetchRankData, getRankBadge, - enable, - disable, - clearCache + formatRankText, + getRegionDisplay, + getOpGGUrl, + clearCache, + showTooltips, + isLoading, + cacheSize: () => rankCache.size() }; -} +} \ No newline at end of file diff --git a/src/site/twitch.tv/modules/eloward/composables/useGameDetection.ts b/src/site/twitch.tv/modules/eloward/composables/useGameDetection.ts index 98b8f59f..bdd69b52 100644 --- a/src/site/twitch.tv/modules/eloward/composables/useGameDetection.ts +++ b/src/site/twitch.tv/modules/eloward/composables/useGameDetection.ts @@ -1,50 +1,40 @@ -import { ref, computed } from "vue"; +import { computed } from "vue"; +import { useChannelContext } from "@/composable/channel/useChannelContext"; +// Constants for League of Legends detection (to be used in future implementation) +// eslint-disable-next-line @typescript-eslint/no-unused-vars const LEAGUE_OF_LEGENDS_ID = "21779"; +// eslint-disable-next-line @typescript-eslint/no-unused-vars const LEAGUE_OF_LEGENDS_NAME = "League of Legends"; -interface GameInfo { - displayName?: string; - name?: string; - title?: string; - id?: string; - gameId?: string; -} - export function useGameDetection() { - const currentGame = ref(""); - const currentGameId = ref(""); - + // For now, return a simple implementation that always returns false + // This will be enhanced later with proper Twitch API integration + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const ctx = useChannelContext(); + const isLeagueStream = computed(() => { - return currentGameId.value === LEAGUE_OF_LEGENDS_ID || - currentGame.value === LEAGUE_OF_LEGENDS_NAME || - currentGame.value.toLowerCase().includes("league of legends"); + // TODO: Implement proper game detection using Twitch API + // For now, return false to avoid TypeScript errors + // Will use ctx.id and LEAGUE_OF_LEGENDS_ID/LEAGUE_OF_LEGENDS_NAME in future implementation + return false; }); - function updateStreamInfo(streamData: any) { - if (!streamData) return; - - // Try to get game information from various possible sources - const gameInfo: GameInfo = streamData.game || streamData.gameInfo || streamData.category; - - if (gameInfo) { - currentGame.value = gameInfo.displayName || gameInfo.name || gameInfo.title || ""; - currentGameId.value = gameInfo.id || gameInfo.gameId || ""; - } - } + const currentGame = computed(() => { + // TODO: Implement proper game detection using Twitch API + // For now, return empty string + return ""; + }); - function setGameInfo(gameName: string, gameId?: string) { - currentGame.value = gameName; - if (gameId) { - currentGameId.value = gameId; - } - } + const currentGameId = computed(() => { + // TODO: Implement proper game detection using Twitch API + // For now, return empty string + return ""; + }); return { currentGame, currentGameId, isLeagueStream, - updateStreamInfo, - setGameInfo }; } From 79e7f74711b8b69d4627433608ac96b78f64e471 Mon Sep 17 00:00:00 2001 From: SunnyWang0 Date: Mon, 13 Oct 2025 22:10:55 -0700 Subject: [PATCH 03/24] fix --- src/app/chat/UserTag.vue | 50 ++-- .../modules/eloward/EloWardModule.vue | 20 +- .../eloward/components/EloWardBadge.vue | 95 +++++--- .../eloward/components/EloWardTooltip.vue | 45 ++-- .../eloward/composables/useEloWardRanks.ts | 188 ++++++++------- .../eloward/composables/useGameDetection.ts | 223 ++++++++++++++++-- 6 files changed, 396 insertions(+), 225 deletions(-) diff --git a/src/app/chat/UserTag.vue b/src/app/chat/UserTag.vue index c90d8e6f..f29b030e 100644 --- a/src/app/chat/UserTag.vue +++ b/src/app/chat/UserTag.vue @@ -8,17 +8,14 @@ - - + + (null); const elowardEnabled = useConfig("eloward.enabled"); -const elowardLeagueOnly = useConfig("eloward.league_only"); const tagRef = ref(); const showUserCard = ref(false); @@ -204,18 +200,12 @@ const stop = watch( // EloWard rank badge logic - fetch immediately without debounce for faster display const fetchEloWardBadge = async () => { - // Check if EloWard is enabled - if (!elowardEnabled.value) { + // Check if EloWard is enabled and we're on a League stream + if (!elowardEnabled.value || !gameDetection.isLeagueStream.value) { elowardBadge.value = null; return; } - - // Check if we should only show on League streams - if (elowardLeagueOnly.value && !gameDetection.isLeagueStream.value) { - elowardBadge.value = null; - return; - } - + // Fetch rank data (already cached and deduplicated in the composable) const rankData = await elowardRanks.fetchRankData(props.user.username); elowardBadge.value = rankData ? elowardRanks.getRankBadge(rankData) : null; @@ -227,21 +217,17 @@ watchEffect(() => { }); // Watch for settings changes -watch([elowardEnabled, elowardLeagueOnly], () => { +watch(elowardEnabled, () => { fetchEloWardBadge(); }); // Watch for game changes -watch(() => gameDetection.isLeagueStream.value, (isLeague) => { - if (elowardLeagueOnly.value) { - // Fetch badge if now on League stream, clear if not - if (isLeague) { - fetchEloWardBadge(); - } else { - elowardBadge.value = null; - } - } -}); +watch( + () => gameDetection.isLeagueStream.value, + () => { + fetchEloWardBadge(); + }, +); \ No newline at end of file + diff --git a/src/site/twitch.tv/modules/eloward/components/EloWardTooltip.vue b/src/site/twitch.tv/modules/eloward/components/EloWardTooltip.vue index c9a3a340..d1b6ed31 100644 --- a/src/site/twitch.tv/modules/eloward/components/EloWardTooltip.vue +++ b/src/site/twitch.tv/modules/eloward/components/EloWardTooltip.vue @@ -1,11 +1,7 @@ @@ -71,9 +88,11 @@ const handleClick = () => { margin-right: 0.3rem; cursor: pointer; transition: transform 0.1s ease; + position: relative; &:hover { transform: scale(1.1); + z-index: 1000; } .eloward-badge-img { @@ -82,6 +101,23 @@ const handleClick = () => { object-fit: contain; } + // Support for animated WebP badges + &.eloward-animated .eloward-badge-img { + image-rendering: auto; + image-rendering: crisp-edges; + image-rendering: optimize-contrast; + } + + .eloward-tooltip-wrapper { + position: absolute; + bottom: 100%; + left: 50%; + transform: translateX(-50%); + margin-bottom: 8px; + z-index: 10000; + pointer-events: none; + } + // Rank-specific positioning adjustments &.eloward-iron { transform: scale(1.3) translate(-1.5px, 1.5px); @@ -148,33 +184,43 @@ const handleClick = () => { &.eloward-iron { transform: scale(1.4) translate(-1.5px, 1.5px); } + &.eloward-bronze { transform: scale(1.3) translate(-1.5px, 3px); } + &.eloward-silver { transform: scale(1.3) translate(-1.5px, 2.5px); } + &.eloward-gold { transform: scale(1.32) translate(-1.5px, 3.5px); } + &.eloward-platinum { transform: scale(1.32) translate(-1.5px, 4px); } + &.eloward-emerald { transform: scale(1.33) translate(-1.5px, 4px); } + &.eloward-diamond { transform: scale(1.33) translate(-1.5px, 3.25px); } + &.eloward-master { transform: scale(1.3) translate(-1.5px, 4px); } + &.eloward-grandmaster { transform: scale(1.2) translate(-1.5px, 4.5px); } + &.eloward-challenger { transform: scale(1.32) translate(-1.5px, 4px); } + &.eloward-unranked { transform: scale(1.1) translate(-1.5px, 5px); } @@ -192,7 +238,7 @@ const handleClick = () => { } // Mobile responsive -@media (max-width: 400px) { +@media (width <= 400px) { .eloward-rank-badge { width: 18px; height: 18px; diff --git a/src/site/twitch.tv/modules/eloward/components/EloWardTooltip.vue b/src/site/twitch.tv/modules/eloward/components/EloWardTooltip.vue index d1b6ed31..0418b95f 100644 --- a/src/site/twitch.tv/modules/eloward/components/EloWardTooltip.vue +++ b/src/site/twitch.tv/modules/eloward/components/EloWardTooltip.vue @@ -56,7 +56,7 @@ const regionDisplay = computed(() => { padding: 8px 12px; background: var(--color-background-tooltip); border-radius: 6px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); + box-shadow: 0 2px 8px rgba(0, 0, 0, 15%); font-size: 13px; color: var(--color-text-base); max-width: 280px; @@ -114,7 +114,7 @@ const regionDisplay = computed(() => { // Dark theme :global(.tw-root--theme-dark) .eloward-tooltip { - background: rgba(24, 24, 27, 0.95); + background: rgba(24, 24, 27, 95%); .eloward-rank-line { color: #efeff1; @@ -132,7 +132,7 @@ const regionDisplay = computed(() => { // Light theme :global(.tw-root--theme-light) .eloward-tooltip { - background: rgba(255, 255, 255, 0.98); + background: rgba(255, 255, 255, 98%); .eloward-rank-line { color: #0e0e10; diff --git a/src/site/twitch.tv/modules/eloward/composables/useEloWardRanks.ts b/src/site/twitch.tv/modules/eloward/composables/useEloWardRanks.ts index 1b835129..785bbe2b 100644 --- a/src/site/twitch.tv/modules/eloward/composables/useEloWardRanks.ts +++ b/src/site/twitch.tv/modules/eloward/composables/useEloWardRanks.ts @@ -1,4 +1,4 @@ -import { ref } from "vue"; +import { onMounted, ref } from "vue"; import { useConfig } from "@/composable/useSettings"; const API_BASE_URL = "https://eloward-ranks.unleashai.workers.dev/api/ranks/lol"; @@ -6,6 +6,7 @@ const CDN_BASE_URL = "https://eloward-cdn.unleashai.workers.dev/lol"; const DEFAULT_CACHE_DURATION = 60 * 60 * 1000; // 1 hour for positive cache const NEGATIVE_CACHE_DURATION = 15 * 60 * 1000; // 15 minutes for negative cache const MAX_CACHE_SIZE = 500; +const BADGE_CACHE_VERSION = "3"; interface CacheEntry { data: EloWardRankData | null; // null for negative cache @@ -92,6 +93,146 @@ const RANK_TIERS = new Set([ ]); const HIGH_RANKS = new Set(["MASTER", "GRANDMASTER", "CHALLENGER"]); +// Image cache for badge blob URLs +class ImageCache { + private blobUrls: Map = new Map(); + private loadingPromises: Map> = new Map(); + private initialized = false; + + async init() { + if (this.initialized) return; + this.initialized = true; + + // Preconnect to CDN + this.injectPreconnectLinks(); + + // Preload all badge images + const preloadPromises: Promise[] = []; + for (const tier of RANK_TIERS) { + // Preload regular badge + preloadPromises.push(this.preloadImage(tier, false)); + // Preload animated badge for high ranks + if (tier === "master" || tier === "grandmaster" || tier === "challenger") { + preloadPromises.push(this.preloadImage(tier, true)); + } + } + + // Wait for all images to load + await Promise.allSettled(preloadPromises); + } + + private injectPreconnectLinks() { + try { + const head = document.head || document.getElementsByTagName("head")[0]; + if (!head) return; + + const existing = head.querySelector('link[data-eloward-preconnect="cdn"]'); + if (!existing) { + const dnsPrefetch = document.createElement("link"); + dnsPrefetch.setAttribute("rel", "dns-prefetch"); + dnsPrefetch.setAttribute("href", CDN_BASE_URL); + dnsPrefetch.setAttribute("data-eloward-preconnect", "cdn"); + head.appendChild(dnsPrefetch); + + const preconnect = document.createElement("link"); + preconnect.setAttribute("rel", "preconnect"); + preconnect.setAttribute("href", CDN_BASE_URL); + preconnect.setAttribute("crossorigin", "anonymous"); + preconnect.setAttribute("data-eloward-preconnect", "cdn"); + head.appendChild(preconnect); + } + } catch (_) { + // Ignore errors + } + } + + private async preloadImage(tier: string, isAnimated: boolean): Promise { + const key = isAnimated ? `${tier}_premium` : tier; + const extension = isAnimated ? ".webp" : ".png"; + const suffix = isAnimated ? "_premium" : ""; + const url = `${CDN_BASE_URL}/${tier}${suffix}${extension}?v=${BADGE_CACHE_VERSION}`; + + // Check if already loaded + if (this.blobUrls.has(key)) return; + + // Check if already loading + if (this.loadingPromises.has(key)) { + await this.loadingPromises.get(key); + return; + } + + // Start loading + const loadPromise = this.fetchAndCreateBlobUrl(url, key); + this.loadingPromises.set(key, loadPromise); + + try { + await loadPromise; + } finally { + this.loadingPromises.delete(key); + } + } + + private async fetchAndCreateBlobUrl(url: string, key: string): Promise { + try { + const response = await fetch(url, { + mode: "cors", + cache: "default", + credentials: "omit", + }); + + if (!response.ok) { + // Fallback to CDN URL + this.blobUrls.set(key, url); + return url; + } + + const blob = await response.blob(); + const blobUrl = URL.createObjectURL(blob); + this.blobUrls.set(key, blobUrl); + return blobUrl; + } catch (error) { + // Fallback to CDN URL on error + this.blobUrls.set(key, url); + return url; + } + } + + getImageUrl(tier: string, isAnimated: boolean): string { + const key = isAnimated ? `${tier}_premium` : tier; + + // Return cached blob URL if available + if (this.blobUrls.has(key)) { + return this.blobUrls.get(key)!; + } + + // Trigger preload for future use + this.preloadImage(tier, isAnimated).catch(() => {}); + + // Return CDN URL as fallback + const extension = isAnimated ? ".webp" : ".png"; + const suffix = isAnimated ? "_premium" : ""; + return `${CDN_BASE_URL}/${tier}${suffix}${extension}?v=${BADGE_CACHE_VERSION}`; + } + + clear() { + // Revoke all blob URLs + for (const url of this.blobUrls.values()) { + if (url.startsWith("blob:")) { + try { + URL.revokeObjectURL(url); + } catch (_) { + // Ignore errors + } + } + } + this.blobUrls.clear(); + this.loadingPromises.clear(); + } +} + +// Global image cache instance +const imageCache = new ImageCache(); + export interface EloWardRankData { tier: string; division?: string; @@ -196,7 +337,7 @@ export function useEloWardRanks() { } /** - * Get badge data from rank data + * Get badge data from rank data with cached image URLs */ function getRankBadge(rankData: EloWardRankData): EloWardBadge | null { if (!rankData?.tier) return null; @@ -207,9 +348,8 @@ export function useEloWardRanks() { const tierUpper = rankData.tier.toUpperCase(); const shouldAnimate = rankData.animate_badge === true && HIGH_RANKS.has(tierUpper); - const extension = shouldAnimate ? ".webp" : ".png"; - const suffix = shouldAnimate ? "_premium" : ""; - const imageUrl = `${CDN_BASE_URL}/${tier}${suffix}${extension}`; + // Get cached blob URL or CDN URL fallback + const imageUrl = imageCache.getImageUrl(tier, shouldAnimate); return { id: `eloward-${tier}${rankData.division ? `-${rankData.division}` : ""}`, @@ -314,8 +454,23 @@ export function useEloWardRanks() { function clearCache() { rankCache.clear(); pendingRequests.clear(); + imageCache.clear(); } + // Initialize image cache on first use + let initPromise: Promise | null = null; + async function ensureInitialized() { + if (!initPromise) { + initPromise = imageCache.init(); + } + await initPromise; + } + + // Auto-initialize when composable is first used + onMounted(() => { + ensureInitialized().catch(() => {}); + }); + return { fetchRankData, getRankBadge, @@ -326,5 +481,6 @@ export function useEloWardRanks() { isLoading, showTooltips, cacheSize: () => rankCache.size(), + ensureInitialized, }; } diff --git a/src/site/twitch.tv/modules/eloward/composables/useGameDetection.ts b/src/site/twitch.tv/modules/eloward/composables/useGameDetection.ts index c50bf18a..aa6a6910 100644 --- a/src/site/twitch.tv/modules/eloward/composables/useGameDetection.ts +++ b/src/site/twitch.tv/modules/eloward/composables/useGameDetection.ts @@ -1,4 +1,4 @@ -import { computed, onMounted, onUnmounted, ref } from "vue"; +import { computed, onMounted, onUnmounted, ref, watch } from "vue"; const LEAGUE_OF_LEGENDS_ID = "21779"; const LEAGUE_OF_LEGENDS_NAME = "League of Legends"; @@ -6,8 +6,10 @@ const LEAGUE_OF_LEGENDS_NAME = "League of Legends"; export function useGameDetection() { const currentGame = ref(""); const currentGameId = ref(""); + const lastPathname = ref(""); + const hasCheckedForCurrentPage = ref(false); - let gameObserver: MutationObserver | null = null; + let checkTimeout: number | null = null; /** * Extract game name from Twitch directory URL @@ -34,7 +36,6 @@ export function useGameDetection() { const vodGame = document.querySelector('a[data-a-target="video-info-game-boxart-link"] p'); const text = vodGame?.textContent?.trim(); if (text) { - console.log("[EloWard 7TV] VOD game detected from DOM:", text); return text; } @@ -44,7 +45,6 @@ export function useGameDetection() { ); const text2 = previewGameLink?.textContent?.trim(); if (text2) { - console.log("[EloWard 7TV] VOD game detected from preview card:", text2); return text2; } @@ -60,7 +60,6 @@ export function useGameDetection() { const link = header?.querySelector('a[data-a-target="stream-game-link"]'); const text = link?.textContent?.trim(); if (text) { - console.log("[EloWard 7TV] Game detected from stream-game-link:", text); return text; } @@ -68,14 +67,12 @@ export function useGameDetection() { const anyCat = header?.querySelector('a[href^="/directory/category/"]'); const text2 = anyCat?.textContent?.trim(); if (text2) { - console.log("[EloWard 7TV] Game detected from category link:", text2); return text2; } // Priority 3: derive from href slug if text is empty const byHref = extractGameFromHref(link?.getAttribute("href") || anyCat?.getAttribute("href") || null); if (byHref) { - console.log("[EloWard 7TV] Game detected from href:", byHref); return byHref; } @@ -85,13 +82,11 @@ export function useGameDetection() { ); const globalText = globalCat?.textContent?.trim(); if (globalText) { - console.log("[EloWard 7TV] Game detected from global search:", globalText); return globalText; } const byHrefGlobal = extractGameFromHref(globalCat?.getAttribute("href") || null); if (byHrefGlobal) { - console.log("[EloWard 7TV] Game detected from global href:", byHrefGlobal); return byHrefGlobal; } @@ -106,9 +101,14 @@ export function useGameDetection() { } /** - * Update the current game + * Update the current game - only check once or twice per page */ function updateGame() { + // Don't check if we already checked for this page + if (hasCheckedForCurrentPage.value && window.location.pathname === lastPathname.value) { + return; + } + let detectedGame: string | null = null; if (isVodPage()) { @@ -118,75 +118,67 @@ export function useGameDetection() { } if (detectedGame !== currentGame.value) { - const previousGame = currentGame.value || "none"; currentGame.value = detectedGame || ""; // Set game ID if it's League of Legends if (detectedGame === LEAGUE_OF_LEGENDS_NAME) { currentGameId.value = LEAGUE_OF_LEGENDS_ID; - console.log(`[EloWard 7TV] ✅ League of Legends stream detected! (was: ${previousGame})`); + // League of Legends detected } else { currentGameId.value = ""; - if (detectedGame) { - console.log(`[EloWard 7TV] Game changed to: ${detectedGame} (was: ${previousGame})`); - } else { - console.log(`[EloWard 7TV] No game detected (was: ${previousGame})`); - } + // Different game or no game detected } } + + // Mark as checked for current page + hasCheckedForCurrentPage.value = true; + lastPathname.value = window.location.pathname; } /** - * Start observing DOM for game changes + * Start checking for game category */ function startObserving() { - if (gameObserver) return; + // Clear any existing timeout + if (checkTimeout) { + clearTimeout(checkTimeout); + checkTimeout = null; + } - console.log("[EloWard 7TV] Starting game detection observer..."); + // Reset check flag for new page + hasCheckedForCurrentPage.value = false; // Initial check updateGame(); - // Set up mutation observer for game changes - gameObserver = new MutationObserver(() => { - updateGame(); - }); - - // Observe the body for changes to game links - gameObserver.observe(document.body, { - childList: true, - subtree: true, - attributes: true, - attributeFilter: ["href"], - }); - - // Also check periodically in case mutations are missed - const interval = setInterval(() => { + // Second check after 3 seconds to account for slow page load + checkTimeout = window.setTimeout(() => { updateGame(); - }, 5000); // Check every 5 seconds - - // Store interval ID for cleanup - (gameObserver as unknown as { _intervalId: number })._intervalId = interval; + checkTimeout = null; + }, 3000); } /** - * Stop observing DOM + * Stop observing */ function stopObserving() { - if (gameObserver) { - console.log("[EloWard 7TV] Stopping game detection observer"); - gameObserver.disconnect(); - - // Clear the interval - const intervalId = (gameObserver as unknown as { _intervalId?: number })._intervalId; - if (intervalId) { - clearInterval(intervalId); - } - - gameObserver = null; + if (checkTimeout) { + clearTimeout(checkTimeout); + checkTimeout = null; } } + // Watch for navigation changes + watch( + () => window.location.pathname, + (newPath, oldPath) => { + if (newPath !== oldPath) { + // Reset and check again on navigation + startObserving(); + } + }, + ); + // Computed property to check if it's a League stream const isLeagueStream = computed(() => { const isLeague = @@ -202,7 +194,7 @@ export function useGameDetection() { // Small delay to ensure DOM is ready setTimeout(() => { startObserving(); - }, 1000); + }, 500); }); // Clean up when unmounted From 7aa94e964055d914903b35214e5eee725a1e8771 Mon Sep 17 00:00:00 2001 From: SunnyWang0 Date: Tue, 14 Oct 2025 13:36:45 -0700 Subject: [PATCH 05/24] clean --- src/site/twitch.tv/modules/eloward/EloWardModule.vue | 6 ------ .../modules/eloward/composables/useEloWardRanks.ts | 2 -- 2 files changed, 8 deletions(-) diff --git a/src/site/twitch.tv/modules/eloward/EloWardModule.vue b/src/site/twitch.tv/modules/eloward/EloWardModule.vue index 20a48be4..418028c9 100644 --- a/src/site/twitch.tv/modules/eloward/EloWardModule.vue +++ b/src/site/twitch.tv/modules/eloward/EloWardModule.vue @@ -62,11 +62,5 @@ export const config = [ hint: "Show League of Legends rank badges next to usernames when watching League of Legends streams", defaultValue: true, }), - declareConfig("eloward.show_tooltips", "TOGGLE", { - path: ["Chat", "EloWard"], - label: "Show Rank Tooltips", - hint: "Display detailed rank information when hovering over badges", - defaultValue: true, - }), ]; diff --git a/src/site/twitch.tv/modules/eloward/composables/useEloWardRanks.ts b/src/site/twitch.tv/modules/eloward/composables/useEloWardRanks.ts index 785bbe2b..caed860a 100644 --- a/src/site/twitch.tv/modules/eloward/composables/useEloWardRanks.ts +++ b/src/site/twitch.tv/modules/eloward/composables/useEloWardRanks.ts @@ -255,7 +255,6 @@ export interface EloWardBadge { export function useEloWardRanks() { const enabled = useConfig("eloward.enabled"); - const showTooltips = useConfig("eloward.show_tooltips"); const isLoading = ref(false); @@ -479,7 +478,6 @@ export function useEloWardRanks() { getOpGGUrl, clearCache, isLoading, - showTooltips, cacheSize: () => rankCache.size(), ensureInitialized, }; From a5f9c8e89cb094aa31ba5a0cdee1a55bebce16e8 Mon Sep 17 00:00:00 2001 From: SunnyWang0 Date: Tue, 14 Oct 2025 13:39:35 -0700 Subject: [PATCH 06/24] clean up code --- .../modules/eloward/EloWardModule.vue | 3 - .../eloward/composables/useEloWardRanks.ts | 165 +----------------- 2 files changed, 8 insertions(+), 160 deletions(-) diff --git a/src/site/twitch.tv/modules/eloward/EloWardModule.vue b/src/site/twitch.tv/modules/eloward/EloWardModule.vue index 418028c9..a2a01564 100644 --- a/src/site/twitch.tv/modules/eloward/EloWardModule.vue +++ b/src/site/twitch.tv/modules/eloward/EloWardModule.vue @@ -18,9 +18,6 @@ const elowardRanks = useEloWardRanks(); const gameDetection = useGameDetection(); onMounted(async () => { - // Initialize image cache for instant badge display - await elowardRanks.ensureInitialized(); - if (dependenciesMet.value) { markAsReady(); } diff --git a/src/site/twitch.tv/modules/eloward/composables/useEloWardRanks.ts b/src/site/twitch.tv/modules/eloward/composables/useEloWardRanks.ts index caed860a..44f7845d 100644 --- a/src/site/twitch.tv/modules/eloward/composables/useEloWardRanks.ts +++ b/src/site/twitch.tv/modules/eloward/composables/useEloWardRanks.ts @@ -1,4 +1,4 @@ -import { onMounted, ref } from "vue"; +import { ref } from "vue"; import { useConfig } from "@/composable/useSettings"; const API_BASE_URL = "https://eloward-ranks.unleashai.workers.dev/api/ranks/lol"; @@ -93,146 +93,13 @@ const RANK_TIERS = new Set([ ]); const HIGH_RANKS = new Set(["MASTER", "GRANDMASTER", "CHALLENGER"]); -// Image cache for badge blob URLs -class ImageCache { - private blobUrls: Map = new Map(); - private loadingPromises: Map> = new Map(); - private initialized = false; - - async init() { - if (this.initialized) return; - this.initialized = true; - - // Preconnect to CDN - this.injectPreconnectLinks(); - - // Preload all badge images - const preloadPromises: Promise[] = []; - for (const tier of RANK_TIERS) { - // Preload regular badge - preloadPromises.push(this.preloadImage(tier, false)); - // Preload animated badge for high ranks - if (tier === "master" || tier === "grandmaster" || tier === "challenger") { - preloadPromises.push(this.preloadImage(tier, true)); - } - } - - // Wait for all images to load - await Promise.allSettled(preloadPromises); - } - - private injectPreconnectLinks() { - try { - const head = document.head || document.getElementsByTagName("head")[0]; - if (!head) return; - - const existing = head.querySelector('link[data-eloward-preconnect="cdn"]'); - if (!existing) { - const dnsPrefetch = document.createElement("link"); - dnsPrefetch.setAttribute("rel", "dns-prefetch"); - dnsPrefetch.setAttribute("href", CDN_BASE_URL); - dnsPrefetch.setAttribute("data-eloward-preconnect", "cdn"); - head.appendChild(dnsPrefetch); - - const preconnect = document.createElement("link"); - preconnect.setAttribute("rel", "preconnect"); - preconnect.setAttribute("href", CDN_BASE_URL); - preconnect.setAttribute("crossorigin", "anonymous"); - preconnect.setAttribute("data-eloward-preconnect", "cdn"); - head.appendChild(preconnect); - } - } catch (_) { - // Ignore errors - } - } - - private async preloadImage(tier: string, isAnimated: boolean): Promise { - const key = isAnimated ? `${tier}_premium` : tier; - const extension = isAnimated ? ".webp" : ".png"; - const suffix = isAnimated ? "_premium" : ""; - const url = `${CDN_BASE_URL}/${tier}${suffix}${extension}?v=${BADGE_CACHE_VERSION}`; - - // Check if already loaded - if (this.blobUrls.has(key)) return; - - // Check if already loading - if (this.loadingPromises.has(key)) { - await this.loadingPromises.get(key); - return; - } - - // Start loading - const loadPromise = this.fetchAndCreateBlobUrl(url, key); - this.loadingPromises.set(key, loadPromise); - - try { - await loadPromise; - } finally { - this.loadingPromises.delete(key); - } - } - - private async fetchAndCreateBlobUrl(url: string, key: string): Promise { - try { - const response = await fetch(url, { - mode: "cors", - cache: "default", - credentials: "omit", - }); - - if (!response.ok) { - // Fallback to CDN URL - this.blobUrls.set(key, url); - return url; - } - - const blob = await response.blob(); - const blobUrl = URL.createObjectURL(blob); - this.blobUrls.set(key, blobUrl); - return blobUrl; - } catch (error) { - // Fallback to CDN URL on error - this.blobUrls.set(key, url); - return url; - } - } - - getImageUrl(tier: string, isAnimated: boolean): string { - const key = isAnimated ? `${tier}_premium` : tier; - - // Return cached blob URL if available - if (this.blobUrls.has(key)) { - return this.blobUrls.get(key)!; - } - - // Trigger preload for future use - this.preloadImage(tier, isAnimated).catch(() => {}); - - // Return CDN URL as fallback - const extension = isAnimated ? ".webp" : ".png"; - const suffix = isAnimated ? "_premium" : ""; - return `${CDN_BASE_URL}/${tier}${suffix}${extension}?v=${BADGE_CACHE_VERSION}`; - } - - clear() { - // Revoke all blob URLs - for (const url of this.blobUrls.values()) { - if (url.startsWith("blob:")) { - try { - URL.revokeObjectURL(url); - } catch (_) { - // Ignore errors - } - } - } - this.blobUrls.clear(); - this.loadingPromises.clear(); - } +// Simple image URL generator - no complex caching needed +function getImageUrl(tier: string, isAnimated: boolean): string { + const extension = isAnimated ? ".webp" : ".png"; + const suffix = isAnimated ? "_premium" : ""; + return `${CDN_BASE_URL}/${tier}${suffix}${extension}?v=${BADGE_CACHE_VERSION}`; } -// Global image cache instance -const imageCache = new ImageCache(); - export interface EloWardRankData { tier: string; division?: string; @@ -347,8 +214,8 @@ export function useEloWardRanks() { const tierUpper = rankData.tier.toUpperCase(); const shouldAnimate = rankData.animate_badge === true && HIGH_RANKS.has(tierUpper); - // Get cached blob URL or CDN URL fallback - const imageUrl = imageCache.getImageUrl(tier, shouldAnimate); + // Get image URL directly + const imageUrl = getImageUrl(tier, shouldAnimate); return { id: `eloward-${tier}${rankData.division ? `-${rankData.division}` : ""}`, @@ -453,23 +320,8 @@ export function useEloWardRanks() { function clearCache() { rankCache.clear(); pendingRequests.clear(); - imageCache.clear(); } - // Initialize image cache on first use - let initPromise: Promise | null = null; - async function ensureInitialized() { - if (!initPromise) { - initPromise = imageCache.init(); - } - await initPromise; - } - - // Auto-initialize when composable is first used - onMounted(() => { - ensureInitialized().catch(() => {}); - }); - return { fetchRankData, getRankBadge, @@ -479,6 +331,5 @@ export function useEloWardRanks() { clearCache, isLoading, cacheSize: () => rankCache.size(), - ensureInitialized, }; } From 741c5fe82cff4ac62ec966bdb8ff5bcc6449ec48 Mon Sep 17 00:00:00 2001 From: SunnyWang0 Date: Tue, 14 Oct 2025 13:45:57 -0700 Subject: [PATCH 07/24] clean speed --- src/app/chat/UserTag.vue | 55 +++++++++++++++---- .../eloward/composables/useEloWardRanks.ts | 21 +++++++ 2 files changed, 66 insertions(+), 10 deletions(-) diff --git a/src/app/chat/UserTag.vue b/src/app/chat/UserTag.vue index f29b030e..5965e4bd 100644 --- a/src/app/chat/UserTag.vue +++ b/src/app/chat/UserTag.vue @@ -72,7 +72,7 @@ diff --git a/src/site/twitch.tv/modules/eloward/composables/useEloWardRanks.ts b/src/site/twitch.tv/modules/eloward/composables/useEloWardRanks.ts index 44f7845d..9e7fc4ea 100644 --- a/src/site/twitch.tv/modules/eloward/composables/useEloWardRanks.ts +++ b/src/site/twitch.tv/modules/eloward/composables/useEloWardRanks.ts @@ -125,6 +125,15 @@ export function useEloWardRanks() { const isLoading = ref(false); + /** + * Get cached rank data without triggering API call + */ + function getCachedRankData(username: string): EloWardRankData | null | undefined { + if (!enabled.value || !username) return undefined; + const normalizedUsername = username.toLowerCase(); + return rankCache.get(normalizedUsername); + } + /** * Fetch rank data for a username with caching */ @@ -132,13 +141,18 @@ export function useEloWardRanks() { if (!enabled.value || !username) return null; const normalizedUsername = username.toLowerCase(); + const startTime = performance.now(); // Check cache first (returns undefined if not cached, null if negative cached, or data) const cached = rankCache.get(normalizedUsername); if (cached !== undefined) { + const endTime = performance.now(); + console.log(`[EloWard] Cache hit for ${username} in ${(endTime - startTime).toFixed(2)}ms`); return cached; // Return cached result (can be null for negative cache) } + console.log(`[EloWard] Cache miss for ${username}, making API call`); + // Check if there's already a pending request for this user if (pendingRequests.has(normalizedUsername)) { return pendingRequests.get(normalizedUsername)!; @@ -159,6 +173,10 @@ export function useEloWardRanks() { if (response.status === 404) { // User not found - cache negative result rankCache.set(normalizedUsername, null); + const endTime = performance.now(); + console.log( + `[EloWard] User not found (404) for ${username} in ${(endTime - startTime).toFixed(2)}ms`, + ); return null; } @@ -187,6 +205,8 @@ export function useEloWardRanks() { // Cache successful result rankCache.set(normalizedUsername, rankData); + const endTime = performance.now(); + console.log(`[EloWard] API call completed for ${username} in ${(endTime - startTime).toFixed(2)}ms`); return rankData; } catch (error) { // Network or parsing error - don't cache @@ -324,6 +344,7 @@ export function useEloWardRanks() { return { fetchRankData, + getCachedRankData, getRankBadge, formatRankText, getRegionDisplay, From dba4b2e6bd54b578d06d33e59b87e78208f687f4 Mon Sep 17 00:00:00 2001 From: SunnyWang0 Date: Tue, 14 Oct 2025 17:48:09 -0700 Subject: [PATCH 08/24] changes --- src/app/chat/UserTag.vue | 2 +- .../eloward/components/EloWardBadge.vue | 52 +++++-------------- .../eloward/components/EloWardTooltip.vue | 37 ++++++------- 3 files changed, 29 insertions(+), 62 deletions(-) diff --git a/src/app/chat/UserTag.vue b/src/app/chat/UserTag.vue index 5965e4bd..72a412f1 100644 --- a/src/app/chat/UserTag.vue +++ b/src/app/chat/UserTag.vue @@ -72,7 +72,7 @@ diff --git a/src/site/twitch.tv/modules/eloward/module.txt b/src/site/twitch.tv/modules/eloward/module.txt deleted file mode 100644 index 7dea550f..00000000 --- a/src/site/twitch.tv/modules/eloward/module.txt +++ /dev/null @@ -1,159 +0,0 @@ -EloWard 7TV Module - Goal and Architecture Overview -================================================== - -PROJECT GOAL -============ - -The EloWard 7TV module displays League of Legends rank badges next to usernames in Twitch chat. It provides a lightweight, badge-only experience that integrates natively with 7TV's existing badge system and styling. - -PRIMARY OBJECTIVES -================== - -1. **Badge Display**: Display League of Legends rank badges next to usernames in Twitch chat -2. **Game-Aware Activation**: Only show badges when watching League of Legends streams -3. **Performance Optimized**: Efficient caching and minimal API calls for smooth performance -4. **Native 7TV Integration**: Seamlessly integrate with 7TV's existing badge system and styling -5. **Simplified User Experience**: One-click enable/disable with minimal configuration - -TECHNICAL ARCHITECTURE -====================== - -CORE COMPONENTS ---------------- - -1. **EloWardModule.vue**: Main entry point that declares the module and manages dependencies -2. **useEloWardRanks.ts**: Core composable handling API calls, caching, and rank data management -3. **useGameDetection.ts**: Detects League of Legends streams using Twitch's game information -4. **EloWardBadge.vue**: Badge component for displaying rank images -5. **EloWardTooltip.vue**: Tooltip component showing detailed rank information - -API INTEGRATION ---------------- - -- **Endpoint**: `https://eloward-ranks.unleashai.workers.dev/api/ranks/lol/{username}` -- **Method**: GET requests for rank data -- **Response**: JSON with rank_tier, rank_division, lp, riot_id, region, animate_badge -- **Animate Badge Logic**: Backend processes animate_badge as boolean (requires plus_active AND animate_badge) -- **Caching**: LRU cache with 1-hour positive cache, 15-minute negative cache -- **Error Handling**: Graceful degradation on API failures - -PERFORMANCE OPTIMIZATIONS -========================= - -CACHING STRATEGY ------------------ -- **LRU Cache**: Least Recently Used eviction with access counting -- **Dual Cache Duration**: 1 hour for successful lookups, 15 minutes for failed lookups -- **Request Deduplication**: Prevents duplicate API calls for the same username -- **Memory Management**: Automatic cleanup of pending requests and cache entries - -EFFICIENCY MEASURES -------------------- -- **Immediate Display**: Cache-first approach for instant badge display -- **Non-blocking API**: Async API calls don't block UI rendering -- **7TV Integration**: Uses 7TV's tooltip system and badge infrastructure -- **Minimal Re-renders**: Efficient Vue reactivity with direct config access -- **Smart Cleanup**: Cache cleared when switching channels or disabling module - -GAME DETECTION SYSTEM -==================== - -AUTOMATIC DETECTION -------------------- -- **Game ID**: Detects League of Legends using game ID `21779` -- **Game Name**: Fallback detection using "League of Legends" string matching -- **Stream Monitoring**: Watches for game changes and updates badge visibility -- **Channel Switching**: Automatically detects game when switching between channels - -CONFIGURATION OPTIONS -===================== - -USER SETTINGS -------------- -- **Enable EloWard Rank Badges**: Master toggle for the entire feature -- **Game-Aware Display**: Only show badges on League of Legends streams (default: enabled) -- **Animated Badges**: Backend-controlled animated WebP badges for premium users -- **Tooltip Display**: Show detailed rank information on hover using 7TV's tooltip system - -INTEGRATION POINTS -================== - -7TV BADGE SYSTEM ----------------- -- **Native Integration**: Uses 7TV's existing badge infrastructure and styling -- **Badge Container**: Integrates with 7TV's `seventv-chat-user-badge-list` container -- **Tooltip System**: Uses 7TV's `useTooltip` composable for consistent tooltip behavior -- **Theme Support**: Automatic dark/light theme support through 7TV's CSS variables - -USERTAG COMPONENT ------------------ -- **Primary Integration**: Main integration point with 7TV's UserTag component -- **Badge Placement**: Adds EloWard badges alongside existing Twitch and 7TV badges -- **Responsive Design**: Adapts to different chat layouts and themes - -USER EXPERIENCE FLOW -==================== - -TYPICAL USER JOURNEY --------------------- -1. **Installation**: User installs 7TV extension with EloWard module -2. **Activation**: User enables EloWard in 7TV settings (one-click toggle) -3. **Automatic Detection**: Module detects League of Legends streams automatically -4. **Badge Display**: Rank badges appear next to usernames in chat -5. **Interaction**: Users can hover for tooltips or click badges to open OP.GG profiles - -SIMPLE SETUP ------------- -- **No Account Linking**: Works without any authentication or account connections -- **No Configuration**: Works out-of-the-box with sensible defaults -- **No Data Storage**: No personal data stored locally (only public rank data cached) -- **No Privacy Concerns**: Only fetches publicly available rank information - -DEVELOPMENT PHILOSOPHY -====================== - -SIMPLICITY FIRST ----------------- -- **Single Purpose**: Focus exclusively on badge display -- **Minimal Dependencies**: Leverage existing 7TV infrastructure -- **Clean Code**: TypeScript with proper interfaces and separation of concerns -- **Maintainable**: Clear architecture with composables and components - -PERFORMANCE FOCUSED -------------------- -- **Instant Display**: Cache-first approach provides immediate badge display -- **Efficient Caching**: Smart LRU cache management to minimize API calls -- **Memory Conscious**: Automatic cleanup and request deduplication -- **7TV Native**: Leverages 7TV's infrastructure for optimal performance -- **Resource Light**: Minimal impact on 7TV extension performance - -USER-CENTRIC DESIGN -------------------- -- **Zero Friction**: Enable and forget - no complex setup -- **Intuitive**: Badges appear automatically when appropriate -- **Informative**: Rich tooltips with detailed rank information -- **Accessible**: Works across different themes and layouts - -FUTURE CONSIDERATIONS -===================== - -POTENTIAL ENHANCEMENTS ----------------------- -- **Additional Games**: Support for other competitive games beyond League of Legends -- **Custom Badge Styles**: User-configurable badge appearances -- **Advanced Filtering**: Options to show/hide specific ranks -- **Analytics**: Optional usage statistics and performance metrics - -MAINTENANCE STRATEGY --------------------- -- **API Compatibility**: Monitor EloWard API changes and adapt accordingly -- **7TV Updates**: Ensure compatibility with 7TV extension updates -- **Performance Monitoring**: Track cache hit rates and API usage -- **User Feedback**: Collect and implement user-requested improvements - -CONCLUSION -========== - -The EloWard 7TV module provides a focused, streamlined approach to displaying League of Legends rank badges in Twitch chat. By focusing purely on badge display and integrating seamlessly with 7TV's existing infrastructure, it delivers a simple, performant solution that enhances the Twitch viewing experience for League of Legends fans. - -The module's success depends on its ability to provide value through simple badge display while maintaining excellent performance and user experience. By keeping the scope narrow and the implementation focused, it can deliver a reliable, efficient solution that integrates naturally with 7TV's modern Vue.js architecture and robust caching systems. From 08a24bc5137456d64bf5bb7c814410e71367f3b0 Mon Sep 17 00:00:00 2001 From: SunnyWang0 Date: Wed, 3 Dec 2025 13:46:41 -0800 Subject: [PATCH 19/24] fixing styling and activation bug --- src/app/chat/UserTag.vue | 5 +- .../modules/eloward/EloWardModule.vue | 81 +++++++++++++++++-- .../eloward/components/EloWardBadge.vue | 32 ++++---- .../eloward/composables/useEloWardRanks.ts | 2 +- .../eloward/composables/useGameDetection.ts | 2 +- src/types/tw.module.d.ts | 2 + 6 files changed, 96 insertions(+), 28 deletions(-) diff --git a/src/app/chat/UserTag.vue b/src/app/chat/UserTag.vue index 94401bc3..a4cd699e 100644 --- a/src/app/chat/UserTag.vue +++ b/src/app/chat/UserTag.vue @@ -202,7 +202,7 @@ const componentMountTime = performance.now(); function perfLog(message: string, data?: unknown) { if (DEV_MODE) { const timeSinceMount = (performance.now() - componentMountTime).toFixed(2); - console.log(`[EloWard Badge +${timeSinceMount}ms] ${message}`, data || ""); + console.log(`[EloWard-Badge +${timeSinceMount}ms] ${message}`, data || ""); } } @@ -300,7 +300,8 @@ watch(() => gameDetection.isLeagueStream.value, (newVal) => { .seventv-chat-user-badge-list { display: inline-flex !important; - align-items: center !important; + align-items: baseline !important; + vertical-align: baseline !important; gap: 0px !important; margin-right: 0px !important; diff --git a/src/site/twitch.tv/modules/eloward/EloWardModule.vue b/src/site/twitch.tv/modules/eloward/EloWardModule.vue index a2a01564..ffb2a858 100644 --- a/src/site/twitch.tv/modules/eloward/EloWardModule.vue +++ b/src/site/twitch.tv/modules/eloward/EloWardModule.vue @@ -1,35 +1,82 @@ diff --git a/src/site/twitch.tv/modules/eloward/composables/useEloWardRanks.ts b/src/site/twitch.tv/modules/eloward/composables/useEloWardRanks.ts index 847716a4..07bd2244 100644 --- a/src/site/twitch.tv/modules/eloward/composables/useEloWardRanks.ts +++ b/src/site/twitch.tv/modules/eloward/composables/useEloWardRanks.ts @@ -8,6 +8,8 @@ const NEGATIVE_CACHE_DURATION = 15 * 60 * 1000; const MAX_CACHE_SIZE = 500; const BADGE_CACHE_VERSION = "3"; const DEV_MODE = import.meta.env.DEV; +// Temporarily enable logging in production for debugging +const ENABLE_LOGGING = true; interface CacheEntry { data: EloWardRankData | null; // null for negative cache @@ -98,8 +100,8 @@ function getImageUrl(tier: string, isAnimated: boolean): string { } function perfLog(message: string, data?: unknown) { - if (DEV_MODE) { - console.log(`[EloWard-RankAPI] ${message}`, data || ""); + if (DEV_MODE || ENABLE_LOGGING) { + console.log(`[EloWard Perf] ${message}`, data || ""); } } diff --git a/src/site/twitch.tv/modules/eloward/composables/useGameDetection.ts b/src/site/twitch.tv/modules/eloward/composables/useGameDetection.ts index 217659c6..cc269fc6 100644 --- a/src/site/twitch.tv/modules/eloward/composables/useGameDetection.ts +++ b/src/site/twitch.tv/modules/eloward/composables/useGameDetection.ts @@ -3,10 +3,12 @@ import { computed, onMounted, onUnmounted, ref, watch } from "vue"; const LEAGUE_OF_LEGENDS_ID = "21779"; const LEAGUE_OF_LEGENDS_NAME = "League of Legends"; const DEV_MODE = import.meta.env.DEV; +// Temporarily enable logging in production for debugging +const ENABLE_LOGGING = true; function perfLog(message: string, data?: unknown) { - if (DEV_MODE) { - console.log(`[EloWard-GameDetect] ${message}`, data || ""); + if (DEV_MODE || ENABLE_LOGGING) { + console.log(`[EloWard GameDetect] ${message}`, data || ""); } } @@ -111,8 +113,14 @@ export function useGameDetection() { function updateGame() { const startTime = performance.now(); - if (hasCheckedForCurrentPage.value && window.location.pathname === lastPathname.value) { - perfLog("updateGame() - SKIPPED (already checked)"); + // Only skip if we already have a valid game detected (not null) + // This allows retries when DOM wasn't ready + if ( + hasCheckedForCurrentPage.value && + window.location.pathname === lastPathname.value && + currentGame.value !== "" + ) { + perfLog("updateGame() - SKIPPED (already checked with valid result)"); return; } @@ -134,10 +142,16 @@ export function useGameDetection() { }); } else { currentGameId.value = ""; - perfLog("updateGame() - Different game detected", { - game: detectedGame, - duration: `${(performance.now() - startTime).toFixed(2)}ms`, - }); + if (detectedGame === null) { + perfLog("updateGame() - No game detected (DOM not ready?)", { + duration: `${(performance.now() - startTime).toFixed(2)}ms`, + }); + } else { + perfLog("updateGame() - Different game detected", { + game: detectedGame, + duration: `${(performance.now() - startTime).toFixed(2)}ms`, + }); + } } } else { perfLog("updateGame() - No change", { @@ -146,7 +160,10 @@ export function useGameDetection() { }); } - hasCheckedForCurrentPage.value = true; + // Only mark as checked if we got a valid result + if (detectedGame !== null) { + hasCheckedForCurrentPage.value = true; + } lastPathname.value = window.location.pathname; } @@ -161,11 +178,14 @@ export function useGameDetection() { hasCheckedForCurrentPage.value = false; updateGame(); - checkTimeout = window.setTimeout(() => { - perfLog("startObserving() - Second check (1s delay)"); - updateGame(); - checkTimeout = null; - }, 1000); + // Retry multiple times with increasing delays to ensure DOM is ready + const retryDelays = [500, 1500, 3000]; + retryDelays.forEach((delay, index) => { + window.setTimeout(() => { + perfLog(`startObserving() - Retry ${index + 1} (${delay}ms delay)`); + updateGame(); + }, delay); + }); } /** From f6331fc79d3593ac00a6c6ec195dab917d866758 Mon Sep 17 00:00:00 2001 From: SunnyWang0 Date: Wed, 3 Dec 2025 21:19:25 -0800 Subject: [PATCH 21/24] clean up eloward integration --- src/app/chat/UserTag.vue | 67 ++-------------- .../modules/eloward/EloWardModule.vue | 76 ++----------------- .../eloward/composables/useEloWardRanks.ts | 40 ---------- .../eloward/composables/useGameDetection.ts | 38 +--------- 4 files changed, 13 insertions(+), 208 deletions(-) diff --git a/src/app/chat/UserTag.vue b/src/app/chat/UserTag.vue index a4cd699e..e396863c 100644 --- a/src/app/chat/UserTag.vue +++ b/src/app/chat/UserTag.vue @@ -197,29 +197,11 @@ const stop = watch( { immediate: true }, ); -const DEV_MODE = import.meta.env.DEV; -const componentMountTime = performance.now(); -function perfLog(message: string, data?: unknown) { - if (DEV_MODE) { - const timeSinceMount = (performance.now() - componentMountTime).toFixed(2); - console.log(`[EloWard-Badge +${timeSinceMount}ms] ${message}`, data || ""); - } -} - -perfLog(`UserTag CREATED for "${props.user.username}"`); - +// EloWard badge integration const elowardBadge = ref(null); const initializeEloWardBadge = () => { - const startTime = performance.now(); - const timeSinceMount = (startTime - componentMountTime).toFixed(2); - perfLog(`initializeEloWardBadge(${props.user.username}) - START (+${timeSinceMount}ms since component create)`); - if (!elowardEnabled.value || !gameDetection.isLeagueStream.value) { - perfLog(`initializeEloWardBadge(${props.user.username}) - SKIPPED`, { - enabled: elowardEnabled.value, - isLeagueStream: gameDetection.isLeagueStream.value, - }); elowardBadge.value = null; return; } @@ -233,60 +215,25 @@ const initializeEloWardBadge = () => { const cachedData = elowardRanks.getCachedRankData(username); if (cachedData !== undefined) { - const badge = cachedData ? elowardRanks.getRankBadge(cachedData) : null; - elowardBadge.value = badge; - - perfLog(`initializeEloWardBadge(${username}) - COMPLETE (CACHED - INSTANT)`, { - hasBadge: !!badge, - tier: badge?.tier, - total: `${(performance.now() - startTime).toFixed(2)}ms`, - }); + elowardBadge.value = cachedData ? elowardRanks.getRankBadge(cachedData) : null; return; } - perfLog(`initializeEloWardBadge(${username}) - NOT CACHED, fetching from API...`); elowardRanks .fetchRankData(username) .then((rankData) => { - const badge = rankData ? elowardRanks.getRankBadge(rankData) : null; - elowardBadge.value = badge; - perfLog(`initializeEloWardBadge(${username}) - COMPLETE (FETCHED FROM API)`, { - hasBadge: !!badge, - tier: badge?.tier, - totalDuration: `${(performance.now() - startTime).toFixed(2)}ms`, - }); + elowardBadge.value = rankData ? elowardRanks.getRankBadge(rankData) : null; }) .catch(() => { elowardBadge.value = null; - perfLog(`initializeEloWardBadge(${username}) - ERROR`); }); }; -perfLog(`Calling initializeEloWardBadge() synchronously at component setup`); initializeEloWardBadge(); -watch(elowardBadge, (newVal, oldVal) => { - if (newVal && !oldVal) { - const timeSinceMount = (performance.now() - componentMountTime).toFixed(2); - perfLog(`✓ BADGE RENDERED in ${timeSinceMount}ms for "${props.user.username}"`, { - tier: newVal.tier, - imageUrl: newVal.imageUrl, - }); - } -}); - -watch(() => props.user.username, () => { - perfLog(`Username changed, reinitializing badge`); - initializeEloWardBadge(); -}); -watch(elowardEnabled, () => { - perfLog(`EloWard enabled changed to ${elowardEnabled.value}`); - initializeEloWardBadge(); -}); -watch(() => gameDetection.isLeagueStream.value, (newVal) => { - perfLog(`League stream detection changed to ${newVal}`); - initializeEloWardBadge(); -}); +watch(() => props.user.username, initializeEloWardBadge); +watch(elowardEnabled, initializeEloWardBadge); +watch(() => gameDetection.isLeagueStream.value, initializeEloWardBadge);