web-player: extract menu, paged list and play-all components from pages
This commit is contained in:
@@ -0,0 +1,35 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { player } from '$lib/player/player.svelte';
|
||||||
|
import { isQueueCurrent, playAllOrToggle } from '$lib/player/actions';
|
||||||
|
import type { Track } from '$lib/types';
|
||||||
|
import Button from '../primitives/Button.svelte';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
items: Track[];
|
||||||
|
load?: () => Promise<Track[]>;
|
||||||
|
variant?: 'primary' | 'secondary' | 'accent';
|
||||||
|
size?: 'sm' | 'md' | 'lg';
|
||||||
|
idleLabel?: string;
|
||||||
|
activeLabel?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
let {
|
||||||
|
items,
|
||||||
|
load,
|
||||||
|
variant = 'primary',
|
||||||
|
size = 'sm',
|
||||||
|
idleLabel = 'Reproducir',
|
||||||
|
activeLabel = 'Pausar'
|
||||||
|
}: Props = $props();
|
||||||
|
|
||||||
|
const active = $derived(isQueueCurrent(items) && player.playing);
|
||||||
|
|
||||||
|
async function play() {
|
||||||
|
if (load && !isQueueCurrent(items)) playAllOrToggle(await load());
|
||||||
|
else playAllOrToggle(items);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Button {variant} {size} onclick={play} disabled={items.length === 0 && !load}>
|
||||||
|
{active ? activeLabel : idleLabel}
|
||||||
|
</Button>
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import ListEnd from '@lucide/svelte/icons/list-end';
|
||||||
|
import ListPlus from '@lucide/svelte/icons/list-plus';
|
||||||
|
import { player } from '$lib/player/player.svelte';
|
||||||
|
import { fetchAlbumTracks } from '$lib/data/albums';
|
||||||
|
import type { Menu } from '$lib/state/menu.svelte';
|
||||||
|
import type { Playlist } from '$lib/types';
|
||||||
|
import ContextMenu from './ContextMenu.svelte';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
menu: Menu<string>;
|
||||||
|
playlists: Playlist[];
|
||||||
|
}
|
||||||
|
|
||||||
|
let { menu, playlists }: Props = $props();
|
||||||
|
|
||||||
|
async function playNext(albumId: string) {
|
||||||
|
menu.close();
|
||||||
|
player.playNext(await fetchAlbumTracks(albumId));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addToQueue(albumId: string) {
|
||||||
|
menu.close();
|
||||||
|
player.appendToQueue(await fetchAlbumTracks(albumId));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if menu.state}
|
||||||
|
{@const { target: albumId, x, y, openLeft } = menu.state}
|
||||||
|
<ContextMenu
|
||||||
|
{x}
|
||||||
|
{y}
|
||||||
|
{openLeft}
|
||||||
|
onClose={() => menu.close()}
|
||||||
|
items={[
|
||||||
|
{ icon: ListPlus, label: 'Reproducir a continuación', onclick: () => playNext(albumId) },
|
||||||
|
{ icon: ListEnd, label: 'Añadir a la cola', onclick: () => addToQueue(albumId) }
|
||||||
|
]}
|
||||||
|
playlistAction={{
|
||||||
|
action: '?/addAlbumToPlaylist',
|
||||||
|
fields: { albumId },
|
||||||
|
label: 'Añadir álbum a una playlist'
|
||||||
|
}}
|
||||||
|
{playlists}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import ListPlus from '@lucide/svelte/icons/list-plus';
|
||||||
|
import { player } from '$lib/player/player.svelte';
|
||||||
|
import type { Menu } from '$lib/state/menu.svelte';
|
||||||
|
import type { Playlist, Track } from '$lib/types';
|
||||||
|
import ContextMenu from './ContextMenu.svelte';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
menu: Menu<Track>;
|
||||||
|
playlists: Playlist[];
|
||||||
|
}
|
||||||
|
|
||||||
|
let { menu, playlists }: Props = $props();
|
||||||
|
|
||||||
|
function playNext(track: Track) {
|
||||||
|
player.playNext([track]);
|
||||||
|
menu.close();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if menu.state}
|
||||||
|
{@const { target: track, x, y, openLeft } = menu.state}
|
||||||
|
<ContextMenu
|
||||||
|
{x}
|
||||||
|
{y}
|
||||||
|
{openLeft}
|
||||||
|
onClose={() => menu.close()}
|
||||||
|
items={[{ icon: ListPlus, label: 'Reproducir a continuación', onclick: () => playNext(track) }]}
|
||||||
|
playlistAction={{
|
||||||
|
action: '?/addTrack',
|
||||||
|
fields: { trackId: track.id },
|
||||||
|
label: 'Añadir a una playlist'
|
||||||
|
}}
|
||||||
|
{playlists}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
@@ -1,15 +1,23 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import LoaderCircle from '@lucide/svelte/icons/loader-circle';
|
import LoaderCircle from '@lucide/svelte/icons/loader-circle';
|
||||||
|
import Button from './Button.svelte';
|
||||||
import { INFINITE_SCROLL_ROOT_MARGIN } from '$lib/config';
|
import { INFINITE_SCROLL_ROOT_MARGIN } from '$lib/config';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onLoadMore: () => void;
|
onLoadMore: () => void;
|
||||||
hasMore: boolean;
|
hasMore: boolean;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
|
error?: boolean;
|
||||||
rootMargin?: string;
|
rootMargin?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { onLoadMore, hasMore, loading, rootMargin = INFINITE_SCROLL_ROOT_MARGIN }: Props = $props();
|
let {
|
||||||
|
onLoadMore,
|
||||||
|
hasMore,
|
||||||
|
loading,
|
||||||
|
error = false,
|
||||||
|
rootMargin = INFINITE_SCROLL_ROOT_MARGIN
|
||||||
|
}: Props = $props();
|
||||||
|
|
||||||
let sentinel = $state<HTMLDivElement>();
|
let sentinel = $state<HTMLDivElement>();
|
||||||
let visible = $state(false);
|
let visible = $state(false);
|
||||||
@@ -28,12 +36,17 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (visible && hasMore && !loading) onLoadMore();
|
if (visible && hasMore && !loading && !error) onLoadMore();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div bind:this={sentinel} class="flex min-h-12 items-center justify-center py-8">
|
<div bind:this={sentinel} class="flex min-h-12 items-center justify-center py-8">
|
||||||
{#if loading}
|
{#if loading}
|
||||||
<LoaderCircle class="size-5 animate-spin text-fg-2" />
|
<LoaderCircle class="size-5 animate-spin text-fg-2" />
|
||||||
|
{:else if error}
|
||||||
|
<div class="flex items-center gap-3 text-sm text-fg-2">
|
||||||
|
No se pudo cargar más.
|
||||||
|
<Button variant="secondary" size="sm" onclick={onLoadMore}>Reintentar</Button>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { contextMenuPosition, type MenuPosition } from '$lib/utils/menuPosition';
|
||||||
|
|
||||||
|
export type MenuState<T> = { target: T } & MenuPosition;
|
||||||
|
|
||||||
|
export function createMenu<T>() {
|
||||||
|
let state = $state<MenuState<T> | null>(null);
|
||||||
|
|
||||||
|
return {
|
||||||
|
get state() {
|
||||||
|
return state;
|
||||||
|
},
|
||||||
|
open(event: MouseEvent, target: T) {
|
||||||
|
event.preventDefault();
|
||||||
|
state = { target, ...contextMenuPosition(event) };
|
||||||
|
},
|
||||||
|
close() {
|
||||||
|
state = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Menu<T> = ReturnType<typeof createMenu<T>>;
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import { contextMenuPosition, type MenuPosition } from '$lib/utils/menuPosition';
|
|
||||||
import { player } from '$lib/player/player.svelte';
|
|
||||||
import type { Track } from '$lib/types';
|
|
||||||
|
|
||||||
export type TrackMenuState = { track: Track } & MenuPosition;
|
|
||||||
|
|
||||||
export function createTrackMenu() {
|
|
||||||
let state = $state<TrackMenuState | null>(null);
|
|
||||||
|
|
||||||
return {
|
|
||||||
get state() {
|
|
||||||
return state;
|
|
||||||
},
|
|
||||||
open(event: MouseEvent, track: Track) {
|
|
||||||
event.preventDefault();
|
|
||||||
state = { track, ...contextMenuPosition(event) };
|
|
||||||
},
|
|
||||||
close() {
|
|
||||||
state = null;
|
|
||||||
},
|
|
||||||
playNext() {
|
|
||||||
if (!state) return;
|
|
||||||
player.playNext([state.track]);
|
|
||||||
state = null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import type { Paged } from '$lib/types';
|
||||||
|
|
||||||
|
export function createPagedList<T>(
|
||||||
|
fetchPage: (pageNumber: number) => Promise<Paged<T>>,
|
||||||
|
keyOf: (item: T) => string | number
|
||||||
|
) {
|
||||||
|
let items = $state<T[]>([]);
|
||||||
|
let pageNumber = 1;
|
||||||
|
let hasMore = $state(false);
|
||||||
|
let loading = $state(false);
|
||||||
|
let error = $state(false);
|
||||||
|
|
||||||
|
return {
|
||||||
|
get items() {
|
||||||
|
return items;
|
||||||
|
},
|
||||||
|
get hasMore() {
|
||||||
|
return hasMore;
|
||||||
|
},
|
||||||
|
get loading() {
|
||||||
|
return loading;
|
||||||
|
},
|
||||||
|
get error() {
|
||||||
|
return error;
|
||||||
|
},
|
||||||
|
reset(initial: Paged<T> | null) {
|
||||||
|
items = initial?.items ?? [];
|
||||||
|
pageNumber = initial?.pageNumber ?? 1;
|
||||||
|
hasMore = initial?.hasNextPage ?? false;
|
||||||
|
loading = false;
|
||||||
|
error = false;
|
||||||
|
},
|
||||||
|
async loadMore() {
|
||||||
|
if (loading) return;
|
||||||
|
loading = true;
|
||||||
|
error = false;
|
||||||
|
try {
|
||||||
|
const next = await fetchPage(pageNumber + 1);
|
||||||
|
const seen = new Set(items.map(keyOf));
|
||||||
|
items = [...items, ...next.items.filter((item) => !seen.has(keyOf(item)))];
|
||||||
|
pageNumber = next.pageNumber;
|
||||||
|
hasMore = next.hasNextPage;
|
||||||
|
} catch {
|
||||||
|
error = true;
|
||||||
|
} finally {
|
||||||
|
loading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { albumCover } from '$lib/utils/hrefs';
|
import { albumCover } from '$lib/utils/hrefs';
|
||||||
import { player } from '$lib/player/player.svelte';
|
import { player } from '$lib/player/player.svelte';
|
||||||
import { isQueueCurrent, playAllOrToggle } from '$lib/player/actions';
|
import PlayAllButton from '$lib/components/ui/media/PlayAllButton.svelte';
|
||||||
import X from '@lucide/svelte/icons/x';
|
import X from '@lucide/svelte/icons/x';
|
||||||
import Plus from '@lucide/svelte/icons/plus';
|
import Plus from '@lucide/svelte/icons/plus';
|
||||||
import Page from '$lib/components/ui/layout/Page.svelte';
|
import Page from '$lib/components/ui/layout/Page.svelte';
|
||||||
@@ -27,12 +27,6 @@
|
|||||||
let editing = $state(false);
|
let editing = $state(false);
|
||||||
let confirmingDelete = $state(false);
|
let confirmingDelete = $state(false);
|
||||||
|
|
||||||
const isCurrentQueue = $derived(isQueueCurrent(tracks));
|
|
||||||
|
|
||||||
function playAll() {
|
|
||||||
playAllOrToggle(tracks);
|
|
||||||
}
|
|
||||||
|
|
||||||
function playTrackFrom(index: number) {
|
function playTrackFrom(index: number) {
|
||||||
player.playOrToggle(tracks, index);
|
player.playOrToggle(tracks, index);
|
||||||
}
|
}
|
||||||
@@ -69,13 +63,7 @@
|
|||||||
/>
|
/>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
{#snippet actions()}
|
{#snippet actions()}
|
||||||
<Button size="sm" onclick={playAll} disabled={tracks.length === 0}>
|
<PlayAllButton items={tracks} />
|
||||||
{#if isCurrentQueue && player.playing}
|
|
||||||
Pausar
|
|
||||||
{:else}
|
|
||||||
Reproducir
|
|
||||||
{/if}
|
|
||||||
</Button>
|
|
||||||
{#if isOwner}
|
{#if isOwner}
|
||||||
<Button size="sm" variant="secondary" onclick={() => (editing = true)}>Editar</Button>
|
<Button size="sm" variant="secondary" onclick={() => (editing = true)}>Editar</Button>
|
||||||
<Button size="sm" variant="secondary" onclick={() => (confirmingDelete = true)}>
|
<Button size="sm" variant="secondary" onclick={() => (confirmingDelete = true)}>
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Music from '@lucide/svelte/icons/music';
|
import Music from '@lucide/svelte/icons/music';
|
||||||
import { player } from '$lib/player/player.svelte';
|
import { player } from '$lib/player/player.svelte';
|
||||||
import { fetchAlbumTracks } from '$lib/data/albums';
|
import type { Paged, Track } from '$lib/types';
|
||||||
import type { Track } from '$lib/types';
|
|
||||||
import { fmtTime, fmtPlays, plural } from '$lib/utils/format';
|
import { fmtTime, fmtPlays, plural } from '$lib/utils/format';
|
||||||
import Page from '$lib/components/ui/layout/Page.svelte';
|
import Page from '$lib/components/ui/layout/Page.svelte';
|
||||||
import PageHeader from '$lib/components/ui/layout/PageHeader.svelte';
|
import PageHeader from '$lib/components/ui/layout/PageHeader.svelte';
|
||||||
@@ -19,12 +18,10 @@
|
|||||||
import SectionHeading from '$lib/components/ui/layout/SectionHeading.svelte';
|
import SectionHeading from '$lib/components/ui/layout/SectionHeading.svelte';
|
||||||
import { EXPLORE_PAGE_SIZE } from '$lib/config';
|
import { EXPLORE_PAGE_SIZE } from '$lib/config';
|
||||||
import { genreHref, playlistCover } from '$lib/utils/hrefs';
|
import { genreHref, playlistCover } from '$lib/utils/hrefs';
|
||||||
import { createTrackMenu } from '$lib/state/menus.svelte';
|
import { createMenu } from '$lib/state/menu.svelte';
|
||||||
import ContextMenu from '$lib/components/ui/overlay/ContextMenu.svelte';
|
import { createPagedList } from '$lib/state/pagedList.svelte';
|
||||||
import { contextMenuPosition } from '$lib/utils/menuPosition';
|
import TrackContextMenu from '$lib/components/ui/overlay/TrackContextMenu.svelte';
|
||||||
import ListPlus from '@lucide/svelte/icons/list-plus';
|
import AlbumContextMenu from '$lib/components/ui/overlay/AlbumContextMenu.svelte';
|
||||||
import ListEnd from '@lucide/svelte/icons/list-end';
|
|
||||||
import { appendUnique } from '$lib/utils/collections';
|
|
||||||
import { genreInfo, genreTiles } from '$lib/data/genres';
|
import { genreInfo, genreTiles } from '$lib/data/genres';
|
||||||
import {
|
import {
|
||||||
type SearchFilter,
|
type SearchFilter,
|
||||||
@@ -37,8 +34,6 @@
|
|||||||
findTopResult
|
findTopResult
|
||||||
} from '$lib/data/search';
|
} from '$lib/data/search';
|
||||||
|
|
||||||
type AlbumMenuState = { x: number; y: number; openLeft: boolean; albumId: string };
|
|
||||||
|
|
||||||
const TOP_RESULT_KIND_LABEL: Record<TopResult['kind'], string> = {
|
const TOP_RESULT_KIND_LABEL: Record<TopResult['kind'], string> = {
|
||||||
track: 'Canción',
|
track: 'Canción',
|
||||||
album: 'Álbum',
|
album: 'Álbum',
|
||||||
@@ -48,25 +43,33 @@
|
|||||||
|
|
||||||
let { data, form } = $props();
|
let { data, form } = $props();
|
||||||
|
|
||||||
const trackMenu = createTrackMenu();
|
const trackMenu = createMenu<Track>();
|
||||||
let albumMenu = $state<AlbumMenuState | null>(null);
|
const albumMenu = createMenu<string>();
|
||||||
let sfilter = $state<SearchFilter>('all');
|
let sfilter = $state<SearchFilter>('all');
|
||||||
|
|
||||||
let localItems = $state<Track[]>([]);
|
const tracksList = createPagedList<Track>(
|
||||||
let localPage = $state(1);
|
async (pageNumber) => {
|
||||||
let hasMore = $state(false);
|
const params = new URLSearchParams({
|
||||||
let loadingMore = $state(false);
|
pageNumber: String(pageNumber),
|
||||||
|
pageSize: String(EXPLORE_PAGE_SIZE)
|
||||||
|
});
|
||||||
|
if (data.query) params.set('name', data.query);
|
||||||
|
if (data.genre) params.set('genre', data.genre);
|
||||||
|
const res = await fetch(`/api/tracks?${params}`);
|
||||||
|
if (!res.ok) throw new Error(String(res.status));
|
||||||
|
return (await res.json()) as Paged<Track>;
|
||||||
|
},
|
||||||
|
(track) => track.id
|
||||||
|
);
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
trackMenu.close();
|
trackMenu.close();
|
||||||
albumMenu = null;
|
albumMenu.close();
|
||||||
sfilter = data.genre ? 'tracks' : 'all';
|
sfilter = data.genre ? 'tracks' : 'all';
|
||||||
localItems = data.tracks.items;
|
tracksList.reset(data.tracks);
|
||||||
localPage = data.tracks.pageNumber;
|
|
||||||
hasMore = data.tracks.hasNextPage;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const items = $derived(localItems);
|
const items = $derived(tracksList.items);
|
||||||
|
|
||||||
const tiles = $derived(genreTiles(data.genres));
|
const tiles = $derived(genreTiles(data.genres));
|
||||||
const activeGenre = $derived(data.genre ? genreInfo(data.genre) : null);
|
const activeGenre = $derived(data.genre ? genreInfo(data.genre) : null);
|
||||||
@@ -127,52 +130,6 @@
|
|||||||
if (topResult.kind === 'playlist') return `/playlists/${topResult.playlist.id}`;
|
if (topResult.kind === 'playlist') return `/playlists/${topResult.playlist.id}`;
|
||||||
return `/user/${topResult.user.id}`;
|
return `/user/${topResult.user.id}`;
|
||||||
});
|
});
|
||||||
|
|
||||||
function togglePlay(index: number) {
|
|
||||||
player.playOrToggle(items, index);
|
|
||||||
}
|
|
||||||
|
|
||||||
function openAlbumMenu(event: MouseEvent, albumId: string) {
|
|
||||||
event.preventDefault();
|
|
||||||
albumMenu = { ...contextMenuPosition(event), albumId };
|
|
||||||
}
|
|
||||||
|
|
||||||
async function albumPlayNext() {
|
|
||||||
if (!albumMenu) return;
|
|
||||||
const { albumId } = albumMenu;
|
|
||||||
albumMenu = null;
|
|
||||||
player.playNext(await fetchAlbumTracks(albumId));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function albumAddToQueue() {
|
|
||||||
if (!albumMenu) return;
|
|
||||||
const { albumId } = albumMenu;
|
|
||||||
albumMenu = null;
|
|
||||||
player.appendToQueue(await fetchAlbumTracks(albumId));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadMore() {
|
|
||||||
if (loadingMore) return;
|
|
||||||
loadingMore = true;
|
|
||||||
try {
|
|
||||||
const params = new URLSearchParams({
|
|
||||||
pageNumber: String(localPage + 1),
|
|
||||||
pageSize: String(EXPLORE_PAGE_SIZE)
|
|
||||||
});
|
|
||||||
if (data.query) params.set('name', data.query);
|
|
||||||
if (data.genre) params.set('genre', data.genre);
|
|
||||||
const res = await fetch(`/api/tracks?${params}`);
|
|
||||||
if (!res.ok) throw new Error(String(res.status));
|
|
||||||
const next = (await res.json()) as typeof data.tracks;
|
|
||||||
localItems = appendUnique(localItems, next.items, (track) => track.id);
|
|
||||||
localPage = next.pageNumber;
|
|
||||||
hasMore = next.hasNextPage;
|
|
||||||
} catch {
|
|
||||||
hasMore = false;
|
|
||||||
} finally {
|
|
||||||
loadingMore = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
@@ -334,7 +291,7 @@
|
|||||||
active={player.currentId === track.id}
|
active={player.currentId === track.id}
|
||||||
size="lg"
|
size="lg"
|
||||||
trackId={track.id}
|
trackId={track.id}
|
||||||
onclick={() => togglePlay(i)}
|
onclick={() => player.playOrToggle(items, i)}
|
||||||
oncontextmenu={(e) => trackMenu.open(e, track)}
|
oncontextmenu={(e) => trackMenu.open(e, track)}
|
||||||
>
|
>
|
||||||
{#snippet overlay()}
|
{#snippet overlay()}
|
||||||
@@ -352,8 +309,13 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
{#if sfilter === 'tracks' && hasMore}
|
{#if sfilter === 'tracks' && tracksList.hasMore}
|
||||||
<InfiniteScroll onLoadMore={loadMore} {hasMore} loading={loadingMore} />
|
<InfiniteScroll
|
||||||
|
onLoadMore={() => tracksList.loadMore()}
|
||||||
|
hasMore={tracksList.hasMore}
|
||||||
|
loading={tracksList.loading}
|
||||||
|
error={tracksList.error}
|
||||||
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
{:else}
|
{:else}
|
||||||
<EmptyState
|
<EmptyState
|
||||||
@@ -377,7 +339,7 @@
|
|||||||
href="/albums/{album.id}"
|
href="/albums/{album.id}"
|
||||||
size="lg"
|
size="lg"
|
||||||
trackIds={album.coverTrackIds}
|
trackIds={album.coverTrackIds}
|
||||||
oncontextmenu={(e) => openAlbumMenu(e, album.id)}
|
oncontextmenu={(e) => albumMenu.open(e, album.id)}
|
||||||
>
|
>
|
||||||
{#snippet trailing()}
|
{#snippet trailing()}
|
||||||
<span class="hidden shrink-0 text-xs text-fg-2 tabular-nums sm:block">
|
<span class="hidden shrink-0 text-xs text-fg-2 tabular-nums sm:block">
|
||||||
@@ -427,39 +389,5 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</Page>
|
</Page>
|
||||||
|
|
||||||
{#if trackMenu.state}
|
<TrackContextMenu menu={trackMenu} playlists={data.userPlaylists} />
|
||||||
<ContextMenu
|
<AlbumContextMenu menu={albumMenu} playlists={data.userPlaylists} />
|
||||||
x={trackMenu.state.x}
|
|
||||||
y={trackMenu.state.y}
|
|
||||||
openLeft={trackMenu.state.openLeft}
|
|
||||||
onClose={() => trackMenu.close()}
|
|
||||||
items={[
|
|
||||||
{ icon: ListPlus, label: 'Reproducir a continuación', onclick: () => trackMenu.playNext() }
|
|
||||||
]}
|
|
||||||
playlistAction={{
|
|
||||||
action: '?/addTrack',
|
|
||||||
fields: { trackId: trackMenu.state.track.id },
|
|
||||||
label: 'Añadir a una playlist'
|
|
||||||
}}
|
|
||||||
playlists={data.userPlaylists}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if albumMenu}
|
|
||||||
<ContextMenu
|
|
||||||
x={albumMenu.x}
|
|
||||||
y={albumMenu.y}
|
|
||||||
openLeft={albumMenu.openLeft}
|
|
||||||
onClose={() => (albumMenu = null)}
|
|
||||||
items={[
|
|
||||||
{ icon: ListPlus, label: 'Reproducir a continuación', onclick: albumPlayNext },
|
|
||||||
{ icon: ListEnd, label: 'Añadir a la cola', onclick: albumAddToQueue }
|
|
||||||
]}
|
|
||||||
playlistAction={{
|
|
||||||
action: '?/addAlbumToPlaylist',
|
|
||||||
fields: { albumId: albumMenu.albumId },
|
|
||||||
label: 'Añadir álbum a una playlist'
|
|
||||||
}}
|
|
||||||
playlists={data.userPlaylists}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import Heart from '@lucide/svelte/icons/heart';
|
import Heart from '@lucide/svelte/icons/heart';
|
||||||
import X from '@lucide/svelte/icons/x';
|
import X from '@lucide/svelte/icons/x';
|
||||||
import { player } from '$lib/player/player.svelte';
|
import { player } from '$lib/player/player.svelte';
|
||||||
import { isQueueCurrent, playAllOrToggle } from '$lib/player/actions';
|
import PlayAllButton from '$lib/components/ui/media/PlayAllButton.svelte';
|
||||||
import { liked } from '$lib/player/liked.svelte';
|
import { liked } from '$lib/player/liked.svelte';
|
||||||
import { plural } from '$lib/utils/format';
|
import { plural } from '$lib/utils/format';
|
||||||
import Page from '$lib/components/ui/layout/Page.svelte';
|
import Page from '$lib/components/ui/layout/Page.svelte';
|
||||||
@@ -13,15 +13,10 @@
|
|||||||
|
|
||||||
const tracks = $derived(liked.list);
|
const tracks = $derived(liked.list);
|
||||||
const listTracks = $derived(tracks.map((track) => ({ ...track, date: track.likedAt })));
|
const listTracks = $derived(tracks.map((track) => ({ ...track, date: track.likedAt })));
|
||||||
const isCurrentQueue = $derived(isQueueCurrent(tracks));
|
|
||||||
|
|
||||||
function playFrom(index: number) {
|
function playFrom(index: number) {
|
||||||
player.playOrToggle(tracks, index);
|
player.playOrToggle(tracks, index);
|
||||||
}
|
}
|
||||||
|
|
||||||
function playAll() {
|
|
||||||
playAllOrToggle(tracks);
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
@@ -44,9 +39,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
{#snippet actions()}
|
{#snippet actions()}
|
||||||
<Button size="sm" onclick={playAll} disabled={tracks.length === 0}>
|
<PlayAllButton items={tracks} />
|
||||||
{isCurrentQueue && player.playing ? 'Pausar' : 'Reproducir'}
|
|
||||||
</Button>
|
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Shuffle from '@lucide/svelte/icons/shuffle';
|
import Shuffle from '@lucide/svelte/icons/shuffle';
|
||||||
import { player } from '$lib/player/player.svelte';
|
import { player } from '$lib/player/player.svelte';
|
||||||
import { isQueueCurrent, playAllOrToggle, playShuffled } from '$lib/player/actions';
|
import { playShuffled } from '$lib/player/actions';
|
||||||
import { fmtTime, plural } from '$lib/utils/format';
|
import { fmtTime, plural } from '$lib/utils/format';
|
||||||
import Page from '$lib/components/ui/layout/Page.svelte';
|
import Page from '$lib/components/ui/layout/Page.svelte';
|
||||||
import BackLink from '$lib/components/ui/primitives/BackLink.svelte';
|
import BackLink from '$lib/components/ui/primitives/BackLink.svelte';
|
||||||
@@ -10,9 +10,10 @@
|
|||||||
import Artwork from '$lib/components/ui/media/Artwork.svelte';
|
import Artwork from '$lib/components/ui/media/Artwork.svelte';
|
||||||
import PageHeader from '$lib/components/ui/layout/PageHeader.svelte';
|
import PageHeader from '$lib/components/ui/layout/PageHeader.svelte';
|
||||||
import TrackList from '$lib/components/ui/media/TrackList.svelte';
|
import TrackList from '$lib/components/ui/media/TrackList.svelte';
|
||||||
import ContextMenu from '$lib/components/ui/overlay/ContextMenu.svelte';
|
import TrackContextMenu from '$lib/components/ui/overlay/TrackContextMenu.svelte';
|
||||||
import ListPlus from '@lucide/svelte/icons/list-plus';
|
import { createMenu } from '$lib/state/menu.svelte';
|
||||||
import { createTrackMenu } from '$lib/state/menus.svelte';
|
import type { Track } from '$lib/types';
|
||||||
|
import PlayAllButton from '$lib/components/ui/media/PlayAllButton.svelte';
|
||||||
|
|
||||||
let { data, form } = $props();
|
let { data, form } = $props();
|
||||||
|
|
||||||
@@ -20,9 +21,8 @@
|
|||||||
const tracks = $derived(mix.tracks);
|
const tracks = $derived(mix.tracks);
|
||||||
|
|
||||||
const totalSeconds = $derived(tracks.reduce((total, track) => total + track.duration, 0));
|
const totalSeconds = $derived(tracks.reduce((total, track) => total + track.duration, 0));
|
||||||
const isCurrentQueue = $derived(isQueueCurrent(tracks));
|
|
||||||
|
|
||||||
const trackMenu = createTrackMenu();
|
const trackMenu = createMenu<Track>();
|
||||||
|
|
||||||
function playFrom(index: number) {
|
function playFrom(index: number) {
|
||||||
player.playOrToggle(tracks, index);
|
player.playOrToggle(tracks, index);
|
||||||
@@ -52,13 +52,7 @@
|
|||||||
/>
|
/>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
{#snippet actions()}
|
{#snippet actions()}
|
||||||
<Button size="sm" onclick={() => playAllOrToggle(tracks)} disabled={tracks.length === 0}>
|
<PlayAllButton items={tracks} />
|
||||||
{#if isCurrentQueue && player.playing}
|
|
||||||
Pausar
|
|
||||||
{:else}
|
|
||||||
Reproducir
|
|
||||||
{/if}
|
|
||||||
</Button>
|
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -79,24 +73,8 @@
|
|||||||
{tracks}
|
{tracks}
|
||||||
columns={['plays']}
|
columns={['plays']}
|
||||||
onPlay={playFrom}
|
onPlay={playFrom}
|
||||||
oncontextmenu={(e, _track, i) => trackMenu.open(e, tracks[i])}
|
oncontextmenu={(e, track) => trackMenu.open(e, track)}
|
||||||
/>
|
/>
|
||||||
</Page>
|
</Page>
|
||||||
|
|
||||||
{#if trackMenu.state}
|
<TrackContextMenu menu={trackMenu} playlists={data.userPlaylists} />
|
||||||
<ContextMenu
|
|
||||||
x={trackMenu.state.x}
|
|
||||||
y={trackMenu.state.y}
|
|
||||||
openLeft={trackMenu.state.openLeft}
|
|
||||||
onClose={() => trackMenu.close()}
|
|
||||||
items={[
|
|
||||||
{ icon: ListPlus, label: 'Reproducir a continuación', onclick: () => trackMenu.playNext() }
|
|
||||||
]}
|
|
||||||
playlistAction={{
|
|
||||||
action: '?/addTrack',
|
|
||||||
fields: { trackId: trackMenu.state.track.id },
|
|
||||||
label: 'Añadir a una playlist'
|
|
||||||
}}
|
|
||||||
playlists={data.userPlaylists}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import ListMusic from '@lucide/svelte/icons/list-music';
|
import ListMusic from '@lucide/svelte/icons/list-music';
|
||||||
import X from '@lucide/svelte/icons/x';
|
import X from '@lucide/svelte/icons/x';
|
||||||
import { player } from '$lib/player/player.svelte';
|
import { player } from '$lib/player/player.svelte';
|
||||||
import { isQueueCurrent, playAllOrToggle } from '$lib/player/actions';
|
import PlayAllButton from '$lib/components/ui/media/PlayAllButton.svelte';
|
||||||
import Page from '$lib/components/ui/layout/Page.svelte';
|
import Page from '$lib/components/ui/layout/Page.svelte';
|
||||||
import PageHeader from '$lib/components/ui/layout/PageHeader.svelte';
|
import PageHeader from '$lib/components/ui/layout/PageHeader.svelte';
|
||||||
import Modal from '$lib/components/ui/overlay/Modal.svelte';
|
import Modal from '$lib/components/ui/overlay/Modal.svelte';
|
||||||
@@ -25,12 +25,6 @@
|
|||||||
let editing = $state(false);
|
let editing = $state(false);
|
||||||
let confirmingDelete = $state(false);
|
let confirmingDelete = $state(false);
|
||||||
|
|
||||||
const isCurrentQueue = $derived(isQueueCurrent(tracks));
|
|
||||||
|
|
||||||
function playAll() {
|
|
||||||
playAllOrToggle(tracks);
|
|
||||||
}
|
|
||||||
|
|
||||||
function playFrom(index: number) {
|
function playFrom(index: number) {
|
||||||
player.playOrToggle(tracks, index);
|
player.playOrToggle(tracks, index);
|
||||||
}
|
}
|
||||||
@@ -66,13 +60,7 @@
|
|||||||
/>
|
/>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
{#snippet actions()}
|
{#snippet actions()}
|
||||||
<Button size="sm" onclick={playAll} disabled={tracks.length === 0}>
|
<PlayAllButton items={tracks} />
|
||||||
{#if isCurrentQueue && player.playing}
|
|
||||||
Pausar
|
|
||||||
{:else}
|
|
||||||
Reproducir
|
|
||||||
{/if}
|
|
||||||
</Button>
|
|
||||||
{#if isOwner}
|
{#if isOwner}
|
||||||
<Button size="sm" variant="secondary" onclick={() => (editing = true)}>Editar</Button>
|
<Button size="sm" variant="secondary" onclick={() => (editing = true)}>Editar</Button>
|
||||||
<Button size="sm" variant="secondary" onclick={() => (confirmingDelete = true)}>
|
<Button size="sm" variant="secondary" onclick={() => (confirmingDelete = true)}>
|
||||||
|
|||||||
@@ -6,44 +6,29 @@
|
|||||||
import EmptyState from '$lib/components/ui/primitives/EmptyState.svelte';
|
import EmptyState from '$lib/components/ui/primitives/EmptyState.svelte';
|
||||||
import InfiniteScroll from '$lib/components/ui/primitives/InfiniteScroll.svelte';
|
import InfiniteScroll from '$lib/components/ui/primitives/InfiniteScroll.svelte';
|
||||||
import UserRow from '$lib/components/ui/media/UserRow.svelte';
|
import UserRow from '$lib/components/ui/media/UserRow.svelte';
|
||||||
import { appendUnique } from '$lib/utils/collections';
|
import { createPagedList } from '$lib/state/pagedList.svelte';
|
||||||
import { plural } from '$lib/utils/format';
|
import { plural } from '$lib/utils/format';
|
||||||
|
|
||||||
let { data } = $props();
|
let { data } = $props();
|
||||||
|
|
||||||
type ListUser = NonNullable<typeof data.users>['items'][number];
|
type ListUser = NonNullable<typeof data.users>['items'][number];
|
||||||
|
|
||||||
let items = $state<ListUser[]>([]);
|
const list = createPagedList<ListUser>(
|
||||||
let page = $state(1);
|
async (pageNumber) => {
|
||||||
let hasMore = $state(false);
|
const res = await fetch(
|
||||||
let loadingMore = $state(false);
|
`/api/users/${data.profile.id}/${data.list}?pageNumber=${pageNumber}`
|
||||||
|
);
|
||||||
|
if (!res.ok) throw new Error(String(res.status));
|
||||||
|
return (await res.json()) as NonNullable<typeof data.users>;
|
||||||
|
},
|
||||||
|
(user) => user.id
|
||||||
|
);
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => list.reset(data.users));
|
||||||
items = data.users?.items ?? [];
|
|
||||||
page = data.users?.pageNumber ?? 1;
|
|
||||||
hasMore = data.users?.hasNextPage ?? false;
|
|
||||||
});
|
|
||||||
|
|
||||||
const isFollowers = $derived(data.list === 'followers');
|
const isFollowers = $derived(data.list === 'followers');
|
||||||
const title = $derived(isFollowers ? 'Seguidores' : 'Siguiendo');
|
const title = $derived(isFollowers ? 'Seguidores' : 'Siguiendo');
|
||||||
const total = $derived(data.users?.totalItemCount ?? 0);
|
const total = $derived(data.users?.totalItemCount ?? 0);
|
||||||
|
|
||||||
async function loadMore() {
|
|
||||||
if (loadingMore) return;
|
|
||||||
loadingMore = true;
|
|
||||||
try {
|
|
||||||
const res = await fetch(`/api/users/${data.profile.id}/${data.list}?pageNumber=${page + 1}`);
|
|
||||||
if (!res.ok) throw new Error(String(res.status));
|
|
||||||
const next = (await res.json()) as NonNullable<typeof data.users>;
|
|
||||||
items = appendUnique(items, next.items, (u) => u.id);
|
|
||||||
page = next.pageNumber;
|
|
||||||
hasMore = next.hasNextPage;
|
|
||||||
} catch {
|
|
||||||
hasMore = false;
|
|
||||||
} finally {
|
|
||||||
loadingMore = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
@@ -67,7 +52,7 @@
|
|||||||
title="Seguidores privados"
|
title="Seguidores privados"
|
||||||
description="Solo puedes ver los seguidores de {data.profile.name} si os seguís mutuamente."
|
description="Solo puedes ver los seguidores de {data.profile.name} si os seguís mutuamente."
|
||||||
/>
|
/>
|
||||||
{:else if items.length === 0}
|
{:else if list.items.length === 0}
|
||||||
<EmptyState
|
<EmptyState
|
||||||
icon={Users}
|
icon={Users}
|
||||||
title={isFollowers ? 'Sin seguidores' : 'No sigue a nadie'}
|
title={isFollowers ? 'Sin seguidores' : 'No sigue a nadie'}
|
||||||
@@ -77,12 +62,17 @@
|
|||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<ul class="flex flex-col gap-1">
|
<ul class="flex flex-col gap-1">
|
||||||
{#each items as user (user.id)}
|
{#each list.items as user (user.id)}
|
||||||
<li>
|
<li>
|
||||||
<UserRow {user} />
|
<UserRow {user} />
|
||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
</ul>
|
</ul>
|
||||||
<InfiniteScroll onLoadMore={loadMore} {hasMore} loading={loadingMore} />
|
<InfiniteScroll
|
||||||
|
onLoadMore={() => list.loadMore()}
|
||||||
|
hasMore={list.hasMore}
|
||||||
|
loading={list.loading}
|
||||||
|
error={list.error}
|
||||||
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</Page>
|
</Page>
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { playlistCover } from '$lib/utils/hrefs';
|
import { playlistCover } from '$lib/utils/hrefs';
|
||||||
import { goto } from '$app/navigation';
|
|
||||||
import History from '@lucide/svelte/icons/history';
|
import History from '@lucide/svelte/icons/history';
|
||||||
import Shuffle from '@lucide/svelte/icons/shuffle';
|
import Shuffle from '@lucide/svelte/icons/shuffle';
|
||||||
import ListMusic from '@lucide/svelte/icons/list-music';
|
import ListMusic from '@lucide/svelte/icons/list-music';
|
||||||
import { player } from '$lib/player/player.svelte';
|
import { player } from '$lib/player/player.svelte';
|
||||||
import { isQueueCurrent, playAllOrToggle, playShuffled } from '$lib/player/actions';
|
import { playShuffled } from '$lib/player/actions';
|
||||||
import type { Track } from '$lib/types';
|
import type { Track } from '$lib/types';
|
||||||
import { mergeRecentlyPlayed } from '$lib/data/recentlyPlayed';
|
import { mergeRecentlyPlayed } from '$lib/data/recentlyPlayed';
|
||||||
import { fetchPlaylistTracks } from '$lib/data/albums';
|
import { fetchPlaylistTracks } from '$lib/data/albums';
|
||||||
@@ -17,43 +16,20 @@
|
|||||||
import SectionHeading from '$lib/components/ui/layout/SectionHeading.svelte';
|
import SectionHeading from '$lib/components/ui/layout/SectionHeading.svelte';
|
||||||
import EmptyState from '$lib/components/ui/primitives/EmptyState.svelte';
|
import EmptyState from '$lib/components/ui/primitives/EmptyState.svelte';
|
||||||
import Button from '$lib/components/ui/primitives/Button.svelte';
|
import Button from '$lib/components/ui/primitives/Button.svelte';
|
||||||
import ContextMenu from '$lib/components/ui/overlay/ContextMenu.svelte';
|
import PlayAllButton from '$lib/components/ui/media/PlayAllButton.svelte';
|
||||||
import ListPlus from '@lucide/svelte/icons/list-plus';
|
import TrackContextMenu from '$lib/components/ui/overlay/TrackContextMenu.svelte';
|
||||||
import { createTrackMenu } from '$lib/state/menus.svelte';
|
import { createMenu } from '$lib/state/menu.svelte';
|
||||||
import { HOME_CONTINUE_LIMIT } from '$lib/config';
|
import { HOME_CONTINUE_LIMIT } from '$lib/config';
|
||||||
|
|
||||||
let { data } = $props();
|
let { data } = $props();
|
||||||
|
|
||||||
const trackMenu = createTrackMenu();
|
const trackMenu = createMenu<Track>();
|
||||||
|
|
||||||
const mixes = $derived(data.mixes);
|
|
||||||
const playlists = $derived(data.playlists);
|
|
||||||
const spotlight = $derived(data.spotlightPlaylist);
|
const spotlight = $derived(data.spotlightPlaylist);
|
||||||
|
|
||||||
const continueItems = $derived(mergeRecentlyPlayed(data.recentlyPlayed, HOME_CONTINUE_LIMIT));
|
const continueItems = $derived(mergeRecentlyPlayed(data.recentlyPlayed, HOME_CONTINUE_LIMIT));
|
||||||
|
|
||||||
function playContinue(index: number) {
|
|
||||||
player.playOrToggle(continueItems, index);
|
|
||||||
}
|
|
||||||
function playSpotlightTrack(items: Track[], index: number) {
|
|
||||||
player.playOrToggle(items, index);
|
|
||||||
}
|
|
||||||
async function playSpotlight(previewItems: Track[]) {
|
|
||||||
if (!spotlight) return;
|
|
||||||
if (isQueueCurrent(previewItems)) {
|
|
||||||
player.toggle();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
playAllOrToggle(await fetchPlaylistTracks(spotlight.id));
|
|
||||||
}
|
|
||||||
async function shuffleSpotlight() {
|
async function shuffleSpotlight() {
|
||||||
if (!spotlight) return;
|
if (data.spotlightPlaylist) playShuffled(await fetchPlaylistTracks(data.spotlightPlaylist.id));
|
||||||
playShuffled(await fetchPlaylistTracks(spotlight.id));
|
|
||||||
}
|
|
||||||
|
|
||||||
function playMix(mix: (typeof mixes)[number], event: MouseEvent) {
|
|
||||||
event.preventDefault();
|
|
||||||
player.playQueue(mix.tracks, 0);
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -90,11 +66,14 @@
|
|||||||
</p>
|
</p>
|
||||||
<div class="mt-6.5 flex flex-wrap items-center gap-2.5">
|
<div class="mt-6.5 flex flex-wrap items-center gap-2.5">
|
||||||
{#if spotlight}
|
{#if spotlight}
|
||||||
{#await data.spotlightTracks then { items: previewItems }}
|
{#await data.spotlightTracks then { items }}
|
||||||
<Button variant="primary" onclick={() => playSpotlight(previewItems)}>
|
<PlayAllButton
|
||||||
{isQueueCurrent(previewItems) && player.playing ? 'Pausar' : 'Reanudar'}
|
{items}
|
||||||
{spotlight.name}
|
load={() => fetchPlaylistTracks(spotlight.id)}
|
||||||
</Button>
|
size="md"
|
||||||
|
idleLabel="Reanudar {spotlight.name}"
|
||||||
|
activeLabel="Pausar {spotlight.name}"
|
||||||
|
/>
|
||||||
{/await}
|
{/await}
|
||||||
{/if}
|
{/if}
|
||||||
<Button href="/explore" variant="secondary">Explorar música</Button>
|
<Button href="/explore" variant="secondary">Explorar música</Button>
|
||||||
@@ -130,7 +109,7 @@
|
|||||||
{#each continueItems as item, i (item.id)}
|
{#each continueItems as item, i (item.id)}
|
||||||
{@const isCurrent = player.currentId === item.id}
|
{@const isCurrent = player.currentId === item.id}
|
||||||
<ListRow
|
<ListRow
|
||||||
onclick={() => playContinue(i)}
|
onclick={() => player.playOrToggle(continueItems, i)}
|
||||||
oncontextmenu={(e) => trackMenu.open(e, item)}
|
oncontextmenu={(e) => trackMenu.open(e, item)}
|
||||||
active={isCurrent}
|
active={isCurrent}
|
||||||
size="lg"
|
size="lg"
|
||||||
@@ -185,16 +164,19 @@
|
|||||||
{spotlight.description || 'Tu colección, siempre a mano.'}
|
{spotlight.description || 'Tu colección, siempre a mano.'}
|
||||||
</p>
|
</p>
|
||||||
<div class="mt-5 flex gap-2.5">
|
<div class="mt-5 flex gap-2.5">
|
||||||
<Button variant="accent" onclick={() => playSpotlight(previewItems)}
|
<PlayAllButton
|
||||||
>Reproducir</Button
|
items={previewItems}
|
||||||
>
|
load={() => fetchPlaylistTracks(spotlight.id)}
|
||||||
|
variant="accent"
|
||||||
|
size="md"
|
||||||
|
/>
|
||||||
<Button variant="secondary" onclick={shuffleSpotlight}>Aleatorio</Button>
|
<Button variant="secondary" onclick={shuffleSpotlight}>Aleatorio</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col gap-2 p-5.5 sm:p-6">
|
<div class="flex flex-col gap-2 p-5.5 sm:p-6">
|
||||||
{#each previewItems as track, i (track.id)}
|
{#each previewItems as track, i (track.id)}
|
||||||
<ListRow
|
<ListRow
|
||||||
onclick={() => playSpotlightTrack(previewItems, i)}
|
onclick={() => player.playOrToggle(previewItems, i)}
|
||||||
active={player.currentId === track.id}
|
active={player.currentId === track.id}
|
||||||
size="sm"
|
size="sm"
|
||||||
title={track.title}
|
title={track.title}
|
||||||
@@ -224,15 +206,18 @@
|
|||||||
|
|
||||||
<section>
|
<section>
|
||||||
<SectionHeading title="Mixes" subtitle="Generados a partir de lo que más repites" />
|
<SectionHeading title="Mixes" subtitle="Generados a partir de lo que más repites" />
|
||||||
{#if mixes.length > 0}
|
{#if data.mixes.length > 0}
|
||||||
<div class="grid-tiles">
|
<div class="grid-tiles">
|
||||||
{#each mixes as mix, i (mix.id)}
|
{#each data.mixes as mix, i (mix.id)}
|
||||||
<MediaCard
|
<MediaCard
|
||||||
href="/mixes/{mix.id}"
|
href="/mixes/{mix.id}"
|
||||||
title={mix.title}
|
title={mix.title}
|
||||||
subtitle={mix.subtitle}
|
subtitle={mix.subtitle}
|
||||||
trackIds={mix.tracks.map((track) => track.id)}
|
trackIds={mix.tracks.map((track) => track.id)}
|
||||||
onPlay={(event) => playMix(mix, event)}
|
onPlay={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
player.playQueue(mix.tracks, 0);
|
||||||
|
}}
|
||||||
index={i}
|
index={i}
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
@@ -252,11 +237,11 @@
|
|||||||
subtitle="Creadas y guardadas por ti"
|
subtitle="Creadas y guardadas por ti"
|
||||||
href={data.playlistsHasMore ? '/playlists' : undefined}
|
href={data.playlistsHasMore ? '/playlists' : undefined}
|
||||||
/>
|
/>
|
||||||
{#if playlists.length > 0}
|
{#if data.playlists.length > 0}
|
||||||
<div class="grid-wide">
|
<div class="grid-wide">
|
||||||
{#each playlists as playlist, i (playlist.id)}
|
{#each data.playlists as playlist, i (playlist.id)}
|
||||||
<ListRow
|
<ListRow
|
||||||
onclick={() => goto(`/playlists/${playlist.id}`)}
|
href="/playlists/{playlist.id}"
|
||||||
size="lg"
|
size="lg"
|
||||||
variant="card"
|
variant="card"
|
||||||
title={playlist.name}
|
title={playlist.name}
|
||||||
@@ -282,20 +267,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</Page>
|
</Page>
|
||||||
|
|
||||||
{#if trackMenu.state}
|
<TrackContextMenu menu={trackMenu} playlists={data.userPlaylists} />
|
||||||
<ContextMenu
|
|
||||||
x={trackMenu.state.x}
|
|
||||||
y={trackMenu.state.y}
|
|
||||||
openLeft={trackMenu.state.openLeft}
|
|
||||||
onClose={() => trackMenu.close()}
|
|
||||||
items={[
|
|
||||||
{ icon: ListPlus, label: 'Reproducir a continuación', onclick: () => trackMenu.playNext() }
|
|
||||||
]}
|
|
||||||
playlistAction={{
|
|
||||||
action: '?/addTrack',
|
|
||||||
fields: { trackId: trackMenu.state.track.id },
|
|
||||||
label: 'Añadir a una playlist'
|
|
||||||
}}
|
|
||||||
playlists={data.userPlaylists}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user