fix(ui): adopt clean FireflySrTool/NeonTeam character card layout and accurate 99 avatars metadata

This commit is contained in:
CAtlas0784 committed 2026-10-16 23:10:02 +07:00
1 parent 9ec9110905
commit 9b5b4087c9
5 files changed
+1659 -708

No files matched your search

+97 -23
View File
@@ -377,40 +377,47 @@ body {
display: flex;
flex-direction: column;
justify-content: space-between;
min-height: 220px;
min-height: 140px;
position: relative;
cursor: pointer;
border-width: 1.5px;
padding: 1rem;
border-radius: var(--radius-md);
transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.2s ease, border-color 0.2s ease;
}
.avatar-card:hover {
transform: translateY(-4px);
}
/* 5-Star Card Theme */
.avatar-card.card-rarity-5 {
border-color: rgba(251, 191, 36, 0.4);
background: linear-gradient(145deg, rgba(251, 191, 36, 0.05) 0%, rgba(18, 24, 38, 0.8) 100%);
border-color: rgba(251, 191, 36, 0.45);
background: linear-gradient(145deg, rgba(251, 191, 36, 0.08) 0%, rgba(18, 24, 38, 0.95) 100%);
}
.avatar-card.card-rarity-5:hover {
border-color: rgba(251, 191, 36, 0.8);
box-shadow: var(--glow-gold);
border-color: rgba(251, 191, 36, 0.9);
box-shadow: 0 8px 24px rgba(251, 191, 36, 0.25), 0 0 15px rgba(251, 191, 36, 0.15);
}
/* 4-Star Card Theme */
.avatar-card.card-rarity-4 {
border-color: rgba(192, 132, 252, 0.35);
background: linear-gradient(145deg, rgba(192, 132, 252, 0.05) 0%, rgba(18, 24, 38, 0.8) 100%);
border-color: rgba(192, 132, 252, 0.4);
background: linear-gradient(145deg, rgba(192, 132, 252, 0.08) 0%, rgba(18, 24, 38, 0.95) 100%);
}
.avatar-card.card-rarity-4:hover {
border-color: rgba(192, 132, 252, 0.8);
box-shadow: var(--glow-purple);
border-color: rgba(192, 132, 252, 0.9);
box-shadow: 0 8px 24px rgba(192, 132, 252, 0.25), 0 0 15px rgba(192, 132, 252, 0.15);
}
.avatar-badge-rarity {
position: absolute;
top: 0.85rem;
right: 0.85rem;
font-size: 0.75rem;
padding: 0.15rem 0.5rem;
top: 0.75rem;
right: 0.75rem;
font-size: 0.725rem;
padding: 0.15rem 0.45rem;
border-radius: var(--radius-sm);
font-weight: 800;
letter-spacing: 0.05em;
@@ -432,6 +439,12 @@ body {
display: flex;
gap: 0.85rem;
align-items: center;
margin-right: 2.2rem;
}
.avatar-meta-details {
flex: 1;
min-width: 0;
}
/* Circular / Rounded Avatar Portrait */
@@ -523,6 +536,53 @@ body {
font-weight: 600;
}
/* Avatar Card Footer & Status */
.avatar-card-footer {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 0.85rem;
padding-top: 0.65rem;
border-top: 1px solid rgba(255, 255, 255, 0.08);
}
.avatar-status-pill {
display: inline-flex;
align-items: center;
gap: 0.45rem;
background: rgba(0, 0, 0, 0.45);
padding: 0.25rem 0.6rem;
border-radius: 6px;
border: 1px solid var(--border-color);
font-size: 0.775rem;
font-weight: 700;
}
.avatar-lvl {
color: var(--accent-cyan);
}
.avatar-eidolon {
color: #94a3b8;
background: rgba(255, 255, 255, 0.08);
padding: 0.1rem 0.4rem;
border-radius: 4px;
}
.avatar-eidolon.e6 {
color: #0b0d14;
background: linear-gradient(135deg, #fbbf24, #f59e0b);
box-shadow: 0 0 10px rgba(251, 191, 36, 0.6);
font-weight: 900;
}
.btn-card-quick-max {
padding: 0.25rem 0.65rem;
font-size: 0.75rem;
font-weight: 800;
border-radius: 6px;
}
/* Eidolon Selector */
.eidolon-selector {
display: flex;
@@ -537,17 +597,18 @@ body {
.eidolon-buttons {
display: flex;
gap: 0.3rem;
gap: 0.35rem;
flex-wrap: wrap;
}
.eidolon-pip {
width: 28px;
height: 28px;
width: 32px;
height: 32px;
border-radius: 6px;
border: 1.5px solid rgba(255, 255, 255, 0.2);
background: rgba(255, 255, 255, 0.05);
color: #cbd5e1;
font-size: 0.8rem;
font-size: 0.825rem;
font-weight: 800;
cursor: pointer;
display: flex;
@@ -573,12 +634,20 @@ body {
}
.modal-eidolon-pip {
width: 38px;
height: 38px;
font-size: 0.95rem;
width: 44px;
height: 44px;
font-size: 1rem;
font-weight: 900;
border-radius: 8px;
}
.modal-eidolon-pip.active {
background: linear-gradient(135deg, #f59e0b, #fbbf24);
border-color: #fbbf24;
color: #0b0d14;
box-shadow: 0 0 14px rgba(251, 191, 36, 0.6);
}
/* ==========================================================================
Party / Lineup Builder
========================================================================== */
@@ -805,7 +874,7 @@ input:checked + .slider:before {
display: none;
align-items: center;
justify-content: center;
z-index: 999;
z-index: 10000;
padding: 1rem;
}
@@ -817,10 +886,15 @@ input:checked + .slider:before {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
max-width: 600px;
max-width: 640px;
width: 100%;
max-height: 90vh;
overflow-y: auto;
position: relative;
z-index: 10001;
pointer-events: auto;
padding: 1.75rem;
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.7);
box-shadow: 0 25px 60px rgba(0, 0, 0, 0.85);
animation: fadeIn 0.2s ease-out;
}
+21 -6
View File
@@ -368,7 +368,7 @@
<div style="display: flex; flex-direction: column; gap: 0.85rem; font-size: 0.875rem; color: var(--text-muted);">
<div>
<strong style="color: var(--accent-cyan);">⚡ GaC-hkrpg-go (Full Access):</strong><br>
โหมดเฉพาะของเซิร์ฟเวอร์เรา สามารถอ่านและเขียนทั้ง <code>freesr-data.json</code> และ <code>persistent</code> ได้แบบ 100% เรียลไทม์ผ่าน REST API Gateway พร้อมระบบ Ex-Rank และ Teleport
โหมดเฉพาะของเซิร์ฟเวอร์เรา สามารถอ่านและเขียนทั้ง <code>freesr-data.json</code> และ <code>persistent</code> ได้แบบ 100% เรียลไทม์ผ่าน REST API Gateway พร้อมระบบ Ex-Rank
</div>
<div>
<strong style="color: #f87171;">🔥 FireflyGo:</strong><br>
@@ -395,14 +395,17 @@
<button class="btn btn-sm btn-secondary" id="btn-close-avatar-modal">✕</button>
</div>
<div style="display: flex; gap: 1.25rem; align-items: center; margin-bottom: 1.25rem;">
<div style="display: flex; gap: 1.25rem; align-items: center; margin-bottom: 1.25rem; flex-wrap: wrap;">
<div class="avatar-image-container" style="width: 76px; height: 76px;">
<img id="modal-char-img" class="avatar-img" src="" alt="">
</div>
<div>
<div style="flex: 1; min-width: 180px;">
<div id="modal-char-name" style="font-size: 1.25rem; font-weight: 800;"></div>
<div id="modal-char-meta" style="font-size: 0.85rem; color: var(--text-muted); margin-top: 0.25rem;"></div>
<div id="modal-char-meta" style="font-size: 0.85rem; color: var(--text-muted); margin-top: 0.25rem; display: flex; gap: 0.35rem; align-items: center; flex-wrap: wrap;"></div>
</div>
<button type="button" class="btn btn-gold btn-sm" id="btn-modal-max-all-char">
⭐ MAX ALL (Lv.80 / E6 / 10)
</button>
</div>
<div class="grid-2" style="gap: 1rem; margin-bottom: 1rem;">
@@ -449,9 +452,21 @@
</div>
</div>
<div class="form-group" style="margin-top: 1rem;">
<label class="form-label">สกิลและร่องรอย (Skills & Traces)</label>
<div style="background: rgba(0,0,0,0.3); border: 1px solid var(--border-color); border-radius: var(--radius-sm); padding: 0.75rem; display: flex; justify-content: space-between; align-items: center;">
<div style="font-size: 0.85rem; color: var(--text-muted);">
Basic ATK (Lv.6), Skill (Lv.10), Ultimate (Lv.10), Talent (Lv.10) & ทุกโหนดเสริม
</div>
<button type="button" class="btn btn-sm btn-gold" id="btn-modal-max-skills">
⭐ อัพสกิล 10/10
</button>
</div>
</div>
<div style="display: flex; gap: 0.75rem; margin-top: 1.5rem; justify-content: flex-end; flex-wrap: wrap;">
<button type="button" class="btn btn-gold" id="btn-modal-max-skills">
⭐ อัพสกิล & Traces ทั้งหมด (10/10)
<button type="button" class="btn btn-secondary" id="btn-modal-cancel-avatar">
✕ ปิด
</button>
<button type="button" class="btn btn-success" id="btn-modal-save-avatar">
💾 บันทึกตัวละคร
+83 -36
View File
@@ -308,7 +308,7 @@ async function refreshStatus() {
avatar_count: Object.keys(freesr.data.avatars || {}).length,
lightcone_count: (freesr.data.lightcones || []).length,
relic_count: (freesr.data.relics || []).length,
lineups: freesr.data.lineups || DEFAULT_LINEUP,
lineups: freesr.data.lineups || freesr.data.battle_config?.custom_battle_lineup || DEFAULT_LINEUP,
scene: freesr.data.scene,
position: freesr.data.position,
ex_ranks: []
@@ -345,13 +345,21 @@ async function refreshStatus() {
el.statAvatars.textContent = state.avatarsList.length;
// Keep rendering with current or default lineup so UI is always functional
renderLineup(state.statusData?.lineups || DEFAULT_LINEUP);
loadAvatars();
}
}
// Render party lineup cards
function renderLineup(lineups) {
el.lineupContainer.innerHTML = '';
const currentLineup = lineups || state.statusData?.lineups || DEFAULT_LINEUP;
let currentLineup = lineups || state.statusData?.lineups || DEFAULT_LINEUP;
// Handle array format if provided
if (Array.isArray(currentLineup)) {
const mapped = {};
currentLineup.forEach((id, idx) => { mapped[idx] = id; });
currentLineup = mapped;
}
for (let slot = 0; slot < 4; slot++) {
const avatarId = currentLineup[slot] || currentLineup[String(slot)] || 0;
@@ -532,50 +540,39 @@ function renderAvatarsGrid() {
const card = document.createElement('div');
card.className = `card avatar-card card-rarity-${meta.rarity}`;
const pipsHtml = [0, 1, 2, 3, 4, 5, 6].map(rank => `
<button type="button" class="eidolon-pip ${item.rank >= rank ? 'active' : ''}" data-avatar="${item.avatar_id}" data-rank="${rank}" title="Eidolon ${rank}">
${rank}
</button>
`).join('');
card.innerHTML = `
<span class="avatar-badge-rarity rarity-${meta.rarity}">★ ${meta.rarity}</span>
<div>
<div class="avatar-header-info">
<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">
<span class="element-badge ${elemClass}">${meta.element}</span>
<span class="path-badge">${meta.path}</span>
<span style="font-weight: 700; color: var(--accent-cyan);">Lv. ${item.level}</span>
</div>
<div class="avatar-header-info">
<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 class="avatar-meta-details">
<div class="avatar-name" title="${meta.name}">${meta.name}</div>
<div class="avatar-subinfo">
<span class="element-badge ${elemClass}">${translateElement(meta.element)}</span>
<span class="path-badge">${translatePath(meta.path)}</span>
</div>
</div>
</div>
<div class="eidolon-selector">
<span style="font-size: 0.75rem; color: #cbd5e1; font-weight: 700;">Eidolon:</span>
<div class="eidolon-buttons">
${pipsHtml}
<div class="avatar-card-footer">
<div class="avatar-status-pill">
<span class="avatar-lvl">Lv. ${item.level}</span>
<span class="avatar-eidolon ${item.rank === 6 ? 'e6' : ''}">E${item.rank}</span>
</div>
<button type="button" class="btn btn-sm btn-gold btn-card-quick-max" title="ปรับ Max E6 ทันที">⭐ MAX</button>
</div>
`;
// Eidolon quick button clicks
card.querySelectorAll('.eidolon-pip').forEach(btn => {
btn.addEventListener('click', async (e) => {
e.stopPropagation();
const avatarId = parseInt(btn.getAttribute('data-avatar'), 10);
const rank = parseInt(btn.getAttribute('data-rank'), 10);
await setAvatarRank(avatarId, rank);
});
// Quick Max Button
const btnQuickMax = card.querySelector('.btn-card-quick-max');
btnQuickMax.addEventListener('click', async (e) => {
e.stopPropagation();
await maxSingleAvatar(item.avatar_id);
});
// Clicking card opens full character build modal
@@ -587,6 +584,43 @@ function renderAvatarsGrid() {
});
}
// Quick Max Single Avatar (Lv 80 + Prom 6 + E6 + Max Traces)
async function maxSingleAvatar(avatarId) {
const target = state.avatarsList.find(a => a.avatar_id === avatarId);
if (target) {
target.level = 80;
target.promotion = 6;
target.rank = 6;
}
if (state.fullFreesrData?.avatars?.[avatarId]) {
const av = state.fullFreesrData.avatars[avatarId];
av.level = 80;
av.promotion = 6;
av.data.rank = 6;
if (av.data.skills) {
for (const k in av.data.skills) av.data.skills[k] = 10;
}
}
renderAvatarsGrid();
if (state.isConnected) {
try {
if (state.serverMode === 'gac') {
await apiCall('/api/avatar/rank', 'POST', { avatar_id: avatarId, rank: 6 });
await apiCall('/api/freesr/avatars', 'POST', { avatar_id: avatarId, level: 80, promotion: 6, rank: 6 });
} else {
await saveFreesrChangesToServer();
}
showToast(`⭐ ปรับ ${getAvatarInfo(avatarId).name} เป็น Lv. 80 E6 เรียบร้อย`, 'success');
} catch (err) {
showToast(`⭐ ปรับ ${getAvatarInfo(avatarId).name} เป็น Lv. 80 E6 (บันทึกในเครื่อง)`, 'info');
}
} else {
showToast(`⭐ ปรับ ${getAvatarInfo(avatarId).name} เป็น Lv. 80 E6 (บันทึกในเครื่อง)`, 'info');
}
}
// Character Detail / Build Modal
function openAvatarDetailModal(avatarItem) {
state.selectedAvatarDetail = avatarItem;
@@ -1099,9 +1133,22 @@ function setupEvents() {
// Character Detail Modal Controls
el.btnCloseAvatarModal.addEventListener('click', () => el.modalAvatarDetail.classList.remove('active'));
document.getElementById('btn-modal-cancel-avatar')?.addEventListener('click', () => el.modalAvatarDetail.classList.remove('active'));
el.btnModalSaveAvatar.addEventListener('click', saveAvatarDetails);
el.btnModalMaxSkills.addEventListener('click', maxSkillsForAvatar);
document.getElementById('btn-modal-max-all-char')?.addEventListener('click', async () => {
if (!state.selectedAvatarDetail) return;
el.modalCharLevel.value = 80;
el.modalCharPromotion.value = 6;
state.selectedAvatarDetail.level = 80;
state.selectedAvatarDetail.promotion = 6;
state.selectedAvatarDetail.rank = 6;
renderModalEidolonPips(6);
await maxSkillsForAvatar();
await saveAvatarDetails();
});
// Quick Preset Buttons for Character Modal
document.querySelectorAll('.btn-lvl-preset').forEach(btn => {
btn.addEventListener('click', () => {
+1454 -641
View File
File diff suppressed because it is too large. Load diff
+4 -2
View File
@@ -1289,7 +1289,7 @@ export const TRANSLATIONS = {
}
};
let currentLang = localStorage.getItem('gac_srtool_lang') || 'th';
let currentLang = typeof localStorage !== 'undefined' ? (localStorage.getItem('gac_srtool_lang') || 'th') : 'th';
export function getCurrentLanguage() {
return currentLang;
@@ -1298,7 +1298,9 @@ export function getCurrentLanguage() {
export function setLanguage(langCode) {
if (TRANSLATIONS[langCode]) {
currentLang = langCode;
localStorage.setItem('gac_srtool_lang', langCode);
if (typeof localStorage !== 'undefined') {
localStorage.setItem('gac_srtool_lang', langCode);
}
}
}