forked from Keelrleek/SoulStream
playlists + player: every service's list through one door with its names matched for real, and shuffle that readies exactly what it promises
This commit is contained in:
1 parent
30eb6fe025
commit
50414dae5c
16 files changed
+4157
-154
No files matched your search
+65
-15
@@ -19,6 +19,7 @@
|
||||
let importMode = $state("file"); // "file" | "paste"
|
||||
let importDraft = $state(null);
|
||||
let menuFor = $state(null); // playlist id whose ⋯ menu is open
|
||||
let exportFor = $state(null); // playlist id whose Export flyout is open
|
||||
let confirmDelete = $state(null); // playlist id armed for deletion
|
||||
let creating = $state(false);
|
||||
let newMenuOpen = $state(false);
|
||||
@@ -130,6 +131,13 @@
|
||||
importOpen = true;
|
||||
}
|
||||
|
||||
function importFromYoutubePlaylist() {
|
||||
newMenuOpen = false;
|
||||
importDraft = null;
|
||||
importMode = "ytplaylist";
|
||||
importOpen = true;
|
||||
}
|
||||
|
||||
function open(pl) {
|
||||
goToView("playlist", { playlistId: pl.id, label: pl.name });
|
||||
}
|
||||
@@ -211,6 +219,7 @@
|
||||
|
||||
async function exportAs(pl, format) {
|
||||
menuFor = null;
|
||||
exportFor = null;
|
||||
try {
|
||||
await api.playlistExport(pl.id, format);
|
||||
} catch (e) {
|
||||
@@ -307,6 +316,11 @@
|
||||
YouTube DJ set…
|
||||
<span class="hint">tracks from chapters, description or comments</span>
|
||||
</button>
|
||||
<button class="mi" onclick={importFromYoutubePlaylist}>
|
||||
<Icon name="playlist" size={15} />
|
||||
Playlist from a link…
|
||||
<span class="hint">YouTube · Spotify · Apple Music · TIDAL · Deezer</span>
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -420,21 +434,31 @@
|
||||
<Icon name="layers" size={15} /> Duplicate
|
||||
</button>
|
||||
<div class="rule"></div>
|
||||
<button class="mi" onclick={() => exportAs(pl, "m3u8")}>
|
||||
<Icon name="export" size={15} /> Export M3U8
|
||||
</button>
|
||||
<button class="mi" onclick={() => exportAs(pl, "xspf")}>
|
||||
<Icon name="export" size={15} /> Export XSPF
|
||||
</button>
|
||||
<button class="mi" onclick={() => exportAs(pl, "pls")}>
|
||||
<Icon name="export" size={15} /> Export PLS
|
||||
</button>
|
||||
<button class="mi" onclick={() => exportAs(pl, "csv")}>
|
||||
<Icon name="export" size={15} /> Export CSV
|
||||
</button>
|
||||
<button class="mi" onclick={() => exportAs(pl, "json")}>
|
||||
<Icon name="export" size={15} /> Export backup (JSON)
|
||||
</button>
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class="sub-wrap"
|
||||
onmouseenter={() => (exportFor = pl.id)}
|
||||
onmouseleave={() => (exportFor = null)}
|
||||
>
|
||||
<button
|
||||
class="mi"
|
||||
onclick={() => (exportFor = exportFor === pl.id ? null : pl.id)}
|
||||
aria-haspopup="menu"
|
||||
aria-expanded={exportFor === pl.id}
|
||||
>
|
||||
<Icon name="export" size={15} /> Export
|
||||
<span class="sub-arrow"><Icon name="chevron-right" size={13} /></span>
|
||||
</button>
|
||||
{#if exportFor === pl.id}
|
||||
<div class="menu sub" role="menu">
|
||||
<button class="mi" onclick={() => exportAs(pl, "m3u8")}>M3U8</button>
|
||||
<button class="mi" onclick={() => exportAs(pl, "xspf")}>XSPF</button>
|
||||
<button class="mi" onclick={() => exportAs(pl, "pls")}>PLS</button>
|
||||
<button class="mi" onclick={() => exportAs(pl, "csv")}>CSV</button>
|
||||
<button class="mi" onclick={() => exportAs(pl, "json")}>Backup (JSON)</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="rule"></div>
|
||||
<button class="mi danger" onclick={() => remove(pl)}>
|
||||
<Icon name="trash" size={15} />
|
||||
@@ -641,6 +665,32 @@
|
||||
color: var(--text-muted);
|
||||
padding-left: 14px;
|
||||
}
|
||||
.sub-wrap {
|
||||
position: relative;
|
||||
}
|
||||
.sub-arrow {
|
||||
margin-left: auto;
|
||||
display: inline-flex;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
/* Same glass card, berthed beside its row; the ::before bridge covers
|
||||
the gap so the pointer can cross without closing it. Card menus sit
|
||||
at the grid's right edge as often as not, so the flyout opens LEFT. */
|
||||
.menu.sub {
|
||||
position: absolute;
|
||||
right: calc(100% + 4px);
|
||||
left: auto;
|
||||
top: -6px;
|
||||
min-width: 130px;
|
||||
}
|
||||
.menu.sub::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: -8px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 8px;
|
||||
}
|
||||
.menu .rule {
|
||||
height: 1px;
|
||||
background: var(--border);
|
||||
|
||||
Reference in new issue
Block a user