Add shared keyboard and list helpers with artist FTS
This commit is contained in:
parent
ab7bc36714
commit
7ee735c2db
2 changed files with 201 additions and 109 deletions
|
@ -209,8 +209,8 @@ Overview
|
||||||
Shared Tasks
|
Shared Tasks
|
||||||
- [x] Add small table/list renderer util in `script.js` to build rows safely (uses `escapeHtml`). Implemented via `createTableRenderer`.
|
- [x] Add small table/list renderer util in `script.js` to build rows safely (uses `escapeHtml`). Implemented via `createTableRenderer`.
|
||||||
- [x] Add shared pagination component (Prev/Next, page size select). Propagate `LIMIT/OFFSET`. Implemented via `createPagination`.
|
- [x] Add shared pagination component (Prev/Next, page size select). Propagate `LIMIT/OFFSET`. Implemented via `createPagination`.
|
||||||
- [ ] Add common keyboard handlers: Enter to open selection; Esc to close overlays (already wired globally).
|
- [x] Add common keyboard handlers: Enter to open selection; Esc to close overlays (already wired globally). Implemented via shared `Keyboard` helper in `script.js`.
|
||||||
- [ ] Add loading/empty-state helpers for lists.
|
- [x] Add loading/empty-state helpers for lists. Implemented via `createAsyncListState` utility.
|
||||||
|
|
||||||
Primary Navigation (Hub)
|
Primary Navigation (Hub)
|
||||||
- [x] `tpl-nav` (base): Landing hub to choose "Search", "Browse Artists", "Browse Albums", "Browse Years", "Browse Genres", "Stats".
|
- [x] `tpl-nav` (base): Landing hub to choose "Search", "Browse Artists", "Browse Albums", "Browse Years", "Browse Genres", "Stats".
|
||||||
|
@ -228,7 +228,7 @@ Browse Artists
|
||||||
- [x] `tpl-browse-artists` (base): Alphabetical list, A–Z quick jump, mini filter box; paginated.
|
- [x] `tpl-browse-artists` (base): Alphabetical list, A–Z quick jump, mini filter box; paginated.
|
||||||
- [x] `createBrowseArtistsView(db)`: Loads pages; clicking row opens Artist overlay.
|
- [x] `createBrowseArtistsView(db)`: Loads pages; clicking row opens Artist overlay.
|
||||||
- SQL: `SELECT id, name FROM artists ORDER BY name LIMIT ? OFFSET ?`.
|
- SQL: `SELECT id, name FROM artists ORDER BY name LIMIT ? OFFSET ?`.
|
||||||
- [ ] Optional prefix search using FTS prefix (`WHERE f MATCH 'artist:abc*'`) to accelerate filter.
|
- [x] Optional prefix search using FTS prefix (`WHERE f MATCH 'artist:abc*'`) to accelerate filter. Browse Artists view now favors FTS when filters include ≥2 characters.
|
||||||
|
|
||||||
Browse Albums
|
Browse Albums
|
||||||
- [x] `tpl-browse-albums` (base): List or grid with title, artist badge, year; paginated and sortable by artist/year/title.
|
- [x] `tpl-browse-albums` (base): List or grid with title, artist badge, year; paginated and sortable by artist/year/title.
|
||||||
|
|
288
script.js
288
script.js
|
@ -103,7 +103,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('keydown', (e) => {
|
window.addEventListener('keydown', (e) => {
|
||||||
if (e.key === 'Escape' && overlayStack.length) {
|
if (Keyboard.isEscapeKey(e) && overlayStack.length) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
closeTop();
|
closeTop();
|
||||||
}
|
}
|
||||||
|
@ -338,6 +338,21 @@
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const Keyboard = (() => {
|
||||||
|
function isActivationKey(event) {
|
||||||
|
return event && (event.key === 'Enter' || event.key === ' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
function isEscapeKey(event) {
|
||||||
|
return event && event.key === 'Escape';
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isActivationKey,
|
||||||
|
isEscapeKey,
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
function bindRowActivation(tableEl, handler) {
|
function bindRowActivation(tableEl, handler) {
|
||||||
if (!tableEl || typeof handler !== 'function') return () => {};
|
if (!tableEl || typeof handler !== 'function') return () => {};
|
||||||
function findRow(target) {
|
function findRow(target) {
|
||||||
|
@ -354,7 +369,7 @@
|
||||||
handler(row.dataset.rowId, event);
|
handler(row.dataset.rowId, event);
|
||||||
};
|
};
|
||||||
const onKeyDown = (event) => {
|
const onKeyDown = (event) => {
|
||||||
if (event.key !== 'Enter' && event.key !== ' ') return;
|
if (!Keyboard.isActivationKey(event)) return;
|
||||||
const row = findRow(event.target);
|
const row = findRow(event.target);
|
||||||
if (!row) return;
|
if (!row) return;
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
@ -368,6 +383,69 @@
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createAsyncListState({ table, statusEl, pagination }) {
|
||||||
|
if (!table || !table.el) throw new Error('table required');
|
||||||
|
if (!statusEl) throw new Error('statusEl required');
|
||||||
|
const baseClasses = statusEl.className;
|
||||||
|
|
||||||
|
function applyTone(tone) {
|
||||||
|
statusEl.className = baseClasses;
|
||||||
|
if (tone === 'error') {
|
||||||
|
statusEl.classList.remove('has-text-grey');
|
||||||
|
statusEl.classList.add('has-text-danger');
|
||||||
|
} else {
|
||||||
|
if (baseClasses && baseClasses.includes('has-text-danger')) return;
|
||||||
|
if (!statusEl.classList.contains('has-text-grey') && (!baseClasses || !baseClasses.includes('has-text-danger'))) {
|
||||||
|
statusEl.classList.add('has-text-grey');
|
||||||
|
}
|
||||||
|
statusEl.classList.remove('has-text-danger');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showMessage(text, tone) {
|
||||||
|
applyTone(tone);
|
||||||
|
statusEl.textContent = text;
|
||||||
|
statusEl.hidden = false;
|
||||||
|
table.el.hidden = true;
|
||||||
|
if (pagination) pagination.setDisabled(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showRows({ rows, page, pageSize, total, resultsCount }) {
|
||||||
|
applyTone();
|
||||||
|
table.setRows(rows);
|
||||||
|
statusEl.hidden = true;
|
||||||
|
table.el.hidden = false;
|
||||||
|
if (pagination) {
|
||||||
|
pagination.setState({
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
total,
|
||||||
|
resultsCount: resultsCount !== undefined ? resultsCount : rows.length,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
showIdle(text) {
|
||||||
|
showMessage(text, 'default');
|
||||||
|
},
|
||||||
|
showLoading(text = 'Loading…') {
|
||||||
|
showMessage(text, 'default');
|
||||||
|
},
|
||||||
|
showError(text) {
|
||||||
|
showMessage(text, 'error');
|
||||||
|
},
|
||||||
|
showEmpty(text) {
|
||||||
|
table.clear();
|
||||||
|
showMessage(text, 'default');
|
||||||
|
},
|
||||||
|
showRows,
|
||||||
|
disablePagination() {
|
||||||
|
if (pagination) pagination.setDisabled(true);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
let paginationIdCounter = 0;
|
let paginationIdCounter = 0;
|
||||||
function createPagination({
|
function createPagination({
|
||||||
pageSizes = [10, 25, 50, 100],
|
pageSizes = [10, 25, 50, 100],
|
||||||
|
@ -657,6 +735,8 @@
|
||||||
});
|
});
|
||||||
$results.appendChild(pagination.el);
|
$results.appendChild(pagination.el);
|
||||||
|
|
||||||
|
const listState = createAsyncListState({ table, statusEl: $status, pagination });
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
query: initialQuery,
|
query: initialQuery,
|
||||||
page: 1,
|
page: 1,
|
||||||
|
@ -723,13 +803,6 @@
|
||||||
WHERE f MATCH ?
|
WHERE f MATCH ?
|
||||||
`;
|
`;
|
||||||
|
|
||||||
function setStatus(text) {
|
|
||||||
$status.textContent = text;
|
|
||||||
$status.hidden = false;
|
|
||||||
table.el.hidden = true;
|
|
||||||
pagination.setDisabled(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
function enableSearch() {
|
function enableSearch() {
|
||||||
$q.disabled = false;
|
$q.disabled = false;
|
||||||
$q.removeAttribute('aria-disabled');
|
$q.removeAttribute('aria-disabled');
|
||||||
|
@ -743,14 +816,11 @@
|
||||||
const term = state.query.trim();
|
const term = state.query.trim();
|
||||||
if (!term) {
|
if (!term) {
|
||||||
table.clear();
|
table.clear();
|
||||||
setStatus('Type to search…');
|
listState.showIdle('Type to search…');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$status.textContent = 'Searching…';
|
listState.showLoading('Searching…');
|
||||||
$status.hidden = false;
|
|
||||||
table.el.hidden = true;
|
|
||||||
pagination.setDisabled(true);
|
|
||||||
|
|
||||||
const offset = (state.page - 1) * state.pageSize;
|
const offset = (state.page - 1) * state.pageSize;
|
||||||
let total = 0;
|
let total = 0;
|
||||||
|
@ -766,7 +836,7 @@
|
||||||
|
|
||||||
if (total === 0) {
|
if (total === 0) {
|
||||||
table.clear();
|
table.clear();
|
||||||
$status.textContent = 'No matches found';
|
listState.showEmpty('No matches found');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -784,18 +854,15 @@
|
||||||
searchStmt.free();
|
searchStmt.free();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
$status.textContent = 'Search failed. Check console for details.';
|
listState.showError('Search failed. Check console for details.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
table.setRows(rows);
|
listState.showRows({
|
||||||
$status.hidden = true;
|
rows,
|
||||||
table.el.hidden = false;
|
total,
|
||||||
pagination.setState({
|
|
||||||
page: state.page,
|
page: state.page,
|
||||||
pageSize: state.pageSize,
|
pageSize: state.pageSize,
|
||||||
total,
|
|
||||||
resultsCount: rows.length,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -817,7 +884,7 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
updateSortButtons();
|
updateSortButtons();
|
||||||
setStatus('Type to search…');
|
listState.showIdle('Type to search…');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
kind: 'base',
|
kind: 'base',
|
||||||
|
@ -902,6 +969,8 @@
|
||||||
});
|
});
|
||||||
$results.appendChild(pagination.el);
|
$results.appendChild(pagination.el);
|
||||||
|
|
||||||
|
const listState = createAsyncListState({ table, statusEl: $status, pagination });
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
prefix: initialPrefix,
|
prefix: initialPrefix,
|
||||||
filter: initialFilter,
|
filter: initialFilter,
|
||||||
|
@ -913,7 +982,7 @@
|
||||||
return String(str).replace(/[\\%_]/g, (m) => `\\${m}`);
|
return String(str).replace(/[\\%_]/g, (m) => `\\${m}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildTerm() {
|
function buildLikeTerm() {
|
||||||
const typed = state.filter.trim();
|
const typed = state.filter.trim();
|
||||||
if (typed) return `%${escapeLike(typed)}%`;
|
if (typed) return `%${escapeLike(typed)}%`;
|
||||||
if (state.prefix) return `${escapeLike(state.prefix)}%`;
|
if (state.prefix) return `${escapeLike(state.prefix)}%`;
|
||||||
|
@ -928,12 +997,36 @@
|
||||||
ORDER BY name COLLATE NOCASE
|
ORDER BY name COLLATE NOCASE
|
||||||
LIMIT ? OFFSET ?
|
LIMIT ? OFFSET ?
|
||||||
`;
|
`;
|
||||||
|
const ftsCountSql = `
|
||||||
|
SELECT COUNT(*) AS count FROM (
|
||||||
|
SELECT a.id
|
||||||
|
FROM fts_tracks f
|
||||||
|
JOIN tracks t ON t.id = f.rowid
|
||||||
|
JOIN artists a ON a.id = t.artist_id
|
||||||
|
WHERE f MATCH ?
|
||||||
|
GROUP BY a.id
|
||||||
|
) AS matches
|
||||||
|
`;
|
||||||
|
const ftsRowsSql = `
|
||||||
|
SELECT a.id, a.name
|
||||||
|
FROM fts_tracks f
|
||||||
|
JOIN tracks t ON t.id = f.rowid
|
||||||
|
JOIN artists a ON a.id = t.artist_id
|
||||||
|
WHERE f MATCH ?
|
||||||
|
GROUP BY a.id
|
||||||
|
ORDER BY a.name COLLATE NOCASE
|
||||||
|
LIMIT ? OFFSET ?
|
||||||
|
`;
|
||||||
|
|
||||||
function setStatus(text) {
|
function buildArtistFtsMatch(input) {
|
||||||
$status.textContent = text;
|
const tokens = String(input)
|
||||||
$status.hidden = false;
|
.trim()
|
||||||
table.el.hidden = true;
|
.toLowerCase()
|
||||||
pagination.setDisabled(true);
|
.split(/\s+/)
|
||||||
|
.map((token) => token.replace(/[^0-9a-z]/gi, '').slice(0, 32))
|
||||||
|
.filter((token) => token.length >= 2);
|
||||||
|
if (!tokens.length) return null;
|
||||||
|
return tokens.map((token) => `artist:${token}*`).join(' AND ');
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateJumpButtons() {
|
function updateJumpButtons() {
|
||||||
|
@ -946,47 +1039,69 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadArtistsImmediate() {
|
function loadArtistsImmediate() {
|
||||||
const term = buildTerm();
|
const typedFilter = state.filter.trim();
|
||||||
$status.textContent = 'Loading…';
|
const ftsMatch = buildArtistFtsMatch(typedFilter);
|
||||||
$status.hidden = false;
|
const likeTerm = buildLikeTerm();
|
||||||
table.el.hidden = true;
|
listState.showLoading('Loading…');
|
||||||
pagination.setDisabled(true);
|
|
||||||
|
|
||||||
const offset = (state.page - 1) * state.pageSize;
|
const offset = (state.page - 1) * state.pageSize;
|
||||||
let total = 0;
|
let total = 0;
|
||||||
const rows = [];
|
const rows = [];
|
||||||
|
let usedFts = false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (ftsMatch) {
|
||||||
|
const ftsCountStmt = db.prepare(ftsCountSql);
|
||||||
|
ftsCountStmt.bind([ftsMatch]);
|
||||||
|
if (ftsCountStmt.step()) total = Number(ftsCountStmt.getAsObject().count) || 0;
|
||||||
|
ftsCountStmt.free();
|
||||||
|
|
||||||
|
if (total > 0) {
|
||||||
|
if (offset >= total) state.page = Math.max(1, Math.ceil(total / state.pageSize));
|
||||||
|
const ftsRowsStmt = db.prepare(ftsRowsSql);
|
||||||
|
ftsRowsStmt.bind([ftsMatch, state.pageSize, (state.page - 1) * state.pageSize]);
|
||||||
|
while (ftsRowsStmt.step()) rows.push(ftsRowsStmt.getAsObject());
|
||||||
|
ftsRowsStmt.free();
|
||||||
|
usedFts = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!usedFts) {
|
||||||
const countStmt = db.prepare(countSql);
|
const countStmt = db.prepare(countSql);
|
||||||
countStmt.bind([term]);
|
countStmt.bind([likeTerm]);
|
||||||
if (countStmt.step()) total = Number(countStmt.getAsObject().count) || 0;
|
if (countStmt.step()) total = Number(countStmt.getAsObject().count) || 0;
|
||||||
countStmt.free();
|
countStmt.free();
|
||||||
|
|
||||||
if (total === 0) {
|
if (total === 0) {
|
||||||
table.clear();
|
table.clear();
|
||||||
$status.textContent = 'No artists found';
|
listState.showEmpty('No artists found');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (offset >= total) state.page = Math.max(1, Math.ceil(total / state.pageSize));
|
if (offset >= total) state.page = Math.max(1, Math.ceil(total / state.pageSize));
|
||||||
|
|
||||||
const stmt = db.prepare(rowsSql);
|
const rowsStmt = db.prepare(rowsSql);
|
||||||
stmt.bind([term, state.pageSize, (state.page - 1) * state.pageSize]);
|
rowsStmt.bind([likeTerm, state.pageSize, (state.page - 1) * state.pageSize]);
|
||||||
while (stmt.step()) rows.push(stmt.getAsObject());
|
while (rowsStmt.step()) rows.push(rowsStmt.getAsObject());
|
||||||
stmt.free();
|
rowsStmt.free();
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
$status.textContent = 'Failed to load artists.';
|
listState.showError('Failed to load artists.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
table.setRows(rows);
|
if (total === 0) {
|
||||||
$status.hidden = true;
|
table.clear();
|
||||||
table.el.hidden = false;
|
listState.showEmpty('No artists found');
|
||||||
pagination.setState({
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
listState.showRows({
|
||||||
|
rows,
|
||||||
|
total,
|
||||||
page: state.page,
|
page: state.page,
|
||||||
pageSize: state.pageSize,
|
pageSize: state.pageSize,
|
||||||
total,
|
|
||||||
resultsCount: rows.length,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1022,7 +1137,7 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
updateJumpButtons();
|
updateJumpButtons();
|
||||||
setStatus('Loading…');
|
listState.showLoading('Loading…');
|
||||||
loadArtistsImmediate();
|
loadArtistsImmediate();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -1070,6 +1185,8 @@
|
||||||
});
|
});
|
||||||
$results.appendChild(pagination.el);
|
$results.appendChild(pagination.el);
|
||||||
|
|
||||||
|
const listState = createAsyncListState({ table, statusEl: $status, pagination });
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
sort: initialSort,
|
sort: initialSort,
|
||||||
page: 1,
|
page: 1,
|
||||||
|
@ -1091,18 +1208,8 @@
|
||||||
LIMIT ? OFFSET ?
|
LIMIT ? OFFSET ?
|
||||||
`;
|
`;
|
||||||
|
|
||||||
function setStatus(text) {
|
|
||||||
$status.textContent = text;
|
|
||||||
$status.hidden = false;
|
|
||||||
table.el.hidden = true;
|
|
||||||
pagination.setDisabled(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadAlbumsImmediate() {
|
function loadAlbumsImmediate() {
|
||||||
$status.textContent = 'Loading…';
|
listState.showLoading('Loading…');
|
||||||
$status.hidden = false;
|
|
||||||
table.el.hidden = true;
|
|
||||||
pagination.setDisabled(true);
|
|
||||||
|
|
||||||
let total = 0;
|
let total = 0;
|
||||||
const rows = [];
|
const rows = [];
|
||||||
|
@ -1113,7 +1220,7 @@
|
||||||
|
|
||||||
if (total === 0) {
|
if (total === 0) {
|
||||||
table.clear();
|
table.clear();
|
||||||
$status.textContent = 'No albums found';
|
listState.showEmpty('No albums found');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1127,18 +1234,15 @@
|
||||||
stmt.free();
|
stmt.free();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
$status.textContent = 'Failed to load albums.';
|
listState.showError('Failed to load albums.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
table.setRows(rows);
|
listState.showRows({
|
||||||
$status.hidden = true;
|
rows,
|
||||||
table.el.hidden = false;
|
total,
|
||||||
pagination.setState({
|
|
||||||
page: state.page,
|
page: state.page,
|
||||||
pageSize: state.pageSize,
|
pageSize: state.pageSize,
|
||||||
total,
|
|
||||||
resultsCount: rows.length,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1161,7 +1265,7 @@
|
||||||
UX.openOverlay(createAlbumOverlay(db, albumId));
|
UX.openOverlay(createAlbumOverlay(db, albumId));
|
||||||
});
|
});
|
||||||
|
|
||||||
setStatus('Loading…');
|
listState.showLoading('Loading…');
|
||||||
loadAlbumsImmediate();
|
loadAlbumsImmediate();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -1217,6 +1321,8 @@
|
||||||
});
|
});
|
||||||
$tracksCol.appendChild(pagination.el);
|
$tracksCol.appendChild(pagination.el);
|
||||||
|
|
||||||
|
const trackListState = createAsyncListState({ table: trackTable, statusEl: $tracksStatus, pagination });
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
years: [],
|
years: [],
|
||||||
selectedYear: presetYear,
|
selectedYear: presetYear,
|
||||||
|
@ -1308,17 +1414,11 @@
|
||||||
function loadTracksImmediate() {
|
function loadTracksImmediate() {
|
||||||
if (!Number.isFinite(state.selectedYear)) {
|
if (!Number.isFinite(state.selectedYear)) {
|
||||||
trackTable.clear();
|
trackTable.clear();
|
||||||
trackTable.el.hidden = true;
|
trackListState.showIdle('Select a year to view tracks.');
|
||||||
$tracksStatus.hidden = false;
|
|
||||||
$tracksStatus.textContent = 'Select a year to view tracks.';
|
|
||||||
pagination.setDisabled(true);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$tracksStatus.textContent = 'Loading tracks…';
|
trackListState.showLoading('Loading tracks…');
|
||||||
$tracksStatus.hidden = false;
|
|
||||||
trackTable.el.hidden = true;
|
|
||||||
pagination.setDisabled(true);
|
|
||||||
|
|
||||||
let total = 0;
|
let total = 0;
|
||||||
const rows = [];
|
const rows = [];
|
||||||
|
@ -1330,7 +1430,7 @@
|
||||||
|
|
||||||
if (total === 0) {
|
if (total === 0) {
|
||||||
trackTable.clear();
|
trackTable.clear();
|
||||||
$tracksStatus.textContent = 'No tracks recorded for this year.';
|
trackListState.showEmpty('No tracks recorded for this year.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1343,18 +1443,15 @@
|
||||||
stmt.free();
|
stmt.free();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
$tracksStatus.textContent = 'Failed to load tracks.';
|
trackListState.showError('Failed to load tracks.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
trackTable.setRows(rows);
|
trackListState.showRows({
|
||||||
$tracksStatus.hidden = true;
|
rows,
|
||||||
trackTable.el.hidden = false;
|
total,
|
||||||
pagination.setState({
|
|
||||||
page: state.page,
|
page: state.page,
|
||||||
pageSize: state.pageSize,
|
pageSize: state.pageSize,
|
||||||
total,
|
|
||||||
resultsCount: rows.length,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1429,6 +1526,8 @@
|
||||||
});
|
});
|
||||||
$tracksSection.appendChild(pagination.el);
|
$tracksSection.appendChild(pagination.el);
|
||||||
|
|
||||||
|
const trackListState = createAsyncListState({ table: trackTable, statusEl: $tracksStatus, pagination });
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
genres: [],
|
genres: [],
|
||||||
selectedGenre: presetGenre,
|
selectedGenre: presetGenre,
|
||||||
|
@ -1512,18 +1611,14 @@
|
||||||
function loadTracksImmediate() {
|
function loadTracksImmediate() {
|
||||||
if (!state.selectedGenre) {
|
if (!state.selectedGenre) {
|
||||||
trackTable.clear();
|
trackTable.clear();
|
||||||
trackTable.el.hidden = true;
|
|
||||||
$tracksSection.hidden = true;
|
$tracksSection.hidden = true;
|
||||||
pagination.setDisabled(true);
|
trackListState.showIdle('Select a genre to view tracks.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$tracksSection.hidden = false;
|
$tracksSection.hidden = false;
|
||||||
$selectedGenre.textContent = `Tracks tagged ${state.selectedGenre}`;
|
$selectedGenre.textContent = `Tracks tagged ${state.selectedGenre}`;
|
||||||
$tracksStatus.textContent = 'Loading tracks…';
|
trackListState.showLoading('Loading tracks…');
|
||||||
$tracksStatus.hidden = false;
|
|
||||||
trackTable.el.hidden = true;
|
|
||||||
pagination.setDisabled(true);
|
|
||||||
|
|
||||||
let total = 0;
|
let total = 0;
|
||||||
const rows = [];
|
const rows = [];
|
||||||
|
@ -1535,7 +1630,7 @@
|
||||||
|
|
||||||
if (total === 0) {
|
if (total === 0) {
|
||||||
trackTable.clear();
|
trackTable.clear();
|
||||||
$tracksStatus.textContent = 'No tracks found for this genre.';
|
trackListState.showEmpty('No tracks found for this genre.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1548,18 +1643,15 @@
|
||||||
stmt.free();
|
stmt.free();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
$tracksStatus.textContent = 'Failed to load tracks.';
|
trackListState.showError('Failed to load tracks.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
trackTable.setRows(rows);
|
trackListState.showRows({
|
||||||
$tracksStatus.hidden = true;
|
rows,
|
||||||
trackTable.el.hidden = false;
|
total,
|
||||||
pagination.setState({
|
|
||||||
page: state.page,
|
page: state.page,
|
||||||
pageSize: state.pageSize,
|
pageSize: state.pageSize,
|
||||||
total,
|
|
||||||
resultsCount: rows.length,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue