feat: replace Add Lightcone text dropdown with visual card grid and live search

This commit is contained in:
CAtlas0784 committed 2026-10-17 03:30:30 +07:00
1 parent 030d02f7a1
commit cbe9740918
3 files changed
+334 -53

No files matched your search

+93
View File
@@ -2209,6 +2209,99 @@ input:checked + .slider:before {
border: 1px solid var(--border-color);
}
/* Add Lightcone Visual Grid & Cards */
.add-lc-card {
background: rgba(18, 24, 38, 0.75);
border: 1.5px solid var(--border-color);
border-radius: var(--radius-sm);
padding: 0.5rem 0.6rem;
display: flex;
gap: 0.6rem;
align-items: center;
cursor: pointer;
transition: all 0.15s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
user-select: none;
}
.add-lc-card:hover {
background: rgba(28, 38, 60, 0.95);
border-color: rgba(56, 189, 248, 0.6);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
}
.add-lc-card.selected {
border-color: var(--accent-gold) !important;
background: rgba(251, 191, 36, 0.14) !important;
box-shadow: 0 0 12px rgba(251, 191, 36, 0.45);
}
.add-lc-card.rarity-5 {
border-left: 3.5px solid #eab308;
}
.add-lc-card.rarity-4 {
border-left: 3.5px solid #a855f7;
}
.add-lc-card.rarity-3 {
border-left: 3.5px solid #38bdf8;
}
.add-lc-card-thumb {
width: 42px;
height: 58px;
border-radius: 4px;
object-fit: cover;
flex-shrink: 0;
border: 1px solid rgba(255, 255, 255, 0.15);
background: #0b0d14;
}
.add-lc-card-body {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 0.15rem;
}
.add-lc-card-name {
font-weight: 700;
font-size: 0.825rem;
color: #fff;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.add-lc-card-stars {
font-size: 0.68rem;
color: var(--accent-gold);
line-height: 1;
}
.add-lc-card-sub {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.68rem;
color: var(--text-muted);
}
.add-lc-card-path {
display: inline-flex;
align-items: center;
gap: 0.25rem;
color: var(--accent-cyan);
}
.add-lc-card-id {
font-family: monospace;
opacity: 0.7;
}
/* Rich Skills & Traces */
.skill-row-icon {
width: 38px;
+73 -21
View File
@@ -1141,40 +1141,90 @@
<!-- Add Light Cone Modal -->
<div class="modal-overlay" id="modal-add-lc">
<div class="modal-content" style="max-width: 540px;">
<div class="modal-header">
<h3 style="font-weight: 800;" data-i18n="modalAddLcTitle">🗡️ เพิ่มกรวยแสงลง FreeSR</h3>
<div class="modal-content" style="max-width: 760px; max-height: 88vh; display: flex; flex-direction: column;">
<div class="modal-header" style="padding-bottom: 0.75rem; border-bottom: 1px solid var(--border-color); margin-bottom: 0.75rem;">
<h3 style="font-weight: 800; margin: 0; display: flex; align-items: center; gap: 0.5rem;" data-i18n="modalAddLcTitle">
🗡️ เพิ่มกรวยแสงลง FreeSR
</h3>
<button type="button" class="btn btn-sm btn-secondary" id="btn-close-add-lc-modal">✕</button>
</div>
<form id="form-add-lc">
<div class="form-group">
<label class="form-label" data-i18n="lblSelectLc">เลือกกรวยแสง</label>
<select id="add-lc-select" class="form-select" required style="max-height: 200px;">
<!-- Injected by JS -->
<form id="form-add-lc" style="display: flex; flex-direction: column; flex: 1; min-height: 0;">
<!-- 1. Selected Lightcone Preview Strip -->
<div id="add-lc-preview-banner" style="display: flex; gap: 0.85rem; align-items: center; background: rgba(251, 191, 36, 0.08); border: 1.5px solid var(--accent-gold); border-radius: var(--radius-sm); padding: 0.6rem 0.85rem; margin-bottom: 0.75rem;">
<img id="add-lc-preview-thumb" src="" alt="" style="width: 44px; height: 60px; border-radius: 4px; object-fit: cover; border: 1px solid var(--accent-gold); background: #0b0d14; flex-shrink: 0;">
<div style="flex: 1; min-width: 0;">
<div style="font-size: 0.7rem; color: var(--accent-gold); font-weight: 800; text-transform: uppercase;">
✓ กรวยแสงที่เลือก (Selected)
</div>
<div id="add-lc-preview-name" style="font-weight: 800; font-size: 0.95rem; color: #fff; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">-</div>
<div id="add-lc-preview-meta" style="display: flex; gap: 0.65rem; font-size: 0.72rem; color: var(--text-muted); margin-top: 0.15rem; align-items: center; flex-wrap: wrap;">
<!-- Populated by JS -->
</div>
</div>
<input type="hidden" id="add-lc-selected-id" value="23001">
<input type="hidden" id="add-lc-select" value="23001">
</div>
<!-- 2. Search & Filter Bar -->
<div style="display: flex; gap: 0.5rem; margin-bottom: 0.6rem; flex-wrap: wrap;">
<input type="text" id="add-lc-search" class="form-input" placeholder="🔍 ค้นหากรวยแสง (ชื่อหรือ ID)..." style="flex: 1; min-width: 160px;">
<select id="add-lc-filter-path" class="form-select" style="max-width: 140px;">
<option value="all">ทุกสาย (All)</option>
<option value="Destruction">ทำลายล้าง</option>
<option value="Hunt">ล่าสังหาร</option>
<option value="Erudition">ปัญญา</option>
<option value="Harmony">ประสาน</option>
<option value="Nihility">ลบล้าง</option>
<option value="Preservation">อนุรักษ์</option>
<option value="Abundance">เฟื่องฟู</option>
<option value="Remembrance">ความทรงจำ</option>
<option value="Elation">ปีติ</option>
</select>
<select id="add-lc-filter-rarity" class="form-select" style="max-width: 110px;">
<option value="all">ทุกระดับดาว</option>
<option value="5" selected>★5 ดาว</option>
<option value="4">★4 ดาว</option>
<option value="3">★3 ดาว</option>
</select>
</div>
<div class="grid-3" style="gap: 0.75rem; margin-bottom: 1rem;">
<!-- 3. Visual Grid Container -->
<div id="add-lc-visual-grid" class="add-lc-grid-container" style="flex: 1; min-height: 180px; max-height: 280px; overflow-y: auto; display: grid; grid-template-columns: repeat(auto-fill, minmax(210px, 1fr)); gap: 0.5rem; padding: 0.35rem; background: rgba(0, 0, 0, 0.25); border: 1px solid var(--border-color); border-radius: var(--radius-sm); margin-bottom: 0.75rem;">
<!-- Dynamically populated by JS -->
</div>
<!-- 4. Level, Promotion, Superimpose, and Equip Avatar -->
<div class="grid-4" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: 0.6rem; margin-bottom: 0.75rem; align-items: end;">
<div class="form-group" style="margin-bottom: 0;">
<label class="form-label" data-i18n="modalColLevel">เลเวล (1-80)</label>
<label class="form-label" data-i18n="modalColLevel" style="font-size: 0.75rem;">เลเวล (1-80)</label>
<input type="number" id="add-lc-level" class="form-input" min="1" max="80" value="80" required>
</div>
<div class="form-group" style="margin-bottom: 0;">
<label class="form-label" data-i18n="lblPromotion">ขัดเกลา (0-6)</label>
<label class="form-label" data-i18n="lblPromotion" style="font-size: 0.75rem;">เลื่อนขั้น (0-6)</label>
<input type="number" id="add-lc-promotion" class="form-input" min="0" max="6" value="6" required>
</div>
<div class="form-group" style="margin-bottom: 0;">
<label class="form-label">Superimpose (1-5)</label>
<input type="number" id="add-lc-rank" class="form-input" min="1" max="5" value="5" required>
<label class="form-label" style="font-size: 0.75rem;">Superimpose (ขัดเกลา)</label>
<select id="add-lc-rank" class="form-select" required>
<option value="5" selected>S5 (ขัดเกลา 5)</option>
<option value="4">S4 (ขัดเกลา 4)</option>
<option value="3">S3 (ขัดเกลา 3)</option>
<option value="2">S2 (ขัดเกลา 2)</option>
<option value="1">S1 (ขัดเกลา 1)</option>
</select>
</div>
<div class="form-group" style="margin-bottom: 0;">
<label class="form-label" data-i18n="lblEquipAvatar" style="font-size: 0.75rem;">สวมใส่ให้ตัวละคร (ถ้าต้องการ)</label>
<select id="add-lc-avatar" class="form-select">
<option value="0" data-i18n="unassigned">-- ยังไม่สวมใส่ (เก็บในคลัง) --</option>
<!-- Injected by JS -->
</select>
</div>
</div>
<div class="form-group">
<label class="form-label" data-i18n="lblEquipAvatar">สวมใส่ให้ตัวละคร (ถ้าต้องการ)</label>
<select id="add-lc-avatar" class="form-select">
<option value="0" data-i18n="unassigned">-- ยังไม่สวมใส่ (เก็บในคลัง) --</option>
<!-- Injected by JS -->
</select>
</div>
<div style="display: flex; justify-content: flex-end; gap: 0.5rem; margin-top: 1.25rem;">
<!-- 5. Footer Buttons -->
<div style="display: flex; justify-content: flex-end; gap: 0.5rem; padding-top: 0.6rem; border-top: 1px solid rgba(255, 255, 255, 0.08);">
<button type="button" class="btn btn-secondary" id="btn-cancel-add-lc" data-i18n="btnCancel">ยกเลิก</button>
<button type="submit" class="btn btn-success" data-i18n="btnAddLcSubmit">💾 เพิ่มลง FreeSR</button>
</div>
@@ -1232,6 +1282,8 @@
</div>
</form>
</div>
</div>
<!-- Modal: Select Relic for Specific Slot -->
<div class="modal-overlay" id="modal-select-relic-slot">
<div class="modal-content" style="max-width: 620px;">
+168 -32
View File
@@ -86,6 +86,14 @@ const el = {
btnCancelAddLc: document.getElementById('btn-cancel-add-lc'),
formAddLc: document.getElementById('form-add-lc'),
addLcSelect: document.getElementById('add-lc-select'),
addLcSearch: document.getElementById('add-lc-search'),
addLcFilterPath: document.getElementById('add-lc-filter-path'),
addLcFilterRarity: document.getElementById('add-lc-filter-rarity'),
addLcVisualGrid: document.getElementById('add-lc-visual-grid'),
addLcSelectedId: document.getElementById('add-lc-selected-id'),
addLcPreviewThumb: document.getElementById('add-lc-preview-thumb'),
addLcPreviewName: document.getElementById('add-lc-preview-name'),
addLcPreviewMeta: document.getElementById('add-lc-preview-meta'),
addLcLevel: document.getElementById('add-lc-level'),
addLcPromotion: document.getElementById('add-lc-promotion'),
addLcRank: document.getElementById('add-lc-rank'),
@@ -4124,33 +4132,152 @@ function showRelicInspector(relic) {
// Open Add Light Cone Modal
function openAddLcModal() {
if (!el.modalAddLc) return;
el.addLcSelect.innerHTML = '';
const lcIds = Object.keys(LIGHTCONE_METADATA).sort((a, b) => parseInt(b, 10) - parseInt(a, 10));
lcIds.forEach(id => {
const meta = getLightconeInfo(id);
const opt = document.createElement('option');
opt.value = id;
opt.textContent = `★${meta.rarity} [${meta.path || 'LC'}] ${meta.name} (ID: ${id})`;
el.addLcSelect.appendChild(opt);
});
el.addLcAvatar.innerHTML = `<option value="0">${t('unassigned')}</option>`;
const sortedAvatars = [...state.avatarsList].sort((a, b) => b.avatar_id - a.avatar_id);
sortedAvatars.forEach(av => {
const info = getAvatarInfo(av.avatar_id);
const opt = document.createElement('option');
opt.value = av.avatar_id;
opt.textContent = `${info.name} (ID: ${av.avatar_id})`;
el.addLcAvatar.appendChild(opt);
});
// Reset filters
const searchInput = document.getElementById('add-lc-search');
if (searchInput) searchInput.value = '';
const filterPath = document.getElementById('add-lc-filter-path');
if (filterPath) filterPath.value = 'all';
const filterRarity = document.getElementById('add-lc-filter-rarity');
if (filterRarity) filterRarity.value = 'all';
el.addLcLevel.value = 80;
el.addLcPromotion.value = 6;
el.addLcRank.value = 5;
// Avatars dropdown
if (el.addLcAvatar) {
el.addLcAvatar.innerHTML = `<option value="0">${t('unassigned') || '-- ยังไม่สวมใส่ (เก็บในคลัง) --'}</option>`;
const sortedAvatars = [...state.avatarsList].sort((a, b) => b.avatar_id - a.avatar_id);
sortedAvatars.forEach(av => {
const info = getAvatarInfo(av.avatar_id);
const opt = document.createElement('option');
opt.value = av.avatar_id;
opt.textContent = `${info.name} (ID: ${av.avatar_id})`;
el.addLcAvatar.appendChild(opt);
});
}
if (el.addLcLevel) el.addLcLevel.value = 80;
if (el.addLcPromotion) el.addLcPromotion.value = 6;
if (el.addLcRank) el.addLcRank.value = 5;
const defaultId = LIGHTCONE_METADATA[23065] ? 23065 : 23001;
updateAddLcPreview(defaultId);
renderAddLcVisualGrid();
el.modalAddLc.classList.add('active');
}
// Update Add LC Selected Preview Banner
function updateAddLcPreview(lcId) {
const id = parseInt(lcId, 10);
const info = getLightconeInfo(id);
const meta = LIGHTCONE_METADATA[id] || { path: 'Unknown', rarity: 5 };
const hiddenId = document.getElementById('add-lc-selected-id');
if (hiddenId) hiddenId.value = id;
const legacySelect = document.getElementById('add-lc-select');
if (legacySelect) legacySelect.value = id;
const previewThumb = document.getElementById('add-lc-preview-thumb');
if (previewThumb) {
previewThumb.src = getLightconeArtSrc(id);
previewThumb.onerror = function() { this.src = getLightconeArtFallback(id); };
}
const previewName = document.getElementById('add-lc-preview-name');
if (previewName) {
previewName.textContent = info.name || `Light Cone ${id}`;
}
const previewMeta = document.getElementById('add-lc-preview-meta');
if (previewMeta) {
previewMeta.innerHTML = `
<span style="color: var(--accent-gold); font-weight: 800;">${'★'.repeat(info.rarity || 5)}</span>
<span style="color: var(--accent-cyan); display: inline-flex; align-items: center; gap: 0.25rem;">
${getPathIconHtml(meta.path, 13)} ${info.path}
</span>
<span style="font-family: monospace; opacity: 0.8;">ID: ${id}</span>
`;
}
}
// Render Add LC Visual Grid with Live Search & Filtering
function renderAddLcVisualGrid() {
const grid = document.getElementById('add-lc-visual-grid');
if (!grid) return;
grid.innerHTML = '';
const search = (document.getElementById('add-lc-search')?.value || '').trim().toLowerCase();
const pathFilter = document.getElementById('add-lc-filter-path')?.value || 'all';
const rarityFilter = document.getElementById('add-lc-filter-rarity')?.value || 'all';
const selectedIdInput = document.getElementById('add-lc-selected-id');
const selectedId = parseInt(selectedIdInput?.value || '23001', 10);
const allIds = Object.keys(LIGHTCONE_METADATA)
.map(id => parseInt(id, 10))
.sort((a, b) => {
const metaA = LIGHTCONE_METADATA[a];
const metaB = LIGHTCONE_METADATA[b];
if (metaB.rarity !== metaA.rarity) return metaB.rarity - metaA.rarity;
return b - a;
});
const filtered = allIds.filter(id => {
const meta = LIGHTCONE_METADATA[id];
const info = getLightconeInfo(id);
// Path filter
if (pathFilter !== 'all' && meta.path !== pathFilter) return false;
// Rarity filter
if (rarityFilter !== 'all' && String(meta.rarity) !== String(rarityFilter)) return false;
// Search filter
if (search) {
const matchName = (info.name || '').toLowerCase().includes(search);
const matchId = String(id).includes(search);
const matchPath = (info.path || '').toLowerCase().includes(search);
if (!matchName && !matchId && !matchPath) return false;
}
return true;
});
if (filtered.length === 0) {
grid.innerHTML = `<div style="grid-column: 1/-1; text-align: center; color: var(--text-muted); padding: 2rem;">ไม่พบกรวยแสงตามเงื่อนไขที่ค้นหา</div>`;
return;
}
filtered.forEach(id => {
const meta = LIGHTCONE_METADATA[id];
const info = getLightconeInfo(id);
const isSelected = id === selectedId;
const card = document.createElement('div');
card.className = `add-lc-card rarity-${info.rarity || 5} ${isSelected ? 'selected' : ''}`;
card.dataset.lcId = id;
card.innerHTML = `
<img class="add-lc-card-thumb" src="${getLightconeArtSrc(id)}" onerror="this.src='${getLightconeArtFallback(id)}'" alt="${info.name}" loading="lazy">
<div class="add-lc-card-body">
<div class="add-lc-card-name" title="${info.name}">${info.name}</div>
<div class="add-lc-card-stars">${'★'.repeat(info.rarity || 5)}</div>
<div class="add-lc-card-sub">
<span class="add-lc-card-path">${getPathIconHtml(meta.path, 13)} ${info.path}</span>
<span class="add-lc-card-id">#${id}</span>
</div>
</div>
`;
card.addEventListener('click', () => {
grid.querySelectorAll('.add-lc-card.selected').forEach(c => c.classList.remove('selected'));
card.classList.add('selected');
updateAddLcPreview(id);
});
grid.appendChild(card);
});
}
// Open Add Relic Modal
function openAddRelicModal() {
if (!el.modalAddRelic) return;
@@ -4221,11 +4348,12 @@ function updateRelicMainAffixes(slot) {
async function handleAddLcSubmit(e) {
e.preventDefault();
const itemId = parseInt(el.addLcSelect.value, 10);
const level = parseInt(el.addLcLevel.value, 10) || 80;
const promotion = parseInt(el.addLcPromotion.value, 10) || 6;
const rank = parseInt(el.addLcRank.value, 10) || 5;
const equipAvatar = parseInt(el.addLcAvatar.value, 10) || 0;
const selectedIdInput = document.getElementById('add-lc-selected-id');
const itemId = parseInt(selectedIdInput?.value || el.addLcSelect?.value || '23001', 10);
const level = parseInt(el.addLcLevel?.value || '80', 10) || 80;
const promotion = parseInt(el.addLcPromotion?.value || '6', 10) || 6;
const rank = parseInt(el.addLcRank?.value || '5', 10) || 5;
const equipAvatar = parseInt(el.addLcAvatar?.value || '0', 10) || 0;
if (!state.fullFreesrData) {
state.fullFreesrData = { lightcones: [], relics: [], avatars: {} };
@@ -4240,21 +4368,26 @@ async function handleAddLcSubmit(e) {
});
}
const maxUid = state.fullFreesrData.lightcones.reduce((m, lc) => Math.max(m, lc.internal_uid || 0), 0);
let maxUniqueId = 1000;
state.fullFreesrData.lightcones.forEach(l => {
if ((l.unique_id || 0) >= maxUniqueId) maxUniqueId = (l.unique_id || 0) + 1;
});
const maxInternalUid = state.fullFreesrData.lightcones.reduce((m, lc) => Math.max(m, lc.internal_uid || 0), 0);
const newLc = {
level,
unique_id: maxUniqueId,
internal_uid: maxInternalUid + 1,
item_id: itemId,
equip_avatar: equipAvatar,
rank,
level,
promotion,
internal_uid: maxUid + 1
rank,
equip_avatar: equipAvatar
};
state.fullFreesrData.lightcones.unshift(newLc);
await saveFreesrChangesToServer();
loadEquipment();
el.modalAddLc.classList.remove('active');
showToast(t('toastLcAdded'), 'success');
showToast(t('toastLcAdded') || 'เพิ่มกรวยแสงลง FreeSR เรียบร้อยแล้ว', 'success');
}
async function handleAddRelicSubmit(e) {
@@ -4908,6 +5041,9 @@ function setupEvents() {
if (el.formAddLc) {
el.formAddLc.addEventListener('submit', handleAddLcSubmit);
}
document.getElementById('add-lc-search')?.addEventListener('input', () => renderAddLcVisualGrid());
document.getElementById('add-lc-filter-path')?.addEventListener('change', () => renderAddLcVisualGrid());
document.getElementById('add-lc-filter-rarity')?.addEventListener('change', () => renderAddLcVisualGrid());
if (el.btnOpenAddRelic) {
el.btnOpenAddRelic.addEventListener('click', openAddRelicModal);