Show configured player's avatar too

This commit is contained in:
pleb 2026-04-13 12:55:07 -07:00
parent 976c331834
commit d971936445
5 changed files with 107 additions and 17 deletions

View File

@ -312,6 +312,14 @@ span:empty {
gap: 0.45rem; gap: 0.45rem;
} }
#friendScoresPlayerAvatar {
width: 2rem;
height: 2rem;
border-radius: 50%;
object-fit: cover;
flex-shrink: 0;
}
#friendScoresHeaderImg { #friendScoresHeaderImg {
width: 2.5rem; width: 2.5rem;
height: 2.5rem; height: 2.5rem;
@ -413,6 +421,7 @@ body.bottom #time {
margin-left: 1em; margin-left: 1em;
} }
#settings .beatLeaderPlayerRow,
#settings .debugSongIdRow { #settings .debugSongIdRow {
display: flex; display: flex;
align-items: baseline; align-items: baseline;

View File

@ -39,7 +39,7 @@
</div> </div>
</div> </div>
<div id="friendScores" aria-live="polite"> <div id="friendScores" aria-live="polite">
<div id="friendScoresHeader"><span id="friendScoresHeaderText">frenz!</span> <img id="friendScoresHeaderImg" src="assets/peepohigh.webp" alt=""></div> <div id="friendScoresHeader"><img id="friendScoresPlayerAvatar" src="images/unknown.svg" alt=""> <span id="friendScoresHeaderText">frenz</span> <img id="friendScoresHeaderImg" src="assets/peepohigh.webp" alt=""></div>
<ol id="friendScoresList"></ol> <ol id="friendScoresList"></ol>
<div id="friendScoresEmpty">No map loaded</div> <div id="friendScoresEmpty">No map loaded</div>
</div> </div>
@ -71,7 +71,10 @@
<option value="following">Following (I follow them)</option> <option value="following">Following (I follow them)</option>
<option value="followers">Followers (they follow me)</option> <option value="followers">Followers (they follow me)</option>
</select></label> </select></label>
<label id="beatLeaderPlayerSetting">BeatLeader player id: <input id="beatLeaderPlayerInput" type="text" placeholder="7656119… or alias"></label> <label id="beatLeaderPlayerSetting">Player id: <span class="beatLeaderPlayerRow">
<span class="debugSongIdHint">e.g. <button type="button" id="beatLeaderPlayerExample" title="Fill with pleb's numeric id">pleb</button></span>
<input id="beatLeaderPlayerInput" type="text" placeholder="7656119… or alias" spellcheck="false" autocomplete="off">
</span></label>
<label>Show BSR / map id: <input id="bsrInput" type="checkbox"></label> <label>Show BSR / map id: <input id="bsrInput" type="checkbox"></label>
<label>Position: <select id="positionInput"> <label>Position: <select id="positionInput">
<option value="[false,false]">Top left</option> <option value="[false,false]">Top left</option>

View File

