Merge branch 'main' of github.com:monochrome-music/monochrome

This commit is contained in:
Samidy 2026-03-09 03:11:58 +03:00
commit 0662796d73
10 changed files with 154 additions and 101 deletions

View file

@ -124,7 +124,20 @@
<div id="visualizer-container">
<canvas id="visualizer-canvas"></canvas>
</div>
<div id="fullscreen-video-container" style="display: none; position: absolute; inset: 0; width: 100%; height: 100%; justify-content: center; align-items: center; background: black; z-index: 0;"></div>
<div
id="fullscreen-video-container"
style="
display: none;
position: absolute;
inset: 0;
width: 100%;
height: 100%;
justify-content: center;
align-items: center;
background: black;
z-index: 0;
"
></div>
<button id="toggle-ui-btn" class="fullscreen-ui-toggle" title="Toggle UI">
<svg
xmlns="http://www.w3.org/2000/svg"
@ -168,7 +181,6 @@
/>
<div class="fullscreen-track-info">
<h2 id="fullscreen-track-title"></h2>
<h3 id="fullscreen-track-artist"></h3>
<div class="fullscreen-actions">
@ -3771,8 +3783,8 @@
<div class="info">
<span class="label">Visualizer Brightness</span>
<span class="description"
>Adjust the brightness of the visualizer. Lower this if the visualizer
is too bright for you.</span
>Adjust the brightness of the visualizer. Lower this if the visualizer is
too bright for you.</span
>
</div>
<div style="display: flex; align-items: center; gap: 10px">

View file

