This commit is contained in:
Eduard Prigoana 2025-10-11 19:22:53 +03:00
parent 7f90a278fa
commit 0f1a9841d1
11 changed files with 1550 additions and 1185 deletions

View file

@ -122,7 +122,7 @@
<div id="page-album" class="page"> <div id="page-album" class="page">
<header class="detail-header"> <header class="detail-header">
<img id="album-detail-image" src="" alt="Album Art" class="detail-header-image"> <img id="album-detail-image" src="" alt="" class="detail-header-image">
<div class="detail-header-info"> <div class="detail-header-info">
<div class="type">Album</div> <div class="type">Album</div>
<h1 class="title" id="album-detail-title"></h1> <h1 class="title" id="album-detail-title"></h1>

View file

@ -1,3 +1,4 @@
//js/api.js
import { RATE_LIMIT_ERROR_MESSAGE, deriveTrackQuality, delay } from './utils.js'; import { RATE_LIMIT_ERROR_MESSAGE, deriveTrackQuality, delay } from './utils.js';
import { APICache } from './cache.js'; import { APICache } from './cache.js';

View file

@ -1,3 +1,4 @@
//js/app.js
import { LosslessAPI } from './api.js'; import { LosslessAPI } from './api.js';
import { apiSettings } from './storage.js'; import { apiSettings } from './storage.js';
import { UIRenderer } from './ui.js'; import { UIRenderer } from './ui.js';

View file