@ -65,18 +65,26 @@ async function fetchBLLeaderboardsByHash(hash) {
return []; return [];
} }
} }
async function resolveBeatLeaderPlayerId(playerId) { async function fetchBeatLeaderPlayer(playerId) {
const path = `/player/${encodeURIComponent(playerId)}`; const path = `/player/${encodeURIComponent(playerId)}`;
try { try {
const res = await fetch(beatleaderUrl(path)); const res = await fetch(beatleaderUrl(path));
if (!res.ok) return playerId; if (!res.ok) return null;
const data = await res.json(); const data = await res.json();
const canonicalId = data.id; const id = data.id == null ? playerId : String(data.id);
return canonicalId == null ? playerId : String(canonicalId); const avatar = typeof data.avatar === "string" ? data.avatar.trim() || null : null;
return {
id,
avatar
};
} catch { } catch {
return playerId; return null;
} }
} }
async function resolveBeatLeaderPlayerId(playerId) {
const p = await fetchBeatLeaderPlayer(playerId);
return p?.id ?? playerId;
}
async function fetchLeaderboardScoresById(leaderboardId, maxPages = MAX_LEADERBOARD_SCORE_PAGES) { async function fetchLeaderboardScoresById(leaderboardId, maxPages = MAX_LEADERBOARD_SCORE_PAGES) {
const scores = []; const scores = [];
const pageSize = PAGE_SIZE; const pageSize = PAGE_SIZE;
@ -238,6 +246,7 @@ var beatSaberPlus = {
break; break;
case "handshake": case "handshake":
currentPlayerPlatformId = data.playerPlatformId || ""; currentPlayerPlatformId = data.playerPlatformId || "";
void refreshConfiguredPlayerAvatar();
void refreshMapFriendScores(); void refreshMapFriendScores();
break; break;
default: default:
@ -439,7 +448,30 @@ var friendScoresPanel = must("friendScores");
var friendScoresList = must("friendScoresList"); var friendScoresList = must("friendScoresList");
var friendScoresEmpty = must("friendScoresEmpty"); var friendScoresEmpty = must("friendScoresEmpty");
var friendScoresHeaderText = must("friendScoresHeaderText"); var friendScoresHeaderText = must("friendScoresHeaderText");
var friendScoresPlayerAvatar = must("friendScoresPlayerAvatar");
var friendScoresHeaderImg = must("friendScoresHeaderImg"); var friendScoresHeaderImg = must("friendScoresHeaderImg");
var cachedConfiguredPlayerAvatarKey = "";
var cachedConfiguredPlayerAvatarSrc = "images/unknown.svg";
async function refreshConfiguredPlayerAvatar() {
const key = `${settings.beatLeaderId.trim()}|${currentPlayerPlatformId}`;
const pid = getEffectivePlayerId();
if (!pid) {
cachedConfiguredPlayerAvatarKey = "";
cachedConfiguredPlayerAvatarSrc = "images/unknown.svg";
friendScoresPlayerAvatar.src = cachedConfiguredPlayerAvatarSrc;
return;
}
if (key === cachedConfiguredPlayerAvatarKey) {
friendScoresPlayerAvatar.src = cachedConfiguredPlayerAvatarSrc;
return;
}
const profile = await fetchBeatLeaderPlayer(pid);
const keyAfter = `${settings.beatLeaderId.trim()}|${currentPlayerPlatformId}`;
if (keyAfter !== key) return;
cachedConfiguredPlayerAvatarKey = key;
cachedConfiguredPlayerAvatarSrc = profile?.avatar?.trim() || "images/unknown.svg";
friendScoresPlayerAvatar.src = cachedConfiguredPlayerAvatarSrc;
}
function updateScore(score) { function updateScore(score) {
if (!settings.score) return; if (!settings.score) return;
accuracy.textContent = (score.accuracy * 100).toFixed(1); accuracy.textContent = (score.accuracy * 100).toFixed(1);
@ -568,6 +600,7 @@ async function refreshMapFriendScores() {
} }
window.onhashchange = () => { window.onhashchange = () => {
loadSettings(); loadSettings();
void refreshConfiguredPlayerAvatar();
const debugEl = document.getElementById("debugSongIdInput"); const debugEl = document.getElementById("debugSongIdInput");
if (debugEl) debugEl.value = settings.debugSongId; if (debugEl) debugEl.value = settings.debugSongId;
if (settings.debugSongId.trim()) void applyDebugSong(); if (settings.debugSongId.trim()) void applyDebugSong();
@ -687,6 +720,7 @@ async function bootstrap() {
} }
loadSettings(); loadSettings();
document.head.appendChild(style); document.head.appendChild(style);
void refreshConfiguredPlayerAvatar();
if (settings.debugSongId.trim()) void applyDebugSong(); if (settings.debugSongId.trim()) void applyDebugSong();
else void refreshMapFriendScores(); else void refreshMapFriendScores();
for (const key of [ for (const key of [
@ -717,8 +751,15 @@ async function bootstrap() {
beatLeaderPlayerInput.oninput = () => { beatLeaderPlayerInput.oninput = () => {
settings.beatLeaderId = beatLeaderPlayerInput.value.trim(); settings.beatLeaderId = beatLeaderPlayerInput.value.trim();
saveSettings(); saveSettings();
void refreshConfiguredPlayerAvatar();
void refreshMapFriendScores(); void refreshMapFriendScores();
}; };
must("beatLeaderPlayerExample").onclick = () => {
beatLeaderPlayerInput.value = "76561199407393962";
beatLeaderPlayerInput.dispatchEvent(new Event("input", {
bubbles: true
}));
};
const scale = must("scaleInput"); const scale = must("scaleInput");
scale.valueAsNumber = settings.scale * 100; scale.valueAsNumber = settings.scale * 100;
scale.oninput = () => { scale.oninput = () => {

View File

@ -2,6 +2,7 @@ import type {
BeatLeaderFollower, BeatLeaderFollower,
BeatLeaderLeaderboard, BeatLeaderLeaderboard,
BeatLeaderLeaderboardsByHashResponse, BeatLeaderLeaderboardsByHashResponse,
BeatLeaderPlayer,
BeatLeaderScore, BeatLeaderScore,
FriendMode, FriendMode,
} from "./types.ts"; } from "./types.ts";
@ -27,10 +28,6 @@ function beatleaderUrl(path: string): string {
return `${BASE_URL}${path}`; return `${BASE_URL}${path}`;
} }
interface BeatLeaderPlayerLookup {
id?: string | number | null;
}
export async function fetchBLLeaderboardsByHash(hash: string): Promise<BeatLeaderLeaderboard[]> { export async function fetchBLLeaderboardsByHash(hash: string): Promise<BeatLeaderLeaderboard[]> {
const path = `/leaderboards/hash/${encodeURIComponent(hash)}`; const path = `/leaderboards/hash/${encodeURIComponent(hash)}`;
try { try {
@ -47,19 +44,25 @@ export async function fetchBLLeaderboardsByHash(hash: string): Promise<BeatLeade
} }
} }
async function resolveBeatLeaderPlayerId(playerId: string): Promise<string> { export async function fetchBeatLeaderPlayer(playerId: string): Promise<{ id: string; avatar: string | null } | null> {
const path = `/player/${encodeURIComponent(playerId)}`; const path = `/player/${encodeURIComponent(playerId)}`;
try { try {
const res = await fetch(beatleaderUrl(path)); const res = await fetch(beatleaderUrl(path));
if (!res.ok) return playerId; if (!res.ok) return null;
const data = await res.json() as BeatLeaderPlayerLookup; const data = await res.json() as BeatLeaderPlayer;
const canonicalId = data.id; const id = data.id == null ? playerId : String(data.id);
return canonicalId == null ? playerId : String(canonicalId); const avatar = typeof data.avatar === "string" ? data.avatar.trim() || null : null;
return { id, avatar };
} catch { } catch {
return playerId; return null;
} }
} }
async function resolveBeatLeaderPlayerId(playerId: string): Promise<string> {
const p = await fetchBeatLeaderPlayer(playerId);
return p?.id ?? playerId;
}
async function fetchLeaderboardScoresById( async function fetchLeaderboardScoresById(
leaderboardId: string, leaderboardId: string,
maxPages = MAX_LEADERBOARD_SCORE_PAGES, maxPages = MAX_LEADERBOARD_SCORE_PAGES,

View File

@ -15,6 +15,7 @@ import type { BeatSaverMap } from "./beatsaver.ts";
import { fetchBeatSaverMapById, fetchBeatSaverMeta } from "./beatsaver.ts"; import { fetchBeatSaverMapById, fetchBeatSaverMeta } from "./beatsaver.ts";
import { import {
fetchAllMapScoresByHash, fetchAllMapScoresByHash,
fetchBeatLeaderPlayer,
fetchBLLeaderboardsByHash, fetchBLLeaderboardsByHash,
fetchFriends, fetchFriends,
normalizeAccuracy, normalizeAccuracy,
@ -87,6 +88,7 @@ const beatSaberPlus = {
break; break;
case "handshake": case "handshake":
currentPlayerPlatformId = data.playerPlatformId || ""; currentPlayerPlatformId = data.playerPlatformId || "";
void refreshConfiguredPlayerAvatar();
void refreshMapFriendScores(); void refreshMapFriendScores();
break; break;
default: default:
@ -326,8 +328,33 @@ const friendScoresPanel = must<HTMLElement>("friendScores");
const friendScoresList = must<HTMLOListElement>("friendScoresList"); const friendScoresList = must<HTMLOListElement>("friendScoresList");
const friendScoresEmpty = must<HTMLElement>("friendScoresEmpty"); const friendScoresEmpty = must<HTMLElement>("friendScoresEmpty");
const friendScoresHeaderText = must<HTMLElement>("friendScoresHeaderText"); const friendScoresHeaderText = must<HTMLElement>("friendScoresHeaderText");
const friendScoresPlayerAvatar = must<HTMLImageElement>("friendScoresPlayerAvatar");
const friendScoresHeaderImg = must<HTMLImageElement>("friendScoresHeaderImg"); const friendScoresHeaderImg = must<HTMLImageElement>("friendScoresHeaderImg");
let cachedConfiguredPlayerAvatarKey = "";
let cachedConfiguredPlayerAvatarSrc = "images/unknown.svg";
async function refreshConfiguredPlayerAvatar() {
const key = `${settings.beatLeaderId.trim()}|${currentPlayerPlatformId}`;
const pid = getEffectivePlayerId();
if (!pid) {
cachedConfiguredPlayerAvatarKey = "";
cachedConfiguredPlayerAvatarSrc = "images/unknown.svg";
friendScoresPlayerAvatar.src = cachedConfiguredPlayerAvatarSrc;
return;
}
if (key === cachedConfiguredPlayerAvatarKey) {
friendScoresPlayerAvatar.src = cachedConfiguredPlayerAvatarSrc;
return;
}
const profile = await fetchBeatLeaderPlayer(pid);
const keyAfter = `${settings.beatLeaderId.trim()}|${currentPlayerPlatformId}`;
if (keyAfter !== key) return;
cachedConfiguredPlayerAvatarKey = key;
cachedConfiguredPlayerAvatarSrc = profile?.avatar?.trim() || "images/unknown.svg";
friendScoresPlayerAvatar.src = cachedConfiguredPlayerAvatarSrc;
}
function updateScore(score: Score) { function updateScore(score: Score) {
if (!settings.score) return; if (!settings.score) return;
accuracy.textContent = (score.accuracy * 100).toFixed(1); accuracy.textContent = (score.accuracy * 100).toFixed(1);
@ -465,6 +492,7 @@ async function refreshMapFriendScores() {
window.onhashchange = () => { window.onhashchange = () => {
loadSettings(); loadSettings();
void refreshConfiguredPlayerAvatar();
const debugEl = document.getElementById("debugSongIdInput") as HTMLInputElement | null; const debugEl = document.getElementById("debugSongIdInput") as HTMLInputElement | null;
if (debugEl) debugEl.value = settings.debugSongId; if (debugEl) debugEl.value = settings.debugSongId;
if (settings.debugSongId.trim()) void applyDebugSong(); if (settings.debugSongId.trim()) void applyDebugSong();
@ -590,6 +618,7 @@ async function bootstrap() {
} }
loadSettings(); loadSettings();
document.head.appendChild(style); document.head.appendChild(style);
void refreshConfiguredPlayerAvatar();
if (settings.debugSongId.trim()) void applyDebugSong(); if (settings.debugSongId.trim()) void applyDebugSong();
else void refreshMapFriendScores(); else void refreshMapFriendScores();
@ -618,8 +647,13 @@ async function bootstrap() {
beatLeaderPlayerInput.oninput = () => { beatLeaderPlayerInput.oninput = () => {
settings.beatLeaderId = beatLeaderPlayerInput.value.trim(); settings.beatLeaderId = beatLeaderPlayerInput.value.trim();
saveSettings(); saveSettings();
void refreshConfiguredPlayerAvatar();
void refreshMapFriendScores(); void refreshMapFriendScores();
}; };
must<HTMLButtonElement>("beatLeaderPlayerExample").onclick = () => {
beatLeaderPlayerInput.value = "76561199407393962";
beatLeaderPlayerInput.dispatchEvent(new Event("input", { bubbles: true }));
};
const scale = must<HTMLInputElement>("scaleInput"); const scale = must<HTMLInputElement>("scaleInput");
scale.valueAsNumber = settings.scale * 100; scale.valueAsNumber = settings.scale * 100;