Design Prot Merge #1
@@ -780,7 +780,7 @@ Tareas:
|
|||||||
| P5 | 🟡 | (pendiente) | Hecho: pressable/clickOutside, toggle.svelte.ts, plural() (+minúsculas, Δ visible), helpers de player.svelte.ts (toQueueItem/isQueueCurrent/playAllOrToggle/playShuffled, addToQueue→playNextItem), navigation.svelte.ts (searchHref/appNavLinks — appNavLinks creado pero su migración a Sidebar/TabsBar se deja para P12 tal como indica esa parte), menus.svelte.ts (createTrackMenu, migrado en Inicio/Explorar/Mezcla). **Pendiente a propósito**: la tarea 5 (eliminar el envoltorio `TrackTarget` de tracks.ts y sus 8 accesores) se difiere — es el cambio de mayor riesgo del plan (toca tipos en Inicio/Explorar/Mezcla/menús de forma transversal) y no se pudo verificar visualmente sin Docker; abordarlo junto con P13 cuando se reescriban esas páginas |
|
| P5 | 🟡 | (pendiente) | Hecho: pressable/clickOutside, toggle.svelte.ts, plural() (+minúsculas, Δ visible), helpers de player.svelte.ts (toQueueItem/isQueueCurrent/playAllOrToggle/playShuffled, addToQueue→playNextItem), navigation.svelte.ts (searchHref/appNavLinks — appNavLinks creado pero su migración a Sidebar/TabsBar se deja para P12 tal como indica esa parte), menus.svelte.ts (createTrackMenu, migrado en Inicio/Explorar/Mezcla). **Pendiente a propósito**: la tarea 5 (eliminar el envoltorio `TrackTarget` de tracks.ts y sus 8 accesores) se difiere — es el cambio de mayor riesgo del plan (toca tipos en Inicio/Explorar/Mezcla/menús de forma transversal) y no se pudo verificar visualmente sin Docker; abordarlo junto con P13 cuando se reescriban esas páginas |
|
||||||
| P6 | ✅ | (pendiente) | z-index de AccountMenu unificado a z-(--z-menu) (antes z-20, ahora al mismo nivel que ContextMenu — Δ menor, mejora consistencia); sin QA visual autenticada (Docker no disponible) |
|
| P6 | ✅ | (pendiente) | z-index de AccountMenu unificado a z-(--z-menu) (antes z-20, ahora al mismo nivel que ContextMenu — Δ menor, mejora consistencia); sin QA visual autenticada (Docker no disponible) |
|
||||||
| P7 | ✅ | (pendiente) | MediaIdentity queda "puro" (sin onClick) según spec; las 6 tablas de pista (P8) envuelven su propio `use:pressable` hasta que P8 las sustituya por TrackList. Deltas documentados: fila activa de Sidebar ahora usa bg-accent-tint/text-accent-soft (antes gris neutro, ahora consistente con el resto de filas activas); tarjeta "Tus playlists" de Inicio pierde la línea "por {tu nombre}" (redundante, siempre el propio usuario) y su radio pasa de rounded-panel a rounded-art (consistencia con la tarjeta "Continuar"); filas de Queue "A continuación" pierden el opacity-80 del arte (ListRow no expone esa personalización). Sin QA visual autenticada (Docker no disponible) |
|
| P7 | ✅ | (pendiente) | MediaIdentity queda "puro" (sin onClick) según spec; las 6 tablas de pista (P8) envuelven su propio `use:pressable` hasta que P8 las sustituya por TrackList. Deltas documentados: fila activa de Sidebar ahora usa bg-accent-tint/text-accent-soft (antes gris neutro, ahora consistente con el resto de filas activas); tarjeta "Tus playlists" de Inicio pierde la línea "por {tu nombre}" (redundante, siempre el propio usuario) y su radio pasa de rounded-panel a rounded-art (consistencia con la tarjeta "Continuar"); filas de Queue "A continuación" pierden el opacity-80 del arte (ListRow no expone esa personalización). Sin QA visual autenticada (Docker no disponible) |
|
||||||
| P8 | ⬜ | | |
|
| P8 | ✅ | (pendiente) | Decisión tomada (recomendada por el propio plan, sin bloquear): se elimina la columna "Álbum" que siempre mostraba "—". TrackList unifica TrackTable+TrackRow+TrackIndexCell+TrackMeta; AlbumTrackTable/PlaylistTrackTable/LibraryPicker se inlinean en sus páginas y se borran. Sin QA visual autenticada (Docker no disponible) |
|
||||||
| P9 | ⬜ | | |
|
| P9 | ⬜ | | |
|
||||||
| P10 | ⬜ | | |
|
| P10 | ⬜ | | |
|
||||||
| P11 | ⬜ | | |
|
| P11 | ⬜ | | |
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import Play from '@lucide/svelte/icons/play';
|
|
||||||
import EqBars from './EqBars.svelte';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
index: number;
|
|
||||||
active: boolean;
|
|
||||||
playing: boolean;
|
|
||||||
onToggle: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
let { index, active, playing, onToggle }: Props = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onclick={onToggle}
|
|
||||||
aria-label={active && playing ? 'Pausar' : 'Reproducir'}
|
|
||||||
class="relative grid h-8 w-8 place-items-center rounded text-xs text-muted tabular-nums"
|
|
||||||
>
|
|
||||||
{#if active}
|
|
||||||
<EqBars paused={!playing} />
|
|
||||||
{:else}
|
|
||||||
<span class="group-hover:hidden">{index + 1}</span>
|
|
||||||
<Play class="hidden h-3.5 w-3.5 text-fg group-hover:block" fill="currentColor" />
|
|
||||||
{/if}
|
|
||||||
</button>
|
|
||||||
@@ -0,0 +1,200 @@
|
|||||||
|
<script lang="ts" module>
|
||||||
|
export type TrackListColumn = 'added' | 'uploaded' | 'plays';
|
||||||
|
|
||||||
|
export interface TrackListTrack {
|
||||||
|
id: string | number;
|
||||||
|
title: string;
|
||||||
|
artist?: string | null;
|
||||||
|
ownerUserId?: string | number | null;
|
||||||
|
isExplicit?: boolean;
|
||||||
|
explicit?: boolean;
|
||||||
|
duration?: number | string;
|
||||||
|
listensCount?: number | string;
|
||||||
|
addedAt?: string | number;
|
||||||
|
uploadedAt?: string | number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const COLUMN_PRESET: Record<TrackListColumn, { label: string; width: string }> = {
|
||||||
|
added: { label: 'Añadida', width: '124px' },
|
||||||
|
uploaded: { label: 'Subida', width: '124px' },
|
||||||
|
plays: { label: 'Escuchas', width: '104px' }
|
||||||
|
};
|
||||||
|
|
||||||
|
const INDEX_W = { desktop: '32px', mobile: '28px' };
|
||||||
|
const TITLE_W = { desktop: 'minmax(180px,1fr)', mobile: 'minmax(120px,1fr)' };
|
||||||
|
const DURATION_W = { desktop: '104px', mobile: '52px' };
|
||||||
|
const ACTION_W = { desktop: '36px', mobile: '32px' };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { enhance } from '$app/forms';
|
||||||
|
import type { LucideIcon } from '@lucide/svelte';
|
||||||
|
import Hash from '@lucide/svelte/icons/hash';
|
||||||
|
import Play from '@lucide/svelte/icons/play';
|
||||||
|
import { player } from '$lib/player/player.svelte';
|
||||||
|
import { fmtTime, fmtDate } from '$lib/format';
|
||||||
|
import { pressable } from '$lib/actions/pressable';
|
||||||
|
import MediaIdentity from './MediaIdentity.svelte';
|
||||||
|
import EqBars from './EqBars.svelte';
|
||||||
|
import IconButton from './IconButton.svelte';
|
||||||
|
|
||||||
|
interface RowAction {
|
||||||
|
action?: string;
|
||||||
|
onclick?: (track: TrackListTrack) => void;
|
||||||
|
icon: LucideIcon;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
tracks: TrackListTrack[];
|
||||||
|
columns?: TrackListColumn[];
|
||||||
|
index?: boolean;
|
||||||
|
onPlay: (index: number) => void;
|
||||||
|
rowAction?: RowAction;
|
||||||
|
oncontextmenu?: (event: MouseEvent, track: TrackListTrack, index: number) => void;
|
||||||
|
class?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
let {
|
||||||
|
tracks,
|
||||||
|
columns = [],
|
||||||
|
index = true,
|
||||||
|
onPlay,
|
||||||
|
rowAction,
|
||||||
|
oncontextmenu,
|
||||||
|
class: klass = ''
|
||||||
|
}: Props = $props();
|
||||||
|
|
||||||
|
const gridColumns = $derived(
|
||||||
|
[
|
||||||
|
index ? INDEX_W.desktop : '',
|
||||||
|
TITLE_W.desktop,
|
||||||
|
...columns.map((c) => COLUMN_PRESET[c].width),
|
||||||
|
DURATION_W.desktop,
|
||||||
|
rowAction ? ACTION_W.desktop : ''
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' ')
|
||||||
|
);
|
||||||
|
|
||||||
|
const gridColumnsMobile = $derived(
|
||||||
|
[
|
||||||
|
index ? INDEX_W.mobile : '',
|
||||||
|
TITLE_W.mobile,
|
||||||
|
DURATION_W.mobile,
|
||||||
|
rowAction ? ACTION_W.mobile : ''
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' ')
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="overflow-x-auto {klass}">
|
||||||
|
<div
|
||||||
|
class="grid track-grid items-center gap-3 border-b border-line pr-4 pb-3 pl-2 text-sm text-fg-3 sm:gap-8 sm:pr-6 sm:pl-3"
|
||||||
|
style="--mf-track-cols:{gridColumns}; --mf-track-cols-mobile:{gridColumnsMobile}"
|
||||||
|
>
|
||||||
|
{#if index}
|
||||||
|
<Hash size={16} strokeWidth={1.5} />
|
||||||
|
{/if}
|
||||||
|
<span>Título</span>
|
||||||
|
{#each columns as column (column)}
|
||||||
|
<span class="hidden text-center sm:block">{COLUMN_PRESET[column].label}</span>
|
||||||
|
{/each}
|
||||||
|
<span class="hidden text-center sm:block">Duración</span>
|
||||||
|
{#if rowAction}
|
||||||
|
<span></span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<div class="mt-1 flex flex-col gap-0.5">
|
||||||
|
{#each tracks as track, i (track.id)}
|
||||||
|
{@const active = player.current.id === track.id}
|
||||||
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
|
<div
|
||||||
|
class="group grid track-grid items-center gap-3 rounded-control py-3 pr-4 pl-2 transition-colors sm:gap-8 sm:pr-6 sm:pl-3 {active
|
||||||
|
? 'bg-accent-tint'
|
||||||
|
: 'hover:bg-hover'}"
|
||||||
|
style="--mf-track-cols:{gridColumns}; --mf-track-cols-mobile:{gridColumnsMobile}"
|
||||||
|
oncontextmenu={oncontextmenu ? (event) => oncontextmenu(event, track, i) : undefined}
|
||||||
|
>
|
||||||
|
{#if index}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick={() => onPlay(i)}
|
||||||
|
aria-label={active && player.playing ? 'Pausar' : 'Reproducir'}
|
||||||
|
class="relative grid size-8 place-items-center rounded-tag text-xs text-muted tabular-nums"
|
||||||
|
>
|
||||||
|
{#if active}
|
||||||
|
<EqBars paused={!player.playing} />
|
||||||
|
{:else}
|
||||||
|
<span class="group-hover:hidden">{i + 1}</span>
|
||||||
|
<Play class="hidden size-icon-sm text-fg group-hover:block" fill="currentColor" />
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||||
|
<div
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
use:pressable={() => onPlay(i)}
|
||||||
|
onclick={(event) => event.stopPropagation()}
|
||||||
|
class="flex min-w-0 flex-1 text-left"
|
||||||
|
>
|
||||||
|
<MediaIdentity
|
||||||
|
trackId={track.id}
|
||||||
|
title={track.title}
|
||||||
|
artist={track.artist}
|
||||||
|
ownerUserId={track.ownerUserId}
|
||||||
|
explicit={track.isExplicit ?? track.explicit ?? false}
|
||||||
|
{active}
|
||||||
|
>
|
||||||
|
{#snippet overlay()}
|
||||||
|
{#if !index && active}
|
||||||
|
<EqBars overlay paused={!player.playing} />
|
||||||
|
{/if}
|
||||||
|
{/snippet}
|
||||||
|
</MediaIdentity>
|
||||||
|
</div>
|
||||||
|
{#each columns as column (column)}
|
||||||
|
<span class="hidden truncate text-center text-xs text-muted tabular-nums sm:block">
|
||||||
|
{#if column === 'plays'}
|
||||||
|
{Number(track.listensCount ?? 0)}
|
||||||
|
{:else if column === 'added' && track.addedAt}
|
||||||
|
{fmtDate(track.addedAt)}
|
||||||
|
{:else if column === 'uploaded' && track.uploadedAt}
|
||||||
|
{fmtDate(track.uploadedAt)}
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
{/each}
|
||||||
|
<span class="truncate text-center text-xs text-muted tabular-nums">
|
||||||
|
{fmtTime(Number(track.duration))}
|
||||||
|
</span>
|
||||||
|
{#if rowAction}
|
||||||
|
{#if rowAction.action}
|
||||||
|
<form
|
||||||
|
method="POST"
|
||||||
|
action={rowAction.action}
|
||||||
|
use:enhance={() =>
|
||||||
|
async ({ update }) =>
|
||||||
|
update()}
|
||||||
|
>
|
||||||
|
<input type="hidden" name="trackId" value={track.id} />
|
||||||
|
<IconButton type="submit" label={rowAction.label} size="xs" revealOnHover>
|
||||||
|
<rowAction.icon class="size-icon-sm" />
|
||||||
|
</IconButton>
|
||||||
|
</form>
|
||||||
|
{:else}
|
||||||
|
<IconButton
|
||||||
|
label={rowAction.label}
|
||||||
|
size="xs"
|
||||||
|
revealOnHover
|
||||||
|
onclick={() => rowAction.onclick?.(track)}
|
||||||
|
>
|
||||||
|
<rowAction.icon class="size-icon-sm" />
|
||||||
|
</IconButton>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import type { Snippet } from 'svelte';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
class?: string;
|
|
||||||
children: Snippet;
|
|
||||||
}
|
|
||||||
|
|
||||||
let { class: klass = '', children }: Props = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<span class="truncate text-center text-xs text-muted tabular-nums {klass}">
|
|
||||||
{@render children()}
|
|
||||||
</span>
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { getContext } from 'svelte';
|
|
||||||
import type { Snippet } from 'svelte';
|
|
||||||
import { trackTableKey, type TrackTableContext } from './TrackTable.svelte';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
active?: boolean;
|
|
||||||
class?: string;
|
|
||||||
oncontextmenu?: (event: MouseEvent) => void;
|
|
||||||
children: Snippet;
|
|
||||||
}
|
|
||||||
|
|
||||||
let { active = false, class: klass = '', oncontextmenu, children }: Props = $props();
|
|
||||||
|
|
||||||
const table = getContext<TrackTableContext>(trackTableKey);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
||||||
<div
|
|
||||||
class="group grid track-grid items-center gap-3 rounded-control py-3 pr-4 pl-2 transition-colors sm:gap-8 sm:pr-6 sm:pl-3 {active
|
|
||||||
? 'bg-accent-tint'
|
|
||||||
: 'hover:bg-hover'} {klass}"
|
|
||||||
style="--mf-track-cols:{table.columns}; --mf-track-cols-mobile:{table.columnsMobile}"
|
|
||||||
{oncontextmenu}
|
|
||||||
>
|
|
||||||
{@render children()}
|
|
||||||
</div>
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
<script lang="ts" module>
|
|
||||||
export const trackTableKey = Symbol('track-table');
|
|
||||||
|
|
||||||
export interface TrackTableContext {
|
|
||||||
readonly columns: string;
|
|
||||||
readonly columnsMobile: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TrackMetaColumn {
|
|
||||||
label: string;
|
|
||||||
width: string;
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script lang="ts">
|
|
||||||
import { Hash } from '@lucide/svelte';
|
|
||||||
|
|
||||||
import { setContext } from 'svelte';
|
|
||||||
import type { Snippet } from 'svelte';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
index?: boolean;
|
|
||||||
action?: boolean;
|
|
||||||
meta?: TrackMetaColumn[];
|
|
||||||
class?: string;
|
|
||||||
children: Snippet;
|
|
||||||
}
|
|
||||||
|
|
||||||
let { index = false, action = false, meta = [], class: klass = '', children }: Props = $props();
|
|
||||||
|
|
||||||
const columns = $derived(
|
|
||||||
[
|
|
||||||
index ? '32px' : '',
|
|
||||||
'minmax(180px,1fr)',
|
|
||||||
...meta.map((column) => column.width),
|
|
||||||
'104px',
|
|
||||||
action ? '36px' : ''
|
|
||||||
]
|
|
||||||
.filter((width) => width !== '')
|
|
||||||
.join(' ')
|
|
||||||
);
|
|
||||||
|
|
||||||
const columnsMobile = $derived(
|
|
||||||
[index ? '28px' : '', 'minmax(120px,1fr)', '52px', action ? '32px' : '']
|
|
||||||
.filter((width) => width !== '')
|
|
||||||
.join(' ')
|
|
||||||
);
|
|
||||||
|
|
||||||
setContext<TrackTableContext>(trackTableKey, {
|
|
||||||
get columns() {
|
|
||||||
return columns;
|
|
||||||
},
|
|
||||||
get columnsMobile() {
|
|
||||||
return columnsMobile;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="overflow-x-auto {klass}">
|
|
||||||
<div
|
|
||||||
class="grid track-grid items-center gap-3 border-b border-line pr-4 pb-3 pl-2 text-sm text-fg-3 sm:gap-8 sm:pr-6 sm:pl-3"
|
|
||||||
style="--mf-track-cols:{columns}; --mf-track-cols-mobile:{columnsMobile}"
|
|
||||||
>
|
|
||||||
{#if index}
|
|
||||||
<Hash size={16} strokeWidth={1.5} />
|
|
||||||
{/if}
|
|
||||||
<span class="transition-colors">Título</span>
|
|
||||||
{#each meta as column (column.label)}
|
|
||||||
<span class="hidden text-center transition-colors sm:block">{column.label}</span>
|
|
||||||
{/each}
|
|
||||||
<span class="hidden text-center transition-colors sm:block">Duración</span>
|
|
||||||
{#if action}
|
|
||||||
<span></span>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
<div class="mt-1 flex flex-col gap-0.5">
|
|
||||||
{@render children()}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,13 +1,15 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { player, toQueueItems, isQueueCurrent, playAllOrToggle } from '$lib/player/player.svelte';
|
import { player, toQueueItems, isQueueCurrent, playAllOrToggle } from '$lib/player/player.svelte';
|
||||||
|
import X from '@lucide/svelte/icons/x';
|
||||||
|
import Plus from '@lucide/svelte/icons/plus';
|
||||||
import Page from '$lib/components/ui/Page.svelte';
|
import Page from '$lib/components/ui/Page.svelte';
|
||||||
import Modal from '$lib/components/ui/Modal.svelte';
|
import Modal from '$lib/components/ui/Modal.svelte';
|
||||||
import Alert from '$lib/components/ui/Alert.svelte';
|
import Alert from '$lib/components/ui/Alert.svelte';
|
||||||
import AlbumForm from '$lib/components/ui/AlbumForm.svelte';
|
import AlbumForm from '$lib/components/ui/AlbumForm.svelte';
|
||||||
import Button from '$lib/components/ui/Button.svelte';
|
import Button from '$lib/components/ui/Button.svelte';
|
||||||
|
import SectionHeading from '$lib/components/ui/SectionHeading.svelte';
|
||||||
|
import TrackList from '$lib/components/ui/TrackList.svelte';
|
||||||
import AlbumHeader from './components/AlbumHeader.svelte';
|
import AlbumHeader from './components/AlbumHeader.svelte';
|
||||||
import AlbumTrackTable from './components/AlbumTrackTable.svelte';
|
|
||||||
import LibraryPicker from './components/LibraryPicker.svelte';
|
|
||||||
|
|
||||||
let { data, form } = $props();
|
let { data, form } = $props();
|
||||||
|
|
||||||
@@ -24,6 +26,14 @@
|
|||||||
function playAll() {
|
function playAll() {
|
||||||
playAllOrToggle(toQueueItems(tracks));
|
playAllOrToggle(toQueueItems(tracks));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function playTrackFrom(index: number) {
|
||||||
|
player.playOrToggle(toQueueItems(tracks), index);
|
||||||
|
}
|
||||||
|
|
||||||
|
function playLibraryFrom(index: number) {
|
||||||
|
player.playOrToggle(toQueueItems(library), index);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
@@ -50,11 +60,26 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if tracks.length > 0}
|
{#if tracks.length > 0}
|
||||||
<AlbumTrackTable {tracks} {isOwner} />
|
<TrackList
|
||||||
|
{tracks}
|
||||||
|
onPlay={playTrackFrom}
|
||||||
|
rowAction={isOwner
|
||||||
|
? { action: '?/removeTrack', icon: X, label: 'Quitar del álbum' }
|
||||||
|
: undefined}
|
||||||
|
class="mt-6 sm:mt-8"
|
||||||
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if isOwner && library.length > 0}
|
{#if isOwner && library.length > 0}
|
||||||
<LibraryPicker {library} />
|
<div class="mt-10 sm:mt-12">
|
||||||
|
<SectionHeading title="Tus canciones subidas" />
|
||||||
|
<TrackList
|
||||||
|
tracks={library}
|
||||||
|
index={false}
|
||||||
|
onPlay={playLibraryFrom}
|
||||||
|
rowAction={{ action: '?/addTrack', icon: Plus, label: 'Añadir al álbum' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</Page>
|
</Page>
|
||||||
|
|
||||||
|
|||||||
@@ -1,73 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { enhance } from '$app/forms';
|
|
||||||
import X from '@lucide/svelte/icons/x';
|
|
||||||
import { player, toQueueItems, type ApiTrackLike } from '$lib/player/player.svelte';
|
|
||||||
import { fmtTime } from '$lib/format';
|
|
||||||
import IconButton from '$lib/components/ui/IconButton.svelte';
|
|
||||||
import TrackTable from '$lib/components/ui/TrackTable.svelte';
|
|
||||||
import TrackRow from '$lib/components/ui/TrackRow.svelte';
|
|
||||||
import TrackMeta from '$lib/components/ui/TrackMeta.svelte';
|
|
||||||
import TrackIndexCell from '$lib/components/ui/TrackIndexCell.svelte';
|
|
||||||
import MediaIdentity from '$lib/components/ui/MediaIdentity.svelte';
|
|
||||||
import { pressable } from '$lib/actions/pressable';
|
|
||||||
|
|
||||||
type AlbumTrack = ApiTrackLike & { duration: number | string; listensCount: number | string };
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
tracks: AlbumTrack[];
|
|
||||||
isOwner: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
let { tracks, isOwner }: Props = $props();
|
|
||||||
|
|
||||||
function playFrom(index: number) {
|
|
||||||
player.playOrToggle(toQueueItems(tracks), index);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<TrackTable
|
|
||||||
index
|
|
||||||
action={isOwner}
|
|
||||||
meta={[{ label: 'Escuchas', width: '100px' }]}
|
|
||||||
class="mt-6 sm:mt-8"
|
|
||||||
>
|
|
||||||
{#each tracks as track, i (track.id)}
|
|
||||||
{@const active = player.current.id === track.id}
|
|
||||||
<TrackRow {active}>
|
|
||||||
<TrackIndexCell index={i} {active} playing={player.playing} onToggle={() => playFrom(i)} />
|
|
||||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
||||||
<div
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
use:pressable={() => playFrom(i)}
|
|
||||||
onclick={(event) => event.stopPropagation()}
|
|
||||||
class="flex min-w-0 flex-1 text-left"
|
|
||||||
>
|
|
||||||
<MediaIdentity
|
|
||||||
trackId={track.id}
|
|
||||||
title={track.title}
|
|
||||||
artist={track.artist}
|
|
||||||
ownerUserId={track.ownerUserId}
|
|
||||||
explicit={track.isExplicit}
|
|
||||||
{active}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<TrackMeta class="hidden sm:block">{Number(track.listensCount)}</TrackMeta>
|
|
||||||
<TrackMeta>{fmtTime(Number(track.duration))}</TrackMeta>
|
|
||||||
{#if isOwner}
|
|
||||||
<form
|
|
||||||
method="POST"
|
|
||||||
action="?/removeTrack"
|
|
||||||
use:enhance={() =>
|
|
||||||
async ({ update }) =>
|
|
||||||
update()}
|
|
||||||
>
|
|
||||||
<input type="hidden" name="trackId" value={track.id} />
|
|
||||||
<IconButton type="submit" label="Quitar del álbum" size="xs" revealOnHover>
|
|
||||||
<X class="h-3.5 w-3.5" />
|
|
||||||
</IconButton>
|
|
||||||
</form>
|
|
||||||
{/if}
|
|
||||||
</TrackRow>
|
|
||||||
{/each}
|
|
||||||
</TrackTable>
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { enhance } from '$app/forms';
|
|
||||||
import Plus from '@lucide/svelte/icons/plus';
|
|
||||||
import { player, toQueueItems, type ApiTrackLike } from '$lib/player/player.svelte';
|
|
||||||
import { fmtTime } from '$lib/format';
|
|
||||||
import SectionHeading from '$lib/components/ui/SectionHeading.svelte';
|
|
||||||
import IconButton from '$lib/components/ui/IconButton.svelte';
|
|
||||||
import TrackTable from '$lib/components/ui/TrackTable.svelte';
|
|
||||||
import TrackRow from '$lib/components/ui/TrackRow.svelte';
|
|
||||||
import TrackMeta from '$lib/components/ui/TrackMeta.svelte';
|
|
||||||
import MediaIdentity from '$lib/components/ui/MediaIdentity.svelte';
|
|
||||||
import { pressable } from '$lib/actions/pressable';
|
|
||||||
import EqBars from '$lib/components/ui/EqBars.svelte';
|
|
||||||
|
|
||||||
type LibraryTrack = ApiTrackLike & { duration: number | string; listensCount: number | string };
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
library: LibraryTrack[];
|
|
||||||
}
|
|
||||||
|
|
||||||
let { library }: Props = $props();
|
|
||||||
|
|
||||||
function playFrom(index: number) {
|
|
||||||
player.playOrToggle(toQueueItems(library), index);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="mt-10 sm:mt-12">
|
|
||||||
<SectionHeading title="Tus canciones subidas" />
|
|
||||||
<TrackTable action meta={[{ label: 'Escuchas', width: '100px' }]}>
|
|
||||||
{#each library as track, i (track.id)}
|
|
||||||
{@const active = player.current.id === track.id}
|
|
||||||
<TrackRow {active}>
|
|
||||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
||||||
<div
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
use:pressable={() => playFrom(i)}
|
|
||||||
onclick={(event) => event.stopPropagation()}
|
|
||||||
class="flex min-w-0 flex-1 text-left"
|
|
||||||
>
|
|
||||||
<MediaIdentity
|
|
||||||
trackId={track.id}
|
|
||||||
title={track.title}
|
|
||||||
artist={track.artist}
|
|
||||||
ownerUserId={track.ownerUserId}
|
|
||||||
explicit={track.isExplicit}
|
|
||||||
{active}
|
|
||||||
>
|
|
||||||
{#snippet overlay()}
|
|
||||||
{#if player.current.id === track.id}
|
|
||||||
<EqBars overlay paused={!player.playing} />
|
|
||||||
{/if}
|
|
||||||
{/snippet}
|
|
||||||
</MediaIdentity>
|
|
||||||
</div>
|
|
||||||
<TrackMeta class="hidden sm:block">{Number(track.listensCount)}</TrackMeta>
|
|
||||||
<TrackMeta>{fmtTime(Number(track.duration))}</TrackMeta>
|
|
||||||
<form
|
|
||||||
method="POST"
|
|
||||||
action="?/addTrack"
|
|
||||||
use:enhance={() =>
|
|
||||||
async ({ update }) =>
|
|
||||||
update()}
|
|
||||||
>
|
|
||||||
<input type="hidden" name="trackId" value={track.id} />
|
|
||||||
<IconButton type="submit" label="Añadir al álbum" size="xs" revealOnHover>
|
|
||||||
<Plus class="h-3.5 w-3.5" />
|
|
||||||
</IconButton>
|
|
||||||
</form>
|
|
||||||
</TrackRow>
|
|
||||||
{/each}
|
|
||||||
</TrackTable>
|
|
||||||
</div>
|
|
||||||
@@ -1,26 +1,20 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { enhance } from '$app/forms';
|
|
||||||
import Upload from '@lucide/svelte/icons/upload';
|
import Upload from '@lucide/svelte/icons/upload';
|
||||||
import X from '@lucide/svelte/icons/x';
|
import X from '@lucide/svelte/icons/x';
|
||||||
import Music from '@lucide/svelte/icons/music';
|
import Music from '@lucide/svelte/icons/music';
|
||||||
import { player, toQueueItems } from '$lib/player/player.svelte';
|
import { player, toQueueItems } from '$lib/player/player.svelte';
|
||||||
import { fmtTime, fmtDate, plural } from '$lib/format';
|
import { plural } from '$lib/format';
|
||||||
import Page from '$lib/components/ui/Page.svelte';
|
import Page from '$lib/components/ui/Page.svelte';
|
||||||
import PageHeader from '$lib/components/ui/PageHeader.svelte';
|
import PageHeader from '$lib/components/ui/PageHeader.svelte';
|
||||||
import EmptyState from '$lib/components/ui/EmptyState.svelte';
|
import EmptyState from '$lib/components/ui/EmptyState.svelte';
|
||||||
import Button from '$lib/components/ui/Button.svelte';
|
import Button from '$lib/components/ui/Button.svelte';
|
||||||
import IconButton from '$lib/components/ui/IconButton.svelte';
|
import TrackList from '$lib/components/ui/TrackList.svelte';
|
||||||
import TrackTable from '$lib/components/ui/TrackTable.svelte';
|
|
||||||
import TrackRow from '$lib/components/ui/TrackRow.svelte';
|
|
||||||
import TrackMeta from '$lib/components/ui/TrackMeta.svelte';
|
|
||||||
import TrackIndexCell from '$lib/components/ui/TrackIndexCell.svelte';
|
|
||||||
import MediaIdentity from '$lib/components/ui/MediaIdentity.svelte';
|
|
||||||
import { pressable } from '$lib/actions/pressable';
|
|
||||||
|
|
||||||
let { data } = $props();
|
let { data } = $props();
|
||||||
|
|
||||||
const items = $derived(data.tracks.items);
|
const items = $derived(data.tracks.items);
|
||||||
const total = $derived(Number(data.tracks.totalItemCount));
|
const total = $derived(Number(data.tracks.totalItemCount));
|
||||||
|
const listTracks = $derived(items.map((track) => ({ ...track, uploadedAt: track.createdAt })));
|
||||||
|
|
||||||
function togglePlay(index: number) {
|
function togglePlay(index: number) {
|
||||||
player.playOrToggle(toQueueItems(items), index);
|
player.playOrToggle(toQueueItems(items), index);
|
||||||
@@ -48,54 +42,13 @@
|
|||||||
</PageHeader>
|
</PageHeader>
|
||||||
|
|
||||||
{#if items.length > 0}
|
{#if items.length > 0}
|
||||||
<TrackTable
|
<TrackList
|
||||||
index
|
tracks={listTracks}
|
||||||
action
|
columns={['uploaded', 'plays']}
|
||||||
meta={[
|
onPlay={togglePlay}
|
||||||
{ label: 'Álbum', width: '76px' },
|
rowAction={{ action: '?/deleteTrack', icon: X, label: 'Borrar canción' }}
|
||||||
{ label: 'Subida', width: '124px' },
|
|
||||||
{ label: 'Escuchas', width: '106px' }
|
|
||||||
]}
|
|
||||||
class="mt-7 sm:mt-8"
|
class="mt-7 sm:mt-8"
|
||||||
>
|
/>
|
||||||
{#each items as track, i (track.id)}
|
|
||||||
{@const active = player.current.id === track.id}
|
|
||||||
<TrackRow {active}>
|
|
||||||
<TrackIndexCell
|
|
||||||
index={i}
|
|
||||||
{active}
|
|
||||||
playing={player.playing}
|
|
||||||
onToggle={() => togglePlay(i)}
|
|
||||||
/>
|
|
||||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
||||||
<div
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
use:pressable={() => togglePlay(i)}
|
|
||||||
onclick={(event) => event.stopPropagation()}
|
|
||||||
class="flex min-w-0 flex-1 text-left"
|
|
||||||
>
|
|
||||||
<MediaIdentity trackId={track.id} title={track.title} {active} />
|
|
||||||
</div>
|
|
||||||
<TrackMeta class="hidden sm:block">—</TrackMeta>
|
|
||||||
<TrackMeta class="hidden sm:block">{fmtDate(track.createdAt)}</TrackMeta>
|
|
||||||
<TrackMeta class="hidden sm:block">{Number(track.listensCount)}</TrackMeta>
|
|
||||||
<TrackMeta>{fmtTime(Number(track.duration))}</TrackMeta>
|
|
||||||
<form
|
|
||||||
method="POST"
|
|
||||||
action="?/deleteTrack"
|
|
||||||
use:enhance={() =>
|
|
||||||
async ({ update }) =>
|
|
||||||
update()}
|
|
||||||
>
|
|
||||||
<input type="hidden" name="trackId" value={track.id} />
|
|
||||||
<IconButton type="submit" label="Borrar canción" size="xs" revealOnHover>
|
|
||||||
<X class="h-3.5 w-3.5" />
|
|
||||||
</IconButton>
|
|
||||||
</form>
|
|
||||||
</TrackRow>
|
|
||||||
{/each}
|
|
||||||
</TrackTable>
|
|
||||||
{:else}
|
{:else}
|
||||||
<EmptyState
|
<EmptyState
|
||||||
icon={Music}
|
icon={Music}
|
||||||
|
|||||||
@@ -3,20 +3,15 @@
|
|||||||
import X from '@lucide/svelte/icons/x';
|
import X from '@lucide/svelte/icons/x';
|
||||||
import { player, isQueueCurrent, playAllOrToggle } from '$lib/player/player.svelte';
|
import { player, isQueueCurrent, playAllOrToggle } from '$lib/player/player.svelte';
|
||||||
import { liked } from '$lib/player/liked.svelte';
|
import { liked } from '$lib/player/liked.svelte';
|
||||||
import { fmtDate, fmtTime, plural } from '$lib/format';
|
import { plural } from '$lib/format';
|
||||||
import Page from '$lib/components/ui/Page.svelte';
|
import Page from '$lib/components/ui/Page.svelte';
|
||||||
import CollectionHeader from '$lib/components/ui/CollectionHeader.svelte';
|
import CollectionHeader from '$lib/components/ui/CollectionHeader.svelte';
|
||||||
import Button from '$lib/components/ui/Button.svelte';
|
import Button from '$lib/components/ui/Button.svelte';
|
||||||
import IconButton from '$lib/components/ui/IconButton.svelte';
|
|
||||||
import EmptyState from '$lib/components/ui/EmptyState.svelte';
|
import EmptyState from '$lib/components/ui/EmptyState.svelte';
|
||||||
import TrackTable from '$lib/components/ui/TrackTable.svelte';
|
import TrackList from '$lib/components/ui/TrackList.svelte';
|
||||||
import TrackRow from '$lib/components/ui/TrackRow.svelte';
|
|
||||||
import TrackMeta from '$lib/components/ui/TrackMeta.svelte';
|
|
||||||
import TrackIndexCell from '$lib/components/ui/TrackIndexCell.svelte';
|
|
||||||
import MediaIdentity from '$lib/components/ui/MediaIdentity.svelte';
|
|
||||||
import { pressable } from '$lib/actions/pressable';
|
|
||||||
|
|
||||||
const tracks = $derived(liked.list);
|
const tracks = $derived(liked.list);
|
||||||
|
const listTracks = $derived(tracks.map((track) => ({ ...track, addedAt: track.likedAt })));
|
||||||
const isCurrentQueue = $derived(isQueueCurrent(tracks));
|
const isCurrentQueue = $derived(isQueueCurrent(tracks));
|
||||||
|
|
||||||
function playFrom(index: number) {
|
function playFrom(index: number) {
|
||||||
@@ -55,57 +50,17 @@
|
|||||||
</CollectionHeader>
|
</CollectionHeader>
|
||||||
|
|
||||||
{#if tracks.length > 0}
|
{#if tracks.length > 0}
|
||||||
<TrackTable
|
<TrackList
|
||||||
index
|
tracks={listTracks}
|
||||||
action
|
columns={['added', 'plays']}
|
||||||
meta={[
|
onPlay={playFrom}
|
||||||
{ label: 'Álbum', width: '76px' },
|
rowAction={{
|
||||||
{ label: 'Añadida', width: '124px' },
|
onclick: (track) => liked.toggle({ ...track, artist: track.artist ?? undefined }),
|
||||||
{ label: 'Escuchas', width: '106px' }
|
icon: X,
|
||||||
]}
|
label: 'Quitar de Me gusta'
|
||||||
|
}}
|
||||||
class="mt-6 sm:mt-8"
|
class="mt-6 sm:mt-8"
|
||||||
>
|
/>
|
||||||
{#each tracks as track, i (track.id)}
|
|
||||||
{@const active = player.current.id === track.id}
|
|
||||||
<TrackRow {active}>
|
|
||||||
<TrackIndexCell
|
|
||||||
index={i}
|
|
||||||
{active}
|
|
||||||
playing={player.playing}
|
|
||||||
onToggle={() => playFrom(i)}
|
|
||||||
/>
|
|
||||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
||||||
<div
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
use:pressable={() => playFrom(i)}
|
|
||||||
onclick={(event) => event.stopPropagation()}
|
|
||||||
class="flex min-w-0 flex-1 text-left"
|
|
||||||
>
|
|
||||||
<MediaIdentity
|
|
||||||
trackId={track.id}
|
|
||||||
title={track.title}
|
|
||||||
artist={track.artist}
|
|
||||||
ownerUserId={track.ownerUserId}
|
|
||||||
explicit={track.explicit}
|
|
||||||
{active}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<TrackMeta class="hidden sm:block">—</TrackMeta>
|
|
||||||
<TrackMeta class="hidden sm:block">{fmtDate(track.likedAt)}</TrackMeta>
|
|
||||||
<TrackMeta class="hidden sm:block">{Number(track.listensCount ?? 0)}</TrackMeta>
|
|
||||||
<TrackMeta>{fmtTime(Number(track.duration))}</TrackMeta>
|
|
||||||
<IconButton
|
|
||||||
label="Quitar de Me gusta"
|
|
||||||
size="xs"
|
|
||||||
revealOnHover
|
|
||||||
onclick={() => liked.toggle(track)}
|
|
||||||
>
|
|
||||||
<X class="h-3.5 w-3.5" />
|
|
||||||
</IconButton>
|
|
||||||
</TrackRow>
|
|
||||||
{/each}
|
|
||||||
</TrackTable>
|
|
||||||
{:else}
|
{:else}
|
||||||
<EmptyState
|
<EmptyState
|
||||||
icon={Heart}
|
icon={Heart}
|
||||||
|
|||||||
@@ -10,24 +10,12 @@
|
|||||||
import Alert from '$lib/components/ui/Alert.svelte';
|
import Alert from '$lib/components/ui/Alert.svelte';
|
||||||
import Artwork from '$lib/components/ui/Artwork.svelte';
|
import Artwork from '$lib/components/ui/Artwork.svelte';
|
||||||
import CollectionHeader from '$lib/components/ui/CollectionHeader.svelte';
|
import CollectionHeader from '$lib/components/ui/CollectionHeader.svelte';
|
||||||
import TrackTable from '$lib/components/ui/TrackTable.svelte';
|
import TrackList from '$lib/components/ui/TrackList.svelte';
|
||||||
import TrackRow from '$lib/components/ui/TrackRow.svelte';
|
|
||||||
import TrackMeta from '$lib/components/ui/TrackMeta.svelte';
|
|
||||||
import TrackIndexCell from '$lib/components/ui/TrackIndexCell.svelte';
|
|
||||||
import MediaIdentity from '$lib/components/ui/MediaIdentity.svelte';
|
|
||||||
import { pressable } from '$lib/actions/pressable';
|
|
||||||
import ContextMenu from '$lib/components/ui/ContextMenu.svelte';
|
import ContextMenu from '$lib/components/ui/ContextMenu.svelte';
|
||||||
import ListPlus from '@lucide/svelte/icons/list-plus';
|
import ListPlus from '@lucide/svelte/icons/list-plus';
|
||||||
import { createTrackMenu } from '$lib/menus.svelte';
|
import { createTrackMenu } from '$lib/menus.svelte';
|
||||||
import { mixItemKey, mixItemTarget } from '$lib/mixes';
|
import { mixItemTarget } from '$lib/mixes';
|
||||||
import {
|
import { queueItemForTarget } from '$lib/tracks';
|
||||||
isTargetCurrent,
|
|
||||||
queueItemForTarget,
|
|
||||||
targetArtist,
|
|
||||||
targetId,
|
|
||||||
targetListensCount,
|
|
||||||
targetTitle
|
|
||||||
} from '$lib/tracks';
|
|
||||||
|
|
||||||
let { data, form } = $props();
|
let { data, form } = $props();
|
||||||
|
|
||||||
@@ -35,6 +23,15 @@
|
|||||||
const items = $derived(mix.items);
|
const items = $derived(mix.items);
|
||||||
const targets = $derived(items.map(mixItemTarget));
|
const targets = $derived(items.map(mixItemTarget));
|
||||||
const queue = $derived(targets.map(queueItemForTarget));
|
const queue = $derived(targets.map(queueItemForTarget));
|
||||||
|
const listTracks = $derived(
|
||||||
|
items.map((item) => ({
|
||||||
|
id: item.trackId,
|
||||||
|
title: item.title,
|
||||||
|
artist: item.artist,
|
||||||
|
duration: item.durationSeconds,
|
||||||
|
listensCount: item.listensCount
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
const totalSeconds = $derived(
|
const totalSeconds = $derived(
|
||||||
items.reduce((total, item) => total + Number(item.durationSeconds), 0)
|
items.reduce((total, item) => total + Number(item.durationSeconds), 0)
|
||||||
@@ -96,31 +93,13 @@
|
|||||||
<Alert tone="danger" class="mt-4">{form.message}</Alert>
|
<Alert tone="danger" class="mt-4">{form.message}</Alert>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<TrackTable index meta={[{ label: 'Escuchas', width: '100px' }]} class="mt-6 sm:mt-8">
|
<TrackList
|
||||||
{#each items as item, i (mixItemKey(item))}
|
tracks={listTracks}
|
||||||
{@const active = isTargetCurrent(targets[i])}
|
columns={['plays']}
|
||||||
<TrackRow {active} oncontextmenu={(e) => trackMenu.open(e, targets[i])}>
|
onPlay={playFrom}
|
||||||
<TrackIndexCell index={i} {active} playing={player.playing} onToggle={() => playFrom(i)} />
|
oncontextmenu={(e, _track, i) => trackMenu.open(e, targets[i])}
|
||||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
class="mt-6 sm:mt-8"
|
||||||
<div
|
/>
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
use:pressable={() => playFrom(i)}
|
|
||||||
onclick={(event) => event.stopPropagation()}
|
|
||||||
class="flex min-w-0 flex-1 text-left"
|
|
||||||
>
|
|
||||||
<MediaIdentity
|
|
||||||
trackId={targetId(targets[i])}
|
|
||||||
title={targetTitle(targets[i])}
|
|
||||||
artist={targetArtist(targets[i])}
|
|
||||||
{active}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<TrackMeta class="hidden sm:block">{Number(targetListensCount(targets[i]) ?? 0)}</TrackMeta>
|
|
||||||
<TrackMeta>{fmtTime(Number(item.durationSeconds))}</TrackMeta>
|
|
||||||
</TrackRow>
|
|
||||||
{/each}
|
|
||||||
</TrackTable>
|
|
||||||
</Page>
|
</Page>
|
||||||
|
|
||||||
{#if trackMenu.state}
|
{#if trackMenu.state}
|
||||||
|
|||||||
@@ -1,18 +1,20 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import ListMusic from '@lucide/svelte/icons/list-music';
|
import ListMusic from '@lucide/svelte/icons/list-music';
|
||||||
|
import X from '@lucide/svelte/icons/x';
|
||||||
import { player, toQueueItems, isQueueCurrent, playAllOrToggle } from '$lib/player/player.svelte';
|
import { player, toQueueItems, isQueueCurrent, playAllOrToggle } from '$lib/player/player.svelte';
|
||||||
import Page from '$lib/components/ui/Page.svelte';
|
import Page from '$lib/components/ui/Page.svelte';
|
||||||
import Modal from '$lib/components/ui/Modal.svelte';
|
import Modal from '$lib/components/ui/Modal.svelte';
|
||||||
import PlaylistForm from '$lib/components/ui/PlaylistForm.svelte';
|
import PlaylistForm from '$lib/components/ui/PlaylistForm.svelte';
|
||||||
import Button from '$lib/components/ui/Button.svelte';
|
import Button from '$lib/components/ui/Button.svelte';
|
||||||
import EmptyState from '$lib/components/ui/EmptyState.svelte';
|
import EmptyState from '$lib/components/ui/EmptyState.svelte';
|
||||||
|
import TrackList from '$lib/components/ui/TrackList.svelte';
|
||||||
import PlaylistHeader from './components/PlaylistHeader.svelte';
|
import PlaylistHeader from './components/PlaylistHeader.svelte';
|
||||||
import PlaylistTrackTable from './components/PlaylistTrackTable.svelte';
|
|
||||||
|
|
||||||
let { data, form } = $props();
|
let { data, form } = $props();
|
||||||
|
|
||||||
const playlist = $derived(data.playlist);
|
const playlist = $derived(data.playlist);
|
||||||
const tracks = $derived(data.tracks);
|
const tracks = $derived(data.tracks);
|
||||||
|
const listTracks = $derived(tracks.map((track) => ({ ...track, addedAt: track.createdAt })));
|
||||||
|
|
||||||
let editing = $state(false);
|
let editing = $state(false);
|
||||||
let confirmingDelete = $state(false);
|
let confirmingDelete = $state(false);
|
||||||
@@ -22,6 +24,10 @@
|
|||||||
function playAll() {
|
function playAll() {
|
||||||
playAllOrToggle(toQueueItems(tracks));
|
playAllOrToggle(toQueueItems(tracks));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function playFrom(index: number) {
|
||||||
|
player.playOrToggle(toQueueItems(tracks), index);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
@@ -44,7 +50,13 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{#if tracks.length > 0}
|
{#if tracks.length > 0}
|
||||||
<PlaylistTrackTable {tracks} />
|
<TrackList
|
||||||
|
tracks={listTracks}
|
||||||
|
columns={['added', 'plays']}
|
||||||
|
onPlay={playFrom}
|
||||||
|
rowAction={{ action: '?/removeTrack', icon: X, label: 'Quitar de la playlist' }}
|
||||||
|
class="mt-6 sm:mt-8"
|
||||||
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<EmptyState
|
<EmptyState
|
||||||
icon={ListMusic}
|
icon={ListMusic}
|
||||||
|
|||||||
@@ -1,80 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { enhance } from '$app/forms';
|
|
||||||
import X from '@lucide/svelte/icons/x';
|
|
||||||
import { player, toQueueItems, type ApiTrackLike } from '$lib/player/player.svelte';
|
|
||||||
import { fmtTime, fmtDate } from '$lib/format';
|
|
||||||
import IconButton from '$lib/components/ui/IconButton.svelte';
|
|
||||||
import TrackTable from '$lib/components/ui/TrackTable.svelte';
|
|
||||||
import TrackRow from '$lib/components/ui/TrackRow.svelte';
|
|
||||||
import TrackMeta from '$lib/components/ui/TrackMeta.svelte';
|
|
||||||
import TrackIndexCell from '$lib/components/ui/TrackIndexCell.svelte';
|
|
||||||
import MediaIdentity from '$lib/components/ui/MediaIdentity.svelte';
|
|
||||||
import { pressable } from '$lib/actions/pressable';
|
|
||||||
|
|
||||||
type PlaylistTrack = ApiTrackLike & {
|
|
||||||
createdAt: string;
|
|
||||||
duration: number | string;
|
|
||||||
listensCount: number | string;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
tracks: PlaylistTrack[];
|
|
||||||
}
|
|
||||||
|
|
||||||
let { tracks }: Props = $props();
|
|
||||||
|
|
||||||
function playFrom(index: number) {
|
|
||||||
player.playOrToggle(toQueueItems(tracks), index);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<TrackTable
|
|
||||||
index
|
|
||||||
action
|
|
||||||
meta={[
|
|
||||||
{ label: 'Álbum', width: '76px' },
|
|
||||||
{ label: 'Añadida', width: '124px' },
|
|
||||||
{ label: 'Escuchas', width: '106px' }
|
|
||||||
]}
|
|
||||||
class="mt-6 sm:mt-8"
|
|
||||||
>
|
|
||||||
{#each tracks as track, i (track.id)}
|
|
||||||
{@const active = player.current.id === track.id}
|
|
||||||
<TrackRow {active}>
|
|
||||||
<TrackIndexCell index={i} playing={player.playing} onToggle={() => playFrom(i)} {active} />
|
|
||||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
||||||
<div
|
|
||||||
role="button"
|
|
||||||
tabindex="0"
|
|
||||||
use:pressable={() => playFrom(i)}
|
|
||||||
onclick={(event) => event.stopPropagation()}
|
|
||||||
class="flex min-w-0 flex-1 text-left"
|
|
||||||
>
|
|
||||||
<MediaIdentity
|
|
||||||
trackId={track.id}
|
|
||||||
title={track.title}
|
|
||||||
artist={track.artist}
|
|
||||||
ownerUserId={track.ownerUserId}
|
|
||||||
explicit={track.isExplicit}
|
|
||||||
{active}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<TrackMeta class="hidden sm:block">—</TrackMeta>
|
|
||||||
<TrackMeta class="hidden sm:block">{fmtDate(track.createdAt)}</TrackMeta>
|
|
||||||
<TrackMeta class="hidden sm:block">{Number(track.listensCount)}</TrackMeta>
|
|
||||||
<TrackMeta>{fmtTime(Number(track.duration))}</TrackMeta>
|
|
||||||
<form
|
|
||||||
method="POST"
|
|
||||||
action="?/removeTrack"
|
|
||||||
use:enhance={() =>
|
|
||||||
async ({ update }) =>
|
|
||||||
update()}
|
|
||||||
>
|
|
||||||
<input type="hidden" name="trackId" value={track.id} />
|
|
||||||
<IconButton type="submit" label="Quitar de la playlist" size="xs" revealOnHover>
|
|
||||||
<X class="h-3.5 w-3.5" />
|
|
||||||
</IconButton>
|
|
||||||
</form>
|
|
||||||
</TrackRow>
|
|
||||||
{/each}
|
|
||||||
</TrackTable>
|
|
||||||
Reference in New Issue
Block a user