improved artist ui

This commit is contained in:
cool.gitter.choco
2025-02-05 21:25:45 -06:00
parent 081bb5096c
commit 0ae1ca5a8d
4 changed files with 251 additions and 212 deletions

View File

@@ -74,52 +74,6 @@ body {
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
} }
/* Playlist Header */
#playlist-header {
display: flex;
gap: 20px;
margin-bottom: 2rem;
align-items: center;
padding-bottom: 1.5rem;
border-bottom: 1px solid #2a2a2a;
flex-wrap: wrap;
transition: all 0.3s ease;
}
#playlist-image {
width: 200px;
height: 200px;
object-fit: cover;
border-radius: 8px;
flex-shrink: 0;
box-shadow: 0 4px 8px rgba(0,0,0,0.3);
transition: transform 0.3s ease;
}
#playlist-image:hover {
transform: scale(1.02);
}
#playlist-info {
flex: 1;
min-width: 0;
}
#playlist-name {
font-size: 2.5rem;
margin-bottom: 0.5rem;
background: linear-gradient(90deg, #1db954, #17a44b);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
#playlist-owner,
#playlist-stats,
#playlist-description {
font-size: 1.1rem;
color: #b3b3b3;
margin-bottom: 0.5rem;
}
/* Tracks Container */ /* Tracks Container */
#tracks-container { #tracks-container {
margin-top: 2rem; margin-top: 2rem;
@@ -182,17 +136,6 @@ body {
color: #b3b3b3; color: #b3b3b3;
} }
.track-album {
max-width: 200px;
font-size: 0.9rem;
color: #b3b3b3;
margin-left: 1rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
text-align: right;
}
.track-duration { .track-duration {
width: 60px; width: 60px;
text-align: right; text-align: right;
@@ -244,27 +187,37 @@ body {
transform: scale(0.98); transform: scale(0.98);
} }
/* Circular Variant for Compact Areas (e.g., in a queue list) */ /* Circular Variant for Compact Areas */
.download-btn--circle { .download-btn--circle {
width: 32px; width: 32px;
height: 32px; height: 32px;
padding: 0; padding: 0;
border-radius: 50%; border-radius: 50%;
font-size: 0; font-size: 0; /* Hide any text */
} background-color: #1db954;
border: none;
.download-btn--circle::before {
content: "↓";
font-size: 16px;
color: #fff;
display: inline-block;
}
/* Icon next to text */
.download-btn .btn-icon {
margin-right: 0.5rem;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center;
cursor: pointer;
transition: background-color 0.2s ease, transform 0.2s ease;
margin: 0.5rem;
}
.download-btn--circle img {
width: 20px;
height: 20px;
filter: brightness(0) invert(1);
display: block;
}
.download-btn--circle:hover {
background-color: #17a44b;
transform: scale(1.05);
}
.download-btn--circle:active {
transform: scale(0.98);
} }
/* Home Button Styling */ /* Home Button Styling */
@@ -279,7 +232,7 @@ body {
.home-btn img { .home-btn img {
width: 32px; width: 32px;
height: 32px; height: 32px;
filter: invert(1); /* Makes the SVG icon appear white */ filter: invert(1);
transition: transform 0.2s ease; transition: transform 0.2s ease;
} }
@@ -291,11 +244,8 @@ body {
transform: scale(0.98); transform: scale(0.98);
} }
/* Download Queue Toggle Button */ /* Queue Toggle Button */
.queue-toggle { .queue-toggle {
position: fixed;
bottom: 20px;
right: 20px;
background: #1db954; background: #1db954;
color: #fff; color: #fff;
border: none; border: none;
@@ -308,125 +258,130 @@ body {
box-shadow: 0 2px 8px rgba(0,0,0,0.3); box-shadow: 0 2px 8px rgba(0,0,0,0.3);
transition: background-color 0.3s ease, transform 0.2s ease; transition: background-color 0.3s ease, transform 0.2s ease;
z-index: 1002; z-index: 1002;
/* Remove any fixed positioning by default for mobile; fixed positioning remains for larger screens */
} }
.queue-toggle:hover { /* Actions Container for Small Screens */
background: #1ed760; #album-actions {
transform: scale(1.05); display: flex;
} width: 100%;
justify-content: space-between;
.queue-toggle:active { align-items: center;
transform: scale(1); margin-top: 1rem;
} }
/* Responsive Styles */ /* Responsive Styles */
/* Medium Devices (Tablets) */ /* Medium Devices (Tablets) */
@media (max-width: 768px) { @media (max-width: 768px) {
#album-header, #playlist-header { #album-header {
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
text-align: center; text-align: center;
} }
#album-image, #playlist-image { #album-image {
width: 180px; width: 180px;
height: 180px; height: 180px;
margin-bottom: 1rem; margin-bottom: 1rem;
} }
#album-name, #playlist-name { #album-name {
font-size: 2rem; font-size: 2rem;
} }
#album-artist, #album-artist,
#album-stats, #album-stats {
#playlist-owner, font-size: 1rem;
#playlist-stats,
#playlist-description {
font-size: 1rem;
} }
.track { .track {
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;
} }
.track-album,
.track-duration { .track-duration {
margin-left: 0; margin-left: 0;
margin-top: 0.5rem; margin-top: 0.5rem;
width: 100%; width: 100%;
text-align: left; text-align: left;
} }
} }
/* Small Devices (Mobile Phones) */ /* Small Devices (Mobile Phones) */
@media (max-width: 480px) { @media (max-width: 480px) {
#app { #app {
padding: 10px; padding: 10px;
} }
#album-name, #playlist-name { #album-header {
font-size: 1.75rem; flex-direction: column;
align-items: center;
text-align: center;
} }
/* Center the album cover */
#album-image {
margin: 0 auto;
}
#album-name {
font-size: 1.75rem;
}
#album-artist, #album-artist,
#album-stats, #album-stats,
#album-copyright, #album-copyright {
#playlist-owner, font-size: 0.9rem;
#playlist-stats,
#playlist-description {
font-size: 0.9rem;
} }
.track { .track {
padding: 0.8rem; padding: 0.8rem;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
text-align: center; text-align: center;
} }
.track-number { .track-number {
font-size: 0.9rem; font-size: 0.9rem;
margin-right: 0; margin-right: 0;
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
} }
.track-info {
align-items: center;
}
.track-album,
.track-duration { .track-duration {
margin-left: 0; margin-left: 0;
margin-top: 0.5rem; margin-top: 0.5rem;
width: 100%; width: 100%;
text-align: center; text-align: center;
}
/* Ensure the actions container lays out buttons properly */
#album-actions {
flex-direction: row;
justify-content: space-between;
}
/* Remove extra margins from the queue toggle */
.queue-toggle {
position: static;
margin: 0;
} }
} }
/* Prevent anchor links from appearing all blue */ /* Prevent anchor links from appearing all blue */
a { a {
color: inherit; /* Inherit color from the parent */ color: inherit;
text-decoration: none; /* Remove default underline */ text-decoration: none;
transition: color 0.2s ease; transition: color 0.2s ease;
} }
a:hover, a:hover,
a:focus { a:focus {
color: #1db954; /* Change to a themed green on hover/focus */ color: #1db954;
text-decoration: underline; text-decoration: underline;
} }
/* Override the default pseudo-element for the circular download button */
/* (Optional) Override for circular download button pseudo-element */
.download-btn--circle::before { .download-btn--circle::before {
content: none; content: none;
} }
/* Style the icon inside the circular download button */
.download-btn--circle img {
width: 20px;
height: 20px;
filter: brightness(0) invert(1); /* ensures the icon appears white */
display: block;
}

