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 {
|
.avatar-header-info {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.75rem;
|
gap: 0.85rem;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar-icon-placeholder {
|
.avatar-image-container {
|
||||||
width: 48px;
|
position: relative;
|
||||||
height: 48px;
|
width: 52px;
|
||||||
|
height: 52px;
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
|
overflow: hidden;
|
||||||
background: var(--bg-secondary);
|
background: var(--bg-secondary);
|
||||||
border: 1px solid var(--border-color);
|
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;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|||||||
@@ -245,9 +245,20 @@ function renderLineup(lineups) {
|
|||||||
} else {
|
} else {
|
||||||
const info = getAvatarInfo(avatarId);
|
const info = getAvatarInfo(avatarId);
|
||||||
card.innerHTML = `
|
card.innerHTML = `
|
||||||
|
<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-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-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 style="font-size: 0.75rem; color: var(--text-muted);">${info.element} • ${info.path}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
el.lineupContainer.appendChild(card);
|
el.lineupContainer.appendChild(card);
|
||||||
@@ -298,7 +309,13 @@ function renderAvatarsGrid() {
|
|||||||
<span class="avatar-badge-rarity rarity-${meta.rarity}">★ ${meta.rarity}</span>
|
<span class="avatar-badge-rarity rarity-${meta.rarity}">★ ${meta.rarity}</span>
|
||||||
<div>
|
<div>
|
||||||
<div class="avatar-header-info">
|
<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>
|
||||||
<div class="avatar-name">${meta.name}</div>
|
<div class="avatar-name">${meta.name}</div>
|
||||||
<div class="avatar-subinfo">
|
<div class="avatar-subinfo">
|
||||||
@@ -378,12 +395,21 @@ function renderExRanks(exList) {
|
|||||||
const localizedMeta = getAvatarInfo(item.avatar_id);
|
const localizedMeta = getAvatarInfo(item.avatar_id);
|
||||||
|
|
||||||
card.innerHTML = `
|
card.innerHTML = `
|
||||||
|
<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>
|
||||||
<div style="font-weight: 700; font-size: 1rem;">${localizedMeta.name}</div>
|
<div style="font-weight: 700; font-size: 1rem;">${localizedMeta.name}</div>
|
||||||
<div style="font-size: 0.8rem; color: var(--text-muted);">
|
<div style="font-size: 0.8rem; color: var(--text-muted);">
|
||||||
Avatar ID: ${item.avatar_id} • Ex-Rank Skill ID: ${item.ex_rank_id}
|
Avatar ID: ${item.avatar_id} • Ex-Rank Skill ID: ${item.ex_rank_id}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<label class="switch">
|
<label class="switch">
|
||||||
<input type="checkbox" ${item.unlocked ? 'checked' : ''} data-avatar="${item.avatar_id}">
|
<input type="checkbox" ${item.unlocked ? 'checked' : ''} data-avatar="${item.avatar_id}">
|
||||||
<span class="slider"></span>
|
<span class="slider"></span>
|
||||||
|
|||||||
@@ -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.');
|
||||||
|
})();
|
||||||