feat: add 76 avatar PNG images and integrate into UI cards, lineup, and Ex-Rank
No files matched your search
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 31 KiB |
@@ -368,16 +368,39 @@ body {
|
||||
|
||||
.avatar-header-info {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
gap: 0.85rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.avatar-icon-placeholder {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
.avatar-image-container {
|
||||
position: relative;
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
border-radius: var(--radius-md);
|
||||
overflow: hidden;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.avatar-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform 0.25s ease;
|
||||
}
|
||||
|
||||
.card:hover .avatar-img {
|
||||
transform: scale(1.08);
|
||||
}
|
||||
|
||||
.avatar-icon-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: var(--bg-secondary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
@@ -245,9 +245,20 @@ function renderLineup(lineups) {
|
||||
} else {
|
||||
const info = getAvatarInfo(avatarId);
|
||||
card.innerHTML = `
|
||||
<div style="font-size: 0.75rem; color: var(--accent-cyan);">${t('slot')} ${slot + 1}</div>
|
||||
<div style="font-weight: 700; margin-top: 0.25rem;">${info.name}</div>
|
||||
<div style="font-size: 0.75rem; color: var(--text-muted);">${info.element} • ${info.path}</div>
|
||||
<div style="display: flex; gap: 0.75rem; align-items: center;">
|
||||
<div class="avatar-image-container" style="width: 44px; height: 44px;">
|
||||
<img class="avatar-img"
|
||||
src="./assets/avatars/${avatarId}.png"
|
||||
onerror="if(this.src.indexOf('jsdelivr')===-1){this.src='https://cdn.jsdelivr.net/gh/Mar-7th/StarRailRes@master/icon/avatar/${avatarId}.png';}else{this.style.display='none';this.nextElementSibling.style.display='flex';}"
|
||||
alt="${info.name}">
|
||||
<div class="avatar-icon-placeholder" style="display: none;">${info.name.charAt(0)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-size: 0.75rem; color: var(--accent-cyan);">${t('slot')} ${slot + 1}</div>
|
||||
<div style="font-weight: 700; margin-top: 0.15rem; font-size: 0.95rem;">${info.name}</div>
|
||||
<div style="font-size: 0.75rem; color: var(--text-muted);">${info.element} • ${info.path}</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
el.lineupContainer.appendChild(card);
|
||||
@@ -298,7 +309,13 @@ function renderAvatarsGrid() {
|
||||
<span class="avatar-badge-rarity rarity-${meta.rarity}">★ ${meta.rarity}</span>
|
||||
<div>
|
||||
<div class="avatar-header-info">
|
||||
<div class="avatar-icon-placeholder">${meta.name.charAt(0)}</div>
|
||||
<div class="avatar-image-container">
|
||||
<img class="avatar-img"
|
||||
src="./assets/avatars/${item.avatar_id}.png"
|
||||
onerror="if(this.src.indexOf('jsdelivr')===-1){this.src='https://cdn.jsdelivr.net/gh/Mar-7th/StarRailRes@master/icon/avatar/${item.avatar_id}.png';}else{this.style.display='none';this.nextElementSibling.style.display='flex';}"
|
||||
alt="${meta.name}">
|
||||
<div class="avatar-icon-placeholder" style="display: none;">${meta.name.charAt(0)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="avatar-name">${meta.name}</div>
|
||||
<div class="avatar-subinfo">
|
||||
@@ -378,10 +395,19 @@ function renderExRanks(exList) {
|
||||
const localizedMeta = getAvatarInfo(item.avatar_id);
|
||||
|
||||
card.innerHTML = `
|
||||
<div>
|
||||
<div style="font-weight: 700; font-size: 1rem;">${localizedMeta.name}</div>
|
||||
<div style="font-size: 0.8rem; color: var(--text-muted);">
|
||||
Avatar ID: ${item.avatar_id} • Ex-Rank Skill ID: ${item.ex_rank_id}
|
||||
<div style="display: flex; gap: 0.85rem; align-items: center;">
|
||||
<div class="avatar-image-container" style="width: 44px; height: 44px;">
|
||||
<img class="avatar-img"
|
||||
src="./assets/avatars/${item.avatar_id}.png"
|
||||
onerror="if(this.src.indexOf('jsdelivr')===-1){this.src='https://cdn.jsdelivr.net/gh/Mar-7th/StarRailRes@master/icon/avatar/${item.avatar_id}.png';}else{this.style.display='none';this.nextElementSibling.style.display='flex';}"
|
||||
alt="${localizedMeta.name}">
|
||||
<div class="avatar-icon-placeholder" style="display: none;">${localizedMeta.name.charAt(0)}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style="font-weight: 700; font-size: 1rem;">${localizedMeta.name}</div>
|
||||
<div style="font-size: 0.8rem; color: var(--text-muted);">
|
||||
Avatar ID: ${item.avatar_id} • Ex-Rank Skill ID: ${item.ex_rank_id}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<label class="switch">
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
const fs = require('fs');
|
||||
const https = require('https');
|
||||
const path = require('path');
|
||||
|
||||
const ids = [
|
||||
8001, 8002, 8003, 8004, 8005, 8006, 8007, 8008,
|
||||
1001, 1224, 1002, 1003, 1004, 1008, 1009, 1013,
|
||||
1101, 1102, 1103, 1104, 1105, 1106, 1107, 1108, 1109, 1110, 1111, 1112,
|
||||
1201, 1202, 1203, 1204, 1205, 1206, 1207, 1208, 1209, 1210, 1211, 1212, 1213, 1214, 1215, 1217, 1218, 1220, 1221, 1222, 1223, 1225,
|
||||
1005, 1006, 1301, 1302, 1303, 1304, 1305, 1306, 1307, 1308, 1309, 1310, 1312, 1313, 1314, 1315, 1317,
|
||||
1401, 1402, 1403, 1404, 1405, 1407, 1408, 1409, 1410
|
||||
];
|
||||
|
||||
const targetDir = path.join(__dirname, '..', 'assets', 'avatars');
|
||||
if (!fs.existsSync(targetDir)) fs.mkdirSync(targetDir, { recursive: true });
|
||||
|
||||
function fetchUrl(url, dest) {
|
||||
return new Promise((resolve) => {
|
||||
const req = https.get(url, (res) => {
|
||||
if (res.statusCode === 200) {
|
||||
const file = fs.createWriteStream(dest);
|
||||
res.pipe(file);
|
||||
file.on('finish', () => {
|
||||
file.close();
|
||||
resolve(true);
|
||||
});
|
||||
} else {
|
||||
res.resume();
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
req.on('error', () => resolve(false));
|
||||
req.setTimeout(6000, () => {
|
||||
req.destroy();
|
||||
resolve(false);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function processId(id) {
|
||||
const dest = path.join(targetDir, `${id}.png`);
|
||||
if (fs.existsSync(dest) && fs.statSync(dest).size > 1000) {
|
||||
return { id, status: 'exists' };
|
||||
}
|
||||
|
||||
// 1. Try StarRailRes CDN
|
||||
const cdnUrl = `https://cdn.jsdelivr.net/gh/Mar-7th/StarRailRes@master/icon/avatar/${id}.png`;
|
||||
let ok = await fetchUrl(cdnUrl, dest);
|
||||
if (ok) return { id, status: 'ok (cdn)' };
|
||||
|
||||
// 2. Try raw github
|
||||
const rawUrl = `https://raw.githubusercontent.com/Mar-7th/StarRailRes/master/icon/avatar/${id}.png`;
|
||||
ok = await fetchUrl(rawUrl, dest);
|
||||
if (ok) return { id, status: 'ok (raw)' };
|
||||
|
||||
// Fallbacks for trailblazer female variants if not directly mapped
|
||||
if ([8004, 8006, 8008].includes(id) && fs.existsSync(path.join(targetDir, '8002.png'))) {
|
||||
fs.copyFileSync(path.join(targetDir, '8002.png'), dest);
|
||||
return { id, status: 'fallback 8002' };
|
||||
}
|
||||
if ([8003, 8005, 8007].includes(id) && fs.existsSync(path.join(targetDir, '8001.png'))) {
|
||||
fs.copyFileSync(path.join(targetDir, '8001.png'), dest);
|
||||
return { id, status: 'fallback 8001' };
|
||||
}
|
||||
|
||||
return { id, status: 'not found' };
|
||||
}
|
||||
|
||||
(async () => {
|
||||
console.log(`Starting parallel download for ${ids.length} avatars...`);
|
||||
const chunkSize = 8;
|
||||
for (let i = 0; i < ids.length; i += chunkSize) {
|
||||
const chunk = ids.slice(i, i + chunkSize);
|
||||
const results = await Promise.all(chunk.map(processId));
|
||||
results.forEach(r => console.log(`Avatar ${r.id}: ${r.status}`));
|
||||
}
|
||||
console.log('Finished downloading avatars.');
|
||||
})();
|
||||