View File

@@ -98,7 +98,49 @@ body {
gap: 1rem; gap: 1rem;
} }
/* Track Card (for Albums) */ /* Unified Album Card (Desktop & Mobile) */
.album-card {
background: #181818;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
transition: transform 0.2s ease, box-shadow 0.2s ease;
display: flex;
align-items: center;
padding: 0.5rem;
}
.album-card:hover {
transform: translateY(-5px);
box-shadow: 0 4px 8px rgba(0,0,0,0.3);
}
/* Album Cover Image */
.album-cover {
width: 80px;
height: 80px;
object-fit: cover;
border-radius: 4px;
margin-right: 1rem;
}
/* Album Info */
.album-info {
flex: 1;
}
.album-title {
font-size: 1rem;
font-weight: bold;
margin-bottom: 0.25rem;
}
.album-artist {
font-size: 0.9rem;
color: #b3b3b3;
}
/* Track Card (for Albums or Songs) */
.track { .track {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -221,7 +263,6 @@ body {
margin: 0.5rem; margin: 0.5rem;
} }
/* Image inside circular download button */
.download-btn--circle img { .download-btn--circle img {
width: 20px; width: 20px;
height: 20px; height: 20px;
@@ -302,7 +343,6 @@ body {
font-size: 1.75rem; font-size: 1.75rem;
} }
/* Adjust album card layout */
.track { .track {
padding: 0.8rem; padding: 0.8rem;
flex-direction: column; flex-direction: column;
@@ -327,6 +367,38 @@ body {
width: 100%; width: 100%;
text-align: center; text-align: center;
} }
/* Mobile Album Grid Styles Inspired by Spotify */
.albums-list {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1rem;
}
/* Adjust album card for mobile grid */
.album-card {
flex-direction: column;
padding: 0;
}
.album-cover {
width: 100%;
aspect-ratio: 1 / 1;
margin: 0;
}
.album-info {
padding: 0.5rem;
text-align: center;
}
.album-title {
font-size: 1rem;
}
.album-artist {
font-size: 0.9rem;
}
} }
/* Prevent anchor links from appearing blue */ /* Prevent anchor links from appearing blue */

View File

@@ -29,18 +29,16 @@ document.addEventListener('DOMContentLoaded', () => {
}); });
function renderAlbum(album) { function renderAlbum(album) {
// Hide loading and error messages.
document.getElementById('loading').classList.add('hidden'); document.getElementById('loading').classList.add('hidden');
document.getElementById('error').classList.add('hidden'); document.getElementById('error').classList.add('hidden');
const baseUrl = window.location.origin; const baseUrl = window.location.origin;
// Album header info with embedded links // Set album header info.
// Album name becomes a link to the album page.
document.getElementById('album-name').innerHTML = document.getElementById('album-name').innerHTML =
`<a href="${baseUrl}/album/${album.id}">${album.name}</a>`; `<a href="${baseUrl}/album/${album.id}">${album.name}</a>`;
// Album artists become links to their artist pages.
document.getElementById('album-artist').innerHTML = document.getElementById('album-artist').innerHTML =
`By ${album.artists.map(artist => `<a href="${baseUrl}/artist/${artist.id}">${artist.name}</a>`).join(', ')}`; `By ${album.artists.map(artist => `<a href="${baseUrl}/artist/${artist.id}">${artist.name}</a>`).join(', ')}`;
@@ -54,19 +52,19 @@ function renderAlbum(album) {
const image = album.images[0]?.url || 'placeholder.jpg'; const image = album.images[0]?.url || 'placeholder.jpg';
document.getElementById('album-image').src = image; document.getElementById('album-image').src = image;
// Home Button using SVG icon // Create (if needed) the Home Button.
let homeButton = document.getElementById('homeButton'); let homeButton = document.getElementById('homeButton');
if (!homeButton) { if (!homeButton) {
homeButton = document.createElement('button'); homeButton = document.createElement('button');
homeButton.id = 'homeButton'; homeButton.id = 'homeButton';
homeButton.className = 'home-btn'; homeButton.className = 'home-btn';
// Create an image element for the home icon.
const homeIcon = document.createElement('img'); const homeIcon = document.createElement('img');
homeIcon.src = '/static/images/home.svg'; homeIcon.src = '/static/images/home.svg';
homeIcon.alt = 'Home'; homeIcon.alt = 'Home';
homeButton.appendChild(homeIcon); homeButton.appendChild(homeIcon);
// Insert as first child of album-header.
const headerContainer = document.getElementById('album-header'); const headerContainer = document.getElementById('album-header');
headerContainer.insertBefore(homeButton, headerContainer.firstChild); headerContainer.insertBefore(homeButton, headerContainer.firstChild);
} }
@@ -74,7 +72,7 @@ function renderAlbum(album) {
window.location.href = window.location.origin; window.location.href = window.location.origin;
}); });
// Download Album Button // Create (if needed) the Download Album Button.
let downloadAlbumBtn = document.getElementById('downloadAlbumBtn'); let downloadAlbumBtn = document.getElementById('downloadAlbumBtn');
if (!downloadAlbumBtn) { if (!downloadAlbumBtn) {
downloadAlbumBtn = document.createElement('button'); downloadAlbumBtn = document.createElement('button');
@@ -85,6 +83,7 @@ function renderAlbum(album) {
} }
downloadAlbumBtn.addEventListener('click', () => { downloadAlbumBtn.addEventListener('click', () => {
// Remove any other download buttons (keeping the full-album button in place).
document.querySelectorAll('.download-btn').forEach(btn => { document.querySelectorAll('.download-btn').forEach(btn => {
if (btn.id !== 'downloadAlbumBtn') btn.remove(); if (btn.id !== 'downloadAlbumBtn') btn.remove();
}); });
@@ -92,15 +91,17 @@ function renderAlbum(album) {
downloadAlbumBtn.disabled = true; downloadAlbumBtn.disabled = true;
downloadAlbumBtn.textContent = 'Queueing...'; downloadAlbumBtn.textContent = 'Queueing...';
downloadWholeAlbum(album).then(() => { downloadWholeAlbum(album)
downloadAlbumBtn.textContent = 'Queued!'; .then(() => {
}).catch(err => { downloadAlbumBtn.textContent = 'Queued!';
showError('Failed to queue album download: ' + err.message); })
downloadAlbumBtn.disabled = false; .catch(err => {
}); showError('Failed to queue album download: ' + err.message);
downloadAlbumBtn.disabled = false;
});
}); });
// Render tracks // Render each track.
const tracksList = document.getElementById('tracks-list'); const tracksList = document.getElementById('tracks-list');
tracksList.innerHTML = ''; tracksList.innerHTML = '';
@@ -108,36 +109,54 @@ function renderAlbum(album) {
const trackElement = document.createElement('div'); const trackElement = document.createElement('div');
trackElement.className = 'track'; trackElement.className = 'track';
trackElement.innerHTML = ` trackElement.innerHTML = `
<div class="track-number">${index + 1}</div> <div class="track-number">${index + 1}</div>
<div class="track-info"> <div class="track-info">
<div class="track-name"> <div class="track-name">
<a href="${baseUrl}/track/${track.id}">${track.name}</a> <a href="${baseUrl}/track/${track.id}">${track.name}</a>
</div>
<div class="track-artist">
${track.artists.map(a => `<a href="${baseUrl}/artist/${a.id}">${a.name}</a>`).join(', ')}
</div>
</div> </div>
<div class="track-artist"> <div class="track-duration">${msToTime(track.duration_ms)}</div>
${track.artists.map(a => `<a href="${baseUrl}/artist/${a.id}">${a.name}</a>`).join(', ')} <button class="download-btn download-btn--circle"
</div> data-url="${track.external_urls.spotify}"
</div> data-type="track"
<div class="track-duration">${msToTime(track.duration_ms)}</div> data-name="${track.name}"
<button class="download-btn download-btn--circle" title="Download">
data-url="${track.external_urls.spotify}" <img src="/static/images/download.svg" alt="Download">
data-type="track" </button>
data-name="${track.name}" `;
title="Download">
<img src="/static/images/download.svg" alt="Download">
</button>
`;
tracksList.appendChild(trackElement); tracksList.appendChild(trackElement);
}); });
// Reveal header and track list.
document.getElementById('album-header').classList.remove('hidden'); document.getElementById('album-header').classList.remove('hidden');
document.getElementById('tracks-container').classList.remove('hidden'); document.getElementById('tracks-container').classList.remove('hidden');
attachDownloadListeners(); attachDownloadListeners();
// If on a small screen, re-arrange the action buttons.
if (window.innerWidth <= 480) {
let actionsContainer = document.getElementById('album-actions');
if (!actionsContainer) {
actionsContainer = document.createElement('div');
actionsContainer.id = 'album-actions';
document.getElementById('album-header').appendChild(actionsContainer);
}
// Append in the desired order: Home, Download, then Queue Toggle (if exists).
actionsContainer.innerHTML = ''; // Clear any previous content
actionsContainer.appendChild(document.getElementById('homeButton'));
actionsContainer.appendChild(document.getElementById('downloadAlbumBtn'));
const queueToggle = document.querySelector('.queue-toggle');
if (queueToggle) {
actionsContainer.appendChild(queueToggle);
}
}
} }
async function downloadWholeAlbum(album) { async function downloadWholeAlbum(album) {
const url = album.external_urls.spotify; const url = album.external_urls.spotify;
startDownload(url, 'album', { name: album.name }); return startDownload(url, 'album', { name: album.name });
} }
function msToTime(duration) { function msToTime(duration) {
@@ -160,19 +179,14 @@ function attachDownloadListeners() {
const url = e.currentTarget.dataset.url; const url = e.currentTarget.dataset.url;
const type = e.currentTarget.dataset.type; const type = e.currentTarget.dataset.type;
const name = e.currentTarget.dataset.name || extractName(url); const name = e.currentTarget.dataset.name || extractName(url);
const albumType = e.currentTarget.dataset.albumType; // Remove the button immediately after click.
// Remove the button after click
e.currentTarget.remove(); e.currentTarget.remove();
startDownload(url, type, { name });
// Start the download for this track.
startDownload(url, type, { name }, albumType);
}); });
}); });
} }
async function startDownload(url, type, item, albumType) { async function startDownload(url, type, item, albumType) {
// Retrieve configuration (if any) from localStorage
const config = JSON.parse(localStorage.getItem('activeConfig')) || {}; const config = JSON.parse(localStorage.getItem('activeConfig')) || {};
const { const {
fallback = false, fallback = false,
@@ -186,17 +200,14 @@ async function startDownload(url, type, item, albumType) {
const service = url.includes('open.spotify.com') ? 'spotify' : 'deezer'; const service = url.includes('open.spotify.com') ? 'spotify' : 'deezer';
let apiUrl = ''; let apiUrl = '';
// Build API URL based on the download type.
if (type === 'album') { if (type === 'album') {
apiUrl = `/api/album/download?service=${service}&url=${encodeURIComponent(url)}`; apiUrl = `/api/album/download?service=${service}&url=${encodeURIComponent(url)}`;
} else if (type === 'artist') { } else if (type === 'artist') {
apiUrl = `/api/artist/download?service=${service}&artist_url=${encodeURIComponent(url)}&album_type=${encodeURIComponent(albumType || 'album,single,compilation')}`; apiUrl = `/api/artist/download?service=${service}&artist_url=${encodeURIComponent(url)}&album_type=${encodeURIComponent(albumType || 'album,single,compilation')}`;
} else { } else {
// Default is track download.
apiUrl = `/api/${type}/download?service=${service}&url=${encodeURIComponent(url)}`; apiUrl = `/api/${type}/download?service=${service}&url=${encodeURIComponent(url)}`;
} }
// Append account and quality details.
if (fallback && service === 'spotify') { if (fallback && service === 'spotify') {
apiUrl += `&main=${deezer}&fallback=${spotify}`; apiUrl += `&main=${deezer}&fallback=${spotify}`;
apiUrl += `&quality=${deezerQuality}&fall_quality=${spotifyQuality}`; apiUrl += `&quality=${deezerQuality}&fall_quality=${spotifyQuality}`;
@@ -212,9 +223,12 @@ async function startDownload(url, type, item, albumType) {
try { try {
const response = await fetch(apiUrl); const response = await fetch(apiUrl);
const data = await response.json(); const data = await response.json();
// Add the download to the queue using the working queue implementation.
downloadQueue.addDownload(item, type, data.prg_file); downloadQueue.addDownload(item, type, data.prg_file);
} catch (error) { } catch (error) {
showError('Download failed: ' + error.message); showError('Download failed: ' + error.message);
} }
} }
function extractName(url) {
return url;
}

View File

@@ -85,12 +85,14 @@ function renderArtist(artistData, artistId) {
downloadArtistBtn.disabled = true; downloadArtistBtn.disabled = true;
downloadArtistBtn.textContent = 'Queueing...'; downloadArtistBtn.textContent = 'Queueing...';
downloadWholeArtist(artistData).then(() => { downloadWholeArtist(artistData)
downloadArtistBtn.textContent = 'Queued!'; .then(() => {
}).catch(err => { downloadArtistBtn.textContent = 'Queued!';
showError('Failed to queue artist download: ' + err.message); })
downloadArtistBtn.disabled = false; .catch(err => {
}); showError('Failed to queue artist download: ' + err.message);
downloadArtistBtn.disabled = false;
});
}); });
// Group albums by album type. // Group albums by album type.
@@ -128,24 +130,20 @@ function renderArtist(artistData, artistId) {
// Container for individual albums in this group. // Container for individual albums in this group.
const albumsContainer = document.createElement('div'); const albumsContainer = document.createElement('div');
albumsContainer.className = 'albums-list'; albumsContainer.className = 'albums-list';
albums.forEach((album, index) => { albums.forEach(album => {
const albumElement = document.createElement('div'); const albumElement = document.createElement('div');
albumElement.className = 'track'; // reusing styling from the playlist view // Build a unified album card markup that works for both desktop and mobile.
albumElement.className = 'album-card';
// Use an <a> around the album image and name.
albumElement.innerHTML = ` albumElement.innerHTML = `
<div class="track-number">${index + 1}</div>
<a href="/album/${album.id}" class="album-link"> <a href="/album/${album.id}" class="album-link">
<img class="track-image" src="${album.images[1]?.url || album.images[0]?.url || 'placeholder.jpg'}" <img src="${album.images[1]?.url || album.images[0]?.url || 'placeholder.jpg'}"
alt="Album cover" alt="Album cover"
style="width: 64px; height: 64px; border-radius: 4px; margin-right: 1rem;"> class="album-cover">
</a> </a>
<div class="track-info"> <div class="album-info">
<a href="/album/${album.id}" class="track-name">${album.name}</a> <div class="album-title">${album.name}</div>
<div class="track-artist"></div> <div class="album-artist">${album.artists.map(a => a.name).join(', ')}</div>
</div> </div>
<div class="track-album">${album.release_date}</div>
<div class="track-duration">${album.total_tracks} tracks</div>
<button class="download-btn download-btn--circle" <button class="download-btn download-btn--circle"
data-url="${album.external_urls.spotify}" data-url="${album.external_urls.spotify}"
data-type="album" data-type="album"