style: auto-fix linting issues
This commit is contained in:
parent
b7bc90f4f1
commit
4ede3b2664
23 changed files with 413 additions and 273 deletions
|
|
@ -22,4 +22,4 @@ UI:
|
|||
| QQDL | https://tidal.qqdl.site/ |
|
||||
| Arjix | https://music.arjix.dev/ |
|
||||
| Spofree | https://spo.free.nf |
|
||||
| Mappl | https://mappl.tv/music |
|
||||
| Mappl | https://mappl.tv/music |
|
||||
|
|
|
|||
|
|
@ -67,14 +67,16 @@ class ServerAPI {
|
|||
export async function onRequest(context) {
|
||||
const { request, params, env } = context;
|
||||
const userAgent = request.headers.get('User-Agent') || '';
|
||||
const isBot = /discordbot|twitterbot|facebookexternalhit|bingbot|googlebot|slurp|whatsapp|pinterest|slackbot/i.test(userAgent);
|
||||
const isBot = /discordbot|twitterbot|facebookexternalhit|bingbot|googlebot|slurp|whatsapp|pinterest|slackbot/i.test(
|
||||
userAgent
|
||||
);
|
||||
const albumId = params.id;
|
||||
|
||||
if (isBot && albumId) {
|
||||
try {
|
||||
const api = new ServerAPI();
|
||||
const data = await api.getAlbumMetadata(albumId);
|
||||
const album = data.data || data.album || data;
|
||||
const album = data.data || data.album || data;
|
||||
const tracks = album.items || data.tracks || [];
|
||||
|
||||
if (album && (album.title || album.name)) {
|
||||
|
|
@ -82,9 +84,11 @@ export async function onRequest(context) {
|
|||
const artist = album.artist?.name || 'Unknown Artist';
|
||||
const year = album.releaseDate ? new Date(album.releaseDate).getFullYear() : '';
|
||||
const trackCount = album.numberOfTracks || tracks.length;
|
||||
|
||||
|
||||
const description = `Album by ${artist} • ${year} • ${trackCount} Tracks\nListen on Monochrome`;
|
||||
const imageUrl = album.cover ? api.getCoverUrl(album.cover, '1280') : 'https://monochrome.samidy.com/assets/appicon.png';
|
||||
const imageUrl = album.cover
|
||||
? api.getCoverUrl(album.cover, '1280')
|
||||
: 'https://monochrome.samidy.com/assets/appicon.png';
|
||||
const pageUrl = new URL(request.url).href;
|
||||
|
||||
const metaHtml = `
|
||||
|
|
|
|||
|
|
@ -67,7 +67,9 @@ class ServerAPI {
|
|||
export async function onRequest(context) {
|
||||
const { request, params, env } = context;
|
||||
const userAgent = request.headers.get('User-Agent') || '';
|
||||
const isBot = /discordbot|twitterbot|facebookexternalhit|bingbot|googlebot|slurp|whatsapp|pinterest|slackbot/i.test(userAgent);
|
||||
const isBot = /discordbot|twitterbot|facebookexternalhit|bingbot|googlebot|slurp|whatsapp|pinterest|slackbot/i.test(
|
||||
userAgent
|
||||
);
|
||||
const artistId = params.id;
|
||||
|
||||
if (isBot && artistId) {
|
||||
|
|
@ -79,7 +81,9 @@ export async function onRequest(context) {
|
|||
if (artist && (artist.name || artist.title)) {
|
||||
const name = artist.name || artist.title;
|
||||
const description = `Listen to ${name} on Monochrome`;
|
||||
const imageUrl = artist.picture ? api.getArtistPictureUrl(artist.picture, '750') : 'https://monochrome.samidy.com/assets/appicon.png';
|
||||
const imageUrl = artist.picture
|
||||
? api.getArtistPictureUrl(artist.picture, '750')
|
||||
: 'https://monochrome.samidy.com/assets/appicon.png';
|
||||
const pageUrl = new URL(request.url).href;
|
||||
|
||||
const metaHtml = `
|
||||
|
|
|
|||
|
|
@ -68,7 +68,9 @@ class ServerAPI {
|
|||
export async function onRequest(context) {
|
||||
const { request, params, env } = context;
|
||||
const userAgent = request.headers.get('User-Agent') || '';
|
||||
const isBot = /discordbot|twitterbot|facebookexternalhit|bingbot|googlebot|slurp|whatsapp|pinterest|slackbot/i.test(userAgent);
|
||||
const isBot = /discordbot|twitterbot|facebookexternalhit|bingbot|googlebot|slurp|whatsapp|pinterest|slackbot/i.test(
|
||||
userAgent
|
||||
);
|
||||
const playlistId = params.id;
|
||||
|
||||
if (isBot && playlistId) {
|
||||
|
|
@ -82,7 +84,9 @@ export async function onRequest(context) {
|
|||
const trackCount = playlist.numberOfTracks;
|
||||
const description = `Playlist • ${trackCount} Tracks\nListen on Monochrome`;
|
||||
const imageId = playlist.squareImage || playlist.image;
|
||||
const imageUrl = imageId ? api.getCoverUrl(imageId, '1080') : 'https://monochrome.samidy.com/assets/appicon.png';
|
||||
const imageUrl = imageId
|
||||
? api.getCoverUrl(imageId, '1080')
|
||||
: 'https://monochrome.samidy.com/assets/appicon.png';
|
||||
const pageUrl = new URL(request.url).href;
|
||||
|
||||
const metaHtml = `
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ class ServerAPI {
|
|||
const json = await response.json();
|
||||
const data = json.data || json;
|
||||
const items = Array.isArray(data) ? data : [data];
|
||||
const found = items.find(i => (i.id == id) || (i.item && i.item.id == id));
|
||||
const found = items.find((i) => i.id == id || (i.item && i.item.id == id));
|
||||
if (found) {
|
||||
return found.item || found;
|
||||
}
|
||||
|
|
@ -87,7 +87,9 @@ class ServerAPI {
|
|||
export async function onRequest(context) {
|
||||
const { request, params, env } = context;
|
||||
const userAgent = request.headers.get('User-Agent') || '';
|
||||
const isBot = /discordbot|twitterbot|facebookexternalhit|bingbot|googlebot|slurp|whatsapp|pinterest|slackbot/i.test(userAgent);
|
||||
const isBot = /discordbot|twitterbot|facebookexternalhit|bingbot|googlebot|slurp|whatsapp|pinterest|slackbot/i.test(
|
||||
userAgent
|
||||
);
|
||||
const trackId = params.id;
|
||||
|
||||
if (isBot && trackId) {
|
||||
|
|
@ -112,12 +114,14 @@ export async function onRequest(context) {
|
|||
}
|
||||
}
|
||||
// this prob wont work im js winging it
|
||||
const audioMeta = audioUrl ? `
|
||||
const audioMeta = audioUrl
|
||||
? `
|
||||
<meta property="og:audio" content="${audioUrl}">
|
||||
<meta property="og:audio:type" content="audio/mp4">
|
||||
<meta property="og:video" content="${audioUrl}">
|
||||
<meta property="og:video:type" content="audio/mp4">
|
||||
` : '';
|
||||
`
|
||||
: '';
|
||||
|
||||
const metaHtml = `
|
||||
<!DOCTYPE html>
|
||||
|
|
@ -164,4 +168,4 @@ export async function onRequest(context) {
|
|||
const url = new URL(request.url);
|
||||
url.pathname = '/';
|
||||
return env.ASSETS.fetch(new Request(url, request));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,24 +1,25 @@
|
|||
// functions/userplaylist/[id].js
|
||||
|
||||
|
||||
// note that, since this NEEDS a playlist to yknow, be public, this only works for PUBLIC playlists (and you will need an account)
|
||||
|
||||
export async function onRequest(context) {
|
||||
const { request, params, env } = context;
|
||||
const userAgent = request.headers.get('User-Agent') || '';
|
||||
const isBot = /discordbot|twitterbot|facebookexternalhit|bingbot|googlebot|slurp|whatsapp|pinterest|slackbot/i.test(userAgent);
|
||||
const isBot = /discordbot|twitterbot|facebookexternalhit|bingbot|googlebot|slurp|whatsapp|pinterest|slackbot/i.test(
|
||||
userAgent
|
||||
);
|
||||
const playlistId = params.id;
|
||||
|
||||
if (isBot && playlistId) {
|
||||
try {
|
||||
let pbUrl = `https://monodb.samidy.com/api/collections/user_playlists/records/${playlistId}`;
|
||||
let response = await fetch(pbUrl);
|
||||
|
||||
|
||||
if (!response.ok) {
|
||||
pbUrl = `https://monodb.samidy.com/api/collections/public_playlists/records?filter=(uuid='${playlistId}')`;
|
||||
response = await fetch(pbUrl);
|
||||
pbUrl = `https://monodb.samidy.com/api/collections/public_playlists/records?filter=(uuid='${playlistId}')`;
|
||||
response = await fetch(pbUrl);
|
||||
}
|
||||
|
||||
|
||||
if (response.ok) {
|
||||
let playlist = await response.json();
|
||||
if (playlist.items && Array.isArray(playlist.items) && playlist.items.length > 0) {
|
||||
|
|
@ -30,14 +31,18 @@ export async function onRequest(context) {
|
|||
const title = playlist.name || playlist.title || 'User Playlist';
|
||||
let tracks = [];
|
||||
try {
|
||||
tracks = Array.isArray(playlist.tracks) ? playlist.tracks : (playlist.tracks ? JSON.parse(playlist.tracks) : []);
|
||||
tracks = Array.isArray(playlist.tracks)
|
||||
? playlist.tracks
|
||||
: playlist.tracks
|
||||
? JSON.parse(playlist.tracks)
|
||||
: [];
|
||||
} catch (e) {
|
||||
console.error('Failed to parse tracks JSON', e);
|
||||
}
|
||||
|
||||
const trackCount = tracks.length;
|
||||
const trackCount = tracks.length;
|
||||
const description = `User Playlist • ${trackCount} Tracks\nListen on Monochrome`;
|
||||
|
||||
|
||||
let imageUrl = 'https://monochrome.samidy.com/assets/appicon.png';
|
||||
if (playlist.cover) {
|
||||
if (playlist.cover.startsWith('http')) {
|
||||
|
|
@ -45,7 +50,12 @@ export async function onRequest(context) {
|
|||
} else {
|
||||
imageUrl = `https://monodb.samidy.com/api/files/${playlist.collectionId}/${playlist.id}/${playlist.cover}`;
|
||||
}
|
||||
} else if (tracks.length > 0 && typeof tracks[0] === 'object' && tracks[0].album && tracks[0].album.cover) {
|
||||
} else if (
|
||||
tracks.length > 0 &&
|
||||
typeof tracks[0] === 'object' &&
|
||||
tracks[0].album &&
|
||||
tracks[0].album.cover
|
||||
) {
|
||||
const cover = tracks[0].album.cover;
|
||||
imageUrl = `https://resources.tidal.com/images/${cover.replace(/-/g, '/')}/1280x1280.jpg`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -843,11 +843,11 @@ export class LosslessAPI {
|
|||
const response = await this.fetchWithRetry(`/info/?id=${id}`, { type: 'api' });
|
||||
const json = await response.json();
|
||||
const data = json.data || json;
|
||||
|
||||
|
||||
let track;
|
||||
const items = Array.isArray(data) ? data : [data];
|
||||
const found = items.find(i => (i.id == id) || (i.item && i.item.id == id));
|
||||
|
||||
const found = items.find((i) => i.id == id || (i.item && i.item.id == id));
|
||||
|
||||
if (found) {
|
||||
track = this.prepareTrack(found.item || found);
|
||||
await this.cache.set('track', cacheKey, track);
|
||||
|
|
|
|||
39
js/app.js
39
js/app.js
|
|
@ -15,7 +15,13 @@ import { createRouter, updateTabTitle, navigate } from './router.js';
|
|||
import { initializeSettings } from './settings.js';
|
||||
import { initializePlayerEvents, initializeTrackInteractions, handleTrackAction } from './events.js';
|
||||
import { initializeUIInteractions } from './ui-interactions.js';
|
||||
import { downloadAlbumAsZip, downloadDiscography, downloadPlaylistAsZip, downloadLikedTracks, showNotification } from './downloads.js';
|
||||
import {
|
||||
downloadAlbumAsZip,
|
||||
downloadDiscography,
|
||||
downloadPlaylistAsZip,
|
||||
downloadLikedTracks,
|
||||
showNotification,
|
||||
} from './downloads.js';
|
||||
import { debounce, SVG_PLAY } from './utils.js';
|
||||
import { sidePanelManager } from './side-panel.js';
|
||||
import { db } from './db.js';
|
||||
|
|
@ -192,12 +198,13 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||
// i love ios and macos!!!! webkit fucking SUCKS BULLSHIT sorry ios/macos heads yall getting lossless only
|
||||
const ua = navigator.userAgent.toLowerCase();
|
||||
const isIOS = /iphone|ipad|ipod/.test(ua) || (ua.includes('mac') && navigator.maxTouchPoints > 1);
|
||||
const isSafari = ua.includes('safari') && !ua.includes('chrome') && !ua.includes('crios') && !ua.includes('android');
|
||||
const isSafari =
|
||||
ua.includes('safari') && !ua.includes('chrome') && !ua.includes('crios') && !ua.includes('android');
|
||||
|
||||
if (isIOS || isSafari) {
|
||||
const qualitySelect = document.getElementById('streaming-quality-setting');
|
||||
const downloadSelect = document.getElementById('download-quality-setting');
|
||||
|
||||
|
||||
const removeHiRes = (select) => {
|
||||
if (!select) return;
|
||||
const option = select.querySelector('option[value="HI_RES_LOSSLESS"]');
|
||||
|
|
@ -228,7 +235,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||
try {
|
||||
const playlist = await db.getPlaylist(id);
|
||||
const imgElement = document.getElementById('playlist-detail-image');
|
||||
|
||||
|
||||
if (!imgElement) return;
|
||||
|
||||
let container = imgElement.parentElement;
|
||||
|
|
@ -239,7 +246,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||
container.className = 'detail-header-cover-container';
|
||||
imgElement.parentNode.insertBefore(container, imgElement);
|
||||
container.appendChild(imgElement);
|
||||
|
||||
|
||||
collageElement = document.createElement('div');
|
||||
collageElement.id = 'playlist-detail-collage';
|
||||
collageElement.className = 'detail-header-collage';
|
||||
|
|
@ -389,7 +396,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||
const isCollapsed = document.body.classList.contains('sidebar-collapsed');
|
||||
const toggleBtn = document.getElementById('sidebar-toggle');
|
||||
if (toggleBtn) {
|
||||
toggleBtn.innerHTML = isCollapsed
|
||||
toggleBtn.innerHTML = isCollapsed
|
||||
? '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6"/></svg>'
|
||||
: '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m15 18-6-6 6-6"/></svg>';
|
||||
}
|
||||
|
|
@ -806,10 +813,10 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||
e.stopPropagation();
|
||||
const btn = e.target.closest('.remove-from-playlist-btn');
|
||||
const playlistId = window.location.pathname.split('/')[2];
|
||||
|
||||
|
||||
db.getPlaylist(playlistId).then(async (playlist) => {
|
||||
let trackId = null;
|
||||
|
||||
|
||||
// Prefer ID if available (from sorted view)
|
||||
if (btn.dataset.trackId) {
|
||||
trackId = btn.dataset.trackId;
|
||||
|
|
@ -919,11 +926,15 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||
return;
|
||||
}
|
||||
|
||||
list.innerHTML = playlists.map(p => `
|
||||
list.innerHTML = playlists
|
||||
.map(
|
||||
(p) => `
|
||||
<div class="modal-option" data-id="${p.id}">
|
||||
<span>${p.name}</span>
|
||||
</div>
|
||||
`).join('');
|
||||
`
|
||||
)
|
||||
.join('');
|
||||
|
||||
const closeModal = () => {
|
||||
modal.classList.remove('active');
|
||||
|
|
@ -1207,7 +1218,6 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||
updateTabTitle(player);
|
||||
};
|
||||
|
||||
|
||||
await handleRouteChange();
|
||||
|
||||
window.addEventListener('popstate', handleRouteChange);
|
||||
|
|
@ -1215,7 +1225,12 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||
document.body.addEventListener('click', (e) => {
|
||||
const link = e.target.closest('a');
|
||||
|
||||
if (link && link.origin === window.location.origin && link.target !== '_blank' && !link.hasAttribute('download')) {
|
||||
if (
|
||||
link &&
|
||||
link.origin === window.location.origin &&
|
||||
link.target !== '_blank' &&
|
||||
!link.hasAttribute('download')
|
||||
) {
|
||||
e.preventDefault();
|
||||
navigate(link.pathname);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ export class DashDownloader {
|
|||
adaptationSets.sort((a, b) => {
|
||||
const getMaxBandwidth = (set) => {
|
||||
const reps = Array.from(set.querySelectorAll('Representation'));
|
||||
return reps.length ? Math.max(...reps.map(r => parseInt(r.getAttribute('bandwidth') || '0', 10))) : 0;
|
||||
return reps.length ? Math.max(...reps.map((r) => parseInt(r.getAttribute('bandwidth') || '0', 10))) : 0;
|
||||
};
|
||||
return getMaxBandwidth(b) - getMaxBandwidth(a);
|
||||
});
|
||||
|
|
|
|||
2
js/db.js
2
js/db.js
|
|
@ -545,7 +545,7 @@ export class MusicDatabase {
|
|||
cover: cover,
|
||||
playlists: [],
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now()
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
await this.performTransaction('user_folders', 'readwrite', (store) => store.put(folder));
|
||||
return folder;
|
||||
|
|
|
|||
|
|
@ -545,7 +545,16 @@ function createBulkDownloadNotification(type, name, _totalItems) {
|
|||
notifEl.dataset.bulkType = type;
|
||||
notifEl.dataset.bulkName = name;
|
||||
|
||||
const typeLabel = type === 'album' ? 'Album' : type === 'playlist' ? 'Playlist' : type === 'liked' ? 'Liked Tracks' : type === 'queue' ? 'Queue' : 'Discography';
|
||||
const typeLabel =
|
||||
type === 'album'
|
||||
? 'Album'
|
||||
: type === 'playlist'
|
||||
? 'Playlist'
|
||||
: type === 'liked'
|
||||
? 'Liked Tracks'
|
||||
: type === 'queue'
|
||||
? 'Queue'
|
||||
: 'Discography';
|
||||
|
||||
notifEl.innerHTML = `
|
||||
<div style="display: flex; align-items: start; gap: 0.75rem;">
|
||||
|
|
|
|||
|
|
@ -170,7 +170,10 @@ export function initializePlayerEvents(player, audioPlayer, scrobbler, ui) {
|
|||
const progressBar = document.getElementById('progress-bar');
|
||||
const playerControls = document.querySelector('.player-controls');
|
||||
|
||||
const isTracker = player.currentTrack && (player.currentTrack.isTracker || (player.currentTrack.id && String(player.currentTrack.id).startsWith('tracker-')));
|
||||
const isTracker =
|
||||
player.currentTrack &&
|
||||
(player.currentTrack.isTracker ||
|
||||
(player.currentTrack.id && String(player.currentTrack.id).startsWith('tracker-')));
|
||||
|
||||
if (!waveformSettings.isEnabled() || !player.currentTrack || isTracker) {
|
||||
if (progressBar) {
|
||||
|
|
@ -624,7 +627,7 @@ export async function handleTrackAction(
|
|||
if (nowPlayingLikeBtn && type === 'track' && player?.currentTrack?.id === item.id) {
|
||||
elementsToUpdate.push(nowPlayingLikeBtn);
|
||||
}
|
||||
|
||||
|
||||
const fsLikeBtn = document.getElementById('fs-like-btn');
|
||||
if (fsLikeBtn && type === 'track' && player?.currentTrack?.id === item.id) {
|
||||
elementsToUpdate.push(fsLikeBtn);
|
||||
|
|
|
|||
142
js/lyrics.js
142
js/lyrics.js
|
|
@ -12,28 +12,28 @@ class GeniusManager {
|
|||
}
|
||||
|
||||
getToken() {
|
||||
return "QmS9OvsS-7ifRBKx_ochIPQU7oejIS9Eo_z5iWHmCPyhwLVQID3pYTHJmJTa6z8z"; // idgaf anymore im js hardcoding this lmaooo
|
||||
return 'QmS9OvsS-7ifRBKx_ochIPQU7oejIS9Eo_z5iWHmCPyhwLVQID3pYTHJmJTa6z8z'; // idgaf anymore im js hardcoding this lmaooo
|
||||
}
|
||||
|
||||
async searchTrack(title, artist) {
|
||||
const cleanTitle = title.split('(')[0].split('-')[0].trim();
|
||||
const query = encodeURIComponent(`${cleanTitle} ${artist}`);
|
||||
|
||||
|
||||
const url = `https://api.genius.com/search?q=${query}`;
|
||||
const token = this.getToken();
|
||||
const response = await fetch(`https://corsproxy.io/?${encodeURIComponent(url)}`, {
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error('Failed to search Genius');
|
||||
|
||||
|
||||
const data = await response.json();
|
||||
if (data.response.hits.length === 0) return null;
|
||||
|
||||
const normalize = str => str.toLowerCase().replace(/[^\p{L}\p{N}]/gu, '');
|
||||
const normalize = (str) => str.toLowerCase().replace(/[^\p{L}\p{N}]/gu, '');
|
||||
const targetArtist = normalize(artist);
|
||||
|
||||
const hit = data.response.hits.find(h => {
|
||||
|
||||
const hit = data.response.hits.find((h) => {
|
||||
const hitArtist = normalize(h.result.primary_artist.name);
|
||||
return hitArtist.includes(targetArtist) || targetArtist.includes(hitArtist);
|
||||
});
|
||||
|
|
@ -45,7 +45,7 @@ class GeniusManager {
|
|||
const token = this.getToken();
|
||||
const url = `https://api.genius.com/referents?song_id=${songId}&text_format=plain&per_page=50`;
|
||||
const response = await fetch(`https://corsproxy.io/?${encodeURIComponent(url)}`, {
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
|
||||
if (!response.ok) throw new Error('Failed to fetch annotations');
|
||||
|
|
@ -61,7 +61,7 @@ class GeniusManager {
|
|||
this.loading = true;
|
||||
const artist = Array.isArray(track.artists) ? track.artists[0].name : track.artist.name;
|
||||
const song = await this.searchTrack(track.title, artist);
|
||||
|
||||
|
||||
if (!song) {
|
||||
this.loading = false;
|
||||
return null;
|
||||
|
|
@ -69,7 +69,7 @@ class GeniusManager {
|
|||
|
||||
const referents = await this.getReferents(song.id);
|
||||
const result = { song, referents };
|
||||
|
||||
|
||||
this.cache.set(track.id, result);
|
||||
this.loading = false;
|
||||
return result;
|
||||
|
|
@ -82,25 +82,32 @@ class GeniusManager {
|
|||
|
||||
findAnnotations(lineText, referents) {
|
||||
if (!referents || !lineText) return [];
|
||||
|
||||
const normalize = str => str.toLowerCase().replace(/[^\p{L}\p{N}\s]/gu, '').replace(/\s+/g, ' ').trim();
|
||||
|
||||
const normalize = (str) =>
|
||||
str
|
||||
.toLowerCase()
|
||||
.replace(/[^\p{L}\p{N}\s]/gu, '')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
const normLine = normalize(lineText);
|
||||
|
||||
const getWordSet = (str) => new Set(str.split(' ').filter(w => w.length > 0));
|
||||
const getWordSet = (str) => new Set(str.split(' ').filter((w) => w.length > 0));
|
||||
const lineWords = getWordSet(normLine);
|
||||
|
||||
return referents.filter(ref => {
|
||||
return referents.filter((ref) => {
|
||||
const normFragment = normalize(ref.fragment);
|
||||
|
||||
|
||||
if (normLine.includes(normFragment) || normFragment.includes(normLine)) return true;
|
||||
|
||||
const fragmentWords = getWordSet(normFragment);
|
||||
if (fragmentWords.size === 0 || lineWords.size === 0) return false;
|
||||
|
||||
let matchCount = 0;
|
||||
fragmentWords.forEach(w => { if (lineWords.has(w)) matchCount++; });
|
||||
fragmentWords.forEach((w) => {
|
||||
if (lineWords.has(w)) matchCount++;
|
||||
});
|
||||
|
||||
return (matchCount / Math.min(fragmentWords.size, lineWords.size)) > 0.6;
|
||||
return matchCount / Math.min(fragmentWords.size, lineWords.size) > 0.6;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -454,19 +461,20 @@ export class LyricsManager {
|
|||
this.romajiObserver = new MutationObserver((mutations) => {
|
||||
// Check if any relevant mutation occurred
|
||||
const hasRelevantChange = mutations.some((mutation) => {
|
||||
|
||||
if (mutation.type === 'childList') {
|
||||
let relevant = false;
|
||||
if (mutation.addedNodes.length > 0) {
|
||||
for (const node of mutation.addedNodes) {
|
||||
if (node.nodeType === Node.ELEMENT_NODE && node.classList.contains('genius-indicator')) continue;
|
||||
if (node.nodeType === Node.ELEMENT_NODE && node.classList.contains('genius-indicator'))
|
||||
continue;
|
||||
relevant = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!relevant && mutation.removedNodes.length > 0) {
|
||||
for (const node of mutation.removedNodes) {
|
||||
if (node.nodeType === Node.ELEMENT_NODE && node.classList.contains('genius-indicator')) continue;
|
||||
if (node.nodeType === Node.ELEMENT_NODE && node.classList.contains('genius-indicator'))
|
||||
continue;
|
||||
relevant = true;
|
||||
break;
|
||||
}
|
||||
|
|
@ -628,34 +636,38 @@ export class LyricsManager {
|
|||
|
||||
if (lineElements.length === 0) return;
|
||||
|
||||
|
||||
lineElements.forEach(el => {
|
||||
lineElements.forEach((el) => {
|
||||
el.classList.remove('genius-annotated', 'genius-multi-start', 'genius-multi-end', 'genius-multi-mid');
|
||||
delete el.__geniusAnnotations;
|
||||
});
|
||||
|
||||
const normalize = (str) =>
|
||||
str
|
||||
.toLowerCase()
|
||||
.replace(/[^\p{L}\p{N}\s]/gu, '')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
|
||||
const normalize = str => str.toLowerCase().replace(/[^\p{L}\p{N}\s]/gu, '').replace(/\s+/g, ' ').trim();
|
||||
|
||||
referents.forEach(ref => {
|
||||
referents.forEach((ref) => {
|
||||
const fragment = normalize(ref.fragment);
|
||||
if (!fragment) return;
|
||||
|
||||
|
||||
for (let i = 0; i < lineElements.length; i++) {
|
||||
let combinedText = "";
|
||||
let combinedText = '';
|
||||
let currentLines = [];
|
||||
|
||||
for (let j = i; j < lineElements.length; j++) {
|
||||
const line = lineElements[j];
|
||||
|
||||
const lineClone = line.cloneNode(true);
|
||||
lineClone.querySelectorAll('.time, .timestamp, [class*="time"], .genius-indicator').forEach(n => n.remove());
|
||||
const text = normalize(lineClone.textContent || "");
|
||||
lineClone
|
||||
.querySelectorAll('.time, .timestamp, [class*="time"], .genius-indicator')
|
||||
.forEach((n) => n.remove());
|
||||
const text = normalize(lineClone.textContent || '');
|
||||
|
||||
if (!text) continue;
|
||||
|
||||
if (currentLines.length > 0) combinedText += " ";
|
||||
if (currentLines.length > 0) combinedText += ' ';
|
||||
combinedText += text;
|
||||
currentLines.push(line);
|
||||
|
||||
|
|
@ -664,7 +676,7 @@ export class LyricsManager {
|
|||
el.classList.add('genius-annotated');
|
||||
if (!el.__geniusAnnotations) el.__geniusAnnotations = [];
|
||||
|
||||
if (!el.__geniusAnnotations.some(a => a.id === ref.id)) {
|
||||
if (!el.__geniusAnnotations.some((a) => a.id === ref.id)) {
|
||||
el.__geniusAnnotations.push(ref);
|
||||
}
|
||||
|
||||
|
|
@ -683,10 +695,9 @@ export class LyricsManager {
|
|||
el.appendChild(smiley);
|
||||
}
|
||||
});
|
||||
break;
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
if (combinedText.length > fragment.length + 50) break;
|
||||
}
|
||||
}
|
||||
|
|
@ -754,19 +765,22 @@ export function openLyricsPanel(track, audioPlayer, lyricsManager, forceOpen = f
|
|||
geniusBtn.addEventListener('click', async () => {
|
||||
manager.isGeniusMode = !manager.isGeniusMode;
|
||||
const enabled = manager.isGeniusMode;
|
||||
|
||||
|
||||
geniusBtn.classList.toggle('active-genius', enabled);
|
||||
geniusBtn.style.color = enabled ? '#ffff64' : '';
|
||||
geniusBtn.innerHTML = enabled ? SVG_GENIUS_ACTIVE : SVG_GENIUS_INACTIVE;
|
||||
|
||||
if (enabled) {
|
||||
|
||||
try {
|
||||
geniusBtn.style.opacity = '0.5';
|
||||
await manager.geniusManager.getDataForTrack(track);
|
||||
manager.currentGeniusData = manager.geniusManager.cache.get(track.id);
|
||||
const amLyrics = sidePanelManager.panel.querySelector('am-lyrics');
|
||||
if (amLyrics) manager.applyGeniusAnnotations(amLyrics, manager.geniusManager.cache.get(track.id)?.referents);
|
||||
if (amLyrics)
|
||||
manager.applyGeniusAnnotations(
|
||||
amLyrics,
|
||||
manager.geniusManager.cache.get(track.id)?.referents
|
||||
);
|
||||
} catch (e) {
|
||||
alert(e.message);
|
||||
manager.isGeniusMode = false;
|
||||
|
|
@ -776,13 +790,17 @@ export function openLyricsPanel(track, audioPlayer, lyricsManager, forceOpen = f
|
|||
geniusBtn.style.opacity = '1';
|
||||
}
|
||||
} else {
|
||||
|
||||
const amLyrics = sidePanelManager.panel.querySelector('am-lyrics');
|
||||
if (amLyrics) {
|
||||
const root = amLyrics.shadowRoot || amLyrics;
|
||||
const lineElements = Array.from(root.querySelectorAll('.genius-annotated'));
|
||||
lineElements.forEach(el => {
|
||||
el.classList.remove('genius-annotated', 'genius-multi-start', 'genius-multi-end', 'genius-multi-mid');
|
||||
lineElements.forEach((el) => {
|
||||
el.classList.remove(
|
||||
'genius-annotated',
|
||||
'genius-multi-start',
|
||||
'genius-multi-end',
|
||||
'genius-multi-mid'
|
||||
);
|
||||
delete el.__geniusAnnotations;
|
||||
});
|
||||
}
|
||||
|
|
@ -848,18 +866,22 @@ async function renderLyricsComponent(container, track, audioPlayer, lyricsManage
|
|||
await lyricsManager.loadKuroshiro();
|
||||
}
|
||||
|
||||
|
||||
lyricsManager.fetchLyrics(track.id, track).then(async () => {
|
||||
if (lyricsManager.isGeniusMode) {
|
||||
try {
|
||||
const data = await lyricsManager.geniusManager.getDataForTrack(track);
|
||||
if (data) {
|
||||
lyricsManager.currentGeniusData = data;
|
||||
lyricsManager.applyGeniusAnnotations(amLyrics, data.referents);
|
||||
lyricsManager
|
||||
.fetchLyrics(track.id, track)
|
||||
.then(async () => {
|
||||
if (lyricsManager.isGeniusMode) {
|
||||
try {
|
||||
const data = await lyricsManager.geniusManager.getDataForTrack(track);
|
||||
if (data) {
|
||||
lyricsManager.currentGeniusData = data;
|
||||
lyricsManager.applyGeniusAnnotations(amLyrics, data.referents);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Genius auto-load failed', e);
|
||||
}
|
||||
} catch (e) { console.warn('Genius auto-load failed', e); }
|
||||
}
|
||||
}).catch(e => console.warn('Background lyrics fetch failed', e));
|
||||
}
|
||||
})
|
||||
.catch((e) => console.warn('Background lyrics fetch failed', e));
|
||||
|
||||
// Wait for lyrics to appear, then do an immediate conversion
|
||||
const waitForLyrics = () => {
|
||||
|
|
@ -955,20 +977,21 @@ function setupSync(track, audioPlayer, amLyrics, lyricsManager) {
|
|||
|
||||
const onLineClick = (e) => {
|
||||
if (e.detail && e.detail.timestamp !== undefined) {
|
||||
|
||||
const manager = lyricsManager || sidePanelManager.panel.lyricsManager;
|
||||
if (manager && manager.isGeniusMode) {
|
||||
const timestampSeconds = e.detail.timestamp / 1000;
|
||||
|
||||
|
||||
const lyricsData = manager.lyricsCache.get(track.id);
|
||||
if (lyricsData && lyricsData.subtitles) {
|
||||
const parsed = manager.parseSyncedLyrics(lyricsData.subtitles);
|
||||
|
||||
const line = parsed.find(l => Math.abs(l.time - timestampSeconds) < 1.0);
|
||||
|
||||
const line = parsed.find((l) => Math.abs(l.time - timestampSeconds) < 1.0);
|
||||
|
||||
if (line && line.text && manager.currentGeniusData) {
|
||||
const annotations = manager.geniusManager.findAnnotations(line.text, manager.currentGeniusData.referents);
|
||||
const annotations = manager.geniusManager.findAnnotations(
|
||||
line.text,
|
||||
manager.currentGeniusData.referents
|
||||
);
|
||||
showGeniusAnnotations(annotations, line.text);
|
||||
}
|
||||
}
|
||||
|
|
@ -1003,13 +1026,12 @@ function setupSync(track, audioPlayer, amLyrics, lyricsManager) {
|
|||
}
|
||||
|
||||
function showGeniusAnnotations(annotations, lineText) {
|
||||
|
||||
const existing = document.querySelector('.genius-annotation-modal');
|
||||
if (existing) existing.remove();
|
||||
|
||||
const modal = document.createElement('div');
|
||||
modal.className = 'genius-annotation-modal';
|
||||
|
||||
|
||||
let contentHtml = `
|
||||
<div class="genius-modal-content">
|
||||
<div class="genius-header">
|
||||
|
|
@ -1026,7 +1048,7 @@ function showGeniusAnnotations(annotations, lineText) {
|
|||
</div>
|
||||
`;
|
||||
} else {
|
||||
annotations.forEach(ann => {
|
||||
annotations.forEach((ann) => {
|
||||
const body = ann.annotations[0].body.plain;
|
||||
contentHtml += `
|
||||
<div class="annotation-item">
|
||||
|
|
@ -1043,7 +1065,9 @@ function showGeniusAnnotations(annotations, lineText) {
|
|||
|
||||
modal.querySelector('.close-genius').addEventListener('click', () => modal.remove());
|
||||
|
||||
modal.addEventListener('click', (e) => { if(e.target === modal) modal.remove(); });
|
||||
modal.addEventListener('click', (e) => {
|
||||
if (e.target === modal) modal.remove();
|
||||
});
|
||||
}
|
||||
|
||||
export async function renderLyricsInFullscreen(track, audioPlayer, lyricsManager, container) {
|
||||
|
|
|
|||
|
|
@ -22,22 +22,22 @@ export async function addMetadataToAudio(audioBlob, track, api, quality) {
|
|||
|
||||
const buffer = await audioBlob.slice(0, 4).arrayBuffer();
|
||||
const view = new DataView(buffer);
|
||||
const isFlac = view.byteLength >= 4 &&
|
||||
const isFlac =
|
||||
view.byteLength >= 4 &&
|
||||
view.getUint8(0) === 0x66 && // f
|
||||
view.getUint8(1) === 0x4c && // L
|
||||
view.getUint8(2) === 0x61 && // a
|
||||
view.getUint8(3) === 0x43; // C
|
||||
view.getUint8(3) === 0x43; // C
|
||||
|
||||
const mime = audioBlob.type;
|
||||
const mime = audioBlob.type;
|
||||
|
||||
if (mime === 'audio/flac') {
|
||||
return await addFlacMetadata(audioBlob, track, api);
|
||||
}
|
||||
|
||||
|
||||
if (mime === 'audio/mp4') {
|
||||
return await addM4aMetadata(audioBlob, track, api);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -319,7 +319,10 @@ export class Player {
|
|||
}
|
||||
streamUrl = track.audioUrl;
|
||||
|
||||
if ((!streamUrl || (typeof streamUrl === 'string' && streamUrl.startsWith('blob:'))) && track.remoteUrl) {
|
||||
if (
|
||||
(!streamUrl || (typeof streamUrl === 'string' && streamUrl.startsWith('blob:'))) &&
|
||||
track.remoteUrl
|
||||
) {
|
||||
streamUrl = track.remoteUrl;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ export function navigate(path) {
|
|||
|
||||
export function createRouter(ui) {
|
||||
const router = async () => {
|
||||
|
||||
if (window.location.hash && window.location.hash.length > 1) {
|
||||
const hash = window.location.hash.substring(1);
|
||||
if (hash.includes('/')) {
|
||||
|
|
@ -21,7 +20,7 @@ export function createRouter(ui) {
|
|||
}
|
||||
|
||||
let path = window.location.pathname;
|
||||
|
||||
|
||||
if (path.startsWith('/')) path = path.substring(1);
|
||||
if (path.endsWith('/')) path = path.substring(0, path.length - 1);
|
||||
if (path === '' || path === 'index.html') path = 'home';
|
||||
|
|
|
|||
|
|
@ -626,7 +626,7 @@ export const visualizerSettings = {
|
|||
getSensitivity() {
|
||||
try {
|
||||
const val = localStorage.getItem(this.SENSITIVITY_KEY);
|
||||
if (val === null) return 1.0;
|
||||
if (val === null) return 1.0;
|
||||
return parseFloat(val);
|
||||
} catch {
|
||||
return 1.0;
|
||||
|
|
|
|||
174
js/tracker.js
174
js/tracker.js
|
|
@ -10,14 +10,20 @@ async function loadArtistsData() {
|
|||
const response = await fetch('./artists.ndjson');
|
||||
if (!response.ok) throw new Error('Network response was not ok');
|
||||
const text = await response.text();
|
||||
artistsData = text.trim().split('\n')
|
||||
.filter(line => line.trim())
|
||||
.map(line => {
|
||||
try { return JSON.parse(line); } catch (e) { return null; }
|
||||
artistsData = text
|
||||
.trim()
|
||||
.split('\n')
|
||||
.filter((line) => line.trim())
|
||||
.map((line) => {
|
||||
try {
|
||||
return JSON.parse(line);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter(item => item !== null);
|
||||
.filter((item) => item !== null);
|
||||
} catch (e) {
|
||||
console.error("Failed to load Artists LIst:", e);
|
||||
console.error('Failed to load Artists LIst:', e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -29,11 +35,13 @@ function getSheetId(url) {
|
|||
|
||||
async function fetchTrackerData(sheetId) {
|
||||
try {
|
||||
const response = await fetch(`https://corsproxy.io/?${encodeURIComponent(`https://tracker.israeli.ovh/get/${sheetId}`)}`);
|
||||
const response = await fetch(
|
||||
`https://corsproxy.io/?${encodeURIComponent(`https://tracker.israeli.ovh/get/${sheetId}`)}`
|
||||
);
|
||||
if (!response.ok) return null;
|
||||
return await response.json();
|
||||
} catch (e) {
|
||||
console.error("Failed to fetch tracker data", e);
|
||||
console.error('Failed to fetch tracker data', e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -62,21 +70,21 @@ function getDirectUrl(rawUrl) {
|
|||
function renderLoadButton(container, sheetId, artistName) {
|
||||
container.innerHTML = '';
|
||||
container.style.display = 'block';
|
||||
|
||||
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.style.textAlign = 'center';
|
||||
wrapper.style.padding = '2rem';
|
||||
|
||||
|
||||
const button = document.createElement('button');
|
||||
button.className = 'btn-primary';
|
||||
button.textContent = 'Load Unreleased Projects';
|
||||
button.style.fontSize = '1.1rem';
|
||||
button.style.padding = '1rem 2rem';
|
||||
|
||||
|
||||
button.onclick = async () => {
|
||||
button.textContent = 'Loading...';
|
||||
button.disabled = true;
|
||||
|
||||
|
||||
const trackerData = await fetchTrackerData(sheetId);
|
||||
if (trackerData) {
|
||||
renderTracker(trackerData, container, artistName);
|
||||
|
|
@ -88,7 +96,7 @@ function renderLoadButton(container, sheetId, artistName) {
|
|||
}, 2000);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
wrapper.appendChild(button);
|
||||
container.appendChild(wrapper);
|
||||
}
|
||||
|
|
@ -100,7 +108,7 @@ function renderTracker(trackerData, container, artistName) {
|
|||
Unreleased Songs & Info Provided By <a href="https://artistgrid.cx" target="_blank" style="text-decoration: underline;">ArtistGrid</a>. Consider Donating to Them.
|
||||
</p>
|
||||
`;
|
||||
|
||||
|
||||
const erasContainer = document.createElement('div');
|
||||
erasContainer.className = 'card-grid';
|
||||
erasContainer.style.opacity = '0';
|
||||
|
|
@ -110,11 +118,11 @@ function renderTracker(trackerData, container, artistName) {
|
|||
|
||||
if (!trackerData.eras) return;
|
||||
|
||||
Object.values(trackerData.eras).forEach(era => {
|
||||
Object.values(trackerData.eras).forEach((era) => {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'card';
|
||||
card.style.cursor = 'pointer';
|
||||
|
||||
|
||||
const imgWrapper = document.createElement('div');
|
||||
imgWrapper.className = 'card-image-wrapper';
|
||||
|
||||
|
|
@ -123,13 +131,13 @@ function renderTracker(trackerData, container, artistName) {
|
|||
img.src = era.image ? `https://corsproxy.io/?${encodeURIComponent(era.image)}` : 'assets/logo.svg';
|
||||
img.alt = era.name;
|
||||
img.loading = 'lazy';
|
||||
|
||||
|
||||
imgWrapper.appendChild(img);
|
||||
|
||||
const title = document.createElement('div');
|
||||
title.className = 'card-title';
|
||||
title.textContent = era.name;
|
||||
|
||||
|
||||
const subtitle = document.createElement('div');
|
||||
subtitle.className = 'card-subtitle';
|
||||
subtitle.textContent = era.timeline || 'Unreleased';
|
||||
|
|
@ -137,9 +145,9 @@ function renderTracker(trackerData, container, artistName) {
|
|||
card.appendChild(imgWrapper);
|
||||
card.appendChild(title);
|
||||
card.appendChild(subtitle);
|
||||
|
||||
|
||||
card.onclick = () => showEraSongs(era, artistName);
|
||||
|
||||
|
||||
erasContainer.appendChild(card);
|
||||
});
|
||||
|
||||
|
|
@ -153,7 +161,6 @@ function showEraSongs(era, artistName) {
|
|||
const modal = document.getElementById('tracker-modal');
|
||||
const overlay = modal.querySelector('.modal-overlay');
|
||||
const closeBtn = document.getElementById('close-tracker-modal');
|
||||
|
||||
|
||||
const img = document.getElementById('tracker-header-image');
|
||||
const title = document.getElementById('tracker-header-title');
|
||||
|
|
@ -166,7 +173,7 @@ function showEraSongs(era, artistName) {
|
|||
|
||||
const trackList = document.getElementById('tracker-tracklist');
|
||||
const filterContainer = document.getElementById('tracker-filters');
|
||||
|
||||
|
||||
filterContainer.innerHTML = '';
|
||||
while (trackList.lastElementChild && !trackList.lastElementChild.classList.contains('track-list-header')) {
|
||||
trackList.removeChild(trackList.lastElementChild);
|
||||
|
|
@ -178,15 +185,15 @@ function showEraSongs(era, artistName) {
|
|||
{ label: 'Special', emoji: '✨' },
|
||||
{ label: 'Grails', emoji: '🏆' },
|
||||
{ label: 'Wanted', emoji: '🥇' },
|
||||
{ label: 'Worst Of', emoji: '🗑️' }
|
||||
{ label: 'Worst Of', emoji: '🗑️' },
|
||||
];
|
||||
|
||||
let activeFilter = '';
|
||||
|
||||
const applyFilter = () => {
|
||||
const items = trackList.querySelectorAll('.track-item');
|
||||
|
||||
items.forEach(item => {
|
||||
|
||||
items.forEach((item) => {
|
||||
const titleEl = item.querySelector('.title');
|
||||
if (titleEl) {
|
||||
const title = titleEl.textContent.trim();
|
||||
|
|
@ -199,37 +206,37 @@ function showEraSongs(era, artistName) {
|
|||
});
|
||||
|
||||
const categories = trackList.querySelectorAll('h4');
|
||||
categories.forEach(cat => {
|
||||
categories.forEach((cat) => {
|
||||
let next = cat.nextElementSibling;
|
||||
let hasVisibleItems = false;
|
||||
|
||||
while(next && next.tagName !== 'H4') {
|
||||
|
||||
while (next && next.tagName !== 'H4') {
|
||||
if (next.classList.contains('track-item') && next.style.display !== 'none') {
|
||||
hasVisibleItems = true;
|
||||
break;
|
||||
}
|
||||
next = next.nextElementSibling;
|
||||
}
|
||||
|
||||
|
||||
cat.style.display = hasVisibleItems ? 'block' : 'none';
|
||||
});
|
||||
};
|
||||
|
||||
filters.forEach(filter => {
|
||||
filters.forEach((filter) => {
|
||||
const btn = document.createElement('button');
|
||||
btn.className = 'btn-secondary';
|
||||
btn.textContent = filter.emoji ? `${filter.emoji} ${filter.label}` : filter.label;
|
||||
btn.style.fontSize = '0.85rem';
|
||||
btn.style.padding = '0.4rem 0.8rem';
|
||||
btn.style.borderRadius = '2rem';
|
||||
|
||||
|
||||
if (filter.emoji === '') {
|
||||
btn.style.backgroundColor = 'var(--primary)';
|
||||
btn.style.color = 'var(--primary-foreground)';
|
||||
btn.style.backgroundColor = 'var(--primary)';
|
||||
btn.style.color = 'var(--primary-foreground)';
|
||||
}
|
||||
|
||||
btn.onclick = () => {
|
||||
Array.from(filterContainer.children).forEach(b => {
|
||||
Array.from(filterContainer.children).forEach((b) => {
|
||||
b.style.backgroundColor = '';
|
||||
b.style.color = '';
|
||||
});
|
||||
|
|
@ -248,7 +255,7 @@ function showEraSongs(era, artistName) {
|
|||
if (era.data) {
|
||||
Object.entries(era.data).forEach(([category, songs]) => {
|
||||
if (!songs || songs.length === 0) return;
|
||||
|
||||
|
||||
const catTitle = document.createElement('h4');
|
||||
catTitle.textContent = category;
|
||||
catTitle.style.padding = '1rem 0.5rem 0.5rem';
|
||||
|
|
@ -260,10 +267,10 @@ function showEraSongs(era, artistName) {
|
|||
|
||||
const isValidUrl = (u) => u && typeof u === 'string' && u.trim().length > 0;
|
||||
|
||||
songs.forEach(song => {
|
||||
songs.forEach((song) => {
|
||||
const trackItem = document.createElement('div');
|
||||
trackItem.className = 'track-item';
|
||||
|
||||
|
||||
trackItem.innerHTML = `
|
||||
<div class="track-number">${globalIndex++}</div>
|
||||
<div class="track-item-info">
|
||||
|
|
@ -287,27 +294,40 @@ function showEraSongs(era, artistName) {
|
|||
e.preventDefault();
|
||||
const contextMenu = document.getElementById('context-menu');
|
||||
if (contextMenu) {
|
||||
const rawUrl = (isValidUrl(song.url) ? song.url : null) || (song.urls ? song.urls.find(isValidUrl) : null);
|
||||
const rawUrl =
|
||||
(isValidUrl(song.url) ? song.url : null) || (song.urls ? song.urls.find(isValidUrl) : null);
|
||||
const directUrl = getDirectUrl(rawUrl);
|
||||
|
||||
const track = {
|
||||
id: `tracker-${song.name}`,
|
||||
title: song.name,
|
||||
artist: { name: artistName || document.getElementById('artist-detail-name')?.textContent || 'Unknown Artist' },
|
||||
artists: [{ name: artistName || document.getElementById('artist-detail-name')?.textContent || 'Unknown Artist' }],
|
||||
artist: {
|
||||
name:
|
||||
artistName ||
|
||||
document.getElementById('artist-detail-name')?.textContent ||
|
||||
'Unknown Artist',
|
||||
},
|
||||
artists: [
|
||||
{
|
||||
name:
|
||||
artistName ||
|
||||
document.getElementById('artist-detail-name')?.textContent ||
|
||||
'Unknown Artist',
|
||||
},
|
||||
],
|
||||
album: {
|
||||
title: era.name,
|
||||
cover: era.image
|
||||
cover: era.image,
|
||||
},
|
||||
duration: parseDuration(song.track_length),
|
||||
isTracker: true,
|
||||
audioUrl: directUrl,
|
||||
remoteUrl: directUrl
|
||||
remoteUrl: directUrl,
|
||||
};
|
||||
|
||||
|
||||
contextMenu._contextTrack = track;
|
||||
|
||||
['go-to-album', 'go-to-artist', 'toggle-like', 'download', 'track-mix'].forEach(action => {
|
||||
|
||||
['go-to-album', 'go-to-artist', 'toggle-like', 'download', 'track-mix'].forEach((action) => {
|
||||
const item = contextMenu.querySelector(`[data-action="${action}"]`);
|
||||
if (item) item.style.display = 'none';
|
||||
});
|
||||
|
|
@ -326,7 +346,7 @@ function showEraSongs(era, artistName) {
|
|||
if (hasValidUrl) {
|
||||
trackItem.onclick = async () => {
|
||||
if (song.track_length === '-') {
|
||||
const targetUrl = (song.urls && song.urls.length > 0) ? song.urls[0] : song.url;
|
||||
const targetUrl = song.urls && song.urls.length > 0 ? song.urls[0] : song.url;
|
||||
if (targetUrl) window.open(targetUrl, '_blank');
|
||||
return;
|
||||
}
|
||||
|
|
@ -337,8 +357,9 @@ function showEraSongs(era, artistName) {
|
|||
trackItem.classList.add('loading');
|
||||
const trackNumEl = trackItem.querySelector('.track-number');
|
||||
const originalNum = trackNumEl.textContent;
|
||||
trackNumEl.innerHTML = '<svg class="animate-spin" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle></svg>';
|
||||
|
||||
trackNumEl.innerHTML =
|
||||
'<svg class="animate-spin" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle></svg>';
|
||||
|
||||
let urlsToTry = [];
|
||||
if (isValidUrl(song.url)) {
|
||||
urlsToTry.push(song.url);
|
||||
|
|
@ -349,12 +370,12 @@ function showEraSongs(era, artistName) {
|
|||
|
||||
let audioUrl = null;
|
||||
let successfulUrl = null;
|
||||
|
||||
|
||||
for (let rawUrl of urlsToTry) {
|
||||
console.log(`Trying: ${rawUrl}`);
|
||||
|
||||
|
||||
let downloadUrl = rawUrl;
|
||||
|
||||
|
||||
if (rawUrl.includes('pillows.su/f/')) {
|
||||
const match = rawUrl.match(/pillows\.su\/f\/([a-f0-9]+)/);
|
||||
if (match) {
|
||||
|
|
@ -370,12 +391,14 @@ function showEraSongs(era, artistName) {
|
|||
try {
|
||||
console.log(`Fetching: ${downloadUrl}`);
|
||||
const response = await fetch(downloadUrl);
|
||||
|
||||
|
||||
if (response.ok) {
|
||||
const contentType = response.headers.get('content-type') || '';
|
||||
if (contentType.includes('audio/') ||
|
||||
if (
|
||||
contentType.includes('audio/') ||
|
||||
contentType.includes('mpeg') ||
|
||||
contentType.includes('octet-stream')) {
|
||||
contentType.includes('octet-stream')
|
||||
) {
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
if (arrayBuffer.byteLength > 1000) {
|
||||
const blob = new Blob([arrayBuffer], { type: 'audio/mpeg' });
|
||||
|
|
@ -394,26 +417,43 @@ function showEraSongs(era, artistName) {
|
|||
document.body.style.cursor = 'default';
|
||||
trackItem.classList.remove('loading');
|
||||
trackNumEl.textContent = originalNum;
|
||||
|
||||
|
||||
if (!audioUrl) {
|
||||
alert(`Unable to load this track! :( The source may be unavailable.\n\nTried ${urlsToTry.length} URL(s)`);
|
||||
alert(
|
||||
`Unable to load this track! :( The source may be unavailable.\n\nTried ${urlsToTry.length} URL(s)`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (globalPlayer) {
|
||||
const track = {
|
||||
id: `tracker-${song.name}`,
|
||||
title: song.name,
|
||||
artist: { name: artistName || document.getElementById('artist-detail-name')?.textContent || 'Unknown Artist' },
|
||||
artists: [{ name: artistName || document.getElementById('artist-detail-name')?.textContent || 'Unknown Artist' }],
|
||||
artist: {
|
||||
name:
|
||||
artistName ||
|
||||
document.getElementById('artist-detail-name')?.textContent ||
|
||||
'Unknown Artist',
|
||||
},
|
||||
artists: [
|
||||
{
|
||||
name:
|
||||
artistName ||
|
||||
document.getElementById('artist-detail-name')?.textContent ||
|
||||
'Unknown Artist',
|
||||
},
|
||||
],
|
||||
album: {
|
||||
title: era.name,
|
||||
cover: era.image
|
||||
cover: era.image,
|
||||
},
|
||||
duration: parseDuration(song.track_length),
|
||||
isTracker: true,
|
||||
audioUrl: audioUrl,
|
||||
remoteUrl: successfulUrl || (urlsToTry.length > 0 ? getDirectUrl(urlsToTry[0]) : null) || getDirectUrl(song.url)
|
||||
remoteUrl:
|
||||
successfulUrl ||
|
||||
(urlsToTry.length > 0 ? getDirectUrl(urlsToTry[0]) : null) ||
|
||||
getDirectUrl(song.url),
|
||||
};
|
||||
|
||||
globalPlayer.setQueue([track], 0);
|
||||
|
|
@ -455,18 +495,18 @@ export async function initTracker(player, ui) {
|
|||
const observer = new MutationObserver(async () => {
|
||||
const artistNameEl = document.getElementById('artist-detail-name');
|
||||
const trackerSection = document.getElementById('artist-tracker-section');
|
||||
|
||||
|
||||
if (artistNameEl && trackerSection && artistNameEl.textContent) {
|
||||
const artistName = artistNameEl.textContent.trim();
|
||||
|
||||
|
||||
if (trackerSection.dataset.artist === artistName) return;
|
||||
|
||||
|
||||
trackerSection.dataset.artist = artistName;
|
||||
trackerSection.innerHTML = '';
|
||||
trackerSection.style.display = 'none';
|
||||
|
||||
const artistEntry = artistsData.find(a => a.name.toLowerCase() === artistName.toLowerCase());
|
||||
|
||||
const artistEntry = artistsData.find((a) => a.name.toLowerCase() === artistName.toLowerCase());
|
||||
|
||||
if (artistEntry && artistEntry.url) {
|
||||
const sheetId = getSheetId(artistEntry.url);
|
||||
if (sheetId) {
|
||||
|
|
@ -480,4 +520,4 @@ export async function initTracker(player, ui) {
|
|||
if (artistPage) {
|
||||
observer.observe(artistPage, { attributes: true, childList: true, subtree: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -414,7 +414,7 @@ export function initializeUIInteractions(player, api, ui) {
|
|||
if (sidePanelManager.isActive('queue')) {
|
||||
refreshQueuePanel();
|
||||
}
|
||||
|
||||
|
||||
const overlay = document.getElementById('fullscreen-cover-overlay');
|
||||
if (overlay && getComputedStyle(overlay).display !== 'none') {
|
||||
ui.updateFullscreenMetadata(player.currentTrack, player.getNextTrack());
|
||||
|
|
|
|||
126
js/ui.js
126
js/ui.js
|
|
@ -685,7 +685,7 @@ export class UIRenderer {
|
|||
const nextTrackEl = document.getElementById('fullscreen-next-track');
|
||||
|
||||
const coverUrl = this.api.getCoverUrl(track.album?.cover, '1280');
|
||||
|
||||
|
||||
const fsLikeBtn = document.getElementById('fs-like-btn');
|
||||
if (fsLikeBtn) {
|
||||
this.updateLikeState(fsLikeBtn.parentElement, 'track', track.id);
|
||||
|
|
@ -787,7 +787,7 @@ export class UIRenderer {
|
|||
closeFullscreenCover() {
|
||||
const overlay = document.getElementById('fullscreen-cover-overlay');
|
||||
overlay.style.display = 'none';
|
||||
|
||||
|
||||
const playerBar = document.querySelector('.now-playing-bar');
|
||||
if (playerBar) playerBar.style.removeProperty('display');
|
||||
|
||||
|
|
@ -824,34 +824,38 @@ export class UIRenderer {
|
|||
lastPausedState = isPaused;
|
||||
|
||||
if (isPaused) {
|
||||
playBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="5 3 19 12 5 21 5 3"></polygon></svg>';
|
||||
playBtn.innerHTML =
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="5 3 19 12 5 21 5 3"></polygon></svg>';
|
||||
} else {
|
||||
playBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="6" y="4" width="4" height="16"></rect><rect x="14" y="4" width="4" height="16"></rect></svg>';
|
||||
playBtn.innerHTML =
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="6" y="4" width="4" height="16"></rect><rect x="14" y="4" width="4" height="16"></rect></svg>';
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
updatePlayBtn();
|
||||
|
||||
|
||||
playBtn.onclick = () => {
|
||||
this.player.handlePlayPause();
|
||||
updatePlayBtn();
|
||||
};
|
||||
|
||||
|
||||
prevBtn.onclick = () => this.player.playPrev();
|
||||
nextBtn.onclick = () => this.player.playNext();
|
||||
|
||||
|
||||
shuffleBtn.onclick = () => {
|
||||
this.player.toggleShuffle();
|
||||
shuffleBtn.classList.toggle('active', this.player.shuffleActive);
|
||||
};
|
||||
|
||||
|
||||
repeatBtn.onclick = () => {
|
||||
const mode = this.player.toggleRepeat();
|
||||
repeatBtn.classList.toggle('active', mode !== 0);
|
||||
if (mode === 2) {
|
||||
repeatBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m17 2 4 4-4 4"/><path d="M3 11v-1a4 4 0 0 1 4-4h14"/><path d="m7 22-4-4 4-4"/><path d="M21 13v1a4 4 0 0 1-4 4H3"/><path d="M11 10h1v4"/></svg>';
|
||||
repeatBtn.innerHTML =
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m17 2 4 4-4 4"/><path d="M3 11v-1a4 4 0 0 1 4-4h14"/><path d="m7 22-4-4 4-4"/><path d="M21 13v1a4 4 0 0 1-4 4H3"/><path d="M11 10h1v4"/></svg>';
|
||||
} else {
|
||||
repeatBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m17 2 4 4-4 4"/><path d="M3 11v-1a4 4 0 0 1 4-4h14"/><path d="m7 22-4-4 4-4"/><path d="M21 13v1a4 4 0 0 1-4 4H3"/></svg>';
|
||||
repeatBtn.innerHTML =
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m17 2 4 4-4 4"/><path d="M3 11v-1a4 4 0 0 1 4-4h14"/><path d="m7 22-4-4 4-4"/><path d="M21 13v1a4 4 0 0 1-4 4H3"/></svg>';
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -883,26 +887,27 @@ export class UIRenderer {
|
|||
const mode = this.player.repeatMode;
|
||||
repeatBtn.classList.toggle('active', mode !== 0);
|
||||
if (mode === 2) {
|
||||
repeatBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m17 2 4 4-4 4"/><path d="M3 11v-1a4 4 0 0 1 4-4h14"/><path d="m7 22-4-4 4-4"/><path d="M21 13v1a4 4 0 0 1-4 4H3"/><path d="M11 10h1v4"/></svg>';
|
||||
repeatBtn.innerHTML =
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m17 2 4 4-4 4"/><path d="M3 11v-1a4 4 0 0 1 4-4h14"/><path d="m7 22-4-4 4-4"/><path d="M21 13v1a4 4 0 0 1-4 4H3"/><path d="M11 10h1v4"/></svg>';
|
||||
}
|
||||
|
||||
const update = () => {
|
||||
if (document.getElementById('fullscreen-cover-overlay').style.display === 'none') return;
|
||||
|
||||
|
||||
const duration = audioPlayer.duration || 0;
|
||||
const current = audioPlayer.currentTime || 0;
|
||||
|
||||
|
||||
if (duration > 0) {
|
||||
const percent = (current / duration) * 100;
|
||||
progressFill.style.width = `${percent}%`;
|
||||
currentTimeEl.textContent = formatTime(current);
|
||||
totalDurationEl.textContent = formatTime(duration);
|
||||
}
|
||||
|
||||
|
||||
updatePlayBtn();
|
||||
this.fullscreenUpdateInterval = requestAnimationFrame(update);
|
||||
};
|
||||
|
||||
|
||||
if (this.fullscreenUpdateInterval) cancelAnimationFrame(this.fullscreenUpdateInterval);
|
||||
this.fullscreenUpdateInterval = requestAnimationFrame(update);
|
||||
}
|
||||
|
|
@ -913,7 +918,10 @@ export class UIRenderer {
|
|||
});
|
||||
|
||||
document.querySelectorAll('.sidebar-nav a').forEach((link) => {
|
||||
link.classList.toggle('active', link.pathname === `/${pageId}` || (pageId === 'home' && link.pathname === '/'));
|
||||
link.classList.toggle(
|
||||
'active',
|
||||
link.pathname === `/${pageId}` || (pageId === 'home' && link.pathname === '/')
|
||||
);
|
||||
});
|
||||
|
||||
document.querySelector('.main-content').scrollTop = 0;
|
||||
|
|
@ -939,7 +947,7 @@ export class UIRenderer {
|
|||
const yearEl = document.getElementById('track-detail-year');
|
||||
const albumSection = document.getElementById('track-album-section');
|
||||
const albumTracksContainer = document.getElementById('track-detail-album-tracks');
|
||||
|
||||
|
||||
const playBtn = document.getElementById('play-track-btn');
|
||||
const lyricsBtn = document.getElementById('track-lyrics-btn');
|
||||
const shareBtn = document.getElementById('share-track-btn');
|
||||
|
|
@ -957,11 +965,11 @@ export class UIRenderer {
|
|||
try {
|
||||
const trackData = await this.api.getTrack(trackId);
|
||||
const track = trackData.track;
|
||||
|
||||
|
||||
const coverUrl = this.api.getCoverUrl(track.album?.cover);
|
||||
imageEl.src = coverUrl;
|
||||
imageEl.style.backgroundColor = '';
|
||||
|
||||
|
||||
this.setPageBackground(coverUrl);
|
||||
if (backgroundSettings.isEnabled() && track.album?.cover) {
|
||||
this.extractAndApplyColor(this.api.getCoverUrl(track.album.cover, '80'));
|
||||
|
|
@ -974,7 +982,7 @@ export class UIRenderer {
|
|||
|
||||
artistEl.innerHTML = `<a href="/artist/${track.artist.id}">${escapeHtml(track.artist.name)}</a>`;
|
||||
albumEl.innerHTML = `<a href="/album/${track.album.id}">${escapeHtml(track.album.title)}</a>`;
|
||||
|
||||
|
||||
if (track.album.releaseDate) {
|
||||
const date = new Date(track.album.releaseDate);
|
||||
yearEl.textContent = date.getFullYear();
|
||||
|
|
@ -1004,7 +1012,7 @@ export class UIRenderer {
|
|||
|
||||
this.updateLikeState(likeBtn, 'track', track.id);
|
||||
trackDataStore.set(likeBtn, track);
|
||||
|
||||
|
||||
downloadBtn.dataset.action = 'download';
|
||||
downloadBtn.classList.add('track-action-btn');
|
||||
trackDataStore.set(downloadBtn, track);
|
||||
|
|
@ -1012,10 +1020,10 @@ export class UIRenderer {
|
|||
if (track.album.id) {
|
||||
const albumData = await this.api.getAlbum(track.album.id);
|
||||
const tracks = albumData.tracks;
|
||||
|
||||
|
||||
if (tracks.length > 1) {
|
||||
albumSection.style.display = 'block';
|
||||
const otherTracks = tracks.filter(t => t.id !== track.id);
|
||||
const otherTracks = tracks.filter((t) => t.id !== track.id);
|
||||
this.renderListWithTracks(albumTracksContainer, otherTracks, false);
|
||||
} else {
|
||||
albumSection.style.display = 'none';
|
||||
|
|
@ -1121,7 +1129,7 @@ export class UIRenderer {
|
|||
|
||||
const folders = await db.getFolders();
|
||||
if (foldersContainer) {
|
||||
foldersContainer.innerHTML = folders.map(f => this.createFolderCardHTML(f)).join('');
|
||||
foldersContainer.innerHTML = folders.map((f) => this.createFolderCardHTML(f)).join('');
|
||||
foldersContainer.style.display = folders.length ? 'grid' : 'none';
|
||||
}
|
||||
|
||||
|
|
@ -1965,7 +1973,7 @@ export class UIRenderer {
|
|||
removeBtn.innerHTML =
|
||||
'<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/><line x1="10" y1="11" x2="10" y2="17"/><line x1="14" y1="11" x2="14" y2="17"/></svg>';
|
||||
removeBtn.dataset.trackId = currentTracks[index].id;
|
||||
|
||||
|
||||
const menuBtn = actionsDiv.querySelector('.track-menu-btn');
|
||||
actionsDiv.insertBefore(removeBtn, menuBtn);
|
||||
});
|
||||
|
|
@ -1990,7 +1998,9 @@ export class UIRenderer {
|
|||
} else if (sortType === 'added-oldest') {
|
||||
currentTracks = [...originalTracks].sort((a, b) => (a.addedAt || 0) - (b.addedAt || 0));
|
||||
} else if (sortType === 'title') {
|
||||
currentTracks = [...originalTracks].sort((a, b) => (a.title || '').localeCompare(b.title || ''));
|
||||
currentTracks = [...originalTracks].sort((a, b) =>
|
||||
(a.title || '').localeCompare(b.title || '')
|
||||
);
|
||||
} else if (sortType === 'artist') {
|
||||
currentTracks = [...originalTracks].sort((a, b) => {
|
||||
const artistA = a.artist?.name || a.artists?.[0]?.name || '';
|
||||
|
|
@ -2015,7 +2025,13 @@ export class UIRenderer {
|
|||
}
|
||||
|
||||
// Render Actions (Shuffle, Edit, Delete, Share, Sort)
|
||||
this.updatePlaylistHeaderActions(playlistData, !!ownedPlaylist, tracks, false, !!ownedPlaylist ? applySort : null);
|
||||
this.updatePlaylistHeaderActions(
|
||||
playlistData,
|
||||
!!ownedPlaylist,
|
||||
tracks,
|
||||
false,
|
||||
ownedPlaylist ? applySort : null
|
||||
);
|
||||
|
||||
playBtn.onclick = () => {
|
||||
this.player.setQueue(currentTracks, 0);
|
||||
|
|
@ -2137,9 +2153,11 @@ export class UIRenderer {
|
|||
if (!folder) throw new Error('Folder not found');
|
||||
|
||||
imageEl.src = folder.cover || '/assets/folder.png';
|
||||
imageEl.onerror = () => { imageEl.src = '/assets/folder.png'; };
|
||||
imageEl.onerror = () => {
|
||||
imageEl.src = '/assets/folder.png';
|
||||
};
|
||||
imageEl.style.backgroundColor = '';
|
||||
|
||||
|
||||
titleEl.textContent = folder.name;
|
||||
metaEl.textContent = `Created ${new Date(folder.createdAt).toLocaleDateString()}`;
|
||||
|
||||
|
|
@ -2467,7 +2485,13 @@ export class UIRenderer {
|
|||
const actionsDiv = document.getElementById('page-playlist').querySelector('.detail-header-actions');
|
||||
|
||||
// Cleanup existing dynamic buttons
|
||||
['shuffle-playlist-btn', 'edit-playlist-btn', 'delete-playlist-btn', 'share-playlist-btn', 'sort-playlist-btn'].forEach((id) => {
|
||||
[
|
||||
'shuffle-playlist-btn',
|
||||
'edit-playlist-btn',
|
||||
'delete-playlist-btn',
|
||||
'share-playlist-btn',
|
||||
'sort-playlist-btn',
|
||||
].forEach((id) => {
|
||||
const btn = actionsDiv.querySelector(`#${id}`);
|
||||
if (btn) btn.remove();
|
||||
});
|
||||
|
|
@ -2493,11 +2517,11 @@ export class UIRenderer {
|
|||
sortBtn.className = 'btn-secondary';
|
||||
sortBtn.innerHTML =
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M7 12h10"/><path d="M10 18h4"/></svg><span>Sort</span>';
|
||||
|
||||
|
||||
sortBtn.onclick = (e) => {
|
||||
e.stopPropagation();
|
||||
const menu = document.getElementById('sort-menu');
|
||||
|
||||
|
||||
const rect = sortBtn.getBoundingClientRect();
|
||||
menu.style.top = `${rect.bottom + 5}px`;
|
||||
menu.style.left = `${rect.left}px`;
|
||||
|
|
@ -2517,7 +2541,7 @@ export class UIRenderer {
|
|||
};
|
||||
|
||||
menu.onclick = handleSort;
|
||||
|
||||
|
||||
setTimeout(() => document.addEventListener('click', closeMenu), 0);
|
||||
};
|
||||
fragment.appendChild(sortBtn);
|
||||
|
|
@ -2776,7 +2800,8 @@ export class UIRenderer {
|
|||
document.body.classList.add('sidebar-collapsed');
|
||||
const toggleBtn = document.getElementById('sidebar-toggle');
|
||||
if (toggleBtn) {
|
||||
toggleBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6"/></svg>';
|
||||
toggleBtn.innerHTML =
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6"/></svg>';
|
||||
}
|
||||
|
||||
const imageEl = document.getElementById('track-detail-image');
|
||||
|
|
@ -2788,7 +2813,7 @@ export class UIRenderer {
|
|||
const albumTracksContainer = document.getElementById('track-detail-album-tracks');
|
||||
const similarSection = document.getElementById('track-similar-section');
|
||||
const similarTracksContainer = document.getElementById('track-detail-similar-tracks');
|
||||
|
||||
|
||||
const playBtn = document.getElementById('play-track-btn');
|
||||
const lyricsBtn = document.getElementById('track-lyrics-btn');
|
||||
const shareBtn = document.getElementById('share-track-btn');
|
||||
|
|
@ -2813,11 +2838,11 @@ export class UIRenderer {
|
|||
|
||||
try {
|
||||
const track = await this.api.getTrackMetadata(trackId);
|
||||
|
||||
|
||||
const coverUrl = this.api.getCoverUrl(track.album?.cover);
|
||||
imageEl.src = coverUrl;
|
||||
imageEl.style.backgroundColor = '';
|
||||
|
||||
|
||||
this.setPageBackground(coverUrl);
|
||||
if (backgroundSettings.isEnabled() && track.album?.cover) {
|
||||
this.extractAndApplyColor(this.api.getCoverUrl(track.album.cover, '80'));
|
||||
|
|
@ -2852,7 +2877,7 @@ export class UIRenderer {
|
|||
const date = new Date(track.album.releaseDate);
|
||||
yearEl.textContent = date.getFullYear();
|
||||
}
|
||||
|
||||
|
||||
if (track.copyright || track.album.copyright) {
|
||||
yearEl.textContent += ` • ${track.copyright || track.album.copyright}`;
|
||||
}
|
||||
|
|
@ -2882,7 +2907,7 @@ export class UIRenderer {
|
|||
|
||||
this.updateLikeState(likeBtn, 'track', track.id);
|
||||
trackDataStore.set(likeBtn, track);
|
||||
|
||||
|
||||
downloadBtn.dataset.action = 'download';
|
||||
downloadBtn.classList.add('track-action-btn');
|
||||
trackDataStore.set(downloadBtn, track);
|
||||
|
|
@ -2893,7 +2918,7 @@ export class UIRenderer {
|
|||
const tracks = albumData.tracks;
|
||||
if (tracks.length > 1) {
|
||||
albumSection.style.display = 'block';
|
||||
const otherTracks = tracks.filter(t => t.id != track.id);
|
||||
const otherTracks = tracks.filter((t) => t.id != track.id);
|
||||
this.renderListWithTracks(albumTracksContainer, otherTracks, false, false, true);
|
||||
}
|
||||
} catch (err) {
|
||||
|
|
@ -2901,14 +2926,17 @@ export class UIRenderer {
|
|||
}
|
||||
}
|
||||
|
||||
this.api.getRecommendedTracksForPlaylist([track], 5).then(similarTracks => {
|
||||
if (similarTracks.length > 0) {
|
||||
this.renderListWithTracks(similarTracksContainer, similarTracks, true);
|
||||
similarSection.style.display = 'block';
|
||||
} else {
|
||||
similarSection.style.display = 'none';
|
||||
}
|
||||
}).catch(() => similarSection.style.display = 'none');
|
||||
this.api
|
||||
.getRecommendedTracksForPlaylist([track], 5)
|
||||
.then((similarTracks) => {
|
||||
if (similarTracks.length > 0) {
|
||||
this.renderListWithTracks(similarTracksContainer, similarTracks, true);
|
||||
similarSection.style.display = 'block';
|
||||
} else {
|
||||
similarSection.style.display = 'none';
|
||||
}
|
||||
})
|
||||
.catch(() => (similarSection.style.display = 'none'));
|
||||
|
||||
document.title = `${displayTitle} - ${artistName}`;
|
||||
} catch (e) {
|
||||
|
|
|
|||
20
js/utils.js
20
js/utils.js
|
|
@ -285,10 +285,14 @@ function resizeImageBlob(blob, size) {
|
|||
ctx.imageSmoothingEnabled = true;
|
||||
ctx.imageSmoothingQuality = 'high';
|
||||
ctx.drawImage(img, 0, 0, size, size);
|
||||
canvas.toBlob((resizedBlob) => {
|
||||
if (resizedBlob) resolve(resizedBlob);
|
||||
else reject(new Error('Canvas toBlob failed'));
|
||||
}, blob.type || 'image/jpeg', 0.9);
|
||||
canvas.toBlob(
|
||||
(resizedBlob) => {
|
||||
if (resizedBlob) resolve(resizedBlob);
|
||||
else reject(new Error('Canvas toBlob failed'));
|
||||
},
|
||||
blob.type || 'image/jpeg',
|
||||
0.9
|
||||
);
|
||||
};
|
||||
img.onerror = (e) => {
|
||||
URL.revokeObjectURL(url);
|
||||
|
|
@ -309,18 +313,18 @@ export async function getCoverBlob(api, coverId) {
|
|||
if (sizeStr.includes('x')) {
|
||||
sizeStr = sizeStr.split('x')[0];
|
||||
}
|
||||
|
||||
|
||||
let requestedSize = parseInt(sizeStr, 10);
|
||||
if (isNaN(requestedSize) || requestedSize <= 0) requestedSize = 1280;
|
||||
|
||||
const cacheKey = `${coverId}-${requestedSize}`;
|
||||
if (coverCache.has(cacheKey)) return coverCache.get(cacheKey);
|
||||
|
||||
// Tidal seems to only support these soooo
|
||||
// Tidal seems to only support these soooo
|
||||
const supportedSizes = [80, 160, 320, 640, 1280];
|
||||
let fetchSize = 1280;
|
||||
|
||||
const bestSize = supportedSizes.find(s => s >= requestedSize);
|
||||
|
||||
const bestSize = supportedSizes.find((s) => s >= requestedSize);
|
||||
if (bestSize) {
|
||||
fetchSize = bestSize;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ export class Visualizer {
|
|||
this.isActive = false;
|
||||
this.animationId = null;
|
||||
this.particles = [];
|
||||
|
||||
|
||||
this.kick = 0;
|
||||
this.lastIntensity = 0;
|
||||
this.lastBeatTime = 0;
|
||||
|
|
@ -33,7 +33,7 @@ export class Visualizer {
|
|||
this.source.connect(this.analyser);
|
||||
this.analyser.connect(this.audioContext.destination);
|
||||
} catch (e) {
|
||||
console.warn("Visualizer init failed (likely CORS or already connected):", e);
|
||||
console.warn('Visualizer init failed (likely CORS or already connected):', e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -45,11 +45,11 @@ export class Visualizer {
|
|||
if (this.audioContext.state === 'suspended') {
|
||||
this.audioContext.resume();
|
||||
}
|
||||
|
||||
|
||||
this.resize();
|
||||
window.addEventListener('resize', this.resizeBound);
|
||||
this.canvas.style.display = 'block';
|
||||
|
||||
|
||||
this.particles = [];
|
||||
this.energyAverage = 0.3;
|
||||
this.kick = 0;
|
||||
|
|
@ -61,7 +61,7 @@ export class Visualizer {
|
|||
this.isActive = false;
|
||||
if (this.animationId) cancelAnimationFrame(this.animationId);
|
||||
window.removeEventListener('resize', this.resizeBound);
|
||||
|
||||
|
||||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
this.canvas.style.display = 'none';
|
||||
}
|
||||
|
|
@ -93,15 +93,12 @@ export class Visualizer {
|
|||
|
||||
for (let i = 0; i < 4; i++) bassSum += dataArray[i];
|
||||
const bass = bassSum / 4 / 255;
|
||||
|
||||
|
||||
const intensity = bass * bass;
|
||||
|
||||
const intensity = bass * bass;
|
||||
|
||||
this.energyAverage = this.energyAverage * 0.99 + intensity * 0.01;
|
||||
this.upbeatSmoother = this.upbeatSmoother * 0.92 + intensity * 0.08;
|
||||
|
||||
|
||||
if (visualizerSettings.isSmartIntensityEnabled()) {
|
||||
let target = 0.1;
|
||||
if (this.energyAverage > 0.4) {
|
||||
|
|
@ -113,18 +110,14 @@ export class Visualizer {
|
|||
sensitivity = target;
|
||||
}
|
||||
|
||||
|
||||
let threshold = 0.5;
|
||||
if (this.energyAverage < 0.3) {
|
||||
|
||||
threshold = 0.5 + (0.3 - this.energyAverage) * 2;
|
||||
}
|
||||
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
if (intensity > threshold) {
|
||||
|
||||
if (intensity > this.lastIntensity + 0.05 && now - this.lastBeatTime > 50) {
|
||||
this.kick = 1.0;
|
||||
this.lastBeatTime = now;
|
||||
|
|
@ -145,7 +138,6 @@ export class Visualizer {
|
|||
}
|
||||
this.lastIntensity = intensity;
|
||||
|
||||
|
||||
let shakeX = 0;
|
||||
let shakeY = 0;
|
||||
if (this.kick > 0.1) {
|
||||
|
|
@ -154,8 +146,8 @@ export class Visualizer {
|
|||
shakeY = (Math.random() - 0.5) * shakeAmt;
|
||||
}
|
||||
|
||||
const primaryColor = getComputedStyle(document.documentElement).getPropertyValue('--primary').trim() || '#ffffff';
|
||||
|
||||
const primaryColor =
|
||||
getComputedStyle(document.documentElement).getPropertyValue('--primary').trim() || '#ffffff';
|
||||
|
||||
const particleCount = 180;
|
||||
if (this.particles.length !== particleCount) {
|
||||
|
|
@ -167,22 +159,21 @@ export class Visualizer {
|
|||
vx: (Math.random() - 0.5) * 2,
|
||||
vy: (Math.random() - 0.5) * 2,
|
||||
size: Math.random() * 3 + 1,
|
||||
baseSize: Math.random() * 3 + 1
|
||||
baseSize: Math.random() * 3 + 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ctx.save();
|
||||
ctx.translate(shakeX, shakeY);
|
||||
|
||||
|
||||
ctx.fillStyle = primaryColor;
|
||||
ctx.strokeStyle = primaryColor;
|
||||
|
||||
for (let i = 0; i < this.particles.length; i++) {
|
||||
let p = this.particles[i];
|
||||
|
||||
|
||||
const speedMult = 1 + (intensity * 2) + this.kick * 8 * sensitivity;
|
||||
const speedMult = 1 + intensity * 2 + this.kick * 8 * sensitivity;
|
||||
p.x += p.vx * speedMult;
|
||||
p.y += p.vy * speedMult;
|
||||
|
||||
|
|
@ -196,9 +187,8 @@ export class Visualizer {
|
|||
if (p.y < 0) p.y = h;
|
||||
if (p.y > h) p.y = 0;
|
||||
|
||||
|
||||
const size = p.baseSize * (1 + intensity * 0.5 + this.kick * 0.8 * sensitivity);
|
||||
ctx.globalAlpha = 0.4 + (intensity * 0.2) + this.kick * 0.15 * sensitivity;
|
||||
ctx.globalAlpha = 0.4 + intensity * 0.2 + this.kick * 0.15 * sensitivity;
|
||||
ctx.beginPath();
|
||||
ctx.arc(p.x, p.y, size, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
|
|
@ -207,8 +197,8 @@ export class Visualizer {
|
|||
const p2 = this.particles[j];
|
||||
const dx = p.x - p2.x;
|
||||
const dy = p.y - p2.y;
|
||||
const distSq = dx*dx + dy*dy;
|
||||
|
||||
const distSq = dx * dx + dy * dy;
|
||||
|
||||
const maxDist = 150 + intensity * 50 + this.kick * 50 * sensitivity;
|
||||
const maxDistSq = maxDist * maxDist;
|
||||
|
||||
|
|
@ -225,4 +215,4 @@ export class Visualizer {
|
|||
}
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
21
styles.css
21
styles.css
|
|
@ -2076,7 +2076,7 @@ input:checked + .slider::before {
|
|||
.fullscreen-progress-container .progress-bar {
|
||||
flex: 1;
|
||||
height: 6px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
background: rgb(255, 255, 255, 0.2);
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
|
|
@ -2110,7 +2110,7 @@ input:checked + .slider::before {
|
|||
}
|
||||
|
||||
.fullscreen-buttons button:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
background: rgb(255, 255, 255, 0.1);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
|
|
@ -3193,7 +3193,7 @@ img[src=''] {
|
|||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 6rem 2rem 2rem 2rem;
|
||||
padding: 6rem 2rem 2rem;
|
||||
transition: flex 0.3s ease;
|
||||
}
|
||||
|
||||
|
|
@ -3627,7 +3627,7 @@ img[src=''] {
|
|||
padding: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.now-playing-bar {
|
||||
.now-playing-bar {
|
||||
width: calc(96% - 160px) !important;
|
||||
left: calc(160px + 2%);
|
||||
}
|
||||
|
|
@ -4348,7 +4348,6 @@ body:has(#fullscreen-cover-overlay:not([style*='display: none'])) .now-playing-b
|
|||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
|
||||
/* Genius i love genius brah!! */
|
||||
.genius-annotation-modal {
|
||||
position: fixed;
|
||||
|
|
@ -4357,7 +4356,7 @@ body:has(#fullscreen-cover-overlay:not([style*='display: none'])) .now-playing-b
|
|||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
background: rgb(0, 0, 0, 0.6);
|
||||
backdrop-filter: blur(4px);
|
||||
animation: fade-in 0.2s ease;
|
||||
}
|
||||
|
|
@ -4424,14 +4423,14 @@ body:has(#fullscreen-cover-overlay:not([style*='display: none'])) .now-playing-b
|
|||
}
|
||||
|
||||
.genius-annotated {
|
||||
background-color: rgba(255, 255, 100, 0.1);
|
||||
background-color: rgb(255, 255, 100, 0.1);
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.genius-annotated:hover {
|
||||
background-color: rgba(255, 255, 100, 0.2);
|
||||
background-color: rgb(255, 255, 100, 0.2);
|
||||
}
|
||||
|
||||
.genius-multi-start {
|
||||
|
|
@ -4491,7 +4490,7 @@ body.sidebar-collapsed #sidebar-toggle {
|
|||
#page-track .detail-header-image {
|
||||
width: 350px;
|
||||
height: 350px;
|
||||
box-shadow: 0 30px 60px rgba(0, 0, 0, 0.5);
|
||||
box-shadow: 0 30px 60px rgb(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
#page-track .detail-header-info {
|
||||
|
|
@ -4526,12 +4525,12 @@ body.sidebar-collapsed #sidebar-toggle {
|
|||
width: 250px;
|
||||
height: 250px;
|
||||
}
|
||||
|
||||
#page-track .detail-header-info .title {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.tracker-modal .track-item.loading {
|
||||
opacity: 0.7;
|
||||
pointer-events: none;
|
||||
|
|
@ -4608,4 +4607,4 @@ body.sidebar-collapsed #sidebar-toggle {
|
|||
overflow-y: auto;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue