Spotify imports, last.fm scrobbling library fixed, color improvements & more
This commit is contained in:
parent
9af5a53018
commit
0449e96ffa
9 changed files with 519 additions and 32 deletions
22
index.html
22
index.html
|
|
@ -73,6 +73,12 @@
|
|||
<div class="modal-content" style="background: var(--card); padding: 2rem; border-radius: var(--radius); max-width: 400px; width: 90%;">
|
||||
<h3 id="playlist-modal-title">Create Playlist</h3>
|
||||
<input type="text" id="playlist-name-input" placeholder="Playlist name" style="width: 100%; margin: 1rem 0; padding: 0.5rem; border: 1px solid var(--border); border-radius: var(--radius); background: var(--background); color: var(--foreground);">
|
||||
<div id="csv-import-section" style="display: none; margin: 1rem 0; padding: 1rem; border: 1px solid var(--border); border-radius: var(--radius); background: var(--background-secondary);">
|
||||
<p style="margin-bottom: 0.5rem; font-size: 0.9rem;">Import from CSV</p>
|
||||
<p style="font-size: 0.8rem; margin: 0;">Only Spotify Is Supported for now. please use <a href="https://exportify.app/" style="text-decoration: underline;">Exportify</a> to export your playlist into a csv.</p>
|
||||
<br>
|
||||
<input type="file" id="csv-file-input" class="btn-secondary" accept=".csv" style="width: 100%; margin-bottom: 0.5rem;">
|
||||
</div>
|
||||
<div class="modal-actions" style="display: flex; gap: 0.5rem; justify-content: flex-end;">
|
||||
<button id="playlist-modal-cancel" class="btn-secondary">Cancel</button>
|
||||
<button id="playlist-modal-save" class="btn-primary">Save</button>
|
||||
|
|
@ -83,6 +89,22 @@
|
|||
|
||||
<div id="sidebar-overlay"></div>
|
||||
|
||||
<div id="csv-import-progress" class="csv-import-progress" style="display: none;">
|
||||
<div class="progress-header">
|
||||
<h4>Importing Tracks from CSV</h4>
|
||||
<p class="progress-warning">This can take a while depending on your playlist size. Please be patient.</p>
|
||||
</div>
|
||||
<div class="progress-content">
|
||||
<div class="current-track">Preparing import...</div>
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" id="csv-progress-fill"></div>
|
||||
</div>
|
||||
<div class="progress-text">
|
||||
<span id="csv-progress-current">0</span> / <span id="csv-progress-total">0</span> tracks processed
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="app-container">
|
||||
<aside class="sidebar">
|
||||
<div>
|
||||
|
|
|
|||
178
js/app.js
178
js/app.js
|
|
@ -386,6 +386,9 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||
const modal = document.getElementById('playlist-modal');
|
||||
document.getElementById('playlist-modal-title').textContent = 'Create Playlist';
|
||||
document.getElementById('playlist-name-input').value = '';
|
||||
modal.dataset.editingId = '';
|
||||
document.getElementById('csv-import-section').style.display = 'block';
|
||||
document.getElementById('csv-file-input').value = '';
|
||||
modal.style.display = 'flex';
|
||||
document.getElementById('playlist-name-input').focus();
|
||||
}
|
||||
|
|
@ -409,7 +412,67 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||
});
|
||||
} else {
|
||||
// Create
|
||||
db.createPlaylist(name, [], '').then(playlist => {
|
||||
const csvFileInput = document.getElementById('csv-file-input');
|
||||
let tracks = [];
|
||||
|
||||
if (csvFileInput.files.length > 0) {
|
||||
// Import from CSV
|
||||
const file = csvFileInput.files[0];
|
||||
const progressElement = document.getElementById('csv-import-progress');
|
||||
const progressFill = document.getElementById('csv-progress-fill');
|
||||
const progressCurrent = document.getElementById('csv-progress-current');
|
||||
const progressTotal = document.getElementById('csv-progress-total');
|
||||
const currentTrackElement = progressElement.querySelector('.current-track');
|
||||
|
||||
try {
|
||||
// Show progress bar
|
||||
progressElement.style.display = 'block';
|
||||
progressFill.style.width = '0%';
|
||||
progressCurrent.textContent = '0';
|
||||
currentTrackElement.textContent = 'Reading CSV file...';
|
||||
|
||||
const csvText = await file.text();
|
||||
const lines = csvText.trim().split('\n');
|
||||
const totalTracks = Math.max(0, lines.length - 1);
|
||||
progressTotal.textContent = totalTracks.toString();
|
||||
|
||||
const result = await parseCSV(csvText, api, (progress) => {
|
||||
const percentage = totalTracks > 0 ? (progress.current / totalTracks) * 100 : 0;
|
||||
progressFill.style.width = `${Math.min(percentage, 100)}%`;
|
||||
progressCurrent.textContent = progress.current.toString();
|
||||
currentTrackElement.textContent = progress.currentTrack;
|
||||
});
|
||||
|
||||
tracks = result.tracks;
|
||||
const missingTracks = result.missingTracks;
|
||||
|
||||
if (tracks.length === 0) {
|
||||
alert('No valid tracks found in the CSV file! Please check the format.');
|
||||
progressElement.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
console.log(`Imported ${tracks.length} tracks from CSV`);
|
||||
|
||||
// if theres missing songs, warn the user
|
||||
if (missingTracks.length > 0) {
|
||||
setTimeout(() => {
|
||||
showMissingTracksNotification(missingTracks);
|
||||
}, 500);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to parse CSV!', error);
|
||||
alert('Failed to parse CSV file! ' + error.message);
|
||||
progressElement.style.display = 'none';
|
||||
return;
|
||||
} finally {
|
||||
// Hide progress bar
|
||||
setTimeout(() => {
|
||||
progressElement.style.display = 'none';
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
db.createPlaylist(name, tracks, '').then(playlist => {
|
||||
syncManager.syncUserPlaylist(playlist, 'create');
|
||||
ui.renderLibraryPage();
|
||||
modal.style.display = 'none';
|
||||
|
|
@ -431,6 +494,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||
document.getElementById('playlist-modal-title').textContent = 'Edit Playlist';
|
||||
document.getElementById('playlist-name-input').value = playlist.name;
|
||||
modal.dataset.editingId = playlistId;
|
||||
document.getElementById('csv-import-section').style.display = 'none';
|
||||
modal.style.display = 'flex';
|
||||
document.getElementById('playlist-name-input').focus();
|
||||
}
|
||||
|
|
@ -456,6 +520,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||
document.getElementById('playlist-modal-title').textContent = 'Edit Playlist';
|
||||
document.getElementById('playlist-name-input').value = playlist.name;
|
||||
modal.dataset.editingId = playlistId;
|
||||
document.getElementById('csv-import-section').style.display = 'none';
|
||||
modal.style.display = 'flex';
|
||||
document.getElementById('playlist-name-input').focus();
|
||||
}
|
||||
|
|
@ -815,6 +880,117 @@ function showInstallPrompt(deferredPrompt) {
|
|||
});
|
||||
}
|
||||
|
||||
function showMissingTracksNotification(missingTracks) {
|
||||
const modal = document.createElement('div');
|
||||
modal.className = 'missing-tracks-modal-overlay';
|
||||
modal.innerHTML = `
|
||||
<div class="missing-tracks-modal">
|
||||
<div class="missing-tracks-header">
|
||||
<h3>Note</h3>
|
||||
<button class="close-missing-tracks">×</button>
|
||||
</div>
|
||||
<div class="missing-tracks-content">
|
||||
<p>Unfortunately some songs weren't able to be added. This could be an issue with our import system, try searching for the song and adding it. But it could also be due to Monochrome not having it sadly :(</p>
|
||||
<div class="missing-tracks-list">
|
||||
<h4>Missing Tracks:</h4>
|
||||
<ul>
|
||||
${missingTracks.map(track => `<li>${track}</li>`).join('')}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="missing-tracks-actions">
|
||||
<button class="btn-secondary" id="close-missing-tracks-btn">OK</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
document.body.appendChild(modal);
|
||||
|
||||
const closeModal = () => modal.remove();
|
||||
|
||||
modal.addEventListener('click', (e) => {
|
||||
if (e.target === modal || e.target.classList.contains('close-missing-tracks') || e.target.id === 'close-missing-tracks-btn') {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function parseCSV(csvText, api, onProgress) {
|
||||
const lines = csvText.trim().split('\n');
|
||||
if (lines.length < 2) return [];
|
||||
|
||||
const headers = lines[0].split(',').map(h => h.replace(/"/g, '').trim());
|
||||
const rows = lines.slice(1);
|
||||
|
||||
const tracks = [];
|
||||
const missingTracks = [];
|
||||
const totalTracks = rows.length;
|
||||
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const row = rows[i];
|
||||
const values = row.split(',').map(v => v.replace(/"/g, '').trim());
|
||||
if (values.length >= headers.length) {
|
||||
let trackTitle = '';
|
||||
let artistNames = '';
|
||||
|
||||
headers.forEach((header, index) => {
|
||||
const value = values[index];
|
||||
switch (header.toLowerCase()) {
|
||||
case 'track name':
|
||||
case 'title':
|
||||
case 'song':
|
||||
trackTitle = value;
|
||||
break;
|
||||
case 'artist name(s)':
|
||||
case 'artist':
|
||||
case 'artists':
|
||||
artistNames = value;
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
if (onProgress) {
|
||||
onProgress({
|
||||
current: i,
|
||||
total: totalTracks,
|
||||
currentTrack: trackTitle || 'Unknown track'
|
||||
});
|
||||
}
|
||||
|
||||
// Search for the track in hifi tidal api's catalog
|
||||
if (trackTitle && artistNames) {
|
||||
try {
|
||||
const searchQuery = `${trackTitle} ${artistNames}`;
|
||||
const searchResults = await api.searchTracks(searchQuery);
|
||||
|
||||
if (searchResults.items && searchResults.items.length > 0) {
|
||||
// Use the first result
|
||||
const foundTrack = searchResults.items[0];
|
||||
tracks.push(foundTrack);
|
||||
console.log(`Found track: "${trackTitle}" by ${artistNames}`);
|
||||
} else {
|
||||
console.warn(`Track not found: "${trackTitle}" by ${artistNames}`);
|
||||
missingTracks.push(`${trackTitle} - ${artistNames}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error searching for track "${trackTitle}":`, error);
|
||||
missingTracks.push(`${trackTitle} - ${artistNames}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// yayyy its finished :P
|
||||
if (onProgress) {
|
||||
onProgress({
|
||||
current: totalTracks,
|
||||
total: totalTracks,
|
||||
currentTrack: 'Import complete'
|
||||
});
|
||||
}
|
||||
|
||||
return { tracks, missingTracks };
|
||||
}
|
||||
|
||||
function showKeyboardShortcuts() {
|
||||
const modal = document.createElement('div');
|
||||
modal.className = 'shortcuts-modal-overlay';
|
||||
|
|
|
|||
3
js/db.js
3
js/db.js
|
|
@ -177,7 +177,8 @@ export class MusicDatabase {
|
|||
album: item.album ? {
|
||||
id: item.album.id,
|
||||
cover: item.album.cover,
|
||||
releaseDate: item.album.releaseDate || null
|
||||
releaseDate: item.album.releaseDate || null,
|
||||
vibrantColor: item.album.vibrantColor || null
|
||||
} : null,
|
||||
// Fallback date
|
||||
streamStartDate: item.streamStartDate || null,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
//js/lastfm.js
|
||||
import { delay } from './utils.js';
|
||||
import { delay, getTrackArtists } from './utils.js';
|
||||
|
||||
export class LastFMScrobbler {
|
||||
constructor() {
|
||||
|
|
@ -155,7 +155,7 @@ export class LastFMScrobbler {
|
|||
|
||||
try {
|
||||
const params = {
|
||||
artist: track.artist?.name || 'Unknown Artist',
|
||||
artist: track.artist?.name || track.artists?.[0]?.name || 'Unknown Artist',
|
||||
track: track.title
|
||||
};
|
||||
|
||||
|
|
@ -205,7 +205,7 @@ export class LastFMScrobbler {
|
|||
const timestamp = Math.floor(Date.now() / 1000);
|
||||
|
||||
const params = {
|
||||
artist: this.currentTrack.artist?.name || 'Unknown Artist',
|
||||
artist: this.currentTrack.artist?.name || this.currentTrack.artists?.[0]?.name || 'Unknown Artist',
|
||||
track: this.currentTrack.title,
|
||||
timestamp: timestamp
|
||||
};
|
||||
|
|
@ -237,7 +237,7 @@ export class LastFMScrobbler {
|
|||
|
||||
try {
|
||||
const params = {
|
||||
artist: track.artist?.name || 'Unknown Artist',
|
||||
artist: track.artist?.name || track.artists?.[0]?.name || 'Unknown Artist',
|
||||
track: track.title
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
//js/lyrics.js
|
||||
import { getTrackTitle, getTrackArtists, SVG_DOWNLOAD, SVG_CLOSE } from './utils.js';
|
||||
import { getTrackTitle, getTrackArtists, buildTrackFilename, SVG_DOWNLOAD, SVG_CLOSE } from './utils.js';
|
||||
import { sidePanelManager } from './side-panel.js';
|
||||
|
||||
export class LyricsManager {
|
||||
|
|
@ -136,7 +136,7 @@ export class LyricsManager {
|
|||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `${getTrackArtists(track)} - ${getTrackTitle(track)}.lrc`;
|
||||
a.download = buildTrackFilename(track, 'LOSSLESS').replace(/\.flac$/, '.lrc');
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ export class Player {
|
|||
}
|
||||
const totalDurationEl = document.getElementById('total-duration');
|
||||
if (totalDurationEl) totalDurationEl.textContent = formatTime(track.duration);
|
||||
document.title = `${trackTitle} • ${track.artist?.name || 'Unknown'}`;
|
||||
document.title = `${trackTitle} • ${getTrackArtists(track)}`;
|
||||
|
||||
this.updatePlayingTrackIndicator();
|
||||
this.updateMediaSession(track);
|
||||
|
|
@ -202,7 +202,7 @@ export class Player {
|
|||
if (mixBtn) {
|
||||
mixBtn.style.display = (track.mixes && track.mixes.TRACK_MIX) ? 'flex' : 'none';
|
||||
}
|
||||
document.title = `${trackTitle} • ${track.artist?.name || 'Unknown'}`;
|
||||
document.title = `${trackTitle} • ${getTrackArtists(track)}`;
|
||||
|
||||
this.updatePlayingTrackIndicator();
|
||||
this.updateMediaSession(track);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
//router.js
|
||||
import { getTrackArtists } from './utils.js';
|
||||
|
||||
export function createRouter(ui) {
|
||||
const router = () => {
|
||||
const path = window.location.hash.substring(1) || "home";
|
||||
|
|
@ -44,7 +46,7 @@ export function createRouter(ui) {
|
|||
export function updateTabTitle(player) {
|
||||
if (player.currentTrack) {
|
||||
const track = player.currentTrack;
|
||||
document.title = `${track.title} • ${track.artist?.name || 'Unknown'} - Monochrome`;
|
||||
document.title = `${track.title} • ${getTrackArtists(track)}`;
|
||||
} else {
|
||||
const hash = window.location.hash;
|
||||
if (hash.includes('#album/') || hash.includes('#playlist/')) {
|
||||
|
|
|
|||
46
js/ui.js
46
js/ui.js
|
|
@ -11,6 +11,7 @@ export class UIRenderer {
|
|||
this.player = player;
|
||||
this.currentTrack = null;
|
||||
this.searchAbortController = null;
|
||||
this.vibrantColorCache = new Map();
|
||||
}
|
||||
|
||||
// Helper for Heart Icon
|
||||
|
|
@ -27,6 +28,15 @@ export class UIRenderer {
|
|||
return;
|
||||
}
|
||||
|
||||
// Check cache first
|
||||
if (this.vibrantColorCache.has(url)) {
|
||||
const cachedColor = this.vibrantColorCache.get(url);
|
||||
if (cachedColor) {
|
||||
this.setVibrantColor(cachedColor);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const img = new Image();
|
||||
img.crossOrigin = 'Anonymous';
|
||||
// Add cache buster to bypass opaque response in cache
|
||||
|
|
@ -37,16 +47,20 @@ export class UIRenderer {
|
|||
try {
|
||||
const color = getVibrantColorFromImage(img);
|
||||
if (color) {
|
||||
this.vibrantColorCache.set(url, color);
|
||||
this.setVibrantColor(color);
|
||||
} else {
|
||||
this.vibrantColorCache.set(url, null);
|
||||
this.resetVibrantColor();
|
||||
}
|
||||
} catch (e) {
|
||||
this.vibrantColorCache.set(url, null);
|
||||
this.resetVibrantColor();
|
||||
}
|
||||
};
|
||||
|
||||
img.onerror = () => {
|
||||
this.vibrantColorCache.set(url, null);
|
||||
this.resetVibrantColor();
|
||||
};
|
||||
}
|
||||
|
|
@ -87,8 +101,8 @@ export class UIRenderer {
|
|||
return;
|
||||
}
|
||||
|
||||
if (backgroundSettings.isEnabled() && this.currentTrack?.album?.vibrantColor) {
|
||||
this.setVibrantColor(this.currentTrack.album.vibrantColor);
|
||||
if (backgroundSettings.isEnabled() && this.currentTrack?.album?.cover) {
|
||||
this.extractAndApplyColor(this.api.getCoverUrl(this.currentTrack.album.cover, '80'));
|
||||
} else {
|
||||
this.resetVibrantColor();
|
||||
}
|
||||
|
|
@ -509,11 +523,6 @@ export class UIRenderer {
|
|||
}
|
||||
|
||||
resetVibrantColor() {
|
||||
if (backgroundSettings.isEnabled() && this.currentTrack?.album?.vibrantColor) {
|
||||
this.setVibrantColor(this.currentTrack.album.vibrantColor);
|
||||
return;
|
||||
}
|
||||
|
||||
const root = document.documentElement;
|
||||
root.style.removeProperty('--primary');
|
||||
root.style.removeProperty('--primary-foreground');
|
||||
|
|
@ -545,8 +554,7 @@ async showFullscreenCover(track, nextTrack, lyricsManager, audioPlayer) {
|
|||
|
||||
nextTrackEl.classList.remove('animate-in');
|
||||
void nextTrackEl.offsetWidth;
|
||||
nextTrackEl.classList.add('animate-in');
|
||||
} else {
|
||||
nextTrackEl.classList.add('animate-in'); } else {
|
||||
nextTrackEl.style.display = 'none';
|
||||
nextTrackEl.classList.remove('animate-in');
|
||||
}
|
||||
|
|
@ -924,9 +932,7 @@ async showFullscreenCover(track, nextTrack, lyricsManager, audioPlayer) {
|
|||
|
||||
// Set background and vibrant color
|
||||
this.setPageBackground(coverUrl);
|
||||
if (backgroundSettings.isEnabled() && album.vibrantColor) {
|
||||
this.setVibrantColor(album.vibrantColor);
|
||||
} else {
|
||||
if (backgroundSettings.isEnabled() && album.cover) {
|
||||
this.extractAndApplyColor(this.api.getCoverUrl(album.cover, '80'));
|
||||
}
|
||||
|
||||
|
|
@ -983,7 +989,7 @@ async showFullscreenCover(track, nextTrack, lyricsManager, audioPlayer) {
|
|||
albumLikeBtn.classList.toggle('active', isLiked);
|
||||
}
|
||||
|
||||
document.title = `${album.title} - ${album.artist.name} - Monochrome`;
|
||||
document.title = `${album.title} - ${album.artist.name}`;
|
||||
|
||||
// "More from Artist" and Related Sections
|
||||
const moreAlbumsSection = document.getElementById('album-section-more-albums');
|
||||
|
|
@ -1191,7 +1197,7 @@ async showFullscreenCover(track, nextTrack, lyricsManager, audioPlayer) {
|
|||
numberOfTracks: userPlaylist.tracks ? userPlaylist.tracks.length : 0,
|
||||
isUserPlaylist: true
|
||||
});
|
||||
document.title = `${userPlaylist.name} - Monochrome`;
|
||||
document.title = userPlaylist.name;
|
||||
} else {
|
||||
// Render API playlist
|
||||
const { playlist, tracks } = await this.api.getPlaylist(playlistId);
|
||||
|
|
@ -1200,7 +1206,7 @@ async showFullscreenCover(track, nextTrack, lyricsManager, audioPlayer) {
|
|||
if (imageId) {
|
||||
imageEl.src = this.api.getCoverUrl(imageId, '1080');
|
||||
this.setPageBackground(imageEl.src);
|
||||
|
||||
|
||||
this.extractAndApplyColor(this.api.getCoverUrl(imageId, '160'));
|
||||
} else {
|
||||
imageEl.src = 'assets/appicon.png';
|
||||
|
|
@ -1242,7 +1248,7 @@ async showFullscreenCover(track, nextTrack, lyricsManager, audioPlayer) {
|
|||
}
|
||||
|
||||
recentActivityManager.addPlaylist(playlist);
|
||||
document.title = `${playlist.title || 'Artist Mix'} - Monochrome`;
|
||||
document.title = playlist.title || 'Artist Mix';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load playlist:", error);
|
||||
|
|
@ -1294,9 +1300,9 @@ async showFullscreenCover(track, nextTrack, lyricsManager, audioPlayer) {
|
|||
} else {
|
||||
imageEl.src = 'assets/appicon.png';
|
||||
this.setPageBackground(null);
|
||||
this.resetVibrantColor();
|
||||
}
|
||||
}
|
||||
this.resetVibrantColor();
|
||||
|
||||
imageEl.style.backgroundColor = '';
|
||||
|
||||
|
|
@ -1336,7 +1342,7 @@ async showFullscreenCover(track, nextTrack, lyricsManager, audioPlayer) {
|
|||
mixLikeBtn.classList.toggle('active', isLiked);
|
||||
}
|
||||
|
||||
document.title = `${displayTitle} - Monochrome`;
|
||||
document.title = displayTitle;
|
||||
} catch (error) {
|
||||
console.error("Failed to load mix:", error);
|
||||
tracklistContainer.innerHTML = createPlaceholder(`Could not load mix details. ${error.message}`);
|
||||
|
|
@ -1403,7 +1409,7 @@ async showFullscreenCover(track, nextTrack, lyricsManager, audioPlayer) {
|
|||
|
||||
// Set background
|
||||
this.setPageBackground(imageEl.src);
|
||||
|
||||
|
||||
// Extract vibrant color using robust image extraction (160x160 for speed/accuracy balance)
|
||||
const artistPic160 = this.api.getArtistPictureUrl(artist.picture, '160');
|
||||
this.extractAndApplyColor(artistPic160);
|
||||
|
|
@ -1458,7 +1464,7 @@ async showFullscreenCover(track, nextTrack, lyricsManager, audioPlayer) {
|
|||
|
||||
recentActivityManager.addArtist(artist);
|
||||
|
||||
document.title = `${artist.name} - Monochrome`;
|
||||
document.title = artist.name;
|
||||
} catch (error) {
|
||||
console.error("Failed to load artist:", error);
|
||||
tracksContainer.innerHTML = albumsContainer.innerHTML =
|
||||
|
|
|
|||
282
styles.css
282
styles.css
|
|
@ -3463,4 +3463,284 @@ img:not([src]), img[src=''] {
|
|||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* fuck this chud ass shit bro */
|
||||
.csv-import-progress {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 1.5rem;
|
||||
box-shadow: var(--shadow-xl);
|
||||
z-index: 10001;
|
||||
max-width: 400px;
|
||||
min-width: 350px;
|
||||
animation: slideIn 0.3s ease;
|
||||
}
|
||||
|
||||
.csv-import-progress .progress-header {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.csv-import-progress .progress-header h4 {
|
||||
margin: 0 0 0.5rem 0;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
.csv-import-progress .progress-warning {
|
||||
font-size: 0.85rem;
|
||||
color: var(--muted-foreground);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.csv-import-progress .progress-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.csv-import-progress .current-track {
|
||||
font-size: 0.9rem;
|
||||
color: var(--foreground);
|
||||
font-weight: 500;
|
||||
min-height: 1.2rem;
|
||||
}
|
||||
|
||||
.csv-import-progress .progress-bar {
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
background: var(--secondary);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.csv-import-progress .progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, var(--primary), var(--highlight));
|
||||
border-radius: 4px;
|
||||
width: 0%;
|
||||
transition: width 0.3s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.csv-import-progress .progress-fill::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent 0%,
|
||||
rgba(255, 255, 255, 0.3) 50%,
|
||||
transparent 100%
|
||||
);
|
||||
animation: shimmer 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% { transform: translateX(-100%); }
|
||||
100% { transform: translateX(100%); }
|
||||
}
|
||||
|
||||
.csv-import-progress .progress-text {
|
||||
font-size: 0.85rem;
|
||||
color: var(--muted-foreground);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.csv-import-progress .progress-text span {
|
||||
font-weight: 600;
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.csv-import-progress {
|
||||
bottom: 10px;
|
||||
right: 10px;
|
||||
left: 10px;
|
||||
max-width: none;
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* OH NO SOME SONGS WERENT FOUND FUCK ME FUCK ME */
|
||||
.missing-tracks-modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10000;
|
||||
animation: fadeIn 0.2s ease;
|
||||
}
|
||||
|
||||
.missing-tracks-modal {
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
max-width: 600px;
|
||||
width: 90%;
|
||||
max-height: 85vh;
|
||||
overflow-y: auto;
|
||||
box-shadow: var(--shadow-xl);
|
||||
animation: scaleIn 0.2s ease;
|
||||
}
|
||||
|
||||
.missing-tracks-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 2rem 2rem 1.5rem 2rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.missing-tracks-header h3 {
|
||||
margin: 0;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
color: var(--foreground);
|
||||
letter-spacing: -0.025em;
|
||||
}
|
||||
|
||||
.close-missing-tracks {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--muted-foreground);
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: var(--radius);
|
||||
transition: all var(--transition);
|
||||
}
|
||||
|
||||
.close-missing-tracks:hover {
|
||||
background: var(--secondary);
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
.missing-tracks-content {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.missing-tracks-content p {
|
||||
margin-bottom: 2rem;
|
||||
line-height: 1.7;
|
||||
color: var(--foreground);
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.missing-tracks-list h4 {
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
color: var(--foreground);
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.missing-tracks-list ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
background: var(--secondary);
|
||||
}
|
||||
|
||||
.missing-tracks-list li {
|
||||
padding: 0.75rem 1.25rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
color: var(--foreground);
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.missing-tracks-list li:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.missing-tracks-list li:nth-child(even) {
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
}
|
||||
|
||||
.missing-tracks-actions {
|
||||
padding: 1.5rem 2rem 2rem 2rem;
|
||||
border-top: 1px solid var(--border);
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.missing-tracks-actions .btn-secondary {
|
||||
min-width: 100px;
|
||||
padding: 0.75rem 1.5rem;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.missing-tracks-modal {
|
||||
width: 95%;
|
||||
max-height: 90vh;
|
||||
margin: 1rem;
|
||||
}
|
||||
|
||||
.missing-tracks-header,
|
||||
.missing-tracks-content,
|
||||
.missing-tracks-actions {
|
||||
padding-left: 1.5rem;
|
||||
padding-right: 1.5rem;
|
||||
}
|
||||
|
||||
.missing-tracks-header {
|
||||
padding-top: 1.5rem;
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.missing-tracks-content {
|
||||
padding-top: 1.5rem;
|
||||
padding-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.missing-tracks-actions {
|
||||
padding-top: 1rem;
|
||||
padding-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.missing-tracks-list ul {
|
||||
max-height: 250px;
|
||||
}
|
||||
|
||||
.missing-tracks-header h3 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.missing-tracks-content p {
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.missing-tracks-list li {
|
||||
padding: 0.625rem 1rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue