Files
client/greyscale/greyscale-app/js/app.js
T
2026-04-17 16:52:51 -04:00

205 lines
6.2 KiB
JavaScript

import { GreyscaleAPI } from './api.js';
import { SimplePlayer } from './player.js';
function formatDuration(seconds) {
if (!Number.isFinite(seconds) || seconds < 0) return '0:00';
const m = Math.floor(seconds / 60);
const s = Math.floor(seconds % 60);
return `${m}:${String(s).padStart(2, '0')}`;
}
function escapeHtml(value) {
return String(value)
.replace(/&/g, '\u0026amp;')
.replace(/</g, '\u0026lt;')
.replace(/>/g, '\u0026gt;')
.replace(/"/g, '\u0026quot;')
.replace(/\u0027/g, '\u0026#39;');
}
const els = {
form: document.getElementById('search-form'),
input: document.getElementById('search-input'),
button: document.getElementById('search-button'),
status: document.getElementById('status-text'),
results: document.getElementById('results-list'),
audio: document.getElementById('audio-player'),
playPauseBtn: document.getElementById('play-pause-btn'),
prevBtn: document.getElementById('prev-btn'),
nextBtn: document.getElementById('next-btn'),
playerCover: document.getElementById('player-cover'),
playerTitle: document.getElementById('player-title'),
playerArtist: document.getElementById('player-artist'),
currentTime: document.getElementById('current-time'),
duration: document.getElementById('duration'),
progress: document.getElementById('progress'),
};
const api = new GreyscaleAPI();
const player = new SimplePlayer({
audio: els.audio,
progressEl: els.progress,
currentTimeEl: els.currentTime,
durationEl: els.duration,
});
let tracks = [];
function setStatus(text, type = 'neutral') {
els.status.textContent = text;
els.status.dataset.type = type;
}
function setSearchLoading(loading) {
els.button.disabled = loading;
els.input.disabled = loading;
els.button.textContent = loading ? 'Searching...' : 'Search';
}
function setPlayButtonState() {
els.playPauseBtn.textContent = els.audio.paused ? '▶' : '❚❚';
}
function updatePlayerMeta(track) {
if (!track) {
els.playerTitle.textContent = 'No track selected';
els.playerArtist.textContent = '-';
els.playerCover.src = '';
return;
}
els.playerTitle.textContent = track.title;
els.playerArtist.textContent = track.artist;
const cover = api.getCoverUrl(track.cover, 320);
els.playerCover.src = cover || '';
}
function renderResults(items) {
if (!items.length) {
els.results.innerHTML = '<li class="result-empty">No tracks found.</li>';
return;
}
els.results.innerHTML = items
.map(
(track, index) => `
<li class="result-item" data-index="${index}">
<img class="result-cover" src="${escapeHtml(api.getCoverUrl(track.cover, 160))}" alt="" loading="lazy" />
<div class="result-meta">
<div class="result-title">${escapeHtml(track.title)}</div>
<div class="result-subtitle">${escapeHtml(track.artist)}${
track.albumTitle ? ` — ${escapeHtml(track.albumTitle)}` : ''
}</div>
</div>
<div class="result-duration">${formatDuration(track.duration)}</div>
</li>
`
)
.join('');
}
async function resolveTrack(track) {
return api.getTrackStream(track.id, 'HI_RES_LOSSLESS');
}
async function playByIndex(index) {
if (!Number.isInteger(index) || index < 0 || index >= tracks.length) return;
player.setQueue(tracks, index);
try {
setStatus(`Loading: ${tracks[index].title}`, 'neutral');
await player.playCurrent(resolveTrack);
setStatus(`Playing: ${tracks[index].title}`, 'ok');
} catch (error) {
console.error(error);
setStatus(`Playback failed: ${error.message || 'Unknown error'}`, 'error');
}
}
async function runSearch(query) {
const trimmed = query.trim();
if (!trimmed) return;
setSearchLoading(true);
setStatus(`Searching for "${trimmed}"...`, 'neutral');
try {
tracks = await api.searchTracks(trimmed);
renderResults(tracks);
setStatus(`Found ${tracks.length} track${tracks.length === 1 ? '' : 's'}.`, 'ok');
} catch (error) {
console.error(error);
tracks = [];
renderResults([]);
setStatus(`Search failed: ${error.message || 'Unknown error'}`, 'error');
} finally {
setSearchLoading(false);
}
}
async function bootstrap() {
setStatus('Loading API instances...', 'neutral');
try {
await api.initInstances();
setStatus('Ready.', 'ok');
} catch (error) {
console.error(error);
setStatus('Instance loading failed, using fallback list.', 'error');
}
els.form.addEventListener('submit', async (event) => {
event.preventDefault();
await runSearch(els.input.value);
});
els.results.addEventListener('click', async (event) => {
const item = event.target.closest('.result-item');
if (!item) return;
const index = Number(item.dataset.index);
await playByIndex(index);
});
player.onTrackChanged = (track) => {
updatePlayerMeta(track);
};
player.setResolver(resolveTrack);
els.playPauseBtn.addEventListener('click', async () => {
try {
await player.togglePlayPause();
} catch (error) {
console.error(error);
setStatus(`Playback error: ${error.message || 'Unknown error'}`, 'error');
}
});
els.prevBtn.addEventListener('click', async () => {
try {
await player.playPrev();
} catch (error) {
console.error(error);
setStatus(`Previous track failed: ${error.message || 'Unknown error'}`, 'error');
}
});
els.nextBtn.addEventListener('click', async () => {
try {
await player.playNext();
} catch (error) {
console.error(error);
setStatus(`Next track failed: ${error.message || 'Unknown error'}`, 'error');
}
});
els.audio.addEventListener('play', setPlayButtonState);
els.audio.addEventListener('pause', setPlayButtonState);
setPlayButtonState();
}
bootstrap();