feat: add atmosphere audio filters
This commit is contained in:
@@ -34,6 +34,8 @@
|
||||
drawerStopAll: document.getElementById('drawer-stop-all'),
|
||||
activeList: document.getElementById('active-list'),
|
||||
activeEmpty: document.getElementById('active-empty'),
|
||||
filterGrid: document.getElementById('filter-grid'),
|
||||
filterStatus: document.getElementById('filter-status'),
|
||||
};
|
||||
|
||||
const state = {
|
||||
@@ -53,6 +55,8 @@
|
||||
sets: [],
|
||||
currentSetId: null,
|
||||
lastVariantPerGroup: new Map(), // anti-immediate-repeat memory
|
||||
filters: [],
|
||||
filter: { id: null, name: null, active: false },
|
||||
};
|
||||
|
||||
state.sets = loadSets();
|
||||
@@ -239,6 +243,53 @@
|
||||
</div>`).join('');
|
||||
}
|
||||
|
||||
function renderFilters() {
|
||||
const activeId = state.filter && state.filter.active ? state.filter.id : null;
|
||||
els.filterStatus.textContent = activeId ? `Active: ${state.filter.name || activeId}` : 'No filter';
|
||||
els.filterStatus.classList.toggle('filter-active', Boolean(activeId));
|
||||
els.filterGrid.innerHTML = state.filters.map((f) => {
|
||||
const active = f.id === activeId ? ' active' : '';
|
||||
return `
|
||||
<article class="filter-card${active}" data-filter-id="${escapeHtml(f.id)}">
|
||||
<div>
|
||||
<strong>${escapeHtml(f.name)}</strong>
|
||||
<span>${escapeHtml(f.description || '')}</span>
|
||||
</div>
|
||||
<div class="filter-actions">
|
||||
<button class="mini${active ? ' ok' : ''}" type="button" data-filter-action="toggle">${active ? 'Clear' : 'Toggle'}</button>
|
||||
<button class="mini momentary" type="button" data-filter-action="hold" title="Hold down to apply, release to clear">Hold</button>
|
||||
</div>
|
||||
</article>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
async function setFilter(id, active = true) {
|
||||
try {
|
||||
const res = await fetchJson('/api/filter', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ id, active }),
|
||||
});
|
||||
state.filter = res.filter || (res.stream && res.stream.filter) || state.filter;
|
||||
renderFilters();
|
||||
setStatus('ok', state.filter && state.filter.active ? `Filter active — ${state.filter.name}` : 'Filter cleared');
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setStatus('err', `Filter failed: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadFilters() {
|
||||
try {
|
||||
const res = await fetchJson('/api/filters');
|
||||
state.filters = Array.isArray(res.filters) ? res.filters : [];
|
||||
state.filter = res.filter || state.filter;
|
||||
renderFilters();
|
||||
} catch (err) {
|
||||
console.warn('filters load failed', err);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadConfig() {
|
||||
try {
|
||||
const cfg = await fetchJson('/api/config');
|
||||
@@ -365,6 +416,11 @@
|
||||
function applyStreamStatus(stream) {
|
||||
if (!stream) return;
|
||||
state.active = stream.active || [];
|
||||
if (stream.filter) {
|
||||
state.filter = stream.filter;
|
||||
if (Array.isArray(stream.filters)) state.filters = stream.filters;
|
||||
renderFilters();
|
||||
}
|
||||
renderActive();
|
||||
}
|
||||
|
||||
@@ -482,6 +538,35 @@
|
||||
|
||||
els.grid.addEventListener('click', gridClick);
|
||||
els.setGrid.addEventListener('click', gridClick);
|
||||
els.filterGrid.addEventListener('click', (e) => {
|
||||
const btn = e.target.closest('[data-filter-action="toggle"]');
|
||||
if (!btn) return;
|
||||
const card = btn.closest('[data-filter-id]');
|
||||
if (!card) return;
|
||||
const id = card.dataset.filterId;
|
||||
const alreadyActive = state.filter && state.filter.active && state.filter.id === id;
|
||||
setFilter(id, !alreadyActive);
|
||||
});
|
||||
|
||||
els.filterGrid.addEventListener('pointerdown', (e) => {
|
||||
const btn = e.target.closest('[data-filter-action="hold"]');
|
||||
if (!btn) return;
|
||||
e.preventDefault();
|
||||
const card = btn.closest('[data-filter-id]');
|
||||
if (!card) return;
|
||||
btn.setPointerCapture?.(e.pointerId);
|
||||
btn.classList.add('holding');
|
||||
setFilter(card.dataset.filterId, true);
|
||||
});
|
||||
for (const eventName of ['pointerup', 'pointercancel', 'lostpointercapture']) {
|
||||
els.filterGrid.addEventListener(eventName, (e) => {
|
||||
const btn = e.target.closest?.('[data-filter-action="hold"]');
|
||||
if (!btn || !btn.classList.contains('holding')) return;
|
||||
btn.classList.remove('holding');
|
||||
setFilter(null, false);
|
||||
});
|
||||
}
|
||||
|
||||
els.categories.addEventListener('click', (e) => {
|
||||
const btn = e.target.closest('.chip'); if (!btn) return;
|
||||
state.activeCategory = btn.dataset.cat || 'all'; renderCategories(); renderGrid();
|
||||
@@ -510,6 +595,7 @@
|
||||
});
|
||||
|
||||
loadConfig();
|
||||
loadFilters();
|
||||
loadEffects();
|
||||
renderSetsSelect();
|
||||
renderActive();
|
||||
|
||||
Reference in New Issue
Block a user