@ -1,3 +1,4 @@
//js/cache.js
export class APICache { export class APICache {
constructor(options = {}) { constructor(options = {}) {
this.memoryCache = new Map(); this.memoryCache = new Map();

View file

@ -1,3 +1,4 @@
//js/player.js
import { REPEAT_MODE, SVG_PLAY, SVG_PAUSE, formatTime } from './utils.js'; import { REPEAT_MODE, SVG_PLAY, SVG_PAUSE, formatTime } from './utils.js';
export class Player { export class Player {

View file

@ -1,3 +1,4 @@
//js/storage.js
export const apiSettings = { export const apiSettings = {
STORAGE_KEY: 'monochrome-api-instances', STORAGE_KEY: 'monochrome-api-instances',
defaultInstances: [ defaultInstances: [

106
js/ui.js
View file

@ -45,6 +45,53 @@ export class UIRenderer {
`; `;
} }
createSkeletonTrack(showCover = false) {
return `
<div class="skeleton-track">
<div class="skeleton skeleton-track-number"></div>
<div class="skeleton-track-info">
${showCover ? '<div class="skeleton skeleton-track-cover"></div>' : ''}
<div class="skeleton-track-details">
<div class="skeleton skeleton-track-title"></div>
<div class="skeleton skeleton-track-artist"></div>
</div>
</div>
<div class="skeleton skeleton-track-duration"></div>
</div>
`;
}
createSkeletonCard(isArtist = false) {
return `
<div class="skeleton-card ${isArtist ? 'artist' : ''}">
<div class="skeleton skeleton-card-image"></div>
<div class="skeleton skeleton-card-title"></div>
<div class="skeleton skeleton-card-subtitle"></div>
</div>
`;
}
createSkeletonDetailHeader(isArtist = false) {
return `
<div class="skeleton-detail-header">
<div class="skeleton skeleton-detail-image ${isArtist ? 'artist' : ''}"></div>
<div class="skeleton-detail-info">
<div class="skeleton skeleton-detail-type"></div>
<div class="skeleton skeleton-detail-title"></div>
<div class="skeleton skeleton-detail-meta"></div>
</div>
</div>
`;
}
createSkeletonTracks(count = 5, showCover = false) {
return `<div class="skeleton-container">${Array(count).fill(0).map(() => this.createSkeletonTrack(showCover)).join('')}</div>`;
}
createSkeletonCards(count = 6, isArtist = false) {
return `<div class="card-grid">${Array(count).fill(0).map(() => this.createSkeletonCard(isArtist)).join('')}</div>`;
}
renderListWithTracks(container, tracks, showCover) { renderListWithTracks(container, tracks, showCover) {
container.innerHTML = tracks.map((track, i) => container.innerHTML = tracks.map((track, i) =>
this.createTrackItemHTML(track, i, showCover) this.createTrackItemHTML(track, i, showCover)
@ -85,7 +132,7 @@ export class UIRenderer {
: createPlaceholder("You haven't viewed any artists yet."); : createPlaceholder("You haven't viewed any artists yet.");
} }
async renderSearchPage(query) { async renderSearchPage(query) {
this.showPage('search'); this.showPage('search');
document.getElementById('search-results-title').textContent = `Search Results for "${query}"`; document.getElementById('search-results-title').textContent = `Search Results for "${query}"`;
@ -93,9 +140,9 @@ async renderSearchPage(query) {
const artistsContainer = document.getElementById('search-artists-container'); const artistsContainer = document.getElementById('search-artists-container');
const albumsContainer = document.getElementById('search-albums-container'); const albumsContainer = document.getElementById('search-albums-container');
tracksContainer.innerHTML = createPlaceholder('Searching...', true); tracksContainer.innerHTML = this.createSkeletonTracks(8, false);
artistsContainer.innerHTML = createPlaceholder('Searching...', true); artistsContainer.innerHTML = this.createSkeletonCards(6, true);
albumsContainer.innerHTML = createPlaceholder('Searching...', true); albumsContainer.innerHTML = this.createSkeletonCards(6, false);
try { try {
const [tracksResult, artistsResult, albumsResult] = await Promise.all([ const [tracksResult, artistsResult, albumsResult] = await Promise.all([
@ -158,19 +205,36 @@ async renderSearchPage(query) {
artistsContainer.innerHTML = errorMsg; artistsContainer.innerHTML = errorMsg;
albumsContainer.innerHTML = errorMsg; albumsContainer.innerHTML = errorMsg;
} }
} }
async renderAlbumPage(albumId) { async renderAlbumPage(albumId) {
this.showPage('album'); this.showPage('album');
const imageEl = document.getElementById('album-detail-image');
const titleEl = document.getElementById('album-detail-title');
const metaEl = document.getElementById('album-detail-meta');
const tracklistContainer = document.getElementById('album-detail-tracklist'); const tracklistContainer = document.getElementById('album-detail-tracklist');
tracklistContainer.innerHTML = createPlaceholder('Loading...', true);
imageEl.src = '';
imageEl.style.backgroundColor = 'var(--muted)';
titleEl.innerHTML = '<div class="skeleton" style="height: 48px; width: 300px; max-width: 90%;"></div>';
metaEl.innerHTML = '<div class="skeleton" style="height: 16px; width: 200px; max-width: 80%;"></div>';
tracklistContainer.innerHTML = `
<div class="track-list-header">
<span style="width: 40px; text-align: center;">#</span>
<span>Title</span>
<span class="duration-header">Duration</span>
</div>
${this.createSkeletonTracks(10, false)}
`;
try { try {
const { album, tracks } = await this.api.getAlbum(albumId); const { album, tracks } = await this.api.getAlbum(albumId);
document.getElementById('album-detail-image').src = this.api.getCoverUrl(album.cover); imageEl.src = this.api.getCoverUrl(album.cover);
document.getElementById('album-detail-title').textContent = album.title; imageEl.style.backgroundColor = '';
document.getElementById('album-detail-meta').innerHTML = titleEl.textContent = album.title;
metaEl.innerHTML =
`By <a href="#artist/${album.artist.id}">${album.artist.name}</a> • ${new Date(album.releaseDate).getFullYear()}`; `By <a href="#artist/${album.artist.id}">${album.artist.name}</a> • ${new Date(album.releaseDate).getFullYear()}`;
tracklistContainer.innerHTML = ` tracklistContainer.innerHTML = `
@ -193,19 +257,27 @@ async renderSearchPage(query) {
async renderArtistPage(artistId) { async renderArtistPage(artistId) {
this.showPage('artist'); this.showPage('artist');
const imageEl = document.getElementById('artist-detail-image');
const nameEl = document.getElementById('artist-detail-name');
const metaEl = document.getElementById('artist-detail-meta');
const tracksContainer = document.getElementById('artist-detail-tracks'); const tracksContainer = document.getElementById('artist-detail-tracks');
const albumsContainer = document.getElementById('artist-detail-albums'); const albumsContainer = document.getElementById('artist-detail-albums');
tracksContainer.innerHTML = albumsContainer.innerHTML = createPlaceholder('Loading...', true); imageEl.src = '';
imageEl.style.backgroundColor = 'var(--muted)';
nameEl.innerHTML = '<div class="skeleton" style="height: 48px; width: 300px; max-width: 90%;"></div>';
metaEl.innerHTML = '<div class="skeleton" style="height: 16px; width: 150px;"></div>';
tracksContainer.innerHTML = this.createSkeletonTracks(5, true);
albumsContainer.innerHTML = this.createSkeletonCards(6, false);
try { try {
const artist = await this.api.getArtist(artistId); const artist = await this.api.getArtist(artistId);
document.getElementById('artist-detail-image').src = imageEl.src = this.api.getArtistPictureUrl(artist.picture, '750');
this.api.getArtistPictureUrl(artist.picture, '750'); imageEl.style.backgroundColor = '';
document.getElementById('artist-detail-name').textContent = artist.name; nameEl.textContent = artist.name;
document.getElementById('artist-detail-meta').textContent = metaEl.textContent = `${artist.popularity} popularity`;
`${artist.popularity} popularity`;
this.renderListWithTracks(tracksContainer, artist.tracks, true); this.renderListWithTracks(tracksContainer, artist.tracks, true);
albumsContainer.innerHTML = artist.albums.map(album => albumsContainer.innerHTML = artist.albums.map(album =>
@ -220,7 +292,7 @@ async renderSearchPage(query) {
} }
} }
renderApiSettings() { renderApiSettings() {
const container = document.getElementById('api-instance-list'); const container = document.getElementById('api-instance-list');
const instances = this.api.settings.getInstances(); const instances = this.api.settings.getInstances();
const defaultInstancesSet = new Set(this.api.settings.defaultInstances); const defaultInstancesSet = new Set(this.api.settings.defaultInstances);
@ -256,5 +328,5 @@ renderApiSettings() {
if (cacheInfo) { if (cacheInfo) {
cacheInfo.textContent = `Cache: ${stats.memoryEntries}/${stats.maxSize} entries`; cacheInfo.textContent = `Cache: ${stats.memoryEntries}/${stats.maxSize} entries`;
} }
} }
} }

View file

@ -1,3 +1,4 @@
//js/utils.js
export const QUALITY = 'LOSSLESS'; export const QUALITY = 'LOSSLESS';
export const REPEAT_MODE = { export const REPEAT_MODE = {

View file

@ -28,7 +28,7 @@
"name": "Search", "name": "Search",
"short_name": "Search", "short_name": "Search",
"description": "Search for music", "description": "Search for music",
"url": "/#search", "url": "/search",
"icons": [ "icons": [
{ {
"src": "https://prigoana.com/favicon.png", "src": "https://prigoana.com/favicon.png",

File diff suppressed because it is too large Load diff

1
sw.js
View file

@ -1,3 +1,4 @@
//sw.js
const CACHE_NAME = 'monochrome-v1'; const CACHE_NAME = 'monochrome-v1';
const urlsToCache = [ const urlsToCache = [
'/', '/',