Design Prot Merge #1

Merged
NotS1mp merged 217 commits from design-prot into master 2026-09-21 15:48:16 +00:00
4 changed files with 71 additions and 24 deletions
Showing only changes of commit 7ae6c0a7a2 - Show all commits
@@ -1,7 +1,17 @@
using System.Text.Json.Serialization;
using Musify.Domain.Entities; using Musify.Domain.Entities;
namespace Musify.Application.Albums.Responses namespace Musify.Application.Albums.Responses
{ {
// [JsonPolymorphic]/[JsonDerivedType] are required here, not optional: without them
// System.Text.Json serializes strictly by the declared CLR type at each call site
// (AlbumApplicationResponse, both as a bare return type and nested inside
// AlbumSearchItemResponse.Album), so an ExternalAlbumApplicationResponse instance would
// silently lose ExternalId/ThumbnailUrl on the wire. This also gives every response a
// "source" discriminator field for free, matching AlbumSearchItemResponse.Source.
[JsonPolymorphic(TypeDiscriminatorPropertyName = "source")]
[JsonDerivedType(typeof(AlbumApplicationResponse), typeDiscriminator: "Local")]
[JsonDerivedType(typeof(ExternalAlbumApplicationResponse), typeDiscriminator: "YouTube")]
public record AlbumApplicationResponse( public record AlbumApplicationResponse(
Guid Id, Guid Id,
string Title, string Title,
@@ -14,9 +24,7 @@ namespace Musify.Application.Albums.Responses
string? LargeImageKeyName, string? LargeImageKeyName,
DateTime CreatedAt, DateTime CreatedAt,
DateTime UpdatedAt, DateTime UpdatedAt,
IReadOnlyList<Guid> CoverTrackIds, IReadOnlyList<Guid> CoverTrackIds)
string? YouTubeAlbumId = null,
string? ThumbnailUrl = null)
{ {
public const int CoverTrackCount = 4; public const int CoverTrackCount = 4;
@@ -45,22 +53,12 @@ namespace Musify.Application.Albums.Responses
int trackCount, int trackCount,
IReadOnlyList<Guid>? coverTrackIds = null) IReadOnlyList<Guid>? coverTrackIds = null)
{ {
var externalAlbum = album as ExternalAlbum; return album switch
return new AlbumApplicationResponse( {
album.Id, ExternalAlbum externalAlbum => ExternalAlbumApplicationResponse.FromEntity(externalAlbum, trackCount, coverTrackIds),
album.Title, UserAlbum userAlbum => FromEntity(userAlbum, trackCount, coverTrackIds),
album.Description, _ => throw new NotSupportedException($"Unsupported album entity type '{album.GetType().Name}'.")
album.ReleaseYear, };
album is UserAlbum userAlbum ? userAlbum.OwnerUserId : 0,
trackCount,
album.Pictures?.SmallName,
album.Pictures?.MediumName,
album.Pictures?.LargeName,
album.CreatedAt,
album.UpdatedAt,
coverTrackIds ?? [],
externalAlbum?.ExternalId,
externalAlbum?.ThumbnailUrl);
} }
} }
} }
@@ -14,7 +14,7 @@ namespace Musify.Application.Albums.Responses
YouTubeAlbumResult? YouTubeAlbum) YouTubeAlbumResult? YouTubeAlbum)
{ {
public static AlbumSearchItemResponse FromAlbum(AlbumApplicationResponse album) => public static AlbumSearchItemResponse FromAlbum(AlbumApplicationResponse album) =>
new(album.YouTubeAlbumId is not null ? TrackSource.YouTube : TrackSource.Local, album, null); new(album is ExternalAlbumApplicationResponse ? TrackSource.YouTube : TrackSource.Local, album, null);
public static AlbumSearchItemResponse FromYouTubeAlbum(YouTubeAlbumResult album) => public static AlbumSearchItemResponse FromYouTubeAlbum(YouTubeAlbumResult album) =>
new(TrackSource.YouTube, null, album); new(TrackSource.YouTube, null, album);
@@ -0,0 +1,47 @@
using Musify.Domain.Entities;
namespace Musify.Application.Albums.Responses
{
public record ExternalAlbumApplicationResponse(
Guid Id,
string Title,
string? Description,
int? ReleaseYear,
long OwnerUserId,
int TrackCount,
string? SmallImageKeyName,
string? MediumImageKeyName,
string? LargeImageKeyName,
DateTime CreatedAt,
DateTime UpdatedAt,
IReadOnlyList<Guid> CoverTrackIds,
string ExternalId,
string? ThumbnailUrl)
: AlbumApplicationResponse(
Id, Title, Description, ReleaseYear, OwnerUserId, TrackCount,
SmallImageKeyName, MediumImageKeyName, LargeImageKeyName,
CreatedAt, UpdatedAt, CoverTrackIds)
{
public static ExternalAlbumApplicationResponse FromEntity(
ExternalAlbum album,
int trackCount,
IReadOnlyList<Guid>? coverTrackIds = null)
{
return new ExternalAlbumApplicationResponse(
album.Id,
album.Title,
album.Description,
album.ReleaseYear,
0,
trackCount,
album.Pictures?.SmallName,
album.Pictures?.MediumName,
album.Pictures?.LargeName,
album.CreatedAt,
album.UpdatedAt,
coverTrackIds ?? [],
album.ExternalId,
album.ThumbnailUrl);
}
}
}
+6 -4
View File
@@ -155,7 +155,9 @@
></div> ></div>
<div class="animate-enter relative flex flex-col gap-2 sm:gap-2.5"> <div class="animate-enter relative flex flex-col gap-2 sm:gap-2.5">
<p class="text-sm tracking-[0.14em] text-fg-2 uppercase sm:text-base">{data.greeting}</p> <p class="text-sm tracking-[0.14em] text-fg-2 uppercase sm:text-base">{data.greeting}</p>
<h1 class="font-display text-3xl leading-[1.05] font-semibold tracking-tight text-fg sm:text-6xl"> <h1
class="font-display text-3xl leading-[1.05] font-semibold tracking-tight text-fg sm:text-6xl"
>
Tu música. Sin límites. Tu música. Sin límites.
</h1> </h1>
</div> </div>
@@ -214,15 +216,15 @@
<Rail> <Rail>
{#each albums as album, i (album.id)} {#each albums as album, i (album.id)}
<a <a
href={album.youTubeAlbumId href={album.source === 'YouTube'
? `/albums/external/youtube/${album.youTubeAlbumId}` ? `/albums/external/youtube/${album.externalId}`
: `/albums/${album.id}`} : `/albums/${album.id}`}
class="group/card animate-enter block w-42 shrink-0 rounded-art focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-bg focus-visible:outline-none" class="group/card animate-enter block w-42 shrink-0 rounded-art focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-bg focus-visible:outline-none"
style="animation-delay:{Math.min(i, 10) * 45}ms" style="animation-delay:{Math.min(i, 10) * 45}ms"
oncontextmenu={(e) => openAlbumMenu(e, album.id)} oncontextmenu={(e) => openAlbumMenu(e, album.id)}
> >
<div class="relative"> <div class="relative">
{#if album.thumbnailUrl} {#if album.source === 'YouTube' && album.thumbnailUrl}
<Cover <Cover
trackId={album.id} trackId={album.id}
src={album.thumbnailUrl} src={album.thumbnailUrl}