Web Player Refactor #2

Merged
NotS1mp merged 50 commits from web-player-ref into master 2026-09-23 15:05:12 +00:00
12 changed files with 18 additions and 17 deletions
Showing only changes of commit fbb102ffbd - Show all commits
+8 -7
View File
@@ -46,7 +46,7 @@ src/routes/
library/ /library Tu biblioteca ⬜ library/ /library Tu biblioteca ⬜
playlists/ /playlists Tus playlists ⬜ playlists/ /playlists Tus playlists ⬜
[id]/ /playlists/:id Detalle playlist ⬜ [id]/ /playlists/:id Detalle playlist ⬜
login logout auth/callback Auth (endpoints) ✅ auth auth/login auth/logout auth/callback Auth (página + endpoints) ✅
api/tracks/[id]/stream Proxy de stream ✅ api/tracks/[id]/stream Proxy de stream ✅
``` ```
@@ -66,10 +66,11 @@ Página de entrada para no autenticados. Vender el producto y llevar a registro/
## 2. Login / Auth ✅ (endpoints) ## 2. Login / Auth ✅ (endpoints)
No es una página visual propia: `/login` inicia el flujo OIDC de Zitadel y redirige. `/auth` es la pantalla de login de Musify; sus botones llevan a `/auth/login`, que inicia
`?mode=register` arranca en registro; `?returnTo=` vuelve a la ruta pedida tras entrar. el flujo OIDC de Zitadel y redirige. `?mode=register` arranca en registro; `?returnTo=`
`/logout` cierra sesión. `/auth/callback` intercambia el código y crea la cookie vuelve a la ruta pedida tras entrar. `/auth/logout` cierra sesión. `/auth/callback`
cifrada `mf_session`. La UI de credenciales la sirve Zitadel, no Musify. intercambia el código y crea la cookie cifrada `mf_session`. La UI de credenciales la
sirve Zitadel, no Musify.
## 3. Explorar / Buscar `/explore` ✅ (app) ## 3. Explorar / Buscar `/explore` ✅ (app)
@@ -101,7 +102,7 @@ Contenido de la página:
- **Pipeline visible**: pasos Subir → Procesar → Publicar con su estado. - **Pipeline visible**: pasos Subir → Procesar → Publicar con su estado.
- **Barra de progreso** de subida y estado de procesado. - **Barra de progreso** de subida y estado de procesado.
- **Estados**: vacío, archivo seleccionado, subiendo, procesando, publicado, error. - **Estados**: vacío, archivo seleccionado, subiendo, procesando, publicado, error.
- **Requiere sesión** (al cablear: redirigir a `/login?returnTo=/upload`). - **Requiere sesión** (al cablear: redirigir a `/auth?returnTo=/upload`).
## 5. Tu biblioteca `/library` ⬜ (app) ## 5. Tu biblioteca `/library` ⬜ (app)
@@ -133,7 +134,7 @@ Listado de las playlists del usuario.
- **Acciones del dueño**: editar (`PUT`), borrar (`DELETE`) la playlist, reordenar (futuro). - **Acciones del dueño**: editar (`PUT`), borrar (`DELETE`) la playlist, reordenar (futuro).
- **Añadir tracks**: desde explorar/biblioteca (`POST /playlists/{id}/tracks/{trackId}`). - **Añadir tracks**: desde explorar/biblioteca (`POST /playlists/{id}/tracks/{trackId}`).
## 8. Perfil de usuario `/users/[id]` ⬜ (futuro) ## 8. Perfil de usuario `/user/[id]` ⬜ (futuro)
Vista pública de un usuario: avatar, nombre, sus canciones y playlists públicas Vista pública de un usuario: avatar, nombre, sus canciones y playlists públicas
(`GET /users/{id}`, `GET /tracks/users/{id}`, `GET /playlists/users/{id}`). (`GET /users/{id}`, `GET /tracks/users/{id}`, `GET /playlists/users/{id}`).
@@ -11,7 +11,7 @@
{#if name} {#if name}
{#if ownerUserId} {#if ownerUserId}
<a <a
href="/u/{ownerUserId}" href="/user/{ownerUserId}"
onclick={(e) => e.stopPropagation()} onclick={(e) => e.stopPropagation()}
class="block max-w-full truncate hover:text-fg-2 hover:underline {klass}" class="block max-w-full truncate hover:text-fg-2 hover:underline {klass}"
> >
@@ -45,7 +45,7 @@
{#if open} {#if open}
<GlassMenu class="absolute top-full right-0 z-(--z-menu) mt-2 overflow-hidden {WIDTH[width]}"> <GlassMenu class="absolute top-full right-0 z-(--z-menu) mt-2 overflow-hidden {WIDTH[width]}">
<MenuItem icon={User} label="Ver perfil" href="/u/{user.sub}" onclick={close} /> <MenuItem icon={User} label="Ver perfil" href="/user/{user.sub}" onclick={close} />
{@render extraItems?.({ close })} {@render extraItems?.({ close })}
{#if accountUrl} {#if accountUrl}
<MenuItem <MenuItem
@@ -61,7 +61,7 @@
label={themeMode.current === 'dark' ? 'Modo blanco' : 'Modo oscuro'} label={themeMode.current === 'dark' ? 'Modo blanco' : 'Modo oscuro'}
onclick={() => themeMode.toggle()} onclick={() => themeMode.toggle()}
/> />
<form method="POST" action="/logout" data-sveltekit-reload> <form method="POST" action="/auth/logout" data-sveltekit-reload>
<MenuItem icon={LogOut} label="Cerrar sesión" type="submit" /> <MenuItem icon={LogOut} label="Cerrar sesión" type="submit" />
</form> </form>
</GlassMenu> </GlassMenu>
+2 -2
View File
@@ -19,7 +19,7 @@ export function createApiClient({ fetch, accessToken }: ApiClientOptions) {
export function requireUser(locals: App.Locals, url: URL): SessionUser { export function requireUser(locals: App.Locals, url: URL): SessionUser {
if (!locals.user) if (!locals.user)
redirect(302, `/login?returnTo=${encodeURIComponent(url.pathname + url.search)}`); redirect(302, `/auth?returnTo=${encodeURIComponent(url.pathname + url.search)}`);
return locals.user; return locals.user;
} }
@@ -30,7 +30,7 @@ export function optionalUser(
): SessionUser | null { ): SessionUser | null {
if (locals.user) return locals.user; if (locals.user) return locals.user;
if (!allowAnonymousListening) if (!allowAnonymousListening)
redirect(302, `/login?returnTo=${encodeURIComponent(url.pathname + url.search)}`); redirect(302, `/auth?returnTo=${encodeURIComponent(url.pathname + url.search)}`);
return null; return null;
} }
@@ -131,7 +131,7 @@
if (!topResult || topResult.kind === 'track') return undefined; if (!topResult || topResult.kind === 'track') return undefined;
if (topResult.kind === 'album') return `/albums/${topResult.album.id}`; if (topResult.kind === 'album') return `/albums/${topResult.album.id}`;
if (topResult.kind === 'playlist') return `/playlists/${topResult.playlist.id}`; if (topResult.kind === 'playlist') return `/playlists/${topResult.playlist.id}`;
return `/u/${topResult.user.id}`; return `/user/${topResult.user.id}`;
}); });
function togglePlay(index: number) { function togglePlay(index: number) {
@@ -400,7 +400,7 @@
<ul class="flex flex-col gap-1"> <ul class="flex flex-col gap-1">
{#each capped(sfilter, users) as u (u.id)} {#each capped(sfilter, users) as u (u.id)}
<li> <li>
<ListRow title={u.name} href="/u/{u.id}" size="lg"> <ListRow title={u.name} href="/user/{u.id}" size="lg">
{#snippet art()} {#snippet art()}
<Avatar name={u.name} src={u.profilePictureUrl} size="md" /> <Avatar name={u.name} src={u.profilePictureUrl} size="md" />
{/snippet} {/snippet}
+2 -2
View File
@@ -5,7 +5,7 @@ import { createApiClient } from '$lib/server/api';
import { authConfig } from '$lib/server/config'; import { authConfig } from '$lib/server/config';
import { LIKED_TRACKS_PAGE_SIZE } from '$lib/config'; import { LIKED_TRACKS_PAGE_SIZE } from '$lib/config';
const PUBLIC_PATHS = new Set(['/login']); const PUBLIC_PATHS = new Set(['/auth']);
const SIDEBAR_PLAYLISTS_LIMIT = 8; const SIDEBAR_PLAYLISTS_LIMIT = 8;
async function fetchSidebarPlaylists( async function fetchSidebarPlaylists(
@@ -43,7 +43,7 @@ export const load: LayoutServerLoad = async ({ locals, url, fetch }) => {
const allowAnonymousListening = await getAllowAnonymousListening(fetch); const allowAnonymousListening = await getAllowAnonymousListening(fetch);
if (!locals.user && !allowAnonymousListening && !PUBLIC_PATHS.has(url.pathname)) { if (!locals.user && !allowAnonymousListening && !PUBLIC_PATHS.has(url.pathname)) {
redirect(302, `/login?returnTo=${encodeURIComponent(url.pathname + url.search)}`); redirect(302, `/auth?returnTo=${encodeURIComponent(url.pathname + url.search)}`);
} }
const accountUrl = authConfig.issuer const accountUrl = authConfig.issuer
+1 -1
View File
@@ -29,7 +29,7 @@
if (data.likedTracks?.length) liked.hydrate(data.likedTracks); if (data.likedTracks?.length) liked.hydrate(data.likedTracks);
}); });
const isAuthPage = $derived(page.url.pathname === '/login'); const isAuthPage = $derived(page.url.pathname === '/auth');
const hasTrack = $derived(player.currentId !== null); const hasTrack = $derived(player.currentId !== null);
const initialThemeCss = `:root{${tokensToCss(buildThemeTokens(ACCENT_HUE, 'dark'))}}:root[data-theme='light']{${tokensToCss(buildThemeTokens(ACCENT_HUE, 'light'))}}`; const initialThemeCss = `:root{${tokensToCss(buildThemeTokens(ACCENT_HUE, 'dark'))}}:root[data-theme='light']{${tokensToCss(buildThemeTokens(ACCENT_HUE, 'light'))}}`;