Skip to content
Open
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
4 changes: 4 additions & 0 deletions CHANGELOG-nightly.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
### 3.1.16.1000

- Added EloWard League of Legends rank badges module

### 3.1.15.1000

- Added required Firefox built-in consent metadata to the manifest
Expand Down
78 changes: 76 additions & 2 deletions src/app/chat/UserTag.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
<!--Badge List -->
<span
v-if="
!hideBadges && ((twitchBadges.length && twitchBadgeSets?.count) || cosmetics.badges.size || sourceData)
!hideBadges &&
((twitchBadges.length && twitchBadgeSets?.count) || cosmetics.badges.size || sourceData || elowardBadge)
"
class="seventv-chat-user-badge-list"
>
Expand Down Expand Up @@ -36,6 +37,9 @@
type="app"
/>
</template>

<!-- EloWard Rank Badge - Rightmost position (closest to username) -->
<EloWardBadge v-if="elowardBadge" :badge="elowardBadge" :username="user.username" />
</span>

<!-- Message Author -->
Expand Down Expand Up @@ -74,6 +78,10 @@ import { useChannelContext } from "@/composable/channel/useChannelContext";
import { useChatProperties } from "@/composable/chat/useChatProperties";
import { useCosmetics } from "@/composable/useCosmetics";
import { useConfig } from "@/composable/useSettings";
import EloWardBadge from "@/site/twitch.tv/modules/eloward/components/EloWardBadge.vue";
import { useEloWardRanks } from "@/site/twitch.tv/modules/eloward/composables/useEloWardRanks";
import type { EloWardBadge as EloWardBadgeType } from "@/site/twitch.tv/modules/eloward/composables/useEloWardRanks";
import { useGameDetection } from "@/site/twitch.tv/modules/eloward/composables/useGameDetection";
import Badge from "./Badge.vue";
import UserCard from "./UserCard.vue";
import UiDraggable from "@/ui/UiDraggable.vue";
Expand Down Expand Up @@ -115,6 +123,11 @@ const twitchBadges = ref<Twitch.ChatBadge[]>([]);
const twitchBadgeSets = toRef(properties, "twitchBadgeSets");
const mentionStyle = useConfig<MentionStyle>("chat.colored_mentions");

// EloWard integration
const elowardRanks = useEloWardRanks();
const gameDetection = useGameDetection();
const elowardEnabled = useConfig<boolean>("eloward.enabled");

const tagRef = ref<HTMLDivElement>();
const showUserCard = ref(false);
const cardHandle = ref<HTMLDivElement>();
Expand Down Expand Up @@ -183,6 +196,58 @@ const stop = watch(
},
{ immediate: true },
);

// EloWard badge integration
const elowardBadge = ref<EloWardBadgeType | null>(null);

// Detect official EloWard Chrome extension
function detectOfficialExtension(): boolean {
const bodyAttr = document.body?.getAttribute("data-eloward-chrome-ext");
const htmlAttr = document.documentElement?.getAttribute("data-eloward-chrome-ext");

return bodyAttr === "active" || htmlAttr === "active";
}

const initializeEloWardBadge = () => {
// Skip if official EloWard Chrome extension is detected
if (detectOfficialExtension()) {
elowardBadge.value = null;
return;
}

if (!elowardEnabled.value || !gameDetection.isLeagueStream.value) {
elowardBadge.value = null;
return;
}

const username = props.user.username;
if (!username) {
elowardBadge.value = null;
return;
}

const cachedData = elowardRanks.getCachedRankData(username);

if (cachedData !== undefined) {
elowardBadge.value = cachedData ? elowardRanks.getRankBadge(cachedData) : null;
return;
}

elowardRanks
.fetchRankData(username)
.then((rankData) => {
elowardBadge.value = rankData ? elowardRanks.getRankBadge(rankData) : null;
})
.catch(() => {
elowardBadge.value = null;
});
};

initializeEloWardBadge();

watch(() => props.user.username, initializeEloWardBadge);
watch(elowardEnabled, initializeEloWardBadge);
watch(() => gameDetection.isLeagueStream.value, initializeEloWardBadge);
</script>

