feat: complete SrTool overhaul with lineup builder, character build modal, equipment, and FireflyGo/RobinSR modes

This commit is contained in:
CAtlas0784 committed 2026-10-16 22:37:28 +07:00
1 parent 5cce9bdd04
commit e1667fa109
3 files changed
+1067 -254

No files matched your search

+500 -56
View File
@@ -1,20 +1,28 @@
import { AVATAR_METADATA, getAvatarInfo } from './avatars-data.js';
import { PRESET_TELEPORTS, getTeleportName } from './teleports-data.js';
import { SUPPORTED_LANGUAGES, getCurrentLanguage, setLanguage, t } from './i18n.js';
import { SUPPORTED_LANGUAGES, getCurrentLanguage, setLanguage, t, translateElement, translatePath } from './i18n.js';
// Application State
const state = {
serverMode: localStorage.getItem('gac_srtool_mode') || 'gac', // 'gac' | 'firefly' | 'robin'
apiUrl: localStorage.getItem('gac_srtool_api_url') || 'http://localhost:21000',
pingIntervalSec: parseInt(localStorage.getItem('gac_srtool_ping_interval') || '10', 10),
isConnected: false,
statusData: null,
avatarsList: [],
currentJsonMode: 'freesr', // 'freesr' or 'persistent'
activeTab: 'tab-dashboard'
fullFreesrData: null,
persistentData: null,
currentJsonMode: 'freesr',
activeTab: 'tab-dashboard',
selectedLineupSlot: null,
selectedAvatarDetail: null
};
// UI Elements
const el = {
serverModeSelect: document.getElementById('server-mode-select'),
settingServerMode: document.getElementById('setting-server-mode'),
lblActiveMode: document.getElementById('lbl-active-mode'),
langSelect: document.getElementById('lang-select'),
statusDot: document.getElementById('status-dot'),
connectionStatusText: document.getElementById('connection-status-text'),
@@ -43,9 +51,25 @@ const el = {
avatarsGrid: document.getElementById('avatars-grid'),
avatarSearch: document.getElementById('avatar-search'),
avatarFilterRarity: document.getElementById('avatar-filter-rarity'),
avatarFilterElement: document.getElementById('avatar-filter-element'),
btnMaxAllAvatars: document.getElementById('btn-max-all-avatars'),
btnReloadAvatars: document.getElementById('btn-reload-avatars'),
// Equipment
lightconesList: document.getElementById('lightcones-list'),
relicsList: document.getElementById('relics-list'),
badgeTotalLc: document.getElementById('badge-total-lc'),
badgeTotalRelics: document.getElementById('badge-total-relics'),
// Persistent Tab
tabBtnPersistent: document.getElementById('tab-btn-persistent'),
formPersistentChars: document.getElementById('form-persistent-chars'),
persistentMcSelect: document.getElementById('persistent-mc-select'),
persistentMarchSelect: document.getElementById('persistent-march-select'),
toggleSwGlobal: document.getElementById('toggle-sw-global'),
toggleCastoriceGlobal: document.getElementById('toggle-castorice-global'),
btnSavePersistentToggles: document.getElementById('btn-save-persistent-toggles'),
// Ex-Rank
exrankList: document.getElementById('exrank-list'),
btnExrankEnableAll: document.getElementById('btn-exrank-enable-all'),
@@ -77,6 +101,26 @@ const el = {
settingPingInterval: document.getElementById('setting-ping-interval'),
btnResetSettings: document.getElementById('btn-reset-settings'),
// Character Detail Modal
modalAvatarDetail: document.getElementById('modal-avatar-detail'),
btnCloseAvatarModal: document.getElementById('btn-close-avatar-modal'),
modalCharTitle: document.getElementById('modal-char-title'),
modalCharImg: document.getElementById('modal-char-img'),
modalCharName: document.getElementById('modal-char-name'),
modalCharMeta: document.getElementById('modal-char-meta'),
modalCharLevel: document.getElementById('modal-char-level'),
modalCharPromotion: document.getElementById('modal-char-promotion'),
modalCharEidolonBtns: document.getElementById('modal-char-eidolon-btns'),
btnModalMaxSkills: document.getElementById('btn-modal-max-skills'),
btnModalSaveAvatar: document.getElementById('btn-modal-save-avatar'),
// Lineup Picker Modal
modalLineupPicker: document.getElementById('modal-lineup-picker'),
btnClosePickerModal: document.getElementById('btn-close-picker-modal'),
modalPickerTitle: document.getElementById('modal-picker-title'),
pickerSearch: document.getElementById('picker-search'),
pickerAvatarsGrid: document.getElementById('picker-avatars-grid'),
toastContainer: document.getElementById('toast-container')
};
@@ -150,7 +194,21 @@ function setConnectionStatus(connected, extraText = '') {
}
}
// Apply multi-language translations across whole UI
// Helper to get element CSS class
function getElementClass(elem) {
const map = {
Fire: 'elem-fire',
Ice: 'elem-ice',
Lightning: 'elem-lightning',
Wind: 'elem-wind',
Physical: 'elem-physical',
Quantum: 'elem-quantum',
Imaginary: 'elem-imaginary'
};
return map[elem] || 'elem-physical';
}
// Apply multi-language translations across UI
function applyTranslations() {
document.querySelectorAll('[data-i18n]').forEach(elem => {
const key = elem.getAttribute('data-i18n');
@@ -162,7 +220,6 @@ function applyTranslations() {
elem.placeholder = t(key);
});
// Re-render localized UI parts
if (state.statusData) {
renderLineup(state.statusData.lineups);
renderExRanks(state.statusData.ex_ranks || []);
@@ -173,7 +230,7 @@ function applyTranslations() {
}
}
// Language selector initialization
// Setup Language Selector
function setupLanguageSelector() {
el.langSelect.innerHTML = '';
SUPPORTED_LANGUAGES.forEach(lang => {
@@ -190,14 +247,73 @@ function setupLanguageSelector() {
const newLang = e.target.value;
setLanguage(newLang);
applyTranslations();
showToast(`Language switched to ${SUPPORTED_LANGUAGES.find(l => l.code === newLang)?.name || newLang}`, 'info');
showToast(`Language: ${SUPPORTED_LANGUAGES.find(l => l.code === newLang)?.name || newLang}`, 'info');
});
}
// Refresh status from /api/status
// Setup Server Mode Selector
function setupServerModes() {
const updateModeDisplay = (mode) => {
state.serverMode = mode;
localStorage.setItem('gac_srtool_mode', mode);
el.serverModeSelect.value = mode;
el.settingServerMode.value = mode;
if (mode === 'gac') {
el.lblActiveMode.textContent = 'GaC-hkrpg-go (เซิร์ฟเวอร์หลัก / Full Control)';
el.lblActiveMode.style.color = 'var(--accent-cyan)';
el.tabBtnPersistent.style.display = 'inline-flex';
} else if (mode === 'firefly') {
el.lblActiveMode.textContent = 'FireflyGo (FreeSR Compatible)';
el.lblActiveMode.style.color = '#f87171';
el.tabBtnPersistent.style.display = 'none';
} else if (mode === 'robin') {
el.lblActiveMode.textContent = 'RobinSR (FreeSR Compatible)';
el.lblActiveMode.style.color = '#c084fc';
el.tabBtnPersistent.style.display = 'none';
}
};
el.serverModeSelect.addEventListener('change', (e) => {
updateModeDisplay(e.target.value);
showToast(`สลับโหมดเซิร์ฟเวอร์เป็น: ${el.lblActiveMode.textContent}`, 'info');
refreshStatus();
});
el.settingServerMode.addEventListener('change', (e) => {
updateModeDisplay(e.target.value);
showToast(`สลับโหมดเซิร์ฟเวอร์เป็น: ${el.lblActiveMode.textContent}`, 'info');
refreshStatus();
});
updateModeDisplay(state.serverMode);
}
// Refresh status from server
async function refreshStatus() {
try {
const data = await apiCall('/api/status');
let data;
if (state.serverMode === 'gac') {
data = await apiCall('/api/status');
} else {
// FireflyGo / RobinSR fallback check
const freesr = await apiCall('/api/freesr').catch(() => null);
if (freesr && freesr.data) {
data = {
status: 200,
avatar_count: Object.keys(freesr.data.avatars || {}).length,
lightcone_count: (freesr.data.lightcones || []).length,
relic_count: (freesr.data.relics || []).length,
lineups: freesr.data.lineups || { 0: 8001 },
scene: freesr.data.scene,
position: freesr.data.position,
ex_ranks: []
};
} else {
throw new Error('Server not reachable');
}
}
state.statusData = data;
setConnectionStatus(true);
@@ -223,45 +339,135 @@ async function refreshStatus() {
}
}
// Render current lineup cards
// Render party lineup cards
function renderLineup(lineups) {
if (!lineups || Object.keys(lineups).length === 0) {
el.lineupContainer.innerHTML = `<div style="color: var(--text-muted); grid-column: 1/-1;">${t('slotEmpty')}</div>`;
return;
}
el.lineupContainer.innerHTML = '';
for (let slot = 0; slot < 4; slot++) {
const avatarId = lineups[slot] || 0;
const card = document.createElement('div');
card.className = 'card';
card.style.padding = '0.85rem';
const avatarId = lineups ? (lineups[slot] || 0) : 0;
const slotCard = document.createElement('div');
slotCard.className = `lineup-slot-card ${avatarId ? 'filled' : ''}`;
slotCard.title = 'คลิกเพื่อเลือกตัวละครลงตำแหน่งนี้';
if (avatarId === 0) {
card.innerHTML = `
<div style="font-size: 0.75rem; color: var(--text-muted);">${t('slot')} ${slot + 1}</div>
<div style="font-weight: 600; color: var(--text-muted); margin-top: 0.25rem;">${t('slotEmpty')}</div>
slotCard.innerHTML = `
<div class="lineup-slot-num">Slot ${slot + 1}</div>
<div style="font-size: 2.2rem; color: var(--text-muted); opacity: 0.5;">➕</div>
<div style="font-weight: 700; color: var(--text-muted); margin-top: 0.5rem;">ว่าง (คลิกเพื่อเพิ่ม)</div>
`;
} else {
const info = getAvatarInfo(avatarId);
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-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>
const elemClass = getElementClass(info.element);
slotCard.innerHTML = `
<div class="lineup-slot-num">Slot ${slot + 1}</div>
<img class="lineup-slot-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';}"
alt="${info.name}">
<div style="font-weight: 800; font-size: 1rem; color: var(--text-highlight);">${info.name}</div>
<div style="display: flex; gap: 0.35rem; margin-top: 0.35rem;">
<span class="element-badge ${elemClass}">${info.element}</span>
<span class="path-badge">${info.path}</span>
</div>
`;
}
el.lineupContainer.appendChild(card);
slotCard.addEventListener('click', () => {
openLineupPicker(slot);
});
el.lineupContainer.appendChild(slotCard);
}
}
// Lineup Picker Modal
function openLineupPicker(slotIndex) {
state.selectedLineupSlot = slotIndex;
el.modalPickerTitle.textContent = `เลือกตัวละครเข้าสู่ Slot ${slotIndex + 1}`;
el.pickerSearch.value = '';
renderPickerGrid();
el.modalLineupPicker.classList.add('active');
}
function renderPickerGrid() {
const search = el.pickerSearch.value.trim().toLowerCase();
el.pickerAvatarsGrid.innerHTML = '';
// Add Remove button
const removeCard = document.createElement('div');
removeCard.className = 'card';
removeCard.style.padding = '0.75rem';
removeCard.style.textAlign = 'center';
removeCard.style.cursor = 'pointer';
removeCard.innerHTML = `
<div style="font-size: 1.5rem; color: var(--accent-red);">❌</div>
<div style="font-size: 0.85rem; font-weight: bold; margin-top: 0.35rem; color: var(--accent-red);">นำออกจากทีม</div>
`;
removeCard.addEventListener('click', async () => {
await assignAvatarToLineup(state.selectedLineupSlot, 0);
el.modalLineupPicker.classList.remove('active');
});
el.pickerAvatarsGrid.appendChild(removeCard);
// Available avatars
state.avatarsList.forEach(item => {
const meta = getAvatarInfo(item.avatar_id);
if (search && !meta.name.toLowerCase().includes(search) && !String(item.avatar_id).includes(search)) {
return;
}
const elemClass = getElementClass(meta.element);
const card = document.createElement('div');
card.className = `card avatar-card card-rarity-${meta.rarity}`;
card.style.minHeight = 'auto';
card.style.padding = '0.85rem';
card.style.cursor = 'pointer';
card.innerHTML = `
<div style="display: flex; align-items: center; gap: 0.65rem;">
<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';}"
alt="${meta.name}">
</div>
<div>
<div style="font-weight: 700; font-size: 0.9rem;">${meta.name}</div>
<div style="font-size: 0.725rem; color: var(--text-muted);">Lv. ${item.level} • E${item.rank}</div>
</div>
</div>
`;
card.addEventListener('click', async () => {
await assignAvatarToLineup(state.selectedLineupSlot, item.avatar_id);
el.modalLineupPicker.classList.remove('active');
});
el.pickerAvatarsGrid.appendChild(card);
});
}
// Assign avatar to lineup slot
async function assignAvatarToLineup(slot, avatarId) {
try {
if (!state.statusData || !state.statusData.lineups) return;
const lineups = { ...state.statusData.lineups };
lineups[slot] = avatarId;
state.statusData.lineups = lineups;
if (state.serverMode === 'gac') {
await apiCall('/api/persistent', 'POST', { lineups });
} else {
// In FreeSR standard mode
const freesr = await apiCall('/api/freesr');
if (freesr && freesr.data) {
freesr.data.lineups = lineups;
await apiCall('/api/freesr', 'POST', freesr.data);
}
}
showToast(`อัปเดตทีม Slot ${slot + 1} เรียบร้อย`, 'success');
renderLineup(lineups);
} catch (err) {
showToast(`เกิดข้อผิดพลาด: ${err.message}`, 'error');
}
}
@@ -280,24 +486,27 @@ async function loadAvatars() {
function renderAvatarsGrid() {
const searchTerm = el.avatarSearch.value.trim().toLowerCase();
const rarityFilter = el.avatarFilterRarity.value;
const elementFilter = el.avatarFilterElement.value;
const filtered = state.avatarsList.filter(item => {
const meta = getAvatarInfo(item.avatar_id);
const matchesSearch = meta.name.toLowerCase().includes(searchTerm) || String(item.avatar_id).includes(searchTerm);
const matchesRarity = rarityFilter === 'all' || String(meta.rarity) === rarityFilter;
return matchesSearch && matchesRarity;
const matchesElement = elementFilter === 'all' || meta.element === elementFilter;
return matchesSearch && matchesRarity && matchesElement;
});
if (filtered.length === 0) {
el.avatarsGrid.innerHTML = '<div style="grid-column: 1/-1; text-align: center; color: var(--text-muted); padding: 2rem;">No matching avatars</div>';
el.avatarsGrid.innerHTML = '<div style="grid-column: 1/-1; text-align: center; color: var(--text-muted); padding: 2rem;">ไม่พบตัวละครที่ตรงกับเงื่อนไข</div>';
return;
}
el.avatarsGrid.innerHTML = '';
filtered.forEach(item => {
const meta = getAvatarInfo(item.avatar_id);
const elemClass = getElementClass(meta.element);
const card = document.createElement('div');
card.className = 'card avatar-card';
card.className = `card avatar-card card-rarity-${meta.rarity}`;
const pipsHtml = [0, 1, 2, 3, 4, 5, 6].map(rank => `
<button class="eidolon-pip ${item.rank >= rank ? 'active' : ''}" data-avatar="${item.avatar_id}" data-rank="${rank}" title="Eidolon ${rank}">
@@ -319,18 +528,16 @@ function renderAvatarsGrid() {
<div>
<div class="avatar-name">${meta.name}</div>
<div class="avatar-subinfo">
<span>Lv. ${item.level}</span>
<span>•</span>
<span>${meta.element}</span>
<span>•</span>
<span>${meta.path}</span>
<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>
</div>
</div>
<div class="eidolon-selector">
<span style="font-size: 0.75rem; color: var(--text-muted); font-weight: 600;">${t('eidolonLabel')}</span>
<span style="font-size: 0.75rem; color: var(--text-muted); font-weight: 700;">${t('eidolonLabel')}</span>
<div class="eidolon-buttons">
${pipsHtml}
</div>
@@ -347,17 +554,121 @@ function renderAvatarsGrid() {
});
});
// Clicking card opens full character build modal!
card.addEventListener('click', () => {
openAvatarDetailModal(item);
});
el.avatarsGrid.appendChild(card);
});
}
// Character Detail / Build Modal
function openAvatarDetailModal(avatarItem) {
state.selectedAvatarDetail = avatarItem;
const meta = getAvatarInfo(avatarItem.avatar_id);
const elemClass = getElementClass(meta.element);
el.modalCharTitle.textContent = `ปรับแต่งตัวละคร: ${meta.name} (ID: ${avatarItem.avatar_id})`;
el.modalCharImg.src = `./assets/avatars/${avatarItem.avatar_id}.png`;
el.modalCharImg.onerror = function() {
this.src = `https://cdn.jsdelivr.net/gh/Mar-7th/StarRailRes@master/icon/avatar/${avatarItem.avatar_id}.png`;
};
el.modalCharName.textContent = meta.name;
el.modalCharMeta.innerHTML = `
<span class="element-badge ${elemClass}">${meta.element}</span>
<span class="path-badge">${meta.path}</span>
<span class="avatar-badge-rarity rarity-${meta.rarity}">★ ${meta.rarity}</span>
`;
el.modalCharLevel.value = avatarItem.level || 80;
el.modalCharPromotion.value = avatarItem.promotion || 6;
// Render modal eidolon pips
el.modalCharEidolonBtns.innerHTML = '';
for (let r = 0; r <= 6; r++) {
const btn = document.createElement('button');
btn.className = `eidolon-pip ${avatarItem.rank >= r ? 'active' : ''}`;
btn.textContent = r;
btn.style.width = '32px';
btn.style.height = '32px';
btn.style.fontSize = '0.9rem';
btn.addEventListener('click', () => {
avatarItem.rank = r;
el.modalCharEidolonBtns.querySelectorAll('.eidolon-pip').forEach((b, idx) => {
if (idx <= r) b.classList.add('active');
else b.classList.remove('active');
});
});
el.modalCharEidolonBtns.appendChild(btn);
}
el.modalAvatarDetail.classList.add('active');
}
// Save avatar details from modal
async function saveAvatarDetails() {
if (!state.selectedAvatarDetail) return;
const aid = state.selectedAvatarDetail.avatar_id;
const level = parseInt(el.modalCharLevel.value, 10) || 80;
const promotion = parseInt(el.modalCharPromotion.value, 10) || 6;
const rank = state.selectedAvatarDetail.rank ?? 6;
try {
await apiCall('/api/freesr/avatars', 'POST', {
avatar_id: aid,
level,
promotion,
rank
});
showToast(`บันทึกตัวละคร ${aid} สำเร็จ (Lv. ${level}, Prom. ${promotion}, E${rank})`, 'success');
el.modalAvatarDetail.classList.remove('active');
// Update local state and re-render
const target = state.avatarsList.find(a => a.avatar_id === aid);
if (target) {
target.level = level;
target.promotion = promotion;
target.rank = rank;
renderAvatarsGrid();
}
} catch (err) {
showToast(`เกิดข้อผิดพลาด: ${err.message}`, 'error');
}
}
// Max Skills & Traces for character
async function maxSkillsForAvatar() {
if (!state.selectedAvatarDetail) return;
const aid = state.selectedAvatarDetail.avatar_id;
try {
// Read freesr data, max skill levels for this avatar, and push back
const freesr = await apiCall('/api/freesr');
if (freesr && freesr.data && freesr.data.avatars && freesr.data.avatars[aid]) {
const av = freesr.data.avatars[aid];
if (av.data && av.data.skills) {
for (const k in av.data.skills) {
av.data.skills[k] = 10;
}
}
av.level = 80;
av.promotion = 6;
av.data.rank = 6;
await apiCall('/api/freesr', 'POST', freesr.data);
showToast(`อัพสกิลและ Traces ทั้งหมดของตัวละคร ${aid} เป็น 10/10 สำเร็จ!`, 'success');
loadAvatars();
}
} catch (err) {
showToast(`เกิดข้อผิดพลาด: ${err.message}`, 'error');
}
}
// Set avatar rank API
async function setAvatarRank(avatarId, rank) {
try {
await apiCall('/api/avatar/rank', 'POST', { avatar_id: avatarId, rank });
showToast(t('avatarRankSuccess', { id: avatarId, rank }), 'success');
// Update local state and re-render
const target = state.avatarsList.find(a => a.avatar_id === avatarId);
if (target) {
target.rank = rank;
@@ -381,10 +692,94 @@ async function maxAllAvatars() {
}
}
// Load Equipment (Lightcones & Relics)
async function loadEquipment() {
try {
const res = await apiCall('/api/freesr');
if (!res || !res.data) return;
state.fullFreesrData = res.data;
const lcs = res.data.lightcones || [];
const relics = res.data.relics || [];
el.badgeTotalLc.textContent = `${lcs.length} ชิ้น`;
el.badgeTotalRelics.textContent = `${relics.length} ชิ้น`;
// Render Lightcones
el.lightconesList.innerHTML = '';
lcs.slice(0, 100).forEach(lc => {
const item = document.createElement('div');
item.className = 'card';
item.style.padding = '0.75rem';
item.style.display = 'flex';
item.style.justifyContent = 'space-between';
item.style.alignItems = 'center';
const equipped = lc.equip_avatar ? getAvatarInfo(lc.equip_avatar).name : 'ไม่ได้สวมใส่';
item.innerHTML = `
<div>
<div style="font-weight: 700; font-size: 0.95rem;">Lightcone ID: ${lc.item_id} (UID: ${lc.internal_uid})</div>
<div style="font-size: 0.775rem; color: var(--text-muted); margin-top: 0.2rem;">
Lv. ${lc.level} • ขัดเกลา S${lc.rank} • สวมใส่โดย: <strong style="color: var(--accent-cyan);">${equipped}</strong>
</div>
</div>
`;
el.lightconesList.appendChild(item);
});
// Render Relics
el.relicsList.innerHTML = '';
relics.slice(0, 100).forEach(r => {
const item = document.createElement('div');
item.className = 'card';
item.style.padding = '0.75rem';
item.style.display = 'flex';
item.style.justifyContent = 'space-between';
item.style.alignItems = 'center';
const equipped = r.equip_avatar ? getAvatarInfo(r.equip_avatar).name : 'ไม่ได้สวมใส่';
const slotNum = r.relic_id % 10;
item.innerHTML = `
<div>
<div style="font-weight: 700; font-size: 0.95rem;">Relic ID: ${r.relic_id} (Slot: ${slotNum})</div>
<div style="font-size: 0.775rem; color: var(--text-muted); margin-top: 0.2rem;">
Lv. ${r.level} • สเตตัสหลัก: ${r.main_affix_id} • สวมใส่โดย: <strong style="color: var(--accent-gold);">${equipped}</strong>
</div>
</div>
`;
el.relicsList.appendChild(item);
});
} catch (err) {
showToast(`โหลดคลังอุปกรณ์ไม่สำเร็จ: ${err.message}`, 'error');
}
}
// Load Persistent Save Settings
async function loadPersistentSettings() {
try {
const res = await apiCall('/api/persistent');
if (!res || !res.persistent) return;
const p = res.persistent;
state.persistentData = p;
if (p.main_character) {
el.persistentMcSelect.value = p.main_character;
}
if (p.march_type) {
el.persistentMarchSelect.value = p.march_type;
}
el.toggleSwGlobal.checked = !!p.enable_sw_global;
el.toggleCastoriceGlobal.checked = !!p.enable_castorice_global;
} catch (err) {
console.warn('Persistent settings not available in current mode:', err.message);
}
}
// Render Ex-Ranks
function renderExRanks(exList) {
if (!exList || exList.length === 0) {
el.exrankList.innerHTML = '<div style="color: var(--text-muted); text-align: center; padding: 1rem;">No Ex-Rank Avatars</div>';
el.exrankList.innerHTML = '<div style="color: var(--text-muted); text-align: center; padding: 1rem;">ไม่มีข้อมูลตัวละคร Ex-Rank</div>';
return;
}
@@ -396,7 +791,7 @@ function renderExRanks(exList) {
card.innerHTML = `
<div style="display: flex; gap: 0.85rem; align-items: center;">
<div class="avatar-image-container" style="width: 44px; height: 44px;">
<div class="avatar-image-container" style="width: 52px; height: 52px;">
<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';}"
@@ -404,7 +799,7 @@ function renderExRanks(exList) {
<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-weight: 800; font-size: 1.05rem;">${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>
@@ -514,7 +909,7 @@ async function loadJsonData(mode) {
async function saveJsonData() {
const text = el.jsonEditor.value.trim();
if (!text) {
showToast('No JSON data to save', 'error');
showToast('ไม่มีข้อมูล JSON ให้บันทึก', 'error');
return;
}
@@ -554,6 +949,10 @@ function setupTabs() {
// On-demand tab refresh
if (targetId === 'tab-avatars' && state.avatarsList.length === 0) {
loadAvatars();
} else if (targetId === 'tab-equipment') {
loadEquipment();
} else if (targetId === 'tab-persistent') {
loadPersistentSettings();
} else if (targetId === 'tab-json') {
loadJsonData(state.currentJsonMode);
}
@@ -590,6 +989,15 @@ function setupEvents() {
}
});
// Character Detail Modal Controls
el.btnCloseAvatarModal.addEventListener('click', () => el.modalAvatarDetail.classList.remove('active'));
el.btnModalSaveAvatar.addEventListener('click', saveAvatarDetails);
el.btnModalMaxSkills.addEventListener('click', maxSkillsForAvatar);
// Lineup Picker Modal Controls
el.btnClosePickerModal.addEventListener('click', () => el.modalLineupPicker.classList.remove('active'));
el.pickerSearch.addEventListener('input', renderPickerGrid);
// Quick Action Buttons
el.btnQuickMaxAll.addEventListener('click', maxAllAvatars);
el.btnMaxAllAvatars.addEventListener('click', maxAllAvatars);
@@ -602,16 +1010,50 @@ function setupEvents() {
el.btnQuickSyncPersistent.addEventListener('click', async () => {
try {
await apiCall('/api/persistent');
showToast('Persistent data synced', 'success');
showToast('ซิงค์ข้อมูล Persistent เรียบร้อย', 'success');
refreshStatus();
} catch (err) {
showToast(`Sync failed: ${err.message}`, 'error');
}
});
// Avatar Search & Filter
// Persistent Form Submit
el.formPersistentChars.addEventListener('submit', async (e) => {
e.preventDefault();
const mc = el.persistentMcSelect.value;
const march = el.persistentMarchSelect.value;
try {
await apiCall('/api/persistent', 'POST', {
main_character: mc,
march_type: march
});
showToast('บันทึกการตั้งค่าตัวละครสำเร็จ!', 'success');
refreshStatus();
} catch (err) {
showToast(`เกิดข้อผิดพลาด: ${err.message}`, 'error');
}
});
// Persistent Toggles
el.btnSavePersistentToggles.addEventListener('click', async () => {
const sw = el.toggleSwGlobal.checked;
const castorice = el.toggleCastoriceGlobal.checked;
try {
await apiCall('/api/persistent', 'POST', {
enable_sw_global: sw,
enable_castorice_global: castorice
});
showToast('บันทึกสวิตช์ Global เรียบร้อย!', 'success');
refreshStatus();
} catch (err) {
showToast(`เกิดข้อผิดพลาด: ${err.message}`, 'error');
}
});
// Avatar Search & Filters
el.avatarSearch.addEventListener('input', renderAvatarsGrid);
el.avatarFilterRarity.addEventListener('change', renderAvatarsGrid);
el.avatarFilterElement.addEventListener('change', renderAvatarsGrid);
el.btnReloadAvatars.addEventListener('click', loadAvatars);
// Ex-Rank All Controls
@@ -657,7 +1099,7 @@ function setupEvents() {
try {
const parsed = JSON.parse(el.jsonEditor.value);
el.jsonEditor.value = JSON.stringify(parsed, null, 2);
showToast('JSON beautified', 'info');
showToast('จัดรูปแบบ JSON สวยงามแล้ว', 'info');
} catch (err) {
showToast(t('jsonInvalid'), 'error');
}
@@ -672,7 +1114,7 @@ function setupEvents() {
a.href = URL.createObjectURL(blob);
a.download = `${state.currentJsonMode}-${Date.now()}.json`;
a.click();
showToast(`Downloaded ${a.download}`, 'success');
showToast(`ดาวน์โหลด ${a.download} เรียบร้อย`, 'success');
});
el.jsonFileInput.addEventListener('change', (e) => {
@@ -683,7 +1125,7 @@ function setupEvents() {
try {
const parsed = JSON.parse(event.target.result);
el.jsonEditor.value = JSON.stringify(parsed, null, 2);
showToast(`Loaded ${file.name}`, 'success');
showToast(`โหลดเนื้อหาไฟล์ ${file.name} เรียบร้อย`, 'success');
} catch (err) {
showToast(t('jsonInvalid'), 'error');
}
@@ -728,11 +1170,13 @@ function startPingLoop() {
// Initialize Application
function init() {
setupServerModes();
setupLanguageSelector();
setupTabs();
setupEvents();
applyTranslations();
refreshStatus();
loadAvatars();
startPingLoop();
}