feat(frontend): show recently played from listening history on home

This commit is contained in:
2026-07-14 13:26:08 +02:00
parent 78caf588be
commit 46a6a9757a
3 changed files with 132 additions and 11 deletions
+92
View File
@@ -161,6 +161,23 @@ export interface paths {
patch?: never; patch?: never;
trace?: never; trace?: never;
}; };
"/tracks/{id}/cover": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/** Get A Track Cover Image. */
get: operations["GetTrackCover"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/tracks/{id}/stream": { "/tracks/{id}/stream": {
parameters: { parameters: {
query?: never; query?: never;
@@ -264,6 +281,23 @@ export interface paths {
patch?: never; patch?: never;
trace?: never; trace?: never;
}; };
"/users/{id}/listening-history": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/** Get A User'S Listening History. */
get: operations["GetListeningHistory"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
} }
export type webhooks = Record<string, never>; export type webhooks = Record<string, never>;
export interface components { export interface components {
@@ -908,6 +942,35 @@ export interface operations {
}; };
}; };
}; };
GetTrackCover: {
parameters: {
query?: {
size?: string;
};
header?: never;
path: {
id: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description OK */
200: {
headers: {
[name: string]: unknown;
};
content?: never;
};
/** @description Not Found */
404: {
headers: {
[name: string]: unknown;
};
content?: never;
};
};
};
GetTrackStream: { GetTrackStream: {
parameters: { parameters: {
query?: never; query?: never;
@@ -1158,4 +1221,33 @@ export interface operations {
}; };
}; };
}; };
GetListeningHistory: {
parameters: {
query?: never;
header?: never;
path: {
id: number | string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description OK */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["TrackApplicationResponse"][];
};
};
/** @description Not Found */
404: {
headers: {
[name: string]: unknown;
};
content?: never;
};
};
};
} }
+10 -2
View File
@@ -12,8 +12,15 @@ export const load: PageServerLoad = async ({ locals, fetch }) => {
params: { path: { userId: locals.user.sub }, query: { pageNumber: 1, pageSize: 8 } } params: { path: { userId: locals.user.sub }, query: { pageNumber: 1, pageSize: 8 } }
}) })
: Promise.resolve({ data: undefined }); : Promise.resolve({ data: undefined });
const recentlyPlayedPromise = locals.user
? api.GET('/users/{id}/listening-history', { params: { path: { id: locals.user.sub } } })
: Promise.resolve({ data: undefined });
const [latest, playlists] = await Promise.all([latestPromise, playlistsPromise]); const [latest, playlists, recentlyPlayed] = await Promise.all([
latestPromise,
playlistsPromise,
recentlyPlayedPromise
]);
const playlistItems = playlists.data?.items ?? []; const playlistItems = playlists.data?.items ?? [];
const covers = await Promise.all( const covers = await Promise.all(
@@ -28,6 +35,7 @@ export const load: PageServerLoad = async ({ locals, fetch }) => {
return { return {
latest: latest.data?.items ?? [], latest: latest.data?.items ?? [],
playlists: playlistItems, playlists: playlistItems,
trackIds: Object.fromEntries(covers) as Record<string, string[]> trackIds: Object.fromEntries(covers) as Record<string, string[]>,
recentlyPlayed: recentlyPlayed.data ?? []
}; };
}; };
+30 -9
View File
@@ -11,6 +11,22 @@
const latest = $derived(data.latest); const latest = $derived(data.latest);
const playlists = $derived(data.playlists); const playlists = $derived(data.playlists);
const trackIds = $derived(data.trackIds); const trackIds = $derived(data.trackIds);
const recentlyPlayed = $derived.by(() => {
const seen = new Set<string>();
const merged: { id: string; title: string }[] = [];
for (const t of player.recentlyPlayed) {
const id = String(t.id);
if (seen.has(id)) continue;
seen.add(id);
merged.push({ id, title: t.title });
}
for (const t of data.recentlyPlayed) {
if (seen.has(t.id)) continue;
seen.add(t.id);
merged.push({ id: t.id, title: t.title });
}
return merged.slice(0, 15);
});
const greeting = (() => { const greeting = (() => {
const h = new Date().getHours(); const h = new Date().getHours();
@@ -41,6 +57,12 @@
if (player.current.id === track.id) player.toggle(); if (player.current.id === track.id) player.toggle();
else player.playQueue(latest, index); else player.playQueue(latest, index);
} }
function playRecent(index: number) {
const track = recentlyPlayed[index];
if (player.current.id === track.id) player.toggle();
else player.playQueue(recentlyPlayed, index);
}
</script> </script>
<svelte:head> <svelte:head>
@@ -61,19 +83,19 @@
</div> </div>
</section> </section>
{#if player.recentlyPlayed.length > 0} {#if recentlyPlayed.length > 0}
<section class="px-8 pb-7 pt-4"> <section class="px-8 pb-3 pt-6">
<h2 class="mb-4 font-display text-xl font-bold">Escuchado recientemente</h2> <h2 class="mb-4 font-display text-xl font-bold">Escuchado recientemente</h2>
<div class="grid max-w-[1400px] grid-cols-[repeat(auto-fill,minmax(260px,1fr))] gap-3.5"> <div class="grid max-w-[1400px] grid-cols-[repeat(auto-fill,minmax(260px,1fr))] gap-3.5">
{#each player.recentlyPlayed as track (track.id)} {#each recentlyPlayed as track, i (track.id)}
<button <button
type="button" type="button"
onclick={() => player.playTrack(track.id)} onclick={() => playRecent(i)}
class="flex items-center gap-3.5 rounded-xl bg-[var(--mf-surface)] p-2 text-left transition hover:bg-[var(--mf-surface-hover)]" class="flex items-center gap-3.5 rounded-xl bg-[var(--mf-surface)] p-2 text-left transition hover:bg-[var(--mf-surface-hover)]"
> >
<Cover <Cover
trackId={track.id} trackId={track.id}
hue={track.hue} hue={hueFor(track.id)}
size="small" size="small"
alt={track.title} alt={track.title}
class="h-14 w-14 flex-shrink-0 rounded-lg" class="h-14 w-14 flex-shrink-0 rounded-lg"
@@ -84,7 +106,6 @@
</Cover> </Cover>
<div class="min-w-0"> <div class="min-w-0">
<div class="truncate text-sm font-semibold text-[var(--mf-text)]">{track.title}</div> <div class="truncate text-sm font-semibold text-[var(--mf-text)]">{track.title}</div>
<div class="truncate text-[12.5px] text-[var(--mf-text-3)]">{track.artist || '—'}</div>
</div> </div>
</button> </button>
{/each} {/each}
@@ -92,7 +113,7 @@
</section> </section>
{/if} {/if}
<section class="px-8 pb-10 pt-6"> <section class="px-8 pb-3 pt-3">
<h2 class="mb-4 font-display text-xl font-bold">Novedades</h2> <h2 class="mb-4 font-display text-xl font-bold">Novedades</h2>
{#if latest.length > 0} {#if latest.length > 0}
<div class="grid max-w-[1400px] grid-cols-[repeat(auto-fill,minmax(160px,1fr))] gap-4"> <div class="grid max-w-[1400px] grid-cols-[repeat(auto-fill,minmax(160px,1fr))] gap-4">
@@ -142,8 +163,8 @@
{/if} {/if}
</section> </section>
<section class="px-8 pb-16 pt-2"> <section class="px-8 pb-16 pt-3">
<h2 class="mb-4 font-display text-xl font-bold">Tus listas</h2> <h2 class="mb-4 font-display text-xl font-bold">Mis listas</h2>
{#if playlists.length > 0} {#if playlists.length > 0}
<div class="grid max-w-[1400px] grid-cols-[repeat(auto-fill,minmax(150px,1fr))] gap-4"> <div class="grid max-w-[1400px] grid-cols-[repeat(auto-fill,minmax(150px,1fr))] gap-4">
{#each playlists as playlist, i (playlist.id)} {#each playlists as playlist, i (playlist.id)}