Checkpoint web-player design-system refactor and backend track genre tagging

Advances the ongoing token/componentization cleanup (anchored player dock,
border-based panels instead of floating shadows, corrected light-mode
accent/gradient tokens) alongside in-progress backend support for track
genre tags.
This commit is contained in:
2026-09-17 17:43:37 +02:00
parent 5ae9955bd0
commit c14db5a711
83 changed files with 1872 additions and 248 deletions
@@ -48,5 +48,7 @@ namespace Musify.Domain.Entities
public ICollection<AlbumHasTrack> AlbumTracks { get; set; } = new List<AlbumHasTrack>();
public ICollection<ListeningHistory> ListeningHistories { get; set; } = new List<ListeningHistory>();
public ICollection<TrackTag> Tags { get; set; } = new List<TrackTag>();
}
}
@@ -0,0 +1,22 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Musify.Domain.ValueObjects;
namespace Musify.Domain.Entities
{
#pragma warning disable CS8618
public class TrackTag
{
[Key]
public Guid Id { get; set; } = Guid.NewGuid();
[Required]
public required Guid TrackId { get; set; }
[Required]
public required Genre Tag { get; set; }
[ForeignKey(nameof(TrackId))]
public Track Track { get; set; }
}
}
@@ -0,0 +1,32 @@
namespace Musify.Domain.ValueObjects
{
public enum Genre
{
Pop = 0,
Rock = 1,
HipHop = 2,
RnB = 3,
Jazz = 4,
Blues = 5,
Classical = 6,
Electronic = 7,
House = 8,
Techno = 9,
Trance = 10,
Dubstep = 11,
DrumAndBass = 12,
Metal = 13,
Punk = 14,
Reggae = 15,
Reggaeton = 16,
Country = 17,
Folk = 18,
Indie = 19,
KPop = 20,
Latin = 21,
Soul = 22,
Funk = 23,
Ambient = 24,
Lofi = 25
}
}
@@ -0,0 +1,54 @@
namespace Musify.Domain.ValueObjects
{
/// <summary>
/// Encodes which genre pairs are mutually exclusive on a single track (e.g. a track cannot
/// be tagged as both Classical and Metal at the same time).
/// </summary>
public static class GenreCompatibility
{
private static readonly HashSet<(Genre, Genre)> IncompatiblePairs = BuildIncompatiblePairs();
private static HashSet<(Genre, Genre)> BuildIncompatiblePairs()
{
var pairs = new HashSet<(Genre, Genre)>();
void Add(Genre a, Genre b)
{
pairs.Add((a, b));
pairs.Add((b, a));
}
var intense = new[] { Genre.Metal, Genre.Punk, Genre.Dubstep, Genre.DrumAndBass };
var mellow = new[] { Genre.Classical, Genre.Ambient, Genre.Lofi };
foreach (var intenseGenre in intense)
foreach (var mellowGenre in mellow)
Add(intenseGenre, mellowGenre);
return pairs;
}
public static bool AreCompatible(Genre a, Genre b) =>
a == b || !IncompatiblePairs.Contains((a, b));
/// <summary>
/// Returns every mutually exclusive genre pair found within the given set of tags.
/// </summary>
public static IReadOnlyCollection<(Genre First, Genre Second)> FindConflicts(IEnumerable<Genre> tags)
{
var distinctTags = tags.Distinct().ToList();
var conflicts = new List<(Genre, Genre)>();
for (var i = 0; i < distinctTags.Count; i++)
{
for (var j = i + 1; j < distinctTags.Count; j++)
{
if (!AreCompatible(distinctTags[i], distinctTags[j]))
conflicts.Add((distinctTags[i], distinctTags[j]));
}
}
return conflicts;
}
}
}