feat(web): merge Song Importer into Global Suche (one unified field)
The importer no longer has its own sidebar/bottom-nav entry. The Global Suche field now handles both jobs: plain text runs the Deezer search; a pasted Spotify/SoundCloud/YouTube link switches into importer mode (badge + "Importieren" button + preview), matching the Android app and tidying the left nav. - New onGlobalInput/tryImporterResolve; importer DOM moved into page-discover; old page-spotify-importer removed; routing cleaned up. - Existing resolvers unchanged (still read the mirrored importer-link-input). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
0a6c81b4a1
commit
934253a036
4 files changed
+110
-81
No files matched your search
+62
-76
@@ -85,9 +85,6 @@
|
||||
<div class="nav-item" onclick="navigate('random')" data-page="random">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="16 3 21 3 21 8"/><line x1="4" y1="20" x2="21" y2="3"/><polyline points="21 16 21 21 16 21"/><line x1="15" y1="15" x2="21" y2="21"/></svg>Zufällig
|
||||
</div>
|
||||
<div class="nav-item" onclick="navigate('spotify-importer')" data-page="spotify-importer">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="8 17 12 21 16 17"/><line x1="12" y1="12" x2="12" y2="21"/><path d="M20.88 18.09A5 5 0 0 0 18 9h-1.26A8 8 0 1 0 3 16.29"/></svg>Song Importer
|
||||
</div>
|
||||
</div>
|
||||
<div class="nav-section" id="nav-abs-section" style="display:none">
|
||||
<div class="nav-section-title">Hörbücher</div>
|
||||
@@ -329,20 +326,76 @@
|
||||
<div id="search-results"></div>
|
||||
</div>
|
||||
|
||||
<!-- DISCOVER (Global) -->
|
||||
<!-- DISCOVER (Global) — Deezer search + link importer unified in ONE field -->
|
||||
<div class="page" id="page-discover">
|
||||
<div class="page-header">
|
||||
<div class="page-title"><span class="blue">Global</span> Suche</div>
|
||||
<span class="badge badge-deezer" style="font-size:11px;padding:4px 10px">Deezer</span>
|
||||
</div>
|
||||
<div class="search-wrap" style="padding:0 0 20px">
|
||||
<div class="search-box" style="background:var(--surface);border-color:var(--border2)">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>
|
||||
<input id="global-search-input" type="text" placeholder="Künstler, Titel, Album suchen…" oninput="onGlobalSearch(this.value)" style="font-size:15px">
|
||||
<div class="search-wrap" style="padding:0 0 12px">
|
||||
<div style="display:flex;gap:8px">
|
||||
<div class="search-box" style="background:var(--surface);border-color:var(--border2);flex:1">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/></svg>
|
||||
<div id="importer-badge" style="display:none;font-size:10px;font-weight:700;padding:2px 7px;border-radius:10px;background:var(--surface2);color:var(--text3);white-space:nowrap;flex-shrink:0">URL</div>
|
||||
<input id="global-search-input" type="text" placeholder="Künstler, Titel, Album — oder Link zum Importieren einfügen…" oninput="onGlobalInput(this.value)" onkeydown="if(event.key==='Enter'){event.preventDefault();tryImporterResolve()}" style="font-size:15px">
|
||||
</div>
|
||||
<button id="importer-btn" class="btn-outline" style="display:none;padding:0 16px;font-size:13px;font-weight:700;white-space:nowrap;flex-shrink:0" onclick="resolveImporterLink()">Importieren</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Link importer — auto-detects Spotify / SoundCloud / YouTube; shown only when a link is in the field -->
|
||||
<div id="importer-area" style="display:none">
|
||||
<div id="importer-hint" style="background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:14px;font-size:12px;color:var(--text2);line-height:1.8;margin-bottom:8px">
|
||||
<div style="display:flex;gap:16px;flex-wrap:wrap">
|
||||
<div style="display:flex;align-items:center;gap:6px"><span style="color:#1DB954;font-weight:700">Spotify</span> — Track, Album oder Playlist</div>
|
||||
<div style="display:flex;align-items:center;gap:6px"><span style="color:#ff5500;font-weight:700">SoundCloud</span> — Track oder Playlist</div>
|
||||
<div style="display:flex;align-items:center;gap:6px"><span style="color:#FF0000;font-weight:700">YouTube</span> — Video oder Playlist</div>
|
||||
</div>
|
||||
<div style="margin-top:6px;color:var(--text3);font-size:11px">Link einfügen → Metadaten werden geladen → via yt-dlp in die Navidrome Library herunterladen.</div>
|
||||
</div>
|
||||
|
||||
<!-- hidden inputs kept for compat with spotify.js / soundcloud.js resolvers -->
|
||||
<input id="importer-link-input" type="hidden">
|
||||
<input id="spotify-link-input" type="hidden">
|
||||
<input id="sc-link-input" type="hidden">
|
||||
|
||||
<!-- SPOTIFY RESULTS -->
|
||||
<div id="spotify-preview" style="display:none;margin-bottom:8px">
|
||||
<div id="spotify-preview-inner"></div>
|
||||
<div id="spotify-enrich-status" style="display:none"></div>
|
||||
<div id="spotify-tracks-section" style="display:none">
|
||||
<div class="section-header" style="margin-top:8px">
|
||||
<span class="section-title" id="spotify-tracks-title">Tracks</span>
|
||||
</div>
|
||||
<div id="spotify-tracks-list" class="song-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SOUNDCLOUD RESULTS -->
|
||||
<div id="sc-preview" style="display:none;margin-bottom:8px">
|
||||
<div id="sc-preview-inner"></div>
|
||||
<div id="sc-tracks-section" style="display:none">
|
||||
<div class="section-header" style="margin-top:8px">
|
||||
<span class="section-title" id="sc-tracks-title">Tracks</span>
|
||||
</div>
|
||||
<div id="sc-tracks-list" class="song-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- YOUTUBE RESULTS -->
|
||||
<div id="yt-preview" style="display:none;margin-bottom:8px">
|
||||
<div id="yt-preview-inner"></div>
|
||||
<div id="yt-tracks-section" style="display:none">
|
||||
<div class="section-header" style="margin-top:8px">
|
||||
<span class="section-title" id="yt-tracks-title">Tracks</span>
|
||||
</div>
|
||||
<div id="yt-tracks-list" class="song-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="global-search-results">
|
||||
<div class="empty-state"><div class="icon">🔍</div><p>Gib einen Suchbegriff ein um loszulegen</p></div>
|
||||
<div class="empty-state"><div class="icon">🔍</div><p>Gib einen Suchbegriff ein — oder füge einen Spotify-/SoundCloud-/YouTube-Link ein zum Importieren</p></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -390,69 +443,6 @@
|
||||
<div id="abs-book-detail-content"></div>
|
||||
</div>
|
||||
|
||||
<!-- SPOTIFY IMPORTER -->
|
||||
<div class="page" id="page-spotify-importer">
|
||||
<div class="page-header">
|
||||
<div class="page-title">Song <em>Importer</em></div>
|
||||
</div>
|
||||
|
||||
<!-- UNIFIED INPUT — auto-detects Spotify vs SoundCloud -->
|
||||
<div class="search-wrap" style="padding:0 0 8px">
|
||||
<div style="display:flex;gap:8px">
|
||||
<div class="search-box" style="background:var(--surface);border-color:var(--border2);flex:1">
|
||||
<div id="importer-badge" style="font-size:10px;font-weight:700;padding:2px 7px;border-radius:10px;background:var(--surface2);color:var(--text3);white-space:nowrap;flex-shrink:0">URL</div>
|
||||
<input id="importer-link-input" type="url" placeholder="Spotify, SoundCloud oder YouTube Link einfügen…" style="font-size:14px" oninput="onImporterInput(this.value)" onkeydown="if(event.key==='Enter'){event.preventDefault();resolveImporterLink()}">
|
||||
</div>
|
||||
<button id="importer-btn" class="btn-outline" style="padding:0 16px;font-size:13px;font-weight:700;white-space:nowrap;flex-shrink:0" onclick="resolveImporterLink()">Importieren</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="importer-hint" style="background:var(--surface);border:1px solid var(--border);border-radius:12px;padding:14px;font-size:12px;color:var(--text2);line-height:1.8;margin-bottom:4px">
|
||||
<div style="display:flex;gap:16px;flex-wrap:wrap">
|
||||
<div style="display:flex;align-items:center;gap:6px"><span style="color:#1DB954;font-weight:700">Spotify</span> — Track, Album oder Playlist</div>
|
||||
<div style="display:flex;align-items:center;gap:6px"><span style="color:#ff5500;font-weight:700">SoundCloud</span> — Track oder Playlist</div>
|
||||
<div style="display:flex;align-items:center;gap:6px"><span style="color:#FF0000;font-weight:700">YouTube</span> — Video oder Playlist</div>
|
||||
</div>
|
||||
<div style="margin-top:6px;color:var(--text3);font-size:11px">Link einfügen → Metadaten werden geladen → via yt-dlp in die Navidrome Library herunterladen.</div>
|
||||
</div>
|
||||
|
||||
<!-- hidden inputs kept for compat with spotify.js / soundcloud.js -->
|
||||
<input id="spotify-link-input" type="hidden">
|
||||
<input id="sc-link-input" type="hidden">
|
||||
|
||||
<!-- SPOTIFY RESULTS -->
|
||||
<div id="spotify-preview" style="display:none;margin-bottom:8px">
|
||||
<div id="spotify-preview-inner"></div>
|
||||
<div id="spotify-enrich-status" style="display:none"></div>
|
||||
<div id="spotify-tracks-section" style="display:none">
|
||||
<div class="section-header" style="margin-top:8px">
|
||||
<span class="section-title" id="spotify-tracks-title">Tracks</span>
|
||||
</div>
|
||||
<div id="spotify-tracks-list" class="song-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SOUNDCLOUD RESULTS -->
|
||||
<div id="sc-preview" style="display:none;margin-bottom:8px">
|
||||
<div id="sc-preview-inner"></div>
|
||||
<div id="sc-tracks-section" style="display:none">
|
||||
<div class="section-header" style="margin-top:8px">
|
||||
<span class="section-title" id="sc-tracks-title">Tracks</span>
|
||||
</div>
|
||||
<div id="sc-tracks-list" class="song-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- YOUTUBE RESULTS -->
|
||||
<div id="yt-preview" style="display:none;margin-bottom:8px">
|
||||
<div id="yt-preview-inner"></div>
|
||||
<div id="yt-tracks-section" style="display:none">
|
||||
<div class="section-header" style="margin-top:8px">
|
||||
<span class="section-title" id="yt-tracks-title">Tracks</span>
|
||||
</div>
|
||||
<div id="yt-tracks-list" class="song-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
@@ -1192,10 +1182,6 @@
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="16 3 21 3 21 8"/><line x1="4" y1="20" x2="21" y2="3"/><polyline points="21 16 21 21 16 21"/><line x1="15" y1="15" x2="21" y2="21"/></svg>
|
||||
Zufällig
|
||||
</button>
|
||||
<button class="bnav-item" onclick="navigate('spotify-importer')" data-page="spotify-importer">
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M8 11.973c2.5-1.473 5.5-.973 7.5.527M9.5 14.473c1.5-.973 3.5-.973 5 .027M7 9c3-1.5 6.5-1 9 1"/></svg>
|
||||
Importer
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
+2
-2
@@ -24,7 +24,7 @@ import {
|
||||
plReorderEnter, plReorderSave, plReorderCancel, toggleAllGenres,
|
||||
} from './views/views.js';
|
||||
import { loadSpotifyImporter, resolveSpotifyLink, clearSpotifyPreview, spotifyDlAll,
|
||||
onImporterInput, resolveImporterLink } from './views/spotify.js';
|
||||
onImporterInput, resolveImporterLink, onGlobalInput, tryImporterResolve } from './views/spotify.js';
|
||||
import { resolveYouTubeLink, executeYouTubeDownload } from './views/youtube.js';
|
||||
import { loadSoundCloudImporter, resolveSoundCloudLink, clearSoundCloudPreview, scDlAll, scDlSingle, scDlTrack } from './views/soundcloud.js';
|
||||
import {
|
||||
@@ -343,7 +343,7 @@ Object.assign(window, {
|
||||
loadAlbums, loadStarred, loadRandom, filterStarred, filterPlaylist,
|
||||
plReorderEnter, plReorderSave, plReorderCancel, toggleAllGenres,
|
||||
loadSpotifyImporter, resolveSpotifyLink, clearSpotifyPreview, spotifyDlAll,
|
||||
onImporterInput, resolveImporterLink,
|
||||
onImporterInput, resolveImporterLink, onGlobalInput, tryImporterResolve,
|
||||
loadSoundCloudImporter, resolveSoundCloudLink, clearSoundCloudPreview, scDlAll, scDlSingle, scDlTrack,
|
||||
executeSpotifyDownload, resolveYouTubeLink, executeYouTubeDownload,
|
||||
openPlModal, closePlModal, addSongsToPlaylist, createAndAddPlaylist, createPlaylistPrompt,
|
||||
|
||||
@@ -62,12 +62,12 @@ export function navigate(page, data=null) {
|
||||
closeHeaderSearch();
|
||||
document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
|
||||
document.querySelectorAll('.nav-item,.bnav-item').forEach(n => n.classList.remove('active','active-blue'));
|
||||
const cls = ['discover','new-releases','recommendations','spotify-importer'].includes(page) ? 'active-blue' : 'active';
|
||||
const cls = ['discover','new-releases','recommendations'].includes(page) ? 'active-blue' : 'active';
|
||||
document.querySelectorAll(`[data-page="${page}"]`).forEach(el => el.classList.add(cls));
|
||||
const pageEl = document.getElementById(`page-${page}`);
|
||||
if(pageEl) { pageEl.classList.add('active'); document.getElementById('main-content').scrollTop = 0; }
|
||||
closeSidebar();
|
||||
if(['discover','new-releases','recommendations','random','spotify-importer'].includes(page)) switchNavMode('discover');
|
||||
if(['discover','new-releases','recommendations','random'].includes(page)) switchNavMode('discover');
|
||||
else if(['home','artists','albums','starred','playlists'].includes(page)) switchNavMode('library');
|
||||
|
||||
if(page==='home') loadHomeData(!S._navJumping); // explicit Home click refreshes; back/forward keeps state
|
||||
@@ -85,7 +85,6 @@ export function navigate(page, data=null) {
|
||||
if(page==='playlists') loadPlaylists();
|
||||
if(page==='playlist-detail' && data) loadPlaylistDetail(data);
|
||||
if(page==='recommendations') loadRecommendations();
|
||||
if(page==='spotify-importer') { loadSpotifyImporter(); loadSoundCloudImporter(); }
|
||||
if(page==='audiobooks') {
|
||||
import('./abs.js').then(m => {
|
||||
if (S.abs.currentLibraryId) m.loadAbsBooks(S.abs.currentLibraryId);
|
||||
|
||||
@@ -47,6 +47,50 @@ export function onImporterInput(val) {
|
||||
}
|
||||
}
|
||||
|
||||
// ─── UNIFIED GLOBAL-SUCHE FIELD ───
|
||||
// One field on the Global-Suche page handles both jobs: a pasted Spotify/SoundCloud/YouTube link
|
||||
// switches into the importer flow; any other text runs the Deezer library search. This replaces the
|
||||
// separate "Song Importer" sidebar entry (the importer now lives inside Global Suche).
|
||||
const _IMPORT_LINK_RE = /^https?:\/\//i;
|
||||
const _IMPORT_HOST_RE = /(?:spotify\.com|soundcloud\.com|youtube\.com|youtu\.be)/i;
|
||||
function _isImportLink(v) { return _IMPORT_LINK_RE.test(v) || _IMPORT_HOST_RE.test(v); }
|
||||
|
||||
export function onGlobalInput(val) {
|
||||
const v = (val || '').trim();
|
||||
const area = document.getElementById('importer-area');
|
||||
const results = document.getElementById('global-search-results');
|
||||
const badge = document.getElementById('importer-badge');
|
||||
const btn = document.getElementById('importer-btn');
|
||||
if (_isImportLink(v)) {
|
||||
// Link → importer mode
|
||||
if (results) results.style.display = 'none';
|
||||
if (area) area.style.display = '';
|
||||
if (badge) badge.style.display = '';
|
||||
if (btn) btn.style.display = '';
|
||||
const hidden = document.getElementById('importer-link-input');
|
||||
if (hidden) hidden.value = v; // mirror so resolveImporterLink() reads it
|
||||
onImporterInput(v); // badge styling + clear stale previews
|
||||
const hint = document.getElementById('importer-hint');
|
||||
if (hint) hint.style.display = ''; // keep the platform hint as a header until a preview loads
|
||||
} else {
|
||||
// Text → Deezer search
|
||||
if (area) area.style.display = 'none';
|
||||
if (badge) badge.style.display = 'none';
|
||||
if (btn) btn.style.display = 'none';
|
||||
if (results) results.style.display = '';
|
||||
window.onGlobalSearch(v);
|
||||
}
|
||||
}
|
||||
|
||||
// Enter in the unified field: resolve the link if it is one (search is already live via oninput).
|
||||
export function tryImporterResolve() {
|
||||
const v = (document.getElementById('global-search-input')?.value || '').trim();
|
||||
if (!_isImportLink(v)) return;
|
||||
const hidden = document.getElementById('importer-link-input');
|
||||
if (hidden) hidden.value = v;
|
||||
resolveImporterLink();
|
||||
}
|
||||
|
||||
export async function resolveImporterLink() {
|
||||
const url = document.getElementById('importer-link-input')?.value.trim();
|
||||
if (!url) return;
|
||||
|
||||
Reference in new issue
Block a user