matched downloads
This commit is contained in:
parent
c5d9b010c9
commit
edbfe6bb74
4 changed files with 2121 additions and 21 deletions
987
web_server.py
987
web_server.py
File diff suppressed because it is too large
Load diff
|
|
@ -586,6 +586,70 @@
|
|||
<!-- Hidden HTML5 Audio Player for Streaming -->
|
||||
<audio id="audio-player" style="display: none;"></audio>
|
||||
|
||||
<!-- Matched Download Modal -->
|
||||
<div class="modal-overlay hidden" id="matching-modal-overlay">
|
||||
<div class="matching-modal" id="matching-modal">
|
||||
<div class="matching-modal-header">
|
||||
<h2 id="matching-modal-title">Match Download to Spotify</h2>
|
||||
<button class="matching-modal-close" onclick="closeMatchingModal()">✕</button>
|
||||
</div>
|
||||
|
||||
<div class="matching-modal-content">
|
||||
<!-- Artist Selection Stage -->
|
||||
<div id="artist-selection-stage" class="selection-stage">
|
||||
<div class="stage-header">
|
||||
<h3 id="artist-stage-title">Step 1: Select the correct Artist</h3>
|
||||
<p class="stage-subtitle">Choose the artist that best matches your download</p>
|
||||
</div>
|
||||
|
||||
<div class="suggestions-section">
|
||||
<h4 class="suggestions-title">Top Suggestions</h4>
|
||||
<div class="suggestions-container" id="artist-suggestions">
|
||||
<!-- Artist suggestion cards will be populated here -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="manual-search-section">
|
||||
<h4 class="suggestions-title">Or, Search Manually</h4>
|
||||
<input type="text" id="artist-search-input" class="search-input" placeholder="Search for an artist...">
|
||||
<div class="suggestions-container" id="artist-manual-results">
|
||||
<!-- Manual search results will be populated here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Album Selection Stage (for album downloads) -->
|
||||
<div id="album-selection-stage" class="selection-stage hidden">
|
||||
<div class="stage-header">
|
||||
<h3 id="album-stage-title">Step 2: Select the correct Album</h3>
|
||||
<p class="stage-subtitle">Choose the album that best matches your download for <span id="selected-artist-name"></span></p>
|
||||
</div>
|
||||
|
||||
<div class="suggestions-section">
|
||||
<h4 class="suggestions-title">Top Suggestions</h4>
|
||||
<div class="suggestions-container" id="album-suggestions">
|
||||
<!-- Album suggestion cards will be populated here -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="manual-search-section">
|
||||
<h4 class="suggestions-title">Or, Search Manually</h4>
|
||||
<input type="text" id="album-search-input" class="search-input" placeholder="Search for an album...">
|
||||
<div class="suggestions-container" id="album-manual-results">
|
||||
<!-- Manual search results will be populated here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="matching-modal-actions">
|
||||
<button id="skip-matching-btn" class="modal-button modal-button--secondary">Skip Matching</button>
|
||||
<button id="cancel-match-btn" class="modal-button modal-button--cancel">Cancel</button>
|
||||
<button id="confirm-match-btn" class="modal-button modal-button--primary" disabled>Confirm Selection</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Version Info Modal -->
|
||||
<div class="version-modal-overlay hidden" id="version-modal-overlay" onclick="closeVersionModal()">
|
||||
<div class="version-modal" onclick="event.stopPropagation()">
|
||||
|
|
|
|||
|
|
@ -865,11 +865,56 @@ async function copyAddress(address, cryptoName) {
|
|||
// ===============================
|
||||
|
||||
function initializeSettings() {
|
||||
// This function is called when the settings page is loaded.
|
||||
// It attaches event listeners to all interactive elements on the page.
|
||||
|
||||
// Main save button
|
||||
const saveButton = document.getElementById('save-settings');
|
||||
const mediaServerType = document.getElementById('media-server-type');
|
||||
|
||||
saveButton.addEventListener('click', saveSettings);
|
||||
mediaServerType.addEventListener('change', updateMediaServerFields);
|
||||
if (saveButton) {
|
||||
saveButton.addEventListener('click', saveSettings);
|
||||
}
|
||||
|
||||
// Server toggle buttons
|
||||
const plexToggle = document.getElementById('plex-toggle');
|
||||
if (plexToggle) {
|
||||
plexToggle.addEventListener('click', () => toggleServer('plex'));
|
||||
}
|
||||
const jellyfinToggle = document.getElementById('jellyfin-toggle');
|
||||
if (jellyfinToggle) {
|
||||
jellyfinToggle.addEventListener('click', () => toggleServer('jellyfin'));
|
||||
}
|
||||
|
||||
// Auto-detect buttons
|
||||
const detectSlskdBtn = document.querySelector('#soulseek-url + .detect-button');
|
||||
if (detectSlskdBtn) {
|
||||
detectSlskdBtn.addEventListener('click', autoDetectSlskd);
|
||||
}
|
||||
const detectPlexBtn = document.querySelector('#plex-container .detect-button');
|
||||
if (detectPlexBtn) {
|
||||
detectPlexBtn.addEventListener('click', autoDetectPlex);
|
||||
}
|
||||
const detectJellyfinBtn = document.querySelector('#jellyfin-container .detect-button');
|
||||
if (detectJellyfinBtn) {
|
||||
detectJellyfinBtn.addEventListener('click', autoDetectJellyfin);
|
||||
}
|
||||
|
||||
// Test connection buttons
|
||||
const testSpotifyBtn = document.querySelector('.api-test-buttons button[onclick="testConnection(\'spotify\')"]');
|
||||
if (testSpotifyBtn) {
|
||||
testSpotifyBtn.addEventListener('click', () => testConnection('spotify'));
|
||||
}
|
||||
const testTidalBtn = document.querySelector('.api-test-buttons button[onclick="testConnection(\'tidal\')"]');
|
||||
if (testTidalBtn) {
|
||||
testTidalBtn.addEventListener('click', () => testConnection('tidal'));
|
||||
}
|
||||
const testSoulseekBtn = document.querySelector('.api-test-buttons button[onclick="testConnection(\'soulseek\')"]');
|
||||
if (testSoulseekBtn) {
|
||||
testSoulseekBtn.addEventListener('click', () => testConnection('soulseek'));
|
||||
}
|
||||
const testServerBtn = document.querySelector('.server-test-btn');
|
||||
if (testServerBtn) {
|
||||
testServerBtn.addEventListener('click', () => testConnection('server'));
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSettingsData() {
|
||||
|
|
@ -1705,6 +1750,7 @@ function displayDownloadsResults(results) {
|
|||
<div class="track-item-actions">
|
||||
<button onclick="streamAlbumTrack(${index}, ${trackIndex})" class="track-stream-btn">▶</button>
|
||||
<button onclick="downloadAlbumTrack(${index}, ${trackIndex})" class="track-download-btn">⬇</button>
|
||||
<button onclick="matchedDownloadAlbumTrack(${index}, ${trackIndex})" class="track-matched-btn" title="Matched Download">🎯</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -1726,6 +1772,7 @@ function displayDownloadsResults(results) {
|
|||
</div>
|
||||
<div class="album-actions" onclick="event.stopPropagation()">
|
||||
<button onclick="downloadAlbum(${index})" class="album-download-btn">⬇ Download Album</button>
|
||||
<button onclick="matchedDownloadAlbum(${index})" class="album-matched-btn">🎯 Matched Album</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="album-track-list" style="display: none;">
|
||||
|
|
@ -1748,8 +1795,9 @@ function displayDownloadsResults(results) {
|
|||
<div class="track-uploader">Shared by ${escapeHtml(result.username || 'Unknown')}</div>
|
||||
</div>
|
||||
<div class="track-actions">
|
||||
<button onclick="streamTrack(${index})" class="track-stream-btn">▶ Stream</button>
|
||||
<button onclick="downloadTrack(${index})" class="track-download-btn">⬇ Download</button>
|
||||
<button onclick="streamTrack(${index})" class="track-stream-btn" title="Stream Track">▶</button>
|
||||
<button onclick="downloadTrack(${index})" class="track-download-btn" title="Download">⬇</button>
|
||||
<button onclick="matchedDownloadTrack(${index})" class="track-matched-btn" title="Matched Download">🎯</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -1813,6 +1861,49 @@ async function downloadAlbum(index) {
|
|||
}
|
||||
}
|
||||
|
||||
// Matched download functions
|
||||
function matchedDownloadTrack(index) {
|
||||
const results = window.currentSearchResults;
|
||||
if (!results || !results[index]) return;
|
||||
|
||||
const track = results[index];
|
||||
console.log('🎯 Starting matched download for single track:', track);
|
||||
|
||||
// Open matching modal for single track
|
||||
openMatchingModal(track, false, null);
|
||||
}
|
||||
|
||||
function matchedDownloadAlbum(index) {
|
||||
const results = window.currentSearchResults;
|
||||
if (!results || !results[index]) return;
|
||||
|
||||
const album = results[index];
|
||||
console.log('🎯 Starting matched download for album:', album);
|
||||
|
||||
// Open matching modal for album download
|
||||
openMatchingModal(album, true, album);
|
||||
}
|
||||
|
||||
function matchedDownloadAlbumTrack(albumIndex, trackIndex) {
|
||||
const results = window.currentSearchResults;
|
||||
if (!results || !results[albumIndex]) return;
|
||||
|
||||
const album = results[albumIndex];
|
||||
if (!album.tracks || !album.tracks[trackIndex]) return;
|
||||
|
||||
const track = album.tracks[trackIndex];
|
||||
|
||||
// Ensure track has necessary properties from parent album
|
||||
track.username = album.username;
|
||||
track.artist = track.artist || album.artist;
|
||||
track.album = album.album_title || album.title;
|
||||
|
||||
console.log('🎯 Starting matched download for album track:', track);
|
||||
|
||||
// Open matching modal for single track (from album context)
|
||||
openMatchingModal(track, false, null);
|
||||
}
|
||||
|
||||
function toggleAlbumExpansion(albumIndex) {
|
||||
const albumCard = document.querySelector(`[data-album-index="${albumIndex}"]`);
|
||||
if (!albumCard) return;
|
||||
|
|
@ -2281,7 +2372,11 @@ const additionalStyles = `
|
|||
// Inject additional styles
|
||||
document.head.insertAdjacentHTML('beforeend', additionalStyles);
|
||||
|
||||
// Global functions (for onclick handlers)
|
||||
// Make functions available globally for onclick handlers
|
||||
window.openMatchingModal = openMatchingModal;
|
||||
window.closeMatchingModal = closeMatchingModal;
|
||||
window.selectArtist = selectArtist;
|
||||
window.selectAlbum = selectAlbum;
|
||||
window.navigateToPage = navigateToPage;
|
||||
window.openKofi = openKofi;
|
||||
window.copyAddress = copyAddress;
|
||||
|
|
@ -2307,6 +2402,10 @@ window.switchDownloadTab = switchDownloadTab;
|
|||
window.cancelDownloadItem = cancelDownloadItem;
|
||||
window.clearFinishedDownloads = clearFinishedDownloads;
|
||||
|
||||
window.matchedDownloadTrack = matchedDownloadTrack;
|
||||
window.matchedDownloadAlbum = matchedDownloadAlbum;
|
||||
window.matchedDownloadAlbumTrack = matchedDownloadAlbumTrack;
|
||||
|
||||
// APPEND THIS JAVASCRIPT SNIPPET (B)
|
||||
|
||||
function initializeFilters() {
|
||||
|
|
@ -2470,4 +2569,567 @@ function calculateRelevanceScore(result, query) {
|
|||
// Add to global scope for onclick
|
||||
window.handleFilterClick = handleFilterClick;
|
||||
|
||||
// END OF JAVASCRIPT SNIPPET (B)
|
||||
// ===============================
|
||||
// MATCHED DOWNLOADS MODAL
|
||||
// ===============================
|
||||
|
||||
// Global state for matching modal
|
||||
let currentMatchingData = {
|
||||
searchResult: null,
|
||||
isAlbumDownload: false,
|
||||
albumResult: null,
|
||||
selectedArtist: null,
|
||||
selectedAlbum: null,
|
||||
currentStage: 'artist' // 'artist' or 'album'
|
||||
};
|
||||
|
||||
let searchTimers = {
|
||||
artist: null,
|
||||
album: null
|
||||
};
|
||||
|
||||
function openMatchingModal(searchResult, isAlbumDownload = false, albumResult = null) {
|
||||
console.log('🎯 Opening matching modal for:', searchResult);
|
||||
|
||||
// Store the current matching data
|
||||
currentMatchingData = {
|
||||
searchResult: searchResult,
|
||||
isAlbumDownload: isAlbumDownload,
|
||||
albumResult: albumResult,
|
||||
selectedArtist: null,
|
||||
selectedAlbum: null,
|
||||
currentStage: 'artist'
|
||||
};
|
||||
|
||||
// Show modal
|
||||
const overlay = document.getElementById('matching-modal-overlay');
|
||||
overlay.classList.remove('hidden');
|
||||
|
||||
// Reset modal state
|
||||
resetModalState();
|
||||
|
||||
// Set appropriate title and stage
|
||||
const modalTitle = document.getElementById('matching-modal-title');
|
||||
const artistStageTitle = document.getElementById('artist-stage-title');
|
||||
|
||||
if (isAlbumDownload) {
|
||||
modalTitle.textContent = 'Match Album Download to Spotify';
|
||||
artistStageTitle.textContent = 'Step 1: Select the correct Artist';
|
||||
document.getElementById('album-selection-stage').style.display = 'block';
|
||||
} else {
|
||||
modalTitle.textContent = 'Match Download to Spotify';
|
||||
artistStageTitle.textContent = 'Select the correct Artist for this Single';
|
||||
document.getElementById('album-selection-stage').style.display = 'none';
|
||||
}
|
||||
|
||||
// Generate initial artist suggestions
|
||||
fetchArtistSuggestions();
|
||||
|
||||
// Setup event listeners
|
||||
setupModalEventListeners();
|
||||
}
|
||||
|
||||
function closeMatchingModal() {
|
||||
const overlay = document.getElementById('matching-modal-overlay');
|
||||
overlay.classList.add('hidden');
|
||||
|
||||
// Clear timers
|
||||
Object.values(searchTimers).forEach(timer => {
|
||||
if (timer) clearTimeout(timer);
|
||||
});
|
||||
|
||||
// Reset state
|
||||
currentMatchingData = {
|
||||
searchResult: null,
|
||||
isAlbumDownload: false,
|
||||
albumResult: null,
|
||||
selectedArtist: null,
|
||||
selectedAlbum: null,
|
||||
currentStage: 'artist'
|
||||
};
|
||||
}
|
||||
|
||||
function resetModalState() {
|
||||
// Show artist stage, hide album stage
|
||||
document.getElementById('artist-selection-stage').classList.remove('hidden');
|
||||
document.getElementById('album-selection-stage').classList.add('hidden');
|
||||
|
||||
// Clear all suggestion containers
|
||||
document.getElementById('artist-suggestions').innerHTML = '';
|
||||
document.getElementById('artist-manual-results').innerHTML = '';
|
||||
document.getElementById('album-suggestions').innerHTML = '';
|
||||
document.getElementById('album-manual-results').innerHTML = '';
|
||||
|
||||
// Clear search inputs
|
||||
document.getElementById('artist-search-input').value = '';
|
||||
document.getElementById('album-search-input').value = '';
|
||||
|
||||
// Reset button states
|
||||
document.getElementById('confirm-match-btn').disabled = true;
|
||||
|
||||
// Reset selections
|
||||
currentMatchingData.selectedArtist = null;
|
||||
currentMatchingData.selectedAlbum = null;
|
||||
currentMatchingData.currentStage = 'artist';
|
||||
}
|
||||
|
||||
function setupModalEventListeners() {
|
||||
// Search input listeners
|
||||
const artistInput = document.getElementById('artist-search-input');
|
||||
const albumInput = document.getElementById('album-search-input');
|
||||
|
||||
artistInput.removeEventListener('input', handleArtistSearch);
|
||||
artistInput.addEventListener('input', handleArtistSearch);
|
||||
|
||||
albumInput.removeEventListener('input', handleAlbumSearch);
|
||||
albumInput.addEventListener('input', handleAlbumSearch);
|
||||
|
||||
// Button listeners
|
||||
const skipBtn = document.getElementById('skip-matching-btn');
|
||||
const cancelBtn = document.getElementById('cancel-match-btn');
|
||||
const confirmBtn = document.getElementById('confirm-match-btn');
|
||||
|
||||
skipBtn.onclick = skipMatching;
|
||||
cancelBtn.onclick = closeMatchingModal;
|
||||
confirmBtn.onclick = confirmMatch;
|
||||
}
|
||||
|
||||
async function fetchArtistSuggestions() {
|
||||
try {
|
||||
showLoadingCards('artist-suggestions', 'Finding artist...');
|
||||
|
||||
const response = await fetch('/api/match/suggestions', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
search_result: currentMatchingData.searchResult,
|
||||
context: 'artist',
|
||||
is_album: currentMatchingData.isAlbumDownload,
|
||||
album_result: currentMatchingData.albumResult
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (data.suggestions) {
|
||||
renderArtistSuggestions(data.suggestions);
|
||||
} else {
|
||||
showNoResultsMessage('artist-suggestions', 'No artist suggestions found');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching artist suggestions:', error);
|
||||
showNoResultsMessage('artist-suggestions', 'Error loading suggestions');
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchAlbumSuggestions() {
|
||||
if (!currentMatchingData.selectedArtist) return;
|
||||
|
||||
try {
|
||||
showLoadingCards('album-suggestions', 'Finding album...');
|
||||
|
||||
const response = await fetch('/api/match/suggestions', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
search_result: currentMatchingData.searchResult,
|
||||
context: 'album',
|
||||
selected_artist: currentMatchingData.selectedArtist
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (data.suggestions) {
|
||||
renderAlbumSuggestions(data.suggestions);
|
||||
} else {
|
||||
showNoResultsMessage('album-suggestions', 'No album suggestions found');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching album suggestions:', error);
|
||||
showNoResultsMessage('album-suggestions', 'Error loading suggestions');
|
||||
}
|
||||
}
|
||||
|
||||
function renderArtistSuggestions(suggestions) {
|
||||
const container = document.getElementById('artist-suggestions');
|
||||
container.innerHTML = '';
|
||||
|
||||
if (!suggestions.length) {
|
||||
showNoResultsMessage('artist-suggestions', 'No artist matches found');
|
||||
return;
|
||||
}
|
||||
|
||||
suggestions.forEach(suggestion => {
|
||||
const card = createArtistCard(suggestion.artist, suggestion.confidence);
|
||||
container.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
function renderAlbumSuggestions(suggestions) {
|
||||
const container = document.getElementById('album-suggestions');
|
||||
container.innerHTML = '';
|
||||
|
||||
if (!suggestions.length) {
|
||||
showNoResultsMessage('album-suggestions', 'No album matches found');
|
||||
return;
|
||||
}
|
||||
|
||||
suggestions.forEach(suggestion => {
|
||||
const card = createAlbumCard(suggestion.album, suggestion.confidence);
|
||||
container.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
function createArtistCard(artist, confidence) {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'suggestion-card';
|
||||
card.onclick = () => selectArtist(artist);
|
||||
|
||||
const imageUrl = artist.image_url || '';
|
||||
const confidencePercent = Math.round(confidence * 100);
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="suggestion-card-overlay"></div>
|
||||
<div class="suggestion-card-content">
|
||||
<div class="suggestion-card-name" title="${escapeHtml(artist.name)}">${escapeHtml(artist.name)}</div>
|
||||
<div class="suggestion-card-details">
|
||||
${artist.genres && artist.genres.length ? escapeHtml(artist.genres.slice(0, 2).join(', ')) : 'Artist'}
|
||||
</div>
|
||||
<div class="suggestion-card-confidence">${confidencePercent}% match</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Set background image if available
|
||||
if (imageUrl) {
|
||||
card.style.backgroundImage = `url(${imageUrl})`;
|
||||
card.style.backgroundSize = 'cover';
|
||||
card.style.backgroundPosition = 'center';
|
||||
}
|
||||
|
||||
return card;
|
||||
}
|
||||
|
||||
function createAlbumCard(album, confidence) {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'suggestion-card';
|
||||
card.onclick = () => selectAlbum(album);
|
||||
|
||||
const imageUrl = album.image_url || '';
|
||||
const confidencePercent = Math.round(confidence * 100);
|
||||
const year = album.release_date ? album.release_date.split('-')[0] : '';
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="suggestion-card-overlay"></div>
|
||||
<div class="suggestion-card-content">
|
||||
<div class="suggestion-card-name" title="${escapeHtml(album.name)}">${escapeHtml(album.name)}</div>
|
||||
<div class="suggestion-card-details">
|
||||
${album.album_type ? escapeHtml(album.album_type.charAt(0).toUpperCase() + album.album_type.slice(1)) : 'Album'}${year ? ` • ${year}` : ''}
|
||||
</div>
|
||||
<div class="suggestion-card-confidence">${confidencePercent}% match</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Set background image if available
|
||||
if (imageUrl) {
|
||||
card.style.backgroundImage = `url(${imageUrl})`;
|
||||
card.style.backgroundSize = 'cover';
|
||||
card.style.backgroundPosition = 'center';
|
||||
}
|
||||
|
||||
return card;
|
||||
}
|
||||
|
||||
function selectArtist(artist) {
|
||||
// Clear previous selections
|
||||
document.querySelectorAll('#artist-suggestions .suggestion-card').forEach(card => {
|
||||
card.classList.remove('selected');
|
||||
});
|
||||
document.querySelectorAll('#artist-manual-results .suggestion-card').forEach(card => {
|
||||
card.classList.remove('selected');
|
||||
});
|
||||
|
||||
// Mark new selection
|
||||
event.currentTarget.classList.add('selected');
|
||||
|
||||
// Store selection
|
||||
currentMatchingData.selectedArtist = artist;
|
||||
|
||||
console.log('🎯 Selected artist:', artist.name);
|
||||
|
||||
if (currentMatchingData.isAlbumDownload) {
|
||||
// Transition to album selection stage
|
||||
transitionToAlbumStage();
|
||||
} else {
|
||||
// Enable confirm button for single downloads
|
||||
document.getElementById('confirm-match-btn').disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
function selectAlbum(album) {
|
||||
// Clear previous selections
|
||||
document.querySelectorAll('#album-suggestions .suggestion-card').forEach(card => {
|
||||
card.classList.remove('selected');
|
||||
});
|
||||
document.querySelectorAll('#album-manual-results .suggestion-card').forEach(card => {
|
||||
card.classList.remove('selected');
|
||||
});
|
||||
|
||||
// Mark new selection
|
||||
event.currentTarget.classList.add('selected');
|
||||
|
||||
// Store selection
|
||||
currentMatchingData.selectedAlbum = album;
|
||||
|
||||
console.log('🎯 Selected album:', album.name);
|
||||
|
||||
// Enable confirm button
|
||||
document.getElementById('confirm-match-btn').disabled = false;
|
||||
}
|
||||
|
||||
function transitionToAlbumStage() {
|
||||
// Hide artist stage
|
||||
document.getElementById('artist-selection-stage').classList.add('hidden');
|
||||
|
||||
// Show album stage
|
||||
const albumStage = document.getElementById('album-selection-stage');
|
||||
albumStage.classList.remove('hidden');
|
||||
|
||||
// Update selected artist name
|
||||
document.getElementById('selected-artist-name').textContent = currentMatchingData.selectedArtist.name;
|
||||
|
||||
// Update current stage
|
||||
currentMatchingData.currentStage = 'album';
|
||||
|
||||
// Fetch album suggestions
|
||||
fetchAlbumSuggestions();
|
||||
}
|
||||
|
||||
function handleArtistSearch(event) {
|
||||
const query = event.target.value.trim();
|
||||
|
||||
// Clear previous timer
|
||||
if (searchTimers.artist) {
|
||||
clearTimeout(searchTimers.artist);
|
||||
}
|
||||
|
||||
if (query.length < 2) {
|
||||
document.getElementById('artist-manual-results').innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
// Debounce search
|
||||
searchTimers.artist = setTimeout(() => {
|
||||
performArtistSearch(query);
|
||||
}, 400);
|
||||
}
|
||||
|
||||
function handleAlbumSearch(event) {
|
||||
const query = event.target.value.trim();
|
||||
|
||||
// Clear previous timer
|
||||
if (searchTimers.album) {
|
||||
clearTimeout(searchTimers.album);
|
||||
}
|
||||
|
||||
if (query.length < 2) {
|
||||
document.getElementById('album-manual-results').innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
// Debounce search
|
||||
searchTimers.album = setTimeout(() => {
|
||||
performAlbumSearch(query);
|
||||
}, 400);
|
||||
}
|
||||
|
||||
async function performArtistSearch(query) {
|
||||
try {
|
||||
showLoadingCards('artist-manual-results', 'Searching artists...');
|
||||
|
||||
const response = await fetch('/api/match/search', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
query: query,
|
||||
context: 'artist'
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (data.results) {
|
||||
renderArtistSearchResults(data.results);
|
||||
} else {
|
||||
showNoResultsMessage('artist-manual-results', 'No artists found');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error searching artists:', error);
|
||||
showNoResultsMessage('artist-manual-results', 'Error searching artists');
|
||||
}
|
||||
}
|
||||
|
||||
async function performAlbumSearch(query) {
|
||||
if (!currentMatchingData.selectedArtist) return;
|
||||
|
||||
try {
|
||||
showLoadingCards('album-manual-results', 'Searching albums...');
|
||||
|
||||
const response = await fetch('/api/match/search', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
query: query,
|
||||
context: 'album',
|
||||
artist_id: currentMatchingData.selectedArtist.id
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (data.results) {
|
||||
renderAlbumSearchResults(data.results);
|
||||
} else {
|
||||
showNoResultsMessage('album-manual-results', 'No albums found');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error searching albums:', error);
|
||||
showNoResultsMessage('album-manual-results', 'Error searching albums');
|
||||
}
|
||||
}
|
||||
|
||||
function renderArtistSearchResults(results) {
|
||||
const container = document.getElementById('artist-manual-results');
|
||||
container.innerHTML = '';
|
||||
|
||||
results.forEach(result => {
|
||||
const card = createArtistCard(result.artist, result.confidence);
|
||||
container.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
function renderAlbumSearchResults(results) {
|
||||
const container = document.getElementById('album-manual-results');
|
||||
container.innerHTML = '';
|
||||
|
||||
results.forEach(result => {
|
||||
const card = createAlbumCard(result.album, result.confidence);
|
||||
container.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
function showLoadingCards(containerId, message) {
|
||||
const container = document.getElementById(containerId);
|
||||
container.innerHTML = `<div class="loading-card">${message}</div>`;
|
||||
}
|
||||
|
||||
function showNoResultsMessage(containerId, message) {
|
||||
const container = document.getElementById(containerId);
|
||||
container.innerHTML = `<div class="loading-card" style="color: rgba(255,255,255,0.5)">${message}</div>`;
|
||||
}
|
||||
|
||||
function skipMatching() {
|
||||
console.log('🎯 Skipping matching, proceeding with normal download');
|
||||
|
||||
// Close modal
|
||||
closeMatchingModal();
|
||||
|
||||
// Start normal download
|
||||
if (currentMatchingData.isAlbumDownload) {
|
||||
// For albums, we need to download each track
|
||||
showToast('⬇️ Starting album download (unmatched)', 'info');
|
||||
// This would need to be implemented to download all album tracks
|
||||
} else {
|
||||
// Single track download
|
||||
startDownload(window.currentSearchResults.indexOf(currentMatchingData.searchResult));
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmMatch() {
|
||||
if (!currentMatchingData.selectedArtist) {
|
||||
showToast('⚠️ Please select an artist first', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentMatchingData.isAlbumDownload && !currentMatchingData.selectedAlbum) {
|
||||
showToast('⚠️ Please select an album first', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('🎯 Confirming match with:', {
|
||||
artist: currentMatchingData.selectedArtist.name,
|
||||
album: currentMatchingData.selectedAlbum?.name
|
||||
});
|
||||
|
||||
// Disable confirm button to prevent double-clicks
|
||||
const confirmBtn = document.getElementById('confirm-match-btn');
|
||||
const originalText = confirmBtn.textContent;
|
||||
confirmBtn.disabled = true;
|
||||
confirmBtn.textContent = 'Starting...';
|
||||
|
||||
const response = await fetch('/api/download/matched', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
search_result: currentMatchingData.searchResult,
|
||||
spotify_artist: currentMatchingData.selectedArtist,
|
||||
spotify_album: currentMatchingData.selectedAlbum || null
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
showToast(`🎯 Matched download started for "${currentMatchingData.selectedArtist.name}"`, 'success');
|
||||
closeMatchingModal();
|
||||
} else {
|
||||
throw new Error(data.error || 'Failed to start matched download');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error starting matched download:', error);
|
||||
showToast(`❌ Error starting matched download: ${error.message}`, 'error');
|
||||
|
||||
// Re-enable confirm button
|
||||
const confirmBtn = document.getElementById('confirm-match-btn');
|
||||
confirmBtn.disabled = false;
|
||||
confirmBtn.textContent = originalText;
|
||||
}
|
||||
}
|
||||
|
||||
function matchedDownloadTrack(trackIndex) {
|
||||
const results = window.currentSearchResults;
|
||||
if (!results || !results[trackIndex]) {
|
||||
console.error('Could not find track for matched download:', trackIndex);
|
||||
showToast('Error preparing matched download.', 'error');
|
||||
return;
|
||||
}
|
||||
const trackData = results[trackIndex];
|
||||
// It's a single track, so isAlbumDownload is false and there's no album context.
|
||||
openMatchingModal(trackData, false, null);
|
||||
}
|
||||
|
||||
function matchedDownloadAlbum(albumIndex) {
|
||||
const results = window.currentSearchResults;
|
||||
if (!results || !results[albumIndex]) {
|
||||
console.error('Could not find album for matched download:', albumIndex);
|
||||
showToast('Error preparing matched download.', 'error');
|
||||
return;
|
||||
}
|
||||
const albumData = results[albumIndex];
|
||||
// The first track is used as a reference for the initial artist search.
|
||||
const firstTrack = albumData.tracks ? albumData.tracks[0] : albumData;
|
||||
openMatchingModal(firstTrack, true, albumData);
|
||||
}
|
||||
|
||||
function matchedDownloadAlbumTrack(albumIndex, trackIndex) {
|
||||
const results = window.currentSearchResults;
|
||||
if (!results || !results[albumIndex] || !results[albumIndex].tracks || !results[albumIndex].tracks[trackIndex]) {
|
||||
console.error('Could not find album track for matched download:', albumIndex, trackIndex);
|
||||
showToast('Error preparing matched download.', 'error');
|
||||
return;
|
||||
}
|
||||
const albumData = results[albumIndex];
|
||||
const trackData = albumData.tracks[trackIndex];
|
||||
|
||||
// Pass the specific track as the main search result, but also pass the full album data for context.
|
||||
openMatchingModal(trackData, true, albumData);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2048,6 +2048,397 @@ body {
|
|||
animation: modalFadeIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
/* ===== MATCHED DOWNLOAD MODAL STYLES ===== */
|
||||
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: rgba(18, 18, 18, 0.85);
|
||||
backdrop-filter: blur(12px);
|
||||
z-index: 10000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 1;
|
||||
transition: opacity 0.4s ease-in-out;
|
||||
}
|
||||
|
||||
.modal-overlay.hidden {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.matching-modal {
|
||||
background: linear-gradient(135deg, #1a1a1a 0%, #121212 100%);
|
||||
border-radius: 20px;
|
||||
border: 1px solid rgba(29, 185, 84, 0.2);
|
||||
width: 1100px;
|
||||
max-width: 95vw;
|
||||
height: 750px;
|
||||
max-height: 95vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 0 25px 80px rgba(0, 0, 0, 0.7), 0 0 0 1px rgba(29, 185, 84, 0.1);
|
||||
transform: scale(1);
|
||||
transition: transform 0.4s ease-in-out;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.modal-overlay.hidden .matching-modal {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
/* Modal Header */
|
||||
.matching-modal-header {
|
||||
padding: 30px 40px 20px 40px;
|
||||
border-bottom: 1px solid rgba(29, 185, 84, 0.15);
|
||||
background: linear-gradient(90deg, rgba(29, 185, 84, 0.03) 0%, transparent 50%);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-top-left-radius: 20px;
|
||||
border-top-right-radius: 20px;
|
||||
}
|
||||
|
||||
#matching-modal-title {
|
||||
color: #ffffff;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.5px;
|
||||
margin: 0;
|
||||
font-family: 'SF Pro Display', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
}
|
||||
|
||||
.matching-modal-close {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.matching-modal-close:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
color: #ffffff;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
/* Modal Content */
|
||||
.matching-modal-content {
|
||||
flex: 1;
|
||||
padding: 30px 40px;
|
||||
overflow-y: auto;
|
||||
background: #121212;
|
||||
}
|
||||
|
||||
.matching-modal-content::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.matching-modal-content::-webkit-scrollbar-track {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.matching-modal-content::-webkit-scrollbar-thumb {
|
||||
background: rgba(29, 185, 84, 0.3);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.matching-modal-content::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(29, 185, 84, 0.5);
|
||||
}
|
||||
|
||||
/* Selection Stages */
|
||||
.selection-stage {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
transition: all 0.4s ease-in-out;
|
||||
}
|
||||
|
||||
.selection-stage.hidden {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.stage-header {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.stage-header h3 {
|
||||
color: #1db954;
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 8px;
|
||||
font-family: 'SF Pro Display', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
}
|
||||
|
||||
.stage-subtitle {
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
margin: 0;
|
||||
font-family: 'SF Pro Text', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
}
|
||||
|
||||
/* Suggestions Sections */
|
||||
.suggestions-section, .manual-search-section {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.suggestions-title {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 20px;
|
||||
font-family: 'SF Pro Text', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
}
|
||||
|
||||
.suggestions-container {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
min-height: 140px;
|
||||
}
|
||||
|
||||
.suggestions-container.loading {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Suggestion Cards */
|
||||
.suggestion-card {
|
||||
width: 220px;
|
||||
height: 130px;
|
||||
background: linear-gradient(135deg, #1e1e1e 0%, #2a2a2a 100%);
|
||||
border: 2px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 16px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.suggestion-card:hover {
|
||||
border-color: rgba(29, 185, 84, 0.6);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px rgba(29, 185, 84, 0.2);
|
||||
}
|
||||
|
||||
.suggestion-card.selected {
|
||||
border-color: #1db954;
|
||||
background: linear-gradient(135deg, rgba(29, 185, 84, 0.1) 0%, rgba(29, 185, 84, 0.05) 100%);
|
||||
box-shadow: 0 0 20px rgba(29, 185, 84, 0.4);
|
||||
}
|
||||
|
||||
.suggestion-card-image {
|
||||
width: 100%;
|
||||
height: 80px;
|
||||
object-fit: cover;
|
||||
border-radius: 14px 14px 0 0;
|
||||
}
|
||||
|
||||
.suggestion-card-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: linear-gradient(to bottom,
|
||||
rgba(18, 18, 18, 0.3) 0%,
|
||||
rgba(18, 18, 18, 0.6) 60%,
|
||||
rgba(18, 18, 18, 0.9) 100%);
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.suggestion-card-content {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 12px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.suggestion-card-name {
|
||||
color: #ffffff;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 4px;
|
||||
line-height: 1.2;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-family: 'SF Pro Display', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
}
|
||||
|
||||
.suggestion-card-details {
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
font-family: 'SF Pro Text', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
}
|
||||
|
||||
.suggestion-card-confidence {
|
||||
background: rgba(29, 185, 84, 0.2);
|
||||
color: #1db954;
|
||||
padding: 2px 8px;
|
||||
border-radius: 8px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
display: inline-block;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* Loading Cards */
|
||||
.loading-card {
|
||||
width: 220px;
|
||||
height: 130px;
|
||||
background: linear-gradient(135deg, #1e1e1e 0%, #2a2a2a 100%);
|
||||
border: 2px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
font-family: 'SF Pro Text', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
}
|
||||
|
||||
.loading-card::before {
|
||||
content: '';
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 2px solid rgba(29, 185, 84, 0.3);
|
||||
border-top: 2px solid #1db954;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Search Input */
|
||||
.search-input {
|
||||
width: 100%;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 2px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 16px;
|
||||
color: #ffffff;
|
||||
padding: 14px 20px;
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
margin-bottom: 20px;
|
||||
transition: border-color 0.2s ease;
|
||||
font-family: 'SF Pro Text', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
}
|
||||
|
||||
.search-input:focus {
|
||||
outline: none;
|
||||
border-color: #1db954;
|
||||
box-shadow: 0 0 0 3px rgba(29, 185, 84, 0.1);
|
||||
}
|
||||
|
||||
.search-input::placeholder {
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
/* Modal Actions */
|
||||
.matching-modal-actions {
|
||||
padding: 20px 40px 30px 40px;
|
||||
border-top: 1px solid rgba(29, 185, 84, 0.15);
|
||||
background: linear-gradient(90deg, rgba(29, 185, 84, 0.02) 0%, transparent 50%);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom-left-radius: 20px;
|
||||
border-bottom-right-radius: 20px;
|
||||
}
|
||||
|
||||
.modal-button {
|
||||
padding: 12px 24px;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-family: 'SF Pro Text', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
}
|
||||
|
||||
.modal-button--primary {
|
||||
background: #1db954;
|
||||
color: #ffffff;
|
||||
min-width: 140px;
|
||||
}
|
||||
|
||||
.modal-button--primary:hover:not(:disabled) {
|
||||
background: #1ed760;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.modal-button--primary:disabled {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.modal-button--secondary {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.modal-button--secondary:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.modal-button--cancel {
|
||||
background: rgba(255, 65, 54, 0.1);
|
||||
color: #ff4136;
|
||||
}
|
||||
|
||||
.modal-button--cancel:hover {
|
||||
background: rgba(255, 65, 54, 0.2);
|
||||
color: #ff6b5a;
|
||||
}
|
||||
|
||||
/* Modal Animation */
|
||||
@keyframes matchingModalFadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.9) translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.modal-overlay:not(.hidden) .matching-modal {
|
||||
animation: matchingModalFadeIn 0.4s ease-out;
|
||||
}
|
||||
|
||||
/* GUI-Matching Search Results Styling */
|
||||
|
||||
/* Single Track Card (SearchResultItem) */
|
||||
|
|
@ -2703,4 +3094,24 @@ body {
|
|||
100% { opacity: 0.7; }
|
||||
}
|
||||
|
||||
/* END OF CSS SNIPPET */
|
||||
.track-matched-btn {
|
||||
background: linear-gradient(to bottom, #9333ea, #7c28c0);
|
||||
border: 1px solid rgba(147, 51, 234, 0.3);
|
||||
border-radius: 6px;
|
||||
color: #fff;
|
||||
padding: 4px 8px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.track-matched-btn:hover {
|
||||
background: linear-gradient(to bottom, #a747fe, #903fdd);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue