Add URL history pills for YouTube, Deezer, and Spotify Link sync tabs
Saves successfully loaded playlist URLs to localStorage and displays them as clickable pills between the input bar and playlist container. Clicking a pill re-loads that URL; X button removes it. Max 10 per source, most recent first. Source-colored hover accents match each tab's brand styling. Also fixes duplicate playlist bug — YouTube and Spotify Public now check for already-loaded playlists before making API calls, preventing broken duplicate cards when the same URL is entered twice.
This commit is contained in:
parent
e2ba879197
commit
a02d14a23a
3 changed files with 255 additions and 0 deletions
|
|
@ -1044,6 +1044,7 @@
|
|||
placeholder="Paste Deezer Playlist URL...">
|
||||
<button id="deezer-parse-btn">Load Playlist</button>
|
||||
</div>
|
||||
<div class="url-history-bar" id="deezer-url-history" style="display:none"></div>
|
||||
<div class="playlist-scroll-container" id="deezer-playlist-container">
|
||||
<div class="playlist-placeholder">Paste a Deezer playlist URL above to get started.</div>
|
||||
</div>
|
||||
|
|
@ -1056,6 +1057,7 @@
|
|||
placeholder="Paste YouTube Music Playlist URL...">
|
||||
<button id="youtube-parse-btn">Parse Playlist</button>
|
||||
</div>
|
||||
<div class="url-history-bar" id="youtube-url-history" style="display:none"></div>
|
||||
<div class="playlist-scroll-container" id="youtube-playlist-container">
|
||||
<div class="playlist-placeholder">Parsed YouTube playlists will appear here.</div>
|
||||
</div>
|
||||
|
|
@ -1068,6 +1070,7 @@
|
|||
placeholder="Paste Spotify Playlist or Album URL...">
|
||||
<button id="spotify-public-parse-btn">Load</button>
|
||||
</div>
|
||||
<div class="url-history-bar" id="spotify-public-url-history" style="display:none"></div>
|
||||
<div class="playlist-scroll-container" id="spotify-public-playlist-container">
|
||||
<div class="playlist-placeholder">Paste a Spotify playlist or album URL above to load tracks without needing Spotify API credentials.</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -7611,6 +7611,9 @@ async function loadSyncData() {
|
|||
|
||||
// Load Beatport charts from backend (always refresh to get latest state)
|
||||
await loadBeatportChartsFromBackend();
|
||||
|
||||
// Render saved URL histories for YouTube, Deezer, Spotify Link tabs
|
||||
initUrlHistories();
|
||||
}
|
||||
|
||||
async function checkForActiveProcesses() {
|
||||
|
|
@ -22377,6 +22380,9 @@ async function loadDeezerPlaylist() {
|
|||
})), { owner: playlist.owner, image_url: playlist.image_url, description: rawUrl });
|
||||
}
|
||||
|
||||
// Save to URL history
|
||||
saveUrlHistory('deezer', rawUrl, playlist.name);
|
||||
|
||||
renderDeezerPlaylists();
|
||||
await loadDeezerPlaylistStatesFromBackend();
|
||||
|
||||
|
|
@ -26479,6 +26485,13 @@ async function parseSpotifyPublicUrl() {
|
|||
return;
|
||||
}
|
||||
|
||||
// Check if already loaded
|
||||
if (_isUrlAlreadyLoaded('spotify-public', url)) {
|
||||
showToast('This playlist is already loaded', 'info');
|
||||
urlInput.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const parseBtn = document.getElementById('spotify-public-parse-btn');
|
||||
if (parseBtn) {
|
||||
parseBtn.disabled = true;
|
||||
|
|
@ -26523,6 +26536,9 @@ async function parseSpotifyPublicUrl() {
|
|||
})), { owner: result.subtitle || '', image_url: '', description: result.url || '' });
|
||||
}
|
||||
|
||||
// Save to URL history
|
||||
saveUrlHistory('spotify-public', url, result.name);
|
||||
|
||||
renderSpotifyPublicPlaylists();
|
||||
await loadSpotifyPublicPlaylistStatesFromBackend();
|
||||
|
||||
|
|
@ -27441,6 +27457,133 @@ async function startSpotifyPublicDownloadMissing(urlHash) {
|
|||
}
|
||||
}
|
||||
|
||||
// ===============================
|
||||
// URL HISTORY (Saved playlist URLs)
|
||||
// ===============================
|
||||
|
||||
const URL_HISTORY_MAX = 10;
|
||||
const URL_HISTORY_SOURCES = {
|
||||
youtube: { key: 'soulsync-url-history-youtube', icon: '▶', inputId: 'youtube-url-input', containerId: 'youtube-url-history', loadFn: () => parseYouTubePlaylist() },
|
||||
deezer: { key: 'soulsync-url-history-deezer', icon: '🎵', inputId: 'deezer-url-input', containerId: 'deezer-url-history', loadFn: () => loadDeezerPlaylist() },
|
||||
'spotify-public': { key: 'soulsync-url-history-spotify-public', icon: '🎧', inputId: 'spotify-public-url-input', containerId: 'spotify-public-url-history', loadFn: () => parseSpotifyPublicUrl() }
|
||||
};
|
||||
|
||||
function getUrlHistory(source) {
|
||||
try {
|
||||
const cfg = URL_HISTORY_SOURCES[source];
|
||||
if (!cfg) return [];
|
||||
const raw = localStorage.getItem(cfg.key);
|
||||
return raw ? JSON.parse(raw) : [];
|
||||
} catch { return []; }
|
||||
}
|
||||
|
||||
function saveUrlHistory(source, url, name) {
|
||||
const cfg = URL_HISTORY_SOURCES[source];
|
||||
if (!cfg || !url) return;
|
||||
let history = getUrlHistory(source);
|
||||
// Remove duplicate (same URL)
|
||||
history = history.filter(h => h.url !== url);
|
||||
// Add to front
|
||||
history.unshift({ url, name: name || url, ts: Date.now() });
|
||||
// Cap
|
||||
if (history.length > URL_HISTORY_MAX) history = history.slice(0, URL_HISTORY_MAX);
|
||||
localStorage.setItem(cfg.key, JSON.stringify(history));
|
||||
renderUrlHistory(source);
|
||||
}
|
||||
|
||||
function removeUrlHistoryEntry(source, url) {
|
||||
const cfg = URL_HISTORY_SOURCES[source];
|
||||
if (!cfg) return;
|
||||
let history = getUrlHistory(source);
|
||||
history = history.filter(h => h.url !== url);
|
||||
localStorage.setItem(cfg.key, JSON.stringify(history));
|
||||
renderUrlHistory(source);
|
||||
}
|
||||
|
||||
function renderUrlHistory(source) {
|
||||
const cfg = URL_HISTORY_SOURCES[source];
|
||||
if (!cfg) return;
|
||||
const container = document.getElementById(cfg.containerId);
|
||||
if (!container) return;
|
||||
const history = getUrlHistory(source);
|
||||
if (history.length === 0) {
|
||||
container.style.display = 'none';
|
||||
container.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
container.style.display = 'flex';
|
||||
container.innerHTML = `<span class="url-history-bar-label">Recent</span>` +
|
||||
history.map(h => {
|
||||
const rawName = h.name.length > 30 ? h.name.substring(0, 28) + '...' : h.name;
|
||||
const safeName = escapeHtml(rawName);
|
||||
const safeTitle = escapeHtml(h.name);
|
||||
const safeUrl = h.url.replace(/"/g, '"');
|
||||
return `<div class="url-history-pill" data-url="${safeUrl}" title="${safeTitle}">
|
||||
<span class="url-history-pill-icon">${cfg.icon}</span>
|
||||
<span class="url-history-pill-name">${safeName}</span>
|
||||
<button class="url-history-pill-remove" data-source="${source}" data-url="${safeUrl}">×</button>
|
||||
</div>`;
|
||||
}).join('');
|
||||
|
||||
// Pill click → fill input and load (skip if already loaded)
|
||||
container.querySelectorAll('.url-history-pill').forEach(pill => {
|
||||
pill.addEventListener('click', (e) => {
|
||||
// Don't trigger if clicking the X button
|
||||
if (e.target.classList.contains('url-history-pill-remove')) return;
|
||||
const pillUrl = pill.dataset.url;
|
||||
if (_isUrlAlreadyLoaded(source, pillUrl)) {
|
||||
showToast('This playlist is already loaded', 'info');
|
||||
return;
|
||||
}
|
||||
const input = document.getElementById(cfg.inputId);
|
||||
if (input) input.value = pillUrl;
|
||||
cfg.loadFn();
|
||||
});
|
||||
});
|
||||
|
||||
// X button click → remove entry
|
||||
container.querySelectorAll('.url-history-pill-remove').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
removeUrlHistoryEntry(btn.dataset.source, btn.dataset.url);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function _isUrlAlreadyLoaded(source, url) {
|
||||
if (source === 'youtube') {
|
||||
// Check for existing YouTube card with this URL
|
||||
const container = document.getElementById('youtube-playlist-container');
|
||||
if (container) {
|
||||
const cards = container.querySelectorAll('.youtube-playlist-card[data-url]');
|
||||
for (const card of cards) {
|
||||
if (card.dataset.url === url) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} else if (source === 'deezer') {
|
||||
// Extract playlist ID from URL and check deezerPlaylists array
|
||||
const match = url.match(/deezer\.com\/(?:[a-z]{2}\/)?playlist\/(\d+)/i);
|
||||
const id = match ? match[1] : (/^\d+$/.test(url) ? url : null);
|
||||
if (id && deezerPlaylists.find(p => String(p.id) === String(id))) return true;
|
||||
return false;
|
||||
} else if (source === 'spotify-public') {
|
||||
// Extract Spotify ID from URL and compare against loaded playlists
|
||||
const spMatch = url.match(/open\.spotify\.com\/(playlist|album)\/([a-zA-Z0-9]+)/);
|
||||
const spId = spMatch ? spMatch[2] : null;
|
||||
if (spId && spotifyPublicPlaylists.some(p => p.id === spId)) return true;
|
||||
// Fallback: direct URL comparison
|
||||
return spotifyPublicPlaylists.some(p => p.url === url);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function initUrlHistories() {
|
||||
for (const source of Object.keys(URL_HISTORY_SOURCES)) {
|
||||
renderUrlHistory(source);
|
||||
}
|
||||
}
|
||||
|
||||
// ===============================
|
||||
// YOUTUBE PLAYLIST FUNCTIONALITY
|
||||
// ===============================
|
||||
|
|
@ -27460,6 +27603,13 @@ async function parseYouTubePlaylist() {
|
|||
return;
|
||||
}
|
||||
|
||||
// Check if already loaded
|
||||
if (_isUrlAlreadyLoaded('youtube', url)) {
|
||||
showToast('This playlist is already loaded', 'info');
|
||||
urlInput.value = '';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('🎬 Parsing YouTube playlist:', url);
|
||||
|
||||
|
|
@ -27485,6 +27635,9 @@ async function parseYouTubePlaylist() {
|
|||
|
||||
console.log('✅ YouTube playlist parsed:', result.name, `(${result.tracks.length} tracks)`);
|
||||
|
||||
// Save to URL history
|
||||
saveUrlHistory('youtube', url, result.name);
|
||||
|
||||
// Update card with parsed data and stay in 'fresh' phase
|
||||
updateYouTubeCardData(result.url_hash, result);
|
||||
updateYouTubeCardPhase(result.url_hash, 'fresh');
|
||||
|
|
|
|||
|
|
@ -10422,6 +10422,105 @@ body {
|
|||
padding: 40px;
|
||||
}
|
||||
|
||||
/* URL History Bar (saved playlist URLs) */
|
||||
.url-history-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 2px;
|
||||
overflow-x: auto;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.url-history-bar::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.url-history-bar-label {
|
||||
font-size: 11px;
|
||||
color: rgba(255, 255, 255, 0.35);
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
.url-history-pill {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 10px 4px 8px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 16px;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
transition: all 0.2s ease;
|
||||
max-width: 220px;
|
||||
}
|
||||
|
||||
.url-history-pill:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-color: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
.url-history-pill-icon {
|
||||
font-size: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.url-history-pill-name {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.url-history-pill:hover .url-history-pill-name {
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
}
|
||||
|
||||
.url-history-pill-remove {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
font-size: 10px;
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
transition: all 0.15s ease;
|
||||
padding: 0;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.url-history-pill-remove:hover {
|
||||
color: #ff4444;
|
||||
background: rgba(255, 68, 68, 0.15);
|
||||
}
|
||||
|
||||
/* Source-specific pill accent on hover */
|
||||
#youtube-url-history .url-history-pill:hover {
|
||||
border-color: rgba(255, 32, 32, 0.3);
|
||||
}
|
||||
#deezer-url-history .url-history-pill:hover {
|
||||
border-color: rgba(162, 56, 255, 0.3);
|
||||
}
|
||||
#spotify-public-url-history .url-history-pill:hover {
|
||||
border-color: rgba(29, 185, 84, 0.3);
|
||||
}
|
||||
|
||||
/* Playlist URL input section (YouTube, Deezer) */
|
||||
.youtube-input-section {
|
||||
display: flex;
|
||||
|
|
|
|||
Loading…
Reference in a new issue