fix(ui): adopt clean FireflySrTool/NeonTeam character card layout and accurate 99 avatars metadata
This commit is contained in:
1 parent
9ec9110905
commit
9b5b4087c9
5 files changed
+1659
-708
No files matched your search
+97
-23
@@ -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
@@ -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">
|
||||
💾 บันทึกตัวละคร
|
||||
|
||||
@@ -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
File diff suppressed because it is too large.
Load diff
+4
-2
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user