Design Prot Merge #1
@@ -7,7 +7,7 @@ using Musify.Application.Contracts;
|
|||||||
namespace Musify.Application.Albums
|
namespace Musify.Application.Albums
|
||||||
{
|
{
|
||||||
public record GetRecentlyListenedAlbumsQuery(long UserId, int Limit)
|
public record GetRecentlyListenedAlbumsQuery(long UserId, int Limit)
|
||||||
: IQuery<ErrorOr<IReadOnlyList<AlbumApplicationResponse>>>;
|
: IQuery<ErrorOr<IReadOnlyList<AlbumApplicationResponse> >>;
|
||||||
|
|
||||||
public class GetRecentlyListenedAlbumsQueryHandler(IDatabase database)
|
public class GetRecentlyListenedAlbumsQueryHandler(IDatabase database)
|
||||||
: IQueryHandler<GetRecentlyListenedAlbumsQuery, ErrorOr<IReadOnlyList<AlbumApplicationResponse>>>
|
: IQueryHandler<GetRecentlyListenedAlbumsQuery, ErrorOr<IReadOnlyList<AlbumApplicationResponse>>>
|
||||||
@@ -24,9 +24,9 @@ namespace Musify.Application.Albums
|
|||||||
listenedTrackIds,
|
listenedTrackIds,
|
||||||
albumTrack => albumTrack.TrackId,
|
albumTrack => albumTrack.TrackId,
|
||||||
history => history.TrackId,
|
history => history.TrackId,
|
||||||
(albumTrack, history) => new { albumTrack.AlbumId, history.ListenedAt })
|
(albumTrack, history) => new { albumTrack.AlbumId, history.ListenedAt } )
|
||||||
.GroupBy(entry => entry.AlbumId)
|
.GroupBy(entry => entry.AlbumId)
|
||||||
.Select(group => new { AlbumId = group.Key, LastListenedAt = group.Max(entry => entry.ListenedAt) })
|
.Select(group => new { AlbumId = group.Key, LastListenedAt = group.Max(entry => entry.ListenedAt) } )
|
||||||
.OrderByDescending(entry => entry.LastListenedAt)
|
.OrderByDescending(entry => entry.LastListenedAt)
|
||||||
.Take(request.Limit)
|
.Take(request.Limit)
|
||||||
.ToListAsync(cancellationToken);
|
.ToListAsync(cancellationToken);
|
||||||
@@ -48,7 +48,7 @@ namespace Musify.Application.Albums
|
|||||||
.Take(AlbumApplicationResponse.CoverTrackCount)
|
.Take(AlbumApplicationResponse.CoverTrackCount)
|
||||||
.Select(albumTrack => albumTrack.TrackId)
|
.Select(albumTrack => albumTrack.TrackId)
|
||||||
.ToList()
|
.ToList()
|
||||||
})
|
} )
|
||||||
.ToDictionaryAsync(entry => entry.Album.Id, cancellationToken);
|
.ToDictionaryAsync(entry => entry.Album.Id, cancellationToken);
|
||||||
|
|
||||||
return ranked
|
return ranked
|
||||||
|
|||||||
@@ -34,6 +34,18 @@ namespace Musify.Application.Configuration
|
|||||||
[Required]
|
[Required]
|
||||||
public string LargePicturesFolder { get; set; } = "LargePictures";
|
public string LargePicturesFolder { get; set; } = "LargePictures";
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public string PresetOriginalPicture { get; set; } = "PresetOriginalPicture.webp";
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public string PresetSmallPicture { get; set; } = "PresetSmallPicture.webp";
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public string PresetMediumPicture { get; set; } = "PresetMediumPicture.webp";
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public string PresetLargePicture { get; set; } = "PresetLargePicture.webp";
|
||||||
|
|
||||||
public string SmallPicturesPath => StorageKey.Combine(ParentFolders, SmallPicturesFolder);
|
public string SmallPicturesPath => StorageKey.Combine(ParentFolders, SmallPicturesFolder);
|
||||||
|
|
||||||
public string MediumPicturesPath => StorageKey.Combine(ParentFolders, MediumPicturesFolder);
|
public string MediumPicturesPath => StorageKey.Combine(ParentFolders, MediumPicturesFolder);
|
||||||
@@ -42,6 +54,12 @@ namespace Musify.Application.Configuration
|
|||||||
|
|
||||||
public string OriginalPicturesPath => StorageKey.Combine(ParentFolders, OriginalPicturesFolder);
|
public string OriginalPicturesPath => StorageKey.Combine(ParentFolders, OriginalPicturesFolder);
|
||||||
|
|
||||||
|
public string PresetSmallPicturePath => StorageKey.Combine(ParentFolders, SmallPicturesFolder, PresetSmallPicture);
|
||||||
|
|
||||||
|
public string PresetMediumPicturePath => StorageKey.Combine(ParentFolders, MediumPicturesFolder, PresetMediumPicture);
|
||||||
|
|
||||||
|
public string PresetLargePicturePath => StorageKey.Combine(ParentFolders, LargePicturesFolder, PresetLargePicture);
|
||||||
|
|
||||||
public string BuildOriginalPicturePath(string pictureName) => StorageKey.Combine(ParentFolders, OriginalPicturesFolder, pictureName);
|
public string BuildOriginalPicturePath(string pictureName) => StorageKey.Combine(ParentFolders, OriginalPicturesFolder, pictureName);
|
||||||
|
|
||||||
public string BuildOriginalPicturePath(long userId, string pictureName) => StorageKey.Combine(UploadsFolder, userId.ToString(), ParentFolders, OriginalPicturesFolder, pictureName);
|
public string BuildOriginalPicturePath(long userId, string pictureName) => StorageKey.Combine(UploadsFolder, userId.ToString(), ParentFolders, OriginalPicturesFolder, pictureName);
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ namespace Musify.Application.PlayLists
|
|||||||
UploadIntentConfiguration uploadIntentConfiguration)
|
UploadIntentConfiguration uploadIntentConfiguration)
|
||||||
: ICommandHandler<CreatePlayListCommand, ErrorOr<PlayListApplicationResponse>>
|
: ICommandHandler<CreatePlayListCommand, ErrorOr<PlayListApplicationResponse>>
|
||||||
{
|
{
|
||||||
|
private sealed record ResolvedPlayListPictures(PlayListPictures Pictures, UploadIntent? Intent);
|
||||||
|
|
||||||
public async ValueTask<ErrorOr<PlayListApplicationResponse>> Handle(CreatePlayListCommand request, CancellationToken cancellationToken)
|
public async ValueTask<ErrorOr<PlayListApplicationResponse>> Handle(CreatePlayListCommand request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var userExists = await database.Users
|
var userExists = await database.Users
|
||||||
@@ -41,35 +43,26 @@ namespace Musify.Application.PlayLists
|
|||||||
return Error.NotFound(description: $"User {request.UserId} not found");
|
return Error.NotFound(description: $"User {request.UserId} not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
UploadIntent? pictureIntent = null;
|
var picturesResult = await ResolvePicturesAsync(request, cancellationToken);
|
||||||
if (request.PictureIntentId.HasValue)
|
if (picturesResult.IsError)
|
||||||
{
|
return picturesResult.Errors;
|
||||||
var validation = await uploadIntentValidator.ValidateAndLoadAsync(
|
|
||||||
uploadIntentConfiguration,
|
|
||||||
request.PictureIntentId.Value, request.UserId, cancellationToken);
|
|
||||||
if (validation.IsError)
|
|
||||||
return validation.Errors;
|
|
||||||
|
|
||||||
pictureIntent = validation.Value;
|
var resolvedPictures = picturesResult.Value;
|
||||||
}
|
|
||||||
|
|
||||||
var playList = new PlayList
|
var playList = new PlayList
|
||||||
{
|
{
|
||||||
UserId = request.UserId,
|
UserId = request.UserId,
|
||||||
Name = request.Name,
|
Name = request.Name,
|
||||||
NormalizedName = request.Name.Trim().ToUpperInvariant(),
|
NormalizedName = request.Name.Trim().ToUpperInvariant(),
|
||||||
Description = string.IsNullOrWhiteSpace(request.Description) ? null : request.Description.Trim(),
|
Description = string.IsNullOrWhiteSpace(request.Description)? null: request.Description.Trim(),
|
||||||
Pictures = pictureIntent == null ? null
|
Pictures = resolvedPictures.Pictures
|
||||||
: new PlayListPictures { OriginalName = pictureIntent.ObjectName }
|
|
||||||
};
|
};
|
||||||
|
|
||||||
await database.PlayLists.AddAsync(playList, cancellationToken);
|
await database.PlayLists.AddAsync(playList, cancellationToken);
|
||||||
|
|
||||||
if (pictureIntent != null)
|
if (resolvedPictures.Intent != null)
|
||||||
{
|
{
|
||||||
var finalPictureKey = playListConfiguration.Routes.BuildOriginalPicturePath(request.UserId, pictureIntent.ObjectName);
|
var finalPictureKey = playListConfiguration.Routes.BuildOriginalPicturePath(request.UserId, resolvedPictures.Pictures.OriginalName);
|
||||||
var publishResult = await PublishCreatePlayListEventAsync(
|
var publishResult = await PublishCreatePlayListEventAsync(playList.Id, resolvedPictures.Intent, finalPictureKey, cancellationToken);
|
||||||
playList.Id, pictureIntent, finalPictureKey, cancellationToken);
|
|
||||||
if (publishResult.IsError)
|
if (publishResult.IsError)
|
||||||
return publishResult.Errors;
|
return publishResult.Errors;
|
||||||
}
|
}
|
||||||
@@ -88,6 +81,35 @@ namespace Musify.Application.PlayLists
|
|||||||
return PlayListApplicationResponse.FromEntity(playList);
|
return PlayListApplicationResponse.FromEntity(playList);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async ValueTask<ErrorOr<ResolvedPlayListPictures>> ResolvePicturesAsync(
|
||||||
|
CreatePlayListCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (!request.PictureIntentId.HasValue)
|
||||||
|
{
|
||||||
|
return new ResolvedPlayListPictures(new PlayListPictures
|
||||||
|
{
|
||||||
|
OriginalName = playListConfiguration.Routes.PresetOriginalPicture,
|
||||||
|
SmallName = playListConfiguration.Routes.PresetSmallPicture,
|
||||||
|
MediumName = playListConfiguration.Routes.PresetMediumPicture,
|
||||||
|
LargeName = playListConfiguration.Routes.PresetLargePicture
|
||||||
|
}, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
var validation = await uploadIntentValidator.ValidateAndLoadAsync(
|
||||||
|
uploadIntentConfiguration, request.PictureIntentId.Value, request.UserId, cancellationToken);
|
||||||
|
|
||||||
|
if (validation.IsError)
|
||||||
|
return validation.Errors;
|
||||||
|
|
||||||
|
return new ResolvedPlayListPictures(new PlayListPictures
|
||||||
|
{
|
||||||
|
OriginalName = validation.Value.ObjectName,
|
||||||
|
SmallName = playListConfiguration.Routes.PresetSmallPicture,
|
||||||
|
MediumName = playListConfiguration.Routes.PresetMediumPicture,
|
||||||
|
LargeName = playListConfiguration.Routes.PresetLargePicture
|
||||||
|
}, validation.Value);
|
||||||
|
}
|
||||||
|
|
||||||
async Task<ErrorOr<Success>> PublishCreatePlayListEventAsync(
|
async Task<ErrorOr<Success>> PublishCreatePlayListEventAsync(
|
||||||
Guid playListId,
|
Guid playListId,
|
||||||
UploadIntent pictureIntent,
|
UploadIntent pictureIntent,
|
||||||
|
|||||||
@@ -69,13 +69,19 @@ namespace Musify.Application.PlayLists
|
|||||||
return validation.Errors;
|
return validation.Errors;
|
||||||
|
|
||||||
pictureIntent = validation.Value;
|
pictureIntent = validation.Value;
|
||||||
playListEntity.Pictures = new PlayListPictures { OriginalName = pictureIntent.ObjectName };
|
playListEntity.Pictures = new PlayListPictures
|
||||||
|
{
|
||||||
|
OriginalName = pictureIntent.ObjectName,
|
||||||
|
SmallName = playListConfiguration.Routes.PresetSmallPicture,
|
||||||
|
MediumName = playListConfiguration.Routes.PresetMediumPicture,
|
||||||
|
LargeName = playListConfiguration.Routes.PresetLargePicture
|
||||||
|
};
|
||||||
finalPictureKey = playListConfiguration.Routes.BuildOriginalPicturePath(request.UserId, pictureIntent.ObjectName);
|
finalPictureKey = playListConfiguration.Routes.BuildOriginalPicturePath(request.UserId, pictureIntent.ObjectName);
|
||||||
}
|
}
|
||||||
|
|
||||||
database.PlayLists.Update(playListEntity);
|
database.PlayLists.Update(playListEntity);
|
||||||
|
|
||||||
if (pictureIntent is not null && finalPictureKey is not null)
|
if (pictureIntent != null&& finalPictureKey != null)
|
||||||
{
|
{
|
||||||
var publishResult = await PublishUpdatePlayListPictureSourceEventAsync(
|
var publishResult = await PublishUpdatePlayListPictureSourceEventAsync(
|
||||||
playListEntity.Id, pictureIntent, finalPictureKey, cancellationToken);
|
playListEntity.Id, pictureIntent, finalPictureKey, cancellationToken);
|
||||||
|
|||||||
@@ -27,7 +27,8 @@ namespace Musify.Domain.Entities
|
|||||||
[MaxLength(256)]
|
[MaxLength(256)]
|
||||||
public string? Description { get; set; }
|
public string? Description { get; set; }
|
||||||
|
|
||||||
public PlayListPictures? Pictures { get; set; }
|
[Required]
|
||||||
|
public required PlayListPictures Pictures { get; set; }
|
||||||
|
|
||||||
[Required]
|
[Required]
|
||||||
public LifeCycleStatus LifeCycleStatus { get; set; } = LifeCycleStatus.Active;
|
public LifeCycleStatus LifeCycleStatus { get; set; } = LifeCycleStatus.Active;
|
||||||
|
|||||||
@@ -1,13 +1,19 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
namespace Musify.Domain.ValueObjects
|
namespace Musify.Domain.ValueObjects
|
||||||
{
|
{
|
||||||
public class PlayListPictures
|
public class PlayListPictures
|
||||||
{
|
{
|
||||||
public string? OriginalName { get; set; }
|
[Required]
|
||||||
|
public required string OriginalName { get; set; }
|
||||||
|
|
||||||
public string? SmallName { get; set; }
|
[Required]
|
||||||
|
public required string SmallName { get; set; }
|
||||||
|
|
||||||
public string? MediumName { get; set; }
|
[Required]
|
||||||
|
public required string MediumName { get; set; }
|
||||||
|
|
||||||
public string? LargeName { get; set; }
|
[Required]
|
||||||
|
public required string LargeName { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ namespace Musify.Infrastructure.MassTransit.Logs
|
|||||||
{
|
{
|
||||||
public record UpdatePlayListPictureLog(
|
public record UpdatePlayListPictureLog(
|
||||||
Guid PlayListId,
|
Guid PlayListId,
|
||||||
string? PreviousOriginalPictureKey,
|
string PreviousOriginalPictureKey,
|
||||||
string? PreviousSmallPictureKey,
|
string PreviousSmallPictureKey,
|
||||||
string? PreviousMediumPictureKey,
|
string PreviousMediumPictureKey,
|
||||||
string? PreviousLargePictureKey);
|
string PreviousLargePictureKey);
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+11
@@ -9,6 +9,7 @@
|
|||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource-variable/inter": "^5.2.8",
|
"@fontsource-variable/inter": "^5.2.8",
|
||||||
|
"@fontsource-variable/sora": "^5.3.0",
|
||||||
"@lucide/svelte": "^1.23.0",
|
"@lucide/svelte": "^1.23.0",
|
||||||
"@sveltejs/adapter-node": "^5.5.7",
|
"@sveltejs/adapter-node": "^5.5.7",
|
||||||
"jose": "^6.2.3",
|
"jose": "^6.2.3",
|
||||||
@@ -183,6 +184,15 @@
|
|||||||
"url": "https://github.com/sponsors/ayuhito"
|
"url": "https://github.com/sponsors/ayuhito"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@fontsource-variable/sora": {
|
||||||
|
"version": "5.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fontsource-variable/sora/-/sora-5.3.0.tgz",
|
||||||
|
"integrity": "sha512-h8kHta4Z8SkfUGeI82TUy1n3rbrKpBRGwuk4t4qCwto5oEHxlbOFD2yGP+Df5fbmTXwKGn+o667vYp0E4K8Whw==",
|
||||||
|
"license": "OFL-1.1",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ayuhito"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@humanfs/core": {
|
"node_modules/@humanfs/core": {
|
||||||
"version": "0.19.2",
|
"version": "0.19.2",
|
||||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
|
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
|
||||||
@@ -1639,6 +1649,7 @@
|
|||||||
"integrity": "sha512-qjhfuTfLXjA4IOzXvz0rTjT01BqEiIgPoUeMwiEjnaHKJMTNo8rH5pYW1a2L/0Dnux2fPC85AeyJoWaGa8WxTA==",
|
"integrity": "sha512-qjhfuTfLXjA4IOzXvz0rTjT01BqEiIgPoUeMwiEjnaHKJMTNo8rH5pYW1a2L/0Dnux2fPC85AeyJoWaGa8WxTA==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -36,6 +36,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource-variable/inter": "^5.2.8",
|
"@fontsource-variable/inter": "^5.2.8",
|
||||||
|
"@fontsource-variable/sora": "^5.3.0",
|
||||||
"@lucide/svelte": "^1.23.0",
|
"@lucide/svelte": "^1.23.0",
|
||||||
"@sveltejs/adapter-node": "^5.5.7",
|
"@sveltejs/adapter-node": "^5.5.7",
|
||||||
"jose": "^6.2.3",
|
"jose": "^6.2.3",
|
||||||
|
|||||||
Vendored
+1
-1
@@ -591,7 +591,7 @@ export interface components {
|
|||||||
};
|
};
|
||||||
CreatePlayListRequest: {
|
CreatePlayListRequest: {
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: null | string;
|
||||||
/** Format: uuid */
|
/** Format: uuid */
|
||||||
pictureIntentId: null | string;
|
pictureIntentId: null | string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
|
|
||||||
<div class="flex flex-col items-center gap-2.5 text-center" style="width:{size}px">
|
<div class="flex flex-col items-center gap-2.5 text-center" style="width:{size}px">
|
||||||
<div
|
<div
|
||||||
class="relative grid shrink-0 place-items-center overflow-hidden rounded-full shadow-art ring-1 ring-line transition duration-300 ease-out ring-inset group-hover/artist:-translate-y-1 group-hover/artist:shadow-art-lg"
|
class="relative grid shrink-0 place-items-center overflow-hidden rounded-full shadow-art ring-1 ring-line transition duration-300 ease-out ring-inset group-hover/artist:shadow-art-lg"
|
||||||
style="width:{size}px;height:{size}px;background:{gradientForHue(hueFor(id))}"
|
style="width:{size}px;height:{size}px;background:{gradientForHue(hueFor(id))}"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts" module>
|
<script lang="ts" module>
|
||||||
export const mediaCardArt =
|
export const mediaCardArt =
|
||||||
'aspect-square w-full rounded-art shadow-art transition duration-300 ease-out group-hover:-translate-y-1 group-hover:shadow-art-lg';
|
'aspect-square w-full rounded-art shadow-art transition duration-300 ease-out group-hover:shadow-art-lg';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import MixArt from './MixArt.svelte';
|
import Cover from './Cover.svelte';
|
||||||
import { hueFor } from '$lib/theme/color';
|
import { gradientForHue, hueFor } from '$lib/theme/color';
|
||||||
import type { Mix } from '$lib/mixes';
|
import { mixItemKey, type Mix } from '$lib/mixes';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
mix: Mix;
|
mix: Mix;
|
||||||
@@ -12,35 +12,53 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
let { mix, featured = false, banner = false, class: klass = '', index = 0 }: Props = $props();
|
let { mix, featured = false, banner = false, class: klass = '', index = 0 }: Props = $props();
|
||||||
|
|
||||||
|
const fillCount = $derived(banner ? 40 : 12);
|
||||||
|
|
||||||
|
const tiles = $derived.by(() => {
|
||||||
|
if (mix.items.length === 0) return [];
|
||||||
|
const out: Mix['items'] = [];
|
||||||
|
while (out.length < fillCount) out.push(...mix.items);
|
||||||
|
return out.slice(0, fillCount);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
href="/mixes/{mix.id}"
|
href="/mixes/{mix.id}"
|
||||||
class="group/tile animate-enter relative block overflow-hidden rounded-art shadow-art ring-1 ring-line transition duration-300 ease-out ring-inset hover:shadow-art-lg focus-visible:ring-2 focus-visible:ring-accent focus-visible:outline-none {klass}"
|
class="group/tile animate-enter relative block overflow-hidden rounded-art shadow-art ring-1 ring-line transition duration-300 ease-out ring-inset hover:shadow-art-lg focus-visible:ring-2 focus-visible:ring-accent focus-visible:outline-none {klass}"
|
||||||
style="animation-delay:{Math.min(index, 10) * 45}ms"
|
style="animation-delay:{Math.min(index, 10) * 45}ms;background:{gradientForHue(hueFor(mix.id))}"
|
||||||
>
|
>
|
||||||
<MixArt
|
{#if tiles.length > 0}
|
||||||
items={mix.items}
|
<div
|
||||||
hue={hueFor(mix.id)}
|
class="absolute inset-0 flex transition duration-300 ease-out group-hover/tile:scale-[1.01]"
|
||||||
class="absolute inset-0 h-full w-full transition duration-500 ease-out group-hover/tile:scale-[1.04]"
|
>
|
||||||
|
{#each tiles as item, i (i)}
|
||||||
|
<Cover
|
||||||
|
trackId={item.trackId ?? item.videoId ?? ''}
|
||||||
|
src={item.thumbnailUrl}
|
||||||
|
hue={hueFor(mixItemKey(item))}
|
||||||
|
size="medium"
|
||||||
|
class="aspect-square h-full shrink-0"
|
||||||
/>
|
/>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<div
|
||||||
|
class="pointer-events-none absolute inset-0 bg-black/10 transition-colors duration-300 group-hover/tile:bg-black/20"
|
||||||
|
></div>
|
||||||
<div
|
<div
|
||||||
class="pointer-events-none absolute inset-0"
|
class="group relative flex h-full flex-col justify-end p-3 sm:p-4 {banner
|
||||||
style="background:linear-gradient(to top, rgba(0,0,0,0.78) 0%, rgba(0,0,0,0.32) 42%, rgba(0,0,0,0.05) 100%)"
|
|
||||||
></div>
|
|
||||||
<div
|
|
||||||
class="relative flex h-full flex-col justify-end p-3 sm:p-4 {banner
|
|
||||||
? 'sm:flex-row sm:items-center sm:justify-between sm:gap-4'
|
? 'sm:flex-row sm:items-center sm:justify-between sm:gap-4'
|
||||||
: ''}"
|
: ''}"
|
||||||
>
|
>
|
||||||
<div class="min-w-0">
|
<div class="min-w-0">
|
||||||
<div
|
<div
|
||||||
class="truncate font-semibold text-white {featured ? 'text-xl sm:text-2xl' : 'text-base'}"
|
class="truncate text-white {featured ? 'text-xl sm:text-2xl' : 'text-base'} group-hover:text-accent-soft transition-colors"
|
||||||
>
|
>
|
||||||
{mix.title}
|
{mix.title}
|
||||||
</div>
|
</div>
|
||||||
{#if mix.subtitle}
|
{#if mix.subtitle}
|
||||||
<div class="mt-0.5 truncate text-xs text-white/70 sm:text-sm">{mix.subtitle}</div>
|
<div class="mt-0.5 truncate text-xs text-muted sm:text-sm">{mix.subtitle}</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,9 +11,8 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<span
|
<span
|
||||||
class="absolute right-2.5 bottom-2.5 grid h-11 w-11 translate-y-2 place-items-center rounded-full bg-accent-soft text-on-accent opacity-0 shadow-lg transition-all group-hover:translate-y-0 group-hover:opacity-100"
|
class="absolute right-2.5 bottom-2.5 grid h-11 w-11 place-items-center rounded-full bg-accent-soft text-on-accent opacity-0 shadow-lg transition-opacity group-hover:opacity-100"
|
||||||
class:!opacity-100={active}
|
class:!opacity-100={active}
|
||||||
class:!translate-y-0={active}
|
|
||||||
>
|
>
|
||||||
{#if active && playing}
|
{#if active && playing}
|
||||||
<Pause class="h-5 w-5" fill="currentColor" />
|
<Pause class="h-5 w-5" fill="currentColor" />
|
||||||
|
|||||||
@@ -11,12 +11,16 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if actions}
|
{#if actions}
|
||||||
<div class="mb-3 flex items-baseline justify-between gap-4 sm:mb-4 {klass}">
|
<div class="mb-3 flex items-baseline justify-between gap-4 sm:mb-4 {klass} tracking-widest">
|
||||||
<h2 class="text-lg tracking-tight text-fg sm:text-[28px]">{title}</h2>
|
<h2 class="font-display font-medium text-fg sm:text-xl">
|
||||||
|
{title}
|
||||||
|
</h2>
|
||||||
{@render actions()}
|
{@render actions()}
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<h2 class="mb-3 text-lg tracking-tight text-fg sm:mb-4 sm:text-[28px] {klass}">
|
<h2
|
||||||
|
class="mb-3 font-display font-medium text-fg sm:mb-4 sm:text-xl {klass}"
|
||||||
|
>
|
||||||
{title}
|
{title}
|
||||||
</h2>
|
</h2>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ export const EXPLORE_ALBUMS_PAGE_SIZE = 10;
|
|||||||
export const HOME_LATEST_PAGE_SIZE = 10;
|
export const HOME_LATEST_PAGE_SIZE = 10;
|
||||||
export const HOME_ALBUMS_LIMIT = 12;
|
export const HOME_ALBUMS_LIMIT = 12;
|
||||||
export const HOME_MIXES_BENTO = 5;
|
export const HOME_MIXES_BENTO = 5;
|
||||||
export const HOME_ARTISTS_LIMIT = 10;
|
|
||||||
export const HOME_SHELF_LIMIT = 6;
|
export const HOME_SHELF_LIMIT = 6;
|
||||||
export const YOUTUBE_FILLER_LIMIT = 27;
|
export const YOUTUBE_FILLER_LIMIT = 27;
|
||||||
export const PLAYLIST_PICKER_PAGE_SIZE = 50;
|
export const PLAYLIST_PICKER_PAGE_SIZE = 50;
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
export interface HomeArtist {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const FILLER_ARTISTS = [
|
|
||||||
'Nova Reyes',
|
|
||||||
'The Paper Lanterns',
|
|
||||||
'Kairo',
|
|
||||||
'Lucía Marés',
|
|
||||||
'Midnight Transit',
|
|
||||||
'Sundown Kids',
|
|
||||||
'Aria Volkov',
|
|
||||||
'El Bosque Rojo',
|
|
||||||
'Halcyon',
|
|
||||||
'Marea Baja'
|
|
||||||
];
|
|
||||||
|
|
||||||
export function pickTopArtists(tracks: { artist?: string | null }[], limit: number): HomeArtist[] {
|
|
||||||
const seen = new Set<string>();
|
|
||||||
const artists: HomeArtist[] = [];
|
|
||||||
|
|
||||||
for (const track of tracks) {
|
|
||||||
for (const name of (track.artist ?? '').split(',').map((part) => part.trim())) {
|
|
||||||
const key = name.toLowerCase();
|
|
||||||
if (!name || seen.has(key)) continue;
|
|
||||||
seen.add(key);
|
|
||||||
artists.push({ id: name, name });
|
|
||||||
if (artists.length >= limit) return artists;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const name of FILLER_ARTISTS) {
|
|
||||||
const key = name.toLowerCase();
|
|
||||||
if (seen.has(key)) continue;
|
|
||||||
seen.add(key);
|
|
||||||
artists.push({ id: name, name });
|
|
||||||
if (artists.length >= limit) break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return artists;
|
|
||||||
}
|
|
||||||
@@ -5,9 +5,9 @@
|
|||||||
--mf-text: #c7c6c5;
|
--mf-text: #c7c6c5;
|
||||||
--mf-text-2: #a5a4a3;
|
--mf-text-2: #a5a4a3;
|
||||||
--mf-text-3: #8c8b8a;
|
--mf-text-3: #8c8b8a;
|
||||||
--mf-text-4: #7f7e7d;
|
--mf-text-4: rgb(120, 120, 120);
|
||||||
|
|
||||||
--mf-surface: rgb(18, 18, 18);
|
--mf-surface: rgb(15, 15, 15);
|
||||||
--mf-surface-2: rgba(255, 255, 255, 0.06);
|
--mf-surface-2: rgba(255, 255, 255, 0.06);
|
||||||
--mf-surface-hover: rgba(255, 255, 255, 0.09);
|
--mf-surface-hover: rgba(255, 255, 255, 0.09);
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,6 @@
|
|||||||
import InfiniteScroll from '$lib/components/ui/InfiniteScroll.svelte';
|
import InfiniteScroll from '$lib/components/ui/InfiniteScroll.svelte';
|
||||||
import Cover from '$lib/components/ui/Cover.svelte';
|
import Cover from '$lib/components/ui/Cover.svelte';
|
||||||
import PlaylistArt from '$lib/components/ui/PlaylistArt.svelte';
|
import PlaylistArt from '$lib/components/ui/PlaylistArt.svelte';
|
||||||
import CoverBadge from '$lib/components/ui/CoverBadge.svelte';
|
|
||||||
import ArtistAvatar from '$lib/components/ui/ArtistAvatar.svelte';
|
import ArtistAvatar from '$lib/components/ui/ArtistAvatar.svelte';
|
||||||
import Rail from '$lib/components/ui/Rail.svelte';
|
import Rail from '$lib/components/ui/Rail.svelte';
|
||||||
import NowPlaying from '$lib/components/ui/NowPlaying.svelte';
|
import NowPlaying from '$lib/components/ui/NowPlaying.svelte';
|
||||||
@@ -223,7 +222,7 @@
|
|||||||
<Page>
|
<Page>
|
||||||
<PageHeader title="Buscar" subtitle="Encuentra canciones, álbumes y personas." />
|
<PageHeader title="Buscar" subtitle="Encuentra canciones, álbumes y personas." />
|
||||||
|
|
||||||
<div class="mt-8">
|
<div class="mt-6">
|
||||||
<Input
|
<Input
|
||||||
type="search"
|
type="search"
|
||||||
icon={Search}
|
icon={Search}
|
||||||
@@ -246,8 +245,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if hasAlbums}
|
{#if hasAlbums}
|
||||||
<div class="mt-10">
|
<div class="mt-8">
|
||||||
<SectionHeading title="Álbumes" />
|
|
||||||
<MediaGrid as="ul" min="168px" minMobile="150px">
|
<MediaGrid as="ul" min="168px" minMobile="150px">
|
||||||
{#each albumEntries as entry, i (entry.kind === 'local' ? entry.album.id : entry.album.albumId)}
|
{#each albumEntries as entry, i (entry.kind === 'local' ? entry.album.id : entry.album.albumId)}
|
||||||
<li>
|
<li>
|
||||||
@@ -264,9 +262,8 @@
|
|||||||
<PlaylistArt
|
<PlaylistArt
|
||||||
trackIds={entry.album.coverTrackIds}
|
trackIds={entry.album.coverTrackIds}
|
||||||
hue={hueFor(entry.album.id)}
|
hue={hueFor(entry.album.id)}
|
||||||
class="aspect-square w-full rounded-art shadow-art ring-1 ring-line transition duration-300 ease-out ring-inset group-hover/card:-translate-y-1 group-hover/card:shadow-art-lg"
|
class="aspect-square w-full rounded-art shadow-art ring-1 ring-line transition duration-300 ease-out ring-inset group-hover/card:shadow-art-lg"
|
||||||
/>
|
/>
|
||||||
<CoverBadge label="Álbum" />
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="mt-2.5 truncate text-fg transition-colors group-hover/card:text-accent-soft"
|
class="mt-2.5 truncate text-fg transition-colors group-hover/card:text-accent-soft"
|
||||||
@@ -291,9 +288,8 @@
|
|||||||
hue={hueFor(entry.album.albumId)}
|
hue={hueFor(entry.album.albumId)}
|
||||||
size="large"
|
size="large"
|
||||||
alt={entry.album.title}
|
alt={entry.album.title}
|
||||||
class="aspect-square w-full rounded-art shadow-art ring-1 ring-line transition duration-300 ease-out ring-inset group-hover/card:-translate-y-1 group-hover/card:shadow-art-lg"
|
class="aspect-square w-full rounded-art shadow-art ring-1 ring-line transition duration-300 ease-out ring-inset group-hover/card:shadow-art-lg"
|
||||||
/>
|
/>
|
||||||
<CoverBadge label="Álbum" />
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="mt-2.5 truncate text-fg transition-colors group-hover/card:text-accent-soft"
|
class="mt-2.5 truncate text-fg transition-colors group-hover/card:text-accent-soft"
|
||||||
@@ -312,7 +308,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if hasUsers}
|
{#if hasUsers}
|
||||||
<div class="mt-10">
|
<div class="mt-8">
|
||||||
<SectionHeading title="Usuarios" />
|
<SectionHeading title="Usuarios" />
|
||||||
<Rail>
|
<Rail>
|
||||||
{#each users as u, i (u.id)}
|
{#each users as u, i (u.id)}
|
||||||
@@ -328,8 +324,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="mt-10">
|
<div class="mt-6">
|
||||||
<SectionHeading title="Canciones" />
|
|
||||||
{#if songRows.length > 0}
|
{#if songRows.length > 0}
|
||||||
<ul class="flex flex-col gap-1">
|
<ul class="flex flex-col gap-1">
|
||||||
{#each songRows as { target: item, seconds }, i (targetId(item))}
|
{#each songRows as { target: item, seconds }, i (targetId(item))}
|
||||||
@@ -363,7 +358,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{#if targetArtist(item)}
|
{#if targetArtist(item)}
|
||||||
<div class="truncate text-sm text-fg-3">{targetArtist(item)}</div>
|
<div class="truncate text-sm text-fg-3">Canción · {targetArtist(item)}</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<span class="shrink-0 text-sm text-muted tabular-nums">{fmtTime(seconds)}</span>
|
<span class="shrink-0 text-sm text-muted tabular-nums">{fmtTime(seconds)}</span>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import '@fontsource-variable/inter/index.css';
|
import '@fontsource-variable/inter/index.css';
|
||||||
|
import '@fontsource-variable/sora/index.css';
|
||||||
import '$lib/theme/theme.css';
|
import '$lib/theme/theme.css';
|
||||||
import './layout.css';
|
import './layout.css';
|
||||||
import favicon from '$lib/assets/favicon.svg';
|
import favicon from '$lib/assets/favicon.svg';
|
||||||
|
|||||||
@@ -4,14 +4,12 @@ import { fetchYoutubeFiller } from '$lib/server/youtube';
|
|||||||
import { addTrackAction, addYouTubeToPlaylistAction } from '$lib/server/playlistActions';
|
import { addTrackAction, addYouTubeToPlaylistAction } from '$lib/server/playlistActions';
|
||||||
import {
|
import {
|
||||||
HOME_ALBUMS_LIMIT,
|
HOME_ALBUMS_LIMIT,
|
||||||
HOME_ARTISTS_LIMIT,
|
|
||||||
HOME_LATEST_PAGE_SIZE,
|
HOME_LATEST_PAGE_SIZE,
|
||||||
HOME_MIXES_BENTO,
|
HOME_MIXES_BENTO,
|
||||||
HOME_SHELF_LIMIT,
|
HOME_SHELF_LIMIT,
|
||||||
YOUTUBE_FILLER_LIMIT
|
YOUTUBE_FILLER_LIMIT
|
||||||
} from '$lib/config';
|
} from '$lib/config';
|
||||||
import { pickGreeting } from '$lib/server/greeting';
|
import { pickGreeting } from '$lib/server/greeting';
|
||||||
import { pickTopArtists } from '$lib/server/topArtists';
|
|
||||||
|
|
||||||
export const load: PageServerLoad = async ({ locals, url, fetch }) => {
|
export const load: PageServerLoad = async ({ locals, url, fetch }) => {
|
||||||
const user = requireUser(locals, url);
|
const user = requireUser(locals, url);
|
||||||
@@ -52,7 +50,6 @@ export const load: PageServerLoad = async ({ locals, url, fetch }) => {
|
|||||||
playlists: playlistItems.slice(0, HOME_SHELF_LIMIT),
|
playlists: playlistItems.slice(0, HOME_SHELF_LIMIT),
|
||||||
playlistsHasMore: playlistItems.length > HOME_SHELF_LIMIT,
|
playlistsHasMore: playlistItems.length > HOME_SHELF_LIMIT,
|
||||||
newReleases: latestItems,
|
newReleases: latestItems,
|
||||||
topArtists: pickTopArtists(latestItems, HOME_ARTISTS_LIMIT),
|
|
||||||
youtubeFiller: fetchYoutubeFiller(
|
youtubeFiller: fetchYoutubeFiller(
|
||||||
api,
|
api,
|
||||||
locals.accessToken,
|
locals.accessToken,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Music from '@lucide/svelte/icons/music';
|
import Music from '@lucide/svelte/icons/music';
|
||||||
import ListMusic from '@lucide/svelte/icons/list-music';
|
import ArrowRight from '@lucide/svelte/icons/arrow-right';
|
||||||
import { player, toQueueItems, type QueueItem } from '$lib/player/player.svelte';
|
import { player, toQueueItems, type QueueItem } from '$lib/player/player.svelte';
|
||||||
import {
|
import {
|
||||||
isTargetCurrent,
|
isTargetCurrent,
|
||||||
@@ -19,11 +19,9 @@
|
|||||||
import MixBentoTile from '$lib/components/ui/MixBentoTile.svelte';
|
import MixBentoTile from '$lib/components/ui/MixBentoTile.svelte';
|
||||||
import Rail from '$lib/components/ui/Rail.svelte';
|
import Rail from '$lib/components/ui/Rail.svelte';
|
||||||
import CoverBadge from '$lib/components/ui/CoverBadge.svelte';
|
import CoverBadge from '$lib/components/ui/CoverBadge.svelte';
|
||||||
import ArtistAvatar from '$lib/components/ui/ArtistAvatar.svelte';
|
|
||||||
import NowPlaying from '$lib/components/ui/NowPlaying.svelte';
|
import NowPlaying from '$lib/components/ui/NowPlaying.svelte';
|
||||||
import EmptyState from '$lib/components/ui/EmptyState.svelte';
|
import EmptyState from '$lib/components/ui/EmptyState.svelte';
|
||||||
import ExplicitBadge from '$lib/components/ui/ExplicitBadge.svelte';
|
import ExplicitBadge from '$lib/components/ui/ExplicitBadge.svelte';
|
||||||
import MediaGrid from '$lib/components/ui/MediaGrid.svelte';
|
|
||||||
import SectionHeading from '$lib/components/ui/SectionHeading.svelte';
|
import SectionHeading from '$lib/components/ui/SectionHeading.svelte';
|
||||||
import TrackContextMenu, {
|
import TrackContextMenu, {
|
||||||
contextMenuStateFor,
|
contextMenuStateFor,
|
||||||
@@ -31,6 +29,7 @@
|
|||||||
} from '$lib/components/TrackContextMenu.svelte';
|
} from '$lib/components/TrackContextMenu.svelte';
|
||||||
import type { YouTubeSong } from '$lib/types';
|
import type { YouTubeSong } from '$lib/types';
|
||||||
import { appendUnique } from '$lib/collections';
|
import { appendUnique } from '$lib/collections';
|
||||||
|
import { HOME_LATEST_PAGE_SIZE } from '$lib/config';
|
||||||
|
|
||||||
let { data } = $props();
|
let { data } = $props();
|
||||||
|
|
||||||
@@ -52,7 +51,6 @@
|
|||||||
const albums = $derived(data.albums);
|
const albums = $derived(data.albums);
|
||||||
const mixes = $derived(data.mixes);
|
const mixes = $derived(data.mixes);
|
||||||
const playlists = $derived(data.playlists);
|
const playlists = $derived(data.playlists);
|
||||||
const artists = $derived(data.topArtists);
|
|
||||||
|
|
||||||
const newTargets = $derived<TrackTarget[]>(
|
const newTargets = $derived<TrackTarget[]>(
|
||||||
data.newReleases.map((track) => ({ kind: 'local' as const, track }))
|
data.newReleases.map((track) => ({ kind: 'local' as const, track }))
|
||||||
@@ -90,7 +88,7 @@
|
|||||||
explicit: item.explicit
|
explicit: item.explicit
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return merged.slice(0, 12);
|
return merged.slice(0, HOME_LATEST_PAGE_SIZE);
|
||||||
});
|
});
|
||||||
|
|
||||||
function playRecent(index: number) {
|
function playRecent(index: number) {
|
||||||
@@ -142,8 +140,9 @@
|
|||||||
|
|
||||||
{#if recentlyPlayed.length > 0}
|
{#if recentlyPlayed.length > 0}
|
||||||
<section class="page-x pt-6 sm:pt-8">
|
<section class="page-x pt-6 sm:pt-8">
|
||||||
<SectionHeading title="Escuchar otra vez" />
|
<div
|
||||||
<MediaGrid min="260px" minMobile="220px">
|
class="grid grid-cols-1 gap-3 sm:grid-cols-2 sm:gap-4 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5"
|
||||||
|
>
|
||||||
{#each recentlyPlayed as track, i (track.id)}
|
{#each recentlyPlayed as track, i (track.id)}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -152,7 +151,7 @@
|
|||||||
aria-label="{player.current.id === track.id && player.playing
|
aria-label="{player.current.id === track.id && player.playing
|
||||||
? 'Pausar'
|
? 'Pausar'
|
||||||
: 'Reproducir'} {track.title}"
|
: 'Reproducir'} {track.title}"
|
||||||
class="group/row flex min-w-0 items-center gap-3 rounded-control bg-surface p-2 text-left transition duration-200"
|
class="group/row flex min-w-0 items-center gap-3 rounded-control bg-surface p-2.5 text-left transition duration-200"
|
||||||
>
|
>
|
||||||
<Cover
|
<Cover
|
||||||
trackId={track.id}
|
trackId={track.id}
|
||||||
@@ -160,7 +159,7 @@
|
|||||||
hue={hueFor(track.id)}
|
hue={hueFor(track.id)}
|
||||||
size="small"
|
size="small"
|
||||||
alt={track.title}
|
alt={track.title}
|
||||||
class="h-14 w-14 shrink-0 rounded-control shadow-art ring-line transition duration-200"
|
class="h-14 w-14 shrink-0 rounded-control shadow-art transition duration-200"
|
||||||
>
|
>
|
||||||
{#if player.current.id === track.id}
|
{#if player.current.id === track.id}
|
||||||
<NowPlaying paused={!player.playing} />
|
<NowPlaying paused={!player.playing} />
|
||||||
@@ -181,12 +180,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
</MediaGrid>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if albums.length > 0}
|
{#if albums.length > 0}
|
||||||
<section class="page-x pt-10 sm:pt-14">
|
<section class="page-x pt-10">
|
||||||
<SectionHeading title="Álbumes recién escuchados">
|
<SectionHeading title="Álbumes recién escuchados">
|
||||||
{#snippet actions()}
|
{#snippet actions()}
|
||||||
<a href="/albums" class="text-sm text-fg-3 transition hover:text-accent-soft">Ver todos</a>
|
<a href="/albums" class="text-sm text-fg-3 transition hover:text-accent-soft">Ver todos</a>
|
||||||
@@ -203,7 +202,7 @@
|
|||||||
<PlaylistArt
|
<PlaylistArt
|
||||||
trackIds={album.coverTrackIds}
|
trackIds={album.coverTrackIds}
|
||||||
hue={hueFor(album.id)}
|
hue={hueFor(album.id)}
|
||||||
class="h-[168px] w-[168px] rounded-art shadow-art ring-1 ring-line transition duration-300 ease-out ring-inset group-hover/card:-translate-y-1 group-hover/card:shadow-art-lg"
|
class="h-[168px] w-[168px] rounded-art shadow-art ring-1 ring-line transition duration-300 ease-out ring-inset group-hover/card:shadow-art-lg"
|
||||||
/>
|
/>
|
||||||
<CoverBadge label="Álbum" />
|
<CoverBadge label="Álbum" />
|
||||||
</div>
|
</div>
|
||||||
@@ -218,9 +217,8 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if mixes.length > 0}
|
{#if mixes.length > 0}
|
||||||
<section class="page-x pt-10 sm:pt-14">
|
<section class="page-x pt-10">
|
||||||
<SectionHeading title="Hecho para ti" />
|
<div class="grid grid-cols-2 gap-3 sm:auto-rows-25 sm:grid-cols-4 sm:gap-4">
|
||||||
<div class="grid grid-cols-2 gap-3 sm:auto-rows-[100px] sm:grid-cols-4 sm:gap-4">
|
|
||||||
{#each mixes as mix, i (mix.id)}
|
{#each mixes as mix, i (mix.id)}
|
||||||
<MixBentoTile
|
<MixBentoTile
|
||||||
{mix}
|
{mix}
|
||||||
@@ -228,12 +226,12 @@
|
|||||||
featured={i === 0}
|
featured={i === 0}
|
||||||
banner={i === 4}
|
banner={i === 4}
|
||||||
class={i === 0
|
class={i === 0
|
||||||
? 'col-span-2 min-h-[168px] sm:row-span-2 sm:min-h-0'
|
? 'col-span-2 min-h-42 sm:row-span-2 sm:min-h-0'
|
||||||
: i === 1
|
: i === 1
|
||||||
? 'col-span-2 min-h-[120px] sm:min-h-0'
|
? 'col-span-2 min-h-30 sm:min-h-0'
|
||||||
: i === 4
|
: i === 4
|
||||||
? 'col-span-2 min-h-[96px] sm:col-span-4 sm:min-h-0'
|
? 'col-span-2 min-h-24 sm:col-span-4 sm:min-h-0'
|
||||||
: 'min-h-[120px] sm:min-h-0'}
|
: 'min-h-30 sm:min-h-0'}
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
@@ -241,21 +239,12 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if playlists.length > 0}
|
{#if playlists.length > 0}
|
||||||
<section class="page-x pt-10 sm:pt-14">
|
<section class="page-x pt-10">
|
||||||
<SectionHeading title="Tus playlists">
|
|
||||||
{#snippet actions()}
|
|
||||||
{#if data.playlistsHasMore}
|
|
||||||
<a href="/playlists" class="text-sm text-fg-3 transition hover:text-accent-soft">
|
|
||||||
Ver todas
|
|
||||||
</a>
|
|
||||||
{/if}
|
|
||||||
{/snippet}
|
|
||||||
</SectionHeading>
|
|
||||||
<Rail>
|
<Rail>
|
||||||
{#each playlists as playlist, i (playlist.id)}
|
{#each playlists as playlist, i (playlist.id)}
|
||||||
<a
|
<a
|
||||||
href="/playlists/{playlist.id}"
|
href="/playlists/{playlist.id}"
|
||||||
class="group/card animate-enter block w-[150px] 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-36 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"
|
||||||
>
|
>
|
||||||
<PlaylistArt
|
<PlaylistArt
|
||||||
@@ -263,21 +252,23 @@
|
|||||||
trackIds={playlist.coverTrackIds}
|
trackIds={playlist.coverTrackIds}
|
||||||
hue={hueFor(playlist.id)}
|
hue={hueFor(playlist.id)}
|
||||||
version={playlist.updatedAt}
|
version={playlist.updatedAt}
|
||||||
class="h-[150px] w-[150px] rounded-art shadow-art ring-1 ring-line transition duration-300 ease-out ring-inset group-hover/card:-translate-y-1 group-hover/card:shadow-art-lg"
|
class="h-36 w-36 rounded-art"
|
||||||
/>
|
/>
|
||||||
<div class="mt-2.5 truncate text-fg transition-colors group-hover/card:text-accent-soft">
|
<div class="mt-2.5 truncate text-fg transition-colors group-hover/card:text-accent-soft">
|
||||||
{playlist.name}
|
{playlist.name}
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-0.5 truncate text-sm text-fg-3">{playlist.description ?? 'Lista'}</div>
|
<div class="mt-0.5 truncate text-sm text-muted">{playlist.description}</div>
|
||||||
</a>
|
</a>
|
||||||
{/each}
|
{/each}
|
||||||
<a
|
<a
|
||||||
href="/playlists"
|
href="/playlists"
|
||||||
class="grid h-[150px] w-[150px] shrink-0 place-items-center rounded-art border border-dashed border-line-strong text-fg-3 transition hover:border-accent hover:text-accent-soft"
|
class="grid h-36 w-36 shrink-0 place-items-center text-fg-3 transition-colors hover:text-accent-soft"
|
||||||
>
|
>
|
||||||
<div class="flex flex-col items-center gap-2">
|
<div class="flex flex-col items-center gap-2">
|
||||||
<ListMusic class="h-6 w-6" />
|
<span class="grid h-8 w-8 place-items-center">
|
||||||
<span class="text-sm">Ver todas</span>
|
<ArrowRight class="h-4 w-4" />
|
||||||
|
</span>
|
||||||
|
Ver todas
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</Rail>
|
</Rail>
|
||||||
@@ -285,7 +276,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if newTargets.length > 0}
|
{#if newTargets.length > 0}
|
||||||
<section class="page-x pt-10 sm:pt-14">
|
<section class="page-x pt-10">
|
||||||
<SectionHeading title="Canciones nuevas" />
|
<SectionHeading title="Canciones nuevas" />
|
||||||
<Rail>
|
<Rail>
|
||||||
{#each newTargets as target, i (targetId(target))}
|
{#each newTargets as target, i (targetId(target))}
|
||||||
@@ -304,7 +295,7 @@
|
|||||||
hue={hueFor(targetId(target))}
|
hue={hueFor(targetId(target))}
|
||||||
size="medium"
|
size="medium"
|
||||||
alt={targetTitle(target)}
|
alt={targetTitle(target)}
|
||||||
class="h-full w-full rounded-art shadow-art ring-1 ring-line transition duration-300 ease-out ring-inset group-hover/card:-translate-y-1 group-hover/card:shadow-art-lg"
|
class="h-full w-full rounded-art shadow-art ring-1 ring-line transition duration-300 ease-out ring-inset group-hover/card:shadow-art-lg"
|
||||||
>
|
>
|
||||||
{#if isTargetCurrent(target)}
|
{#if isTargetCurrent(target)}
|
||||||
<NowPlaying paused={!player.playing} />
|
<NowPlaying paused={!player.playing} />
|
||||||
@@ -329,24 +320,8 @@
|
|||||||
</section>
|
</section>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if artists.length > 0}
|
<!---
|
||||||
<section class="page-x pt-10 sm:pt-14">
|
<section class="page-x pt-10 pb-16">
|
||||||
<SectionHeading title="Top artistas del momento" />
|
|
||||||
<Rail>
|
|
||||||
{#each artists as artist, i (artist.id)}
|
|
||||||
<div
|
|
||||||
class="group/artist animate-enter shrink-0"
|
|
||||||
style="animation-delay:{Math.min(i, 10) * 45}ms"
|
|
||||||
>
|
|
||||||
<ArtistAvatar id={artist.id} name={artist.name} />
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</Rail>
|
|
||||||
</section>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<section class="page-x pt-10 pb-16 sm:pt-14">
|
|
||||||
<SectionHeading title="Recomendado para ti" />
|
|
||||||
{#if recTargets.length > 0}
|
{#if recTargets.length > 0}
|
||||||
<Rail>
|
<Rail>
|
||||||
{#each recTargets as target, i (targetId(target))}
|
{#each recTargets as target, i (targetId(target))}
|
||||||
@@ -365,7 +340,7 @@
|
|||||||
hue={hueFor(targetId(target))}
|
hue={hueFor(targetId(target))}
|
||||||
size="medium"
|
size="medium"
|
||||||
alt={targetTitle(target)}
|
alt={targetTitle(target)}
|
||||||
class="h-full w-full rounded-art shadow-art ring-1 ring-line transition duration-300 ease-out ring-inset group-hover/card:-translate-y-1 group-hover/card:shadow-art-lg"
|
class="h-full w-full rounded-art shadow-art ring-1 ring-line transition duration-300 ease-out ring-inset group-hover/card:shadow-art-lg"
|
||||||
>
|
>
|
||||||
{#if isTargetCurrent(target)}
|
{#if isTargetCurrent(target)}
|
||||||
<NowPlaying paused={!player.playing} />
|
<NowPlaying paused={!player.playing} />
|
||||||
@@ -389,7 +364,7 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<EmptyState icon={Music} title="Todavía no hay recomendaciones" />
|
<EmptyState icon={Music} title="Todavía no hay recomendaciones" />
|
||||||
{/if}
|
{/if}
|
||||||
</section>
|
</section>-->
|
||||||
|
|
||||||
{#if contextMenu}
|
{#if contextMenu}
|
||||||
<TrackContextMenu
|
<TrackContextMenu
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
@theme {
|
@theme {
|
||||||
--font-sans: 'Inter Variable', ui-sans-serif, system-ui, sans-serif;
|
--font-sans: 'Inter Variable', ui-sans-serif, system-ui, sans-serif;
|
||||||
|
--font-display: 'Sora Variable', 'Inter Variable', ui-sans-serif, system-ui, sans-serif;
|
||||||
|
|
||||||
--color-bg: var(--mf-bg);
|
--color-bg: var(--mf-bg);
|
||||||
--color-elevated: var(--mf-elevated);
|
--color-elevated: var(--mf-elevated);
|
||||||
@@ -36,10 +37,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@utility page-x {
|
@utility page-x {
|
||||||
padding-inline: --spacing(4);
|
padding-inline: --spacing(6);
|
||||||
|
|
||||||
@media (width >= 40rem) {
|
@media (width >= 40rem) {
|
||||||
padding-inline: --spacing(8);
|
padding-inline: --spacing(10);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user