<style scoped lang="scss">
Expand All @@ -195,7 +260,11 @@ const stop = watch(
padding: 0.2rem;

.seventv-chat-user-badge-list {
margin-right: 0.25em;
display: inline-flex !important;
align-items: center !important;
vertical-align: baseline !important;
gap: 0 !important;
margin-right: 0 !important;

:deep(img) {
vertical-align: middle;
Expand All @@ -209,6 +278,11 @@ const stop = watch(
.seventv-chat-user-username {
font-weight: 700;
}

// Username spacing after badge list
.seventv-chat-user-badge-list + .seventv-chat-user-username {
margin-left: 2px !important;
}
}

.seventv-user-card-float {
Expand Down
68 changes: 68 additions & 0 deletions src/site/twitch.tv/modules/eloward/EloWardModule.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<template></template>

<script setup lang="ts">
import { watch, onMounted, onUnmounted } from "vue";
import { declareModule } from "@/composable/useModule";
import { useChannelContext } from "@/composable/channel/useChannelContext";
import { declareConfig } from "@/composable/useSettings";
import { useEloWardRanks } from "./composables/useEloWardRanks";
import { useGameDetection } from "./composables/useGameDetection";

const { dependenciesMet, markAsReady } = declareModule("eloward", {
name: "EloWard",
depends_on: ["chat"],
});

const ctx = useChannelContext();
const elowardRanks = useEloWardRanks();
const gameDetection = useGameDetection();

onMounted(async () => {
if (dependenciesMet.value) {
markAsReady();
} else {
const unwatch = watch(dependenciesMet, (met) => {
if (met) {
markAsReady();
unwatch();
}
});
}
});

// Clear cache when leaving League streams
watch(
() => gameDetection.isLeagueStream.value,
(isLeague) => {
if (!isLeague) {
elowardRanks.clearCache();
}
},
);

// Clear cache when switching channels
watch(
() => ctx.id,
(newChannelId, oldChannelId) => {
if (newChannelId !== oldChannelId && newChannelId) {
elowardRanks.clearCache();
}
},
);

onUnmounted(() => {
elowardRanks.clearCache();
});
</script>

<script lang="ts">
// Configuration declarations
export const config = [
declareConfig("eloward.enabled", "TOGGLE", {
path: ["Appearance", "Vanity"],
label: "EloWard Rank Badges",
hint: "Show League of Legends rank badges next to usernames when watching League of Legends streams",
defaultValue: true,
}),
];
</script>
136 changes: 136 additions & 0 deletions src/site/twitch.tv/modules/eloward/components/EloWardBadge.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
<template>
<div
class="seventv-chat-badge eloward-rank-badge"
:class="badgeClasses"
@click="handleClick"
@mouseenter="show(imgRef)"
@mouseleave="hide()"
>
<img
ref="imgRef"
:src="badge.imageUrl"
:srcset="srcset"
:alt="`${badge.tier} rank badge`"
class="eloward-badge-img"
loading="eager"
decoding="async"
fetchpriority="high"
/>
</div>
</template>

<script setup lang="ts">
import { computed, ref } from "vue";
import { useTooltip } from "@/composable/useTooltip";
import EloWardTooltip from "./EloWardTooltip.vue";
import { useEloWardRanks } from "../composables/useEloWardRanks";
import type { EloWardBadge } from "../composables/useEloWardRanks";

const props = defineProps<{
badge: EloWardBadge;
username: string;
}>();

const elowardRanks = useEloWardRanks();
const imgRef = ref<HTMLElement>();

// Use 7TV's tooltip system
const { show, hide } = useTooltip(EloWardTooltip, {
badge: props.badge,
username: props.username,
});

const badgeClasses = computed(() => ({
[`eloward-${props.badge.tier.toLowerCase()}`]: true,
"eloward-animated": props.badge.animated,
}));

// Generate srcset for responsive loading
const srcset = computed(() => {
return `${props.badge.imageUrl} 1x, ${props.badge.imageUrl} 2x`;
});

// Click handler to open OP.GG
const handleClick = () => {
// Use cached data if available
const url = elowardRanks.getOpGGUrl({
tier: props.badge.tier,
division: props.badge.division,
leaguePoints: props.badge.leaguePoints,
summonerName: props.badge.summonerName,
region: props.badge.region,
});

if (url) {
window.open(url, "_blank");
}
};
</script>

<style scoped lang="scss">
// Base badge container - exactly matches standard 7TV badges
.seventv-chat-badge.eloward-rank-badge {
display: inline-block;
vertical-align: baseline;
cursor: pointer;
}

// Image - matches standard badge pattern
.eloward-badge-img {
height: 18px;
width: auto;
vertical-align: middle;
}

// Rank-specific scaling - kept minimal
.eloward-iron .eloward-badge-img {
transform: scale(1.15);
}

.eloward-bronze .eloward-badge-img {
transform: scale(1.1);
}

.eloward-silver .eloward-badge-img {
transform: scale(1.1);
}

.eloward-gold .eloward-badge-img {
transform: scale(1.15);
}

.eloward-platinum .eloward-badge-img {
transform: scale(1.15);
}

.eloward-emerald .eloward-badge-img {
transform: scale(1.15);
}

.eloward-diamond .eloward-badge-img {
transform: scale(1.05);
}

.eloward-master .eloward-badge-img {
transform: scale(1.1);
}

.eloward-grandmaster .eloward-badge-img {
transform: scale(1.05);
}

.eloward-challenger .eloward-badge-img {
transform: scale(1.15);
}

// Theme adjustments
/* stylelint-disable-next-line selector-class-pattern */
:global(.tw-root--theme-dark) .seventv-chat-badge.eloward-rank-badge {
filter: brightness(0.95);
}

/* stylelint-disable-next-line selector-class-pattern */
:global(.tw-root--theme-light) .seventv-chat-badge.eloward-rank-badge {
filter: brightness(1.05) contrast(1.1);
}
</style>
Loading