@ -585,7 +585,9 @@ const syncManager = {
id: playlist.id,
name: playlist.name,
cover: playlist.cover || null,
tracks: playlist.tracks ? playlist.tracks.map((t) => this._minifyItem(t.type || 'track', t)) : [],
tracks: playlist.tracks
? playlist.tracks.map((t) => this._minifyItem(t.type || 'track', t))
: [],
createdAt: playlist.createdAt || Date.now(),
updatedAt: playlist.updatedAt || Date.now(),
numberOfTracks: playlist.tracks ? playlist.tracks.length : 0,

View file

@ -892,10 +892,10 @@ export class LosslessAPI {
const numericArtistId = Number(artistId);
for (const item of videoSearch.items) {
const itemArtistId = item.artist?.id;
const matchesArtist =
itemArtistId === numericArtistId ||
(Array.isArray(item.artists) && item.artists.some(a => a.id === numericArtistId));
const matchesArtist =
itemArtistId === numericArtistId ||
(Array.isArray(item.artists) && item.artists.some((a) => a.id === numericArtistId));
if (matchesArtist && !videoMap.has(item.id)) {
videoMap.set(item.id, item);
}
@ -918,8 +918,9 @@ export class LosslessAPI {
.sort((a, b) => (b.popularity || 0) - (a.popularity || 0))
.slice(0, 15);
const videos = Array.from(videoMap.values())
.sort((a, b) => new Date(b.releaseDate || 0) - new Date(a.releaseDate || 0));
const videos = Array.from(videoMap.values()).sort(
(a, b) => new Date(b.releaseDate || 0) - new Date(a.releaseDate || 0)
);
// Enrich tracks with album release dates
const tracks = options.lightweight ? topTracks : await this.enrichTracksWithAlbumDates(topTracks);
@ -1281,8 +1282,8 @@ export class LosslessAPI {
return null;
};
const manifest = isVideo
? (findValue(lookup, 'manifest') || findValue(lookup, 'Manifest'))
const manifest = isVideo
? findValue(lookup, 'manifest') || findValue(lookup, 'Manifest')
: lookup.info?.manifest;
if (!manifest) {
@ -1325,7 +1326,8 @@ export class LosslessAPI {
console.error('HLS download failed:', hlsError);
throw hlsError;
}
} else { const response = await fetch(streamUrl, {
} else {
const response = await fetch(streamUrl, {
cache: 'no-store',
signal: options.signal,
});
@ -1389,38 +1391,38 @@ export class LosslessAPI {
}
}
if (quality.endsWith('LOSSLESS')) {
try {
switch (losslessContainerSettings.getContainer()) {
case 'flac':
if ((await getExtensionFromBlob(blob)) != 'flac') {
if (quality.endsWith('LOSSLESS')) {
try {
switch (losslessContainerSettings.getContainer()) {
case 'flac':
if ((await getExtensionFromBlob(blob)) != 'flac') {
blob = await ffmpeg(
blob,
{ args: ['-vn', '-map_metadata', '-1', '-map', '0:a', '-c:a', 'flac'] },
'output.flac',
'audio/flac',
onProgress,
options.signal
);
}
break;
case 'alac':
blob = await ffmpeg(
blob,
{ args: ['-vn', '-map_metadata', '-1', '-map', '0:a', '-c:a', 'flac'] },
'output.flac',
'audio/flac',
{ args: ['-c:a', 'alac'] },
'output.m4a',
'audio/mp4',
onProgress,
options.signal
);
}
break;
case 'alac':
blob = await ffmpeg(
blob,
{ args: ['-c:a', 'alac'] },
'output.m4a',
'audio/mp4',
onProgress,
options.signal
);
break;
default:
break;
}
} catch (error) {
if (error?.name === 'AbortError') {
throw error;
}
break;
default:
break;
}
} catch (error) {
if (error?.name === 'AbortError') {
throw error;
}
console.error('Lossless container conversion failed:', error);
}

View file

@ -1074,7 +1074,10 @@ export async function handleTrackAction(
trackDataStore.set(newEl, item);
ui.updateLikeState(newEl, 'video', item.id);
newEl.addEventListener('click', (e) => {
if (e.target.closest('.card-play-btn') || e.target.closest('.card-image-container')) {
if (
e.target.closest('.card-play-btn') ||
e.target.closest('.card-image-container')
) {
e.stopPropagation();
player.playVideo(item);
}

View file

@ -8,7 +8,7 @@ export class HlsDownloader {
const masterText = await response.text();
const variantUrl = this.getBestVariantUrl(masterUrl, masterText);
const mediaResponse = await fetch(variantUrl, { signal });
const mediaText = await mediaResponse.text();

View file

@ -166,7 +166,7 @@ export class MusicAPI {
const data = await response.json();
const result = {
videoUrl: data.videoUrl || null,
hlsUrl: data.animated || null
hlsUrl: data.animated || null,
};
this.videoArtworkCache.set(cacheKey, result);
return result;

View file

@ -203,7 +203,8 @@ export class Player {
if (coverEl) {
const videoCoverUrl = track.videoUrl || track.videoCoverUrl || track.album?.videoCoverUrl || null;
const coverUrl = videoCoverUrl || this.api.getCoverUrl(track.image || track.cover || track.album?.cover);
const coverUrl =
videoCoverUrl || this.api.getCoverUrl(track.image || track.cover || track.album?.cover);
if (videoCoverUrl) {
if (coverEl.tagName === 'IMG') {
@ -453,7 +454,7 @@ export class Player {
...video,
type: 'video',
artist: video.artist || (video.artists && video.artists[0]) || 'Unknown Artist',
album: video.album || { title: 'Video', cover: video.image || video.cover }
album: video.album || { title: 'Video', cover: video.image || video.cover },
};
this.setQueue([videoTrack], 0);
await this.playTrackFromQueue();
@ -490,12 +491,12 @@ export class Player {
const trackInfo = document.querySelector('.now-playing-bar .track-info');
const coverEl = trackInfo?.querySelector('.cover:not(#audio-player)');
if (track.type === 'video') {
if (coverEl) coverEl.style.display = 'none';
if (this.audio) {
const isInFullscreen = document.getElementById('fullscreen-cover-overlay')?.style.display === 'flex';
if (!isInFullscreen) {
this.audio.style.display = 'block';
this.audio.className = 'cover video-cover-mirror';

View file

@ -2143,9 +2143,7 @@ export function initializeSettings(scrobbler, player, api, ui) {
const newDimming = parseFloat(e.target.value);
visualizerSettings.setDimAmount(newDimming);
visualizerDimmingValue.textContent = `${(newDimming * 100).toFixed(0)}%`;
window.dispatchEvent(
new CustomEvent('visualizer-dim-change', { detail: { dimAmount: newDimming } })
);
window.dispatchEvent(new CustomEvent('visualizer-dim-change', { detail: { dimAmount: newDimming } }));
});
}

114
js/ui.js
View file

@ -343,7 +343,7 @@ export class UIRenderer {
const isUnavailable = track.isUnavailable;
const isBlocked = contentBlockingSettings?.shouldHideTrack(track);
const isVideo = track.type === 'video';
let trackImageHTML = '';
if (showCover) {
if (isVideo && this.currentPage === 'playlist') {
@ -351,7 +351,12 @@ export class UIRenderer {
} else if (isVideo && (this.currentPage === 'search' || this.currentPage === 'library')) {
trackImageHTML = `<div class="track-item-cover video-icon-placeholder" style="display: flex; align-items: center; justify-content: center; background: var(--secondary);"><svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" style="opacity: 0.7;"><path d="M8 5v14l11-7z"/></svg></div>`;
} else {
trackImageHTML = this.getCoverHTML(track.image || track.cover || track.album?.cover, 'Track Cover', 'track-item-cover', 'lazy');
trackImageHTML = this.getCoverHTML(
track.image || track.cover || track.album?.cover,
'Track Cover',
'track-item-cover',
'lazy'
);
}
}
@ -365,7 +370,9 @@ export class UIRenderer {
displayIndex = index + 1;
}
const videoIcon = isVideo ? '<span class="video-item-icon" title="Music Video" style="display: inline-flex; align-items: center; margin-right: 4px; color: var(--muted-foreground);"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m22 8-6 4 6 4V8Z"/><rect width="14" height="12" x="2" y="6" rx="2" ry="2"/></svg></span>' : '';
const videoIcon = isVideo
? '<span class="video-item-icon" title="Music Video" style="display: inline-flex; align-items: center; margin-right: 4px; color: var(--muted-foreground);"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m22 8-6 4 6 4V8Z"/><rect width="14" height="12" x="2" y="6" rx="2" ry="2"/></svg></span>'
: '';
const trackNumberHTML = `<div class="track-number">${showCover ? trackImageHTML : displayIndex}</div>`;
const explicitBadge = hasExplicitContent(track) ? this.createExplicitBadge() : '';
const qualityBadge = createQualityBadgeHTML(track);
@ -638,7 +645,13 @@ export class UIRenderer {
href: `/album/${album.id}`,
title: `${escapeHtml(album.title)} ${explicitBadge} ${qualityBadge}`,
subtitle: `${escapeHtml(artistName)}${yearDisplay}${typeLabel}`,
imageHTML: this.getCoverHTML(album.cover, escapeHtml(album.title), 'card-image', 'lazy', album.videoCoverUrl),
imageHTML: this.getCoverHTML(
album.cover,
escapeHtml(album.title),
'card-image',
'lazy',
album.videoCoverUrl
),
actionButtonsHTML: `
<button class="like-btn card-like-btn" data-action="toggle-like" data-type="album" title="Add to Liked">
${this.createHeartIcon(false)}
@ -655,10 +668,10 @@ export class UIRenderer {
createVideoCardHTML(video) {
const duration = formatTime(video.duration);
const artistName = getTrackArtists(video);
const cover = video.image || video.cover;
let imageHTML;
if (cover) {
imageHTML = this.getCoverHTML(cover, escapeHtml(video.title));
} else {
@ -998,7 +1011,8 @@ export class UIRenderer {
if (image) image.style.display = 'block';
if (visualizerContainer) visualizerContainer.style.display = 'block';
const videoCoverUrl = track.videoUrl || track.videoCoverUrl || track.album?.videoCoverUrl || null; const coverUrl = videoCoverUrl || this.api.getCoverUrl(track.album?.cover, '1280');
const videoCoverUrl = track.videoUrl || track.videoCoverUrl || track.album?.videoCoverUrl || null;
const coverUrl = videoCoverUrl || this.api.getCoverUrl(track.album?.cover, '1280');
const fsLikeBtn = document.getElementById('fs-like-btn');
if (fsLikeBtn) {
@ -1150,10 +1164,10 @@ export class UIRenderer {
const coverContainer = document.querySelector('.now-playing-bar .track-info');
const audioPlayer = document.getElementById('audio-player');
const imgCover = coverContainer?.querySelector('.cover:not(#audio-player)');
if (audioPlayer && coverContainer) {
if (imgCover) imgCover.style.display = 'none';
audioPlayer.style.display = 'block';
audioPlayer.classList.add('cover', 'video-cover-mirror');
audioPlayer.style.width = '56px';
@ -1161,7 +1175,7 @@ export class UIRenderer {
audioPlayer.style.borderRadius = 'var(--radius-sm)';
audioPlayer.style.objectFit = 'cover';
audioPlayer.style.gridArea = 'none';
if (audioPlayer.parentElement !== coverContainer) {
coverContainer.insertBefore(audioPlayer, coverContainer.firstChild);
}
@ -1752,7 +1766,9 @@ export class UIRenderer {
if (myPlaylistsContainer) {
if (visiblePlaylists.length) {
myPlaylistsContainer.innerHTML = visiblePlaylists.map((p) => this.createUserPlaylistCardHTML(p)).join('');
myPlaylistsContainer.innerHTML = visiblePlaylists
.map((p) => this.createUserPlaylistCardHTML(p))
.join('');
visiblePlaylists.forEach((playlist) => {
const el = myPlaylistsContainer.querySelector(`[data-user-playlist-id="${playlist.id}"]`);
if (el) {
@ -2231,7 +2247,13 @@ export class UIRenderer {
href: `/track/${track.id}`,
title: `${escapeHtml(getTrackTitle(track))} ${explicitBadge} ${qualityBadge}`,
subtitle: escapeHtml(getTrackArtists(track)),
imageHTML: this.getCoverHTML(track.album?.cover, escapeHtml(track.title), 'card-image', 'lazy', track.videoUrl || track.album?.videoCoverUrl),
imageHTML: this.getCoverHTML(
track.album?.cover,
escapeHtml(track.title),
'card-image',
'lazy',
track.videoUrl || track.album?.videoCoverUrl
),
actionButtonsHTML: `
<button class="like-btn card-like-btn" data-action="toggle-like" data-type="track" title="Add to Liked">
${this.createHeartIcon(false)}
@ -2634,19 +2656,23 @@ export class UIRenderer {
video.replaceWith(img);
};
video.addEventListener('error', (e) => {
if (video.src === result.videoUrl && result.hlsUrl) {
this.setupHlsVideo(video, { videoUrl: null, hlsUrl: result.hlsUrl }, img);
return;
}
console.warn('Video decoding error:', e);
video.replaceWith(img);
}, true);
video.addEventListener(
'error',
(e) => {
if (video.src === result.videoUrl && result.hlsUrl) {
this.setupHlsVideo(video, { videoUrl: null, hlsUrl: result.hlsUrl }, img);
return;
}
console.warn('Video decoding error:', e);
video.replaceWith(img);
},
true
);
img.replaceWith(video);
this.setupHlsVideo(video, result, img);
// If HLS, dont play
const hls = video._hls;
if (hls) {
@ -2932,7 +2958,8 @@ export class UIRenderer {
this.setupHlsVideo(video, result, currentImageEl);
currentImageEl.replaceWith(video);
} }
}
}
});
}
@ -3696,7 +3723,8 @@ export class UIRenderer {
// Try to get cover from first track album
if (tracks.length > 0 && tracks[0].album?.cover) {
const firstTrack = tracks[0];
let videoCoverUrl = firstTrack.videoUrl || firstTrack.videoCoverUrl || firstTrack.album?.videoCoverUrl || null;
let videoCoverUrl =
firstTrack.videoUrl || firstTrack.videoCoverUrl || firstTrack.album?.videoCoverUrl || null;
if (!videoCoverUrl && (firstTrack.album || firstTrack.type === 'video')) {
const fetchArtwork = () => {
@ -3727,14 +3755,17 @@ export class UIRenderer {
};
if (firstTrack.type === 'video') {
this.api.getVideoStreamUrl(firstTrack.id).then((url) => {
if (url) {
firstTrack.videoUrl = url;
this.renderMixPage(mixId);
} else {
fetchArtwork();
}
}).catch(fetchArtwork);
this.api
.getVideoStreamUrl(firstTrack.id)
.then((url) => {
if (url) {
firstTrack.videoUrl = url;
this.renderMixPage(mixId);
} else {
fetchArtwork();
}
})
.catch(fetchArtwork);
} else {
fetchArtwork();
}
@ -4833,14 +4864,17 @@ export class UIRenderer {
};
if (track.type === 'video') {
this.api.getVideoStreamUrl(track.id).then((url) => {
if (url) {
track.videoUrl = url;
this.renderTrackPage(trackId, provider);
} else {
fetchArtwork();
}
}).catch(fetchArtwork);
this.api
.getVideoStreamUrl(track.id)
.then((url) => {
if (url) {
track.videoUrl = url;
this.renderTrackPage(trackId, provider);
} else {
fetchArtwork();
}
})
.catch(fetchArtwork);
} else {
fetchArtwork();
}

View file

@ -5348,7 +5348,7 @@ img[src=''] {
align-items: flex-start;
padding: 2rem 2rem 6rem;
pointer-events: none;
background: linear-gradient(to top, rgba(0,0,0,0.6) 0%, rgba(0,0,0,0.2) 15%, transparent 40%);
background: linear-gradient(to top, rgb(0, 0, 0, 0.6) 0%, rgb(0, 0, 0, 0.2) 15%, transparent 40%);
}
#fullscreen-cover-overlay.is-video-mode .fullscreen-track-info {
@ -5366,13 +5366,13 @@ img[src=''] {
#fullscreen-cover-overlay.is-video-mode #fullscreen-track-title {
font-size: 1.1rem;
font-weight: 600;
text-shadow: 0 1px 3px rgba(0,0,0,0.8);
text-shadow: 0 1px 3px rgb(0, 0, 0, 0.8);
margin-bottom: 0.1rem;
}
#fullscreen-cover-overlay.is-video-mode #fullscreen-track-artist {
font-size: 0.9rem;
text-shadow: 0 1px 2px rgba(0,0,0,0.8);
text-shadow: 0 1px 2px rgb(0, 0, 0, 0.8);
opacity: 0.7;
}
@ -5383,13 +5383,15 @@ img[src=''] {
right: 2rem;
max-width: 500px;
margin: 0 auto;
background: rgba(15, 15, 15, 0.5);
background: rgb(15, 15, 15, 0.5);
backdrop-filter: blur(12px);
padding: 0.6rem 1rem;
border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.05);
box-shadow: 0 4px 20px rgba(0,0,0,0.4);
transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.4s ease;
border: 1px solid rgb(255, 255, 255, 0.05);
box-shadow: 0 4px 20px rgb(0, 0, 0, 0.4);
transition:
transform 0.4s cubic-bezier(0.4, 0, 0.2, 1),
opacity 0.4s ease;
pointer-events: auto;
z-index: 100;
}
@ -5427,7 +5429,6 @@ img[src=''] {
margin-top: 0.5rem;
}
#fullscreen-cover-overlay.ui-hidden .fullscreen-main-view,
#fullscreen-cover-overlay.ui-hidden .fullscreen-controls,
#fullscreen-cover-overlay.ui-hidden #fullscreen-next-track,
@ -8150,7 +8151,7 @@ body:has(#side-panel.active) #close-fullscreen-cover-btn {
position: absolute;
bottom: 8px;
right: 8px;
background: rgba(0, 0, 0, 0.7);
background: rgb(0, 0, 0, 0.7);
color: white;
padding: 2px 6px;
border-radius: 4px;
@ -8178,7 +8179,7 @@ body:has(#side-panel.active) #close-fullscreen-cover-btn {
object-fit: contain;
}
.search-tab[data-tab="videos"] {
.search-tab[data-tab='videos'] {
display: flex;
align-items: center;
gap: 0.5rem;