feat(artists): Artist Banners
This commit is contained in:
parent
4f36b60c82
commit
7dd38bac80
6 changed files with 217 additions and 0 deletions
11
index.html
11
index.html
|
|
@ -2603,6 +2603,7 @@
|
|||
|
||||
<div id="page-artist" class="page">
|
||||
<header class="detail-header">
|
||||
<div id="artist-detail-banner-container" class="detail-header-banner"></div>
|
||||
<img
|
||||
id="artist-detail-image"
|
||||
src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"
|
||||
|
|
@ -3392,6 +3393,16 @@
|
|||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="info">
|
||||
<span class="label">Artist Banners</span>
|
||||
<span class="description">Display video banners on artist pages</span>
|
||||
</div>
|
||||
<label class="toggle-switch">
|
||||
<input type="checkbox" id="artist-banners-toggle" checked />
|
||||
<span class="slider"></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="setting-item">
|
||||
<div class="info">
|
||||
<span class="label">Compact Albums</span>
|
||||
|
|
|
|||
|
|
@ -277,6 +277,47 @@ export class MusicAPI {
|
|||
return this.tidalAPI.getArtistPictureSrcset(this.stripProviderPrefix(id));
|
||||
}
|
||||
|
||||
async getArtistBanner(artistName) {
|
||||
const cacheKey = `banner-${artistName}`.toLowerCase();
|
||||
if (this.videoArtworkCache.has(cacheKey)) {
|
||||
return this.videoArtworkCache.get(cacheKey);
|
||||
}
|
||||
|
||||
try {
|
||||
const url = `https://artwork-boidu-dev.samidy.workers.dev/artist?a=${encodeURIComponent(artistName)}`;
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) return null;
|
||||
const data = await response.json();
|
||||
|
||||
let hlsUrl = null;
|
||||
if (data.animated) {
|
||||
if (typeof data.animated === 'string') {
|
||||
hlsUrl = data.animated;
|
||||
} else if (typeof data.animated === 'object') {
|
||||
hlsUrl = data.animated.hls || data.animated.url || data.animated.hlsUrl || data.animated.videoUrl;
|
||||
|
||||
if (!hlsUrl) {
|
||||
for (const key in data.animated) {
|
||||
if (typeof data.animated[key] === 'string' && data.animated[key].includes('.m3u8')) {
|
||||
hlsUrl = data.animated[key];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const result = {
|
||||
hlsUrl: hlsUrl,
|
||||
};
|
||||
this.videoArtworkCache.set(cacheKey, result);
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.warn('Failed to fetch artist banner:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
extractStreamUrlFromManifest(manifest) {
|
||||
return this.tidalAPI.extractStreamUrlFromManifest(manifest);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {
|
|||
backgroundSettings,
|
||||
dynamicColorSettings,
|
||||
cardSettings,
|
||||
artistBannerSettings,
|
||||
waveformSettings,
|
||||
replayGainSettings,
|
||||
downloadQualitySettings,
|
||||
|
|
@ -5675,6 +5676,15 @@ export async function initializeSettings(scrobbler, player, api, ui) {
|
|||
});
|
||||
}
|
||||
|
||||
// Artist Banners Toggle
|
||||
const artistBannersToggle = document.getElementById('artist-banners-toggle');
|
||||
if (artistBannersToggle) {
|
||||
artistBannersToggle.checked = artistBannerSettings.isEnabled();
|
||||
artistBannersToggle.addEventListener('change', (e) => {
|
||||
artistBannerSettings.setEnabled(e.target.checked);
|
||||
});
|
||||
}
|
||||
|
||||
// Compact Album Toggle
|
||||
const compactAlbumToggle = document.getElementById('compact-album-toggle');
|
||||
if (compactAlbumToggle) {
|
||||
|
|
|
|||
|
|
@ -687,6 +687,23 @@ export const cardSettings = {
|
|||
},
|
||||
};
|
||||
|
||||
export const artistBannerSettings = {
|
||||
STORAGE_KEY: 'artist-banners-enabled',
|
||||
|
||||
isEnabled() {
|
||||
try {
|
||||
const val = localStorage.getItem(this.STORAGE_KEY);
|
||||
return val === null ? true : val === 'true';
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
|
||||
setEnabled(enabled) {
|
||||
localStorage.setItem(this.STORAGE_KEY, enabled ? 'true' : 'false');
|
||||
},
|
||||
};
|
||||
|
||||
export const replayGainSettings = {
|
||||
STORAGE_KEY_MODE: 'replay-gain-mode', // 'off', 'track', 'album'
|
||||
STORAGE_KEY_PREAMP: 'replay-gain-preamp',
|
||||
|
|
|
|||
44
js/ui.js
44
js/ui.js
|
|
@ -29,6 +29,7 @@ import {
|
|||
contentBlockingSettings,
|
||||
settingsUiState,
|
||||
fullscreenCoverNoRoundSettings,
|
||||
artistBannerSettings,
|
||||
} from './storage.js';
|
||||
import { db } from './db.js';
|
||||
import { getVibrantColorFromImage } from './vibrant-color.js';
|
||||
|
|
@ -4775,6 +4776,16 @@ export class UIRenderer {
|
|||
await this.showPage('artist');
|
||||
this.currentArtistId = artistId;
|
||||
|
||||
const bannerContainer = document.getElementById('artist-detail-banner-container');
|
||||
if (bannerContainer) {
|
||||
const oldVideo = bannerContainer.querySelector('video');
|
||||
if (oldVideo && oldVideo._hls) {
|
||||
oldVideo._hls.destroy();
|
||||
}
|
||||
bannerContainer.innerHTML = '';
|
||||
bannerContainer.style.opacity = '0';
|
||||
}
|
||||
|
||||
const imageEl = document.getElementById('artist-detail-image');
|
||||
const nameEl = document.getElementById('artist-detail-name');
|
||||
const metaEl = document.getElementById('artist-detail-meta');
|
||||
|
|
@ -4823,6 +4834,39 @@ export class UIRenderer {
|
|||
try {
|
||||
const artist = await this.api.getArtist(artistId, provider);
|
||||
|
||||
const currentId = this.currentArtistId;
|
||||
this.api
|
||||
.getArtistBanner(artist.name)
|
||||
.then(async (banner) => {
|
||||
if (this.currentArtistId !== currentId) return;
|
||||
|
||||
if (banner && banner.hlsUrl && bannerContainer) {
|
||||
const video = document.createElement('video');
|
||||
video.autoplay = true;
|
||||
video.loop = true;
|
||||
video.muted = true;
|
||||
video.playsInline = true;
|
||||
video.setAttribute('muted', '');
|
||||
video.setAttribute('autoplay', '');
|
||||
video.setAttribute('playsinline', '');
|
||||
video.style.opacity = '1';
|
||||
|
||||
try {
|
||||
await this.setupHlsVideo(video, banner, null);
|
||||
if (this.currentArtistId === currentId) {
|
||||
bannerContainer.appendChild(video);
|
||||
bannerContainer.style.opacity = '1';
|
||||
video.play().catch(() => {});
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to setup artist banner video:', e);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
console.warn('Failed to fetch artist banner:', e);
|
||||
});
|
||||
|
||||
// Handle Biography
|
||||
if (bioEl) {
|
||||
// Pre-define regex patterns for better performance
|
||||
|
|
|
|||
94
styles.css
94
styles.css
|
|
@ -2542,6 +2542,100 @@ body.multi-select-mode .track-item:hover {
|
|||
border-radius: var(--radius-full);
|
||||
}
|
||||
|
||||
.detail-header-banner {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 0;
|
||||
overflow: hidden;
|
||||
opacity: 0;
|
||||
transition: opacity 1s ease-in-out;
|
||||
}
|
||||
|
||||
.detail-header-banner video {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
filter: brightness(0.6);
|
||||
display: block;
|
||||
}
|
||||
|
||||
#page-artist .detail-header {
|
||||
position: relative;
|
||||
padding: 12rem 3rem 4rem 3rem;
|
||||
border-radius: 0;
|
||||
overflow: hidden;
|
||||
margin-top: -8rem;
|
||||
margin-left: calc(var(--spacing-xl) * -1);
|
||||
margin-right: calc(var(--spacing-xl) * -1);
|
||||
margin-bottom: var(--spacing-xl);
|
||||
min-height: 550px;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
background-color: var(--card);
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
#page-artist .detail-header {
|
||||
margin-top: -7rem;
|
||||
margin-left: calc(var(--spacing-lg) * -1);
|
||||
margin-right: calc(var(--spacing-lg) * -1);
|
||||
padding: 10rem 2rem 3rem 2rem;
|
||||
min-height: 450px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
#page-artist .detail-header {
|
||||
margin-top: -6rem;
|
||||
margin-left: calc(var(--spacing-md) * -1);
|
||||
margin-right: calc(var(--spacing-md) * -1);
|
||||
padding: 8rem 1rem 2rem 1rem;
|
||||
min-height: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
#page-artist .detail-header {
|
||||
margin-top: calc((var(--spacing-sm) + var(--spacing-xl)) * -1);
|
||||
margin-left: calc(var(--spacing-sm) * -1);
|
||||
margin-right: calc(var(--spacing-sm) * -1);
|
||||
}
|
||||
}
|
||||
|
||||
.detail-header-banner::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
rgba(0, 0, 0, 0.4) 0%,
|
||||
rgba(0, 0, 0, 0) 40%,
|
||||
rgba(0, 0, 0, 0.2) 70%,
|
||||
var(--background) 100%
|
||||
);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
#page-artist .detail-header-image {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
border: 4px solid var(--background);
|
||||
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.6);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
#page-artist .detail-header-info {
|
||||
z-index: 2;
|
||||
text-shadow: 0 2px 15px rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
|
||||
|
||||
.detail-header-info .type {
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
|
|
|
|||
Loading…
Reference in a new issue