Add likes, follows and playlist visibility; redesign web-player UI
Backend: - New Likes feature (toggle/get liked tracks) and endpoints - User follow/unfollow, listening stats, public user profile - Playlist visibility (public/private) support - Various auth and DTO/response adjustments to support the above Frontend (web-player): - New design system pass: theme tokens/mode (light/dark), glass menu, tabs bar, player dock/extras/queue components, mix tiles, artist link - New routes: Liked, public user profile (/u/[id]), likes API proxy - Search refactor: shared SearchBox component used by TopBar and MobileHeader (search now reachable on mobile), live results as you type instead of requiring Enter, dropped the "Buscando…" quick-result preview - Removed now-unused components (MobileNav, CoverBadge, MixBentoTile, PlayPauseOverlay, SuggestionsTable, userDirectory) - Design handoff reference docs added under web-player/design_handoff_musify_player and web-player/docs/design-system-refactor-plan.md
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
import { toQueueItems, type ApiTrackLike, type TrackSourceKind } from './player.svelte';
|
||||
|
||||
export interface LikedTrack {
|
||||
id: string;
|
||||
title: string;
|
||||
artist?: string;
|
||||
coverUrl?: string;
|
||||
source?: TrackSourceKind;
|
||||
explicit?: boolean;
|
||||
ownerUserId?: string | number | null;
|
||||
likedAt: number;
|
||||
}
|
||||
|
||||
type LikeToggleInput = {
|
||||
id: string | number;
|
||||
title: string;
|
||||
artist?: string;
|
||||
coverUrl?: string;
|
||||
source?: TrackSourceKind;
|
||||
explicit?: boolean;
|
||||
ownerUserId?: string | number | null;
|
||||
};
|
||||
|
||||
function toggleRequestBody(track: LikeToggleInput) {
|
||||
return track.source === 'youtube'
|
||||
? { source: 'YouTube' as const, externalId: String(track.id) }
|
||||
: { trackId: String(track.id) };
|
||||
}
|
||||
|
||||
class LikedStore {
|
||||
entries = $state<Record<string, LikedTrack>>({});
|
||||
private hydrated = false;
|
||||
|
||||
count = $derived(Object.keys(this.entries).length);
|
||||
list = $derived(Object.values(this.entries).sort((a, b) => b.likedAt - a.likedAt));
|
||||
|
||||
hydrate(tracks: (ApiTrackLike & { createdAt: string })[]) {
|
||||
if (this.hydrated) return;
|
||||
this.hydrated = true;
|
||||
|
||||
const items = toQueueItems(tracks);
|
||||
const next: Record<string, LikedTrack> = {};
|
||||
items.forEach((item, i) => {
|
||||
next[String(item.id)] = {
|
||||
id: String(item.id),
|
||||
title: item.title,
|
||||
artist: item.artist,
|
||||
coverUrl: item.coverUrl,
|
||||
source: item.source,
|
||||
explicit: item.explicit,
|
||||
ownerUserId: item.ownerUserId,
|
||||
likedAt: new Date(tracks[i].createdAt).getTime()
|
||||
};
|
||||
});
|
||||
this.entries = next;
|
||||
}
|
||||
|
||||
isLiked(id: string | number): boolean {
|
||||
return String(id) in this.entries;
|
||||
}
|
||||
|
||||
async toggle(track: LikeToggleInput) {
|
||||
const id = String(track.id);
|
||||
const before = this.entries;
|
||||
const wasLiked = id in before;
|
||||
const next = { ...before };
|
||||
if (wasLiked) delete next[id];
|
||||
else {
|
||||
next[id] = {
|
||||
id,
|
||||
title: track.title,
|
||||
artist: track.artist,
|
||||
coverUrl: track.coverUrl,
|
||||
source: track.source,
|
||||
explicit: track.explicit,
|
||||
ownerUserId: track.ownerUserId,
|
||||
likedAt: Date.now()
|
||||
};
|
||||
}
|
||||
this.entries = next;
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/likes', {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify(toggleRequestBody(track))
|
||||
});
|
||||
if (!res.ok) throw new Error('Failed to toggle like');
|
||||
} catch {
|
||||
this.entries = before;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const liked = new LikedStore();
|
||||
Reference in New Issue
Block a user