feat(lucky-mix): instant mix from your preferences

Lucky Mix targets ~50 tracks in one run: pick seeds from your most-played artists/albums and 4+ rated songs, add similar tracks, then fill the mix with random library picks, skipping anything you rated 1–2.
On start it trims the old “upcoming” tail so the queue does not show stale next tracks, starts playback on the first viable seed, routes to Now Playing, and can emit structured steps to the backend debug log.
This commit is contained in:
Maxim Isaev
2026-04-23 18:42:52 +03:00
parent 694567843f
commit ab5c8e0b48
24 changed files with 797 additions and 17 deletions
+191
View File
@@ -2427,6 +2427,197 @@
box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 25%, transparent);
}
/* ─ Lucky Mix overlay (big rolling dice) ─ */
.lucky-mix-overlay {
position: fixed;
inset: 0;
z-index: 10050;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 12px;
background: color-mix(in srgb, var(--ctp-mantle) 84%, transparent);
backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(5px);
pointer-events: all;
}
.lucky-mix-overlay__dice {
position: relative;
width: 240px;
height: 190px;
}
.lucky-mix-cube {
position: absolute;
width: 86px;
height: 86px;
border-radius: 18px;
border: 2px solid color-mix(in srgb, #fff 70%, var(--accent) 30%);
background: linear-gradient(
160deg,
color-mix(in srgb, var(--accent) 72%, #fff 28%) 0%,
color-mix(in srgb, var(--accent) 84%, #000 16%) 100%
);
box-shadow:
0 12px 28px rgba(0, 0, 0, 0.4),
inset -8px -10px 18px rgba(0, 0, 0, 0.2),
inset 6px 8px 16px rgba(255, 255, 255, 0.2);
}
.lucky-mix-cube--a {
left: 10px;
top: 74px;
transform: rotate(-12deg);
animation: luckyCubeA 980ms ease-in-out infinite;
}
.lucky-mix-cube--b {
left: 76px;
top: 18px;
width: 98px;
height: 98px;
transform: rotate(9deg);
animation: luckyCubeB 900ms ease-in-out infinite;
}
.lucky-mix-cube--c {
left: 152px;
top: 82px;
transform: rotate(15deg);
animation: luckyCubeC 1120ms ease-in-out infinite;
}
.lucky-mix-pip {
position: absolute;
width: 14px;
height: 14px;
border-radius: 50%;
background: #fff;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.35);
}
.lucky-mix-pip--tl { top: 16px; left: 16px; }
.lucky-mix-pip--tr { top: 16px; right: 16px; }
.lucky-mix-pip--bl { bottom: 16px; left: 16px; }
.lucky-mix-pip--br { bottom: 16px; right: 16px; }
.lucky-mix-pip--center {
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.lucky-mix-overlay__title {
font-family: var(--font-display);
font-size: clamp(30px, 4.5vw, 52px);
font-weight: 800;
color: #fff;
letter-spacing: 0.01em;
text-shadow: 0 10px 32px rgba(0, 0, 0, 0.45);
}
.lucky-mix-overlay__phase {
font-size: clamp(14px, 2vw, 18px);
color: color-mix(in srgb, #fff 78%, var(--accent) 22%);
font-weight: 600;
}
@keyframes luckyCubeA {
0% { transform: translate(0, 0) rotate(0deg) scale(1); }
50% { transform: translate(-7px, -14px) rotate(-20deg) scale(1.06); }
100% { transform: translate(0, 0) rotate(0deg) scale(1); }
}
@keyframes luckyCubeB {
0% { transform: translate(0, 0) rotate(0deg) scale(1.02); }
50% { transform: translate(6px, -18px) rotate(13deg) scale(1.09); }
100% { transform: translate(0, 0) rotate(0deg) scale(1.02); }
}
@keyframes luckyCubeC {
0% { transform: translate(0, 0) rotate(0deg) scale(1); }
50% { transform: translate(8px, -12px) rotate(17deg) scale(1.04); }
100% { transform: translate(0, 0) rotate(0deg) scale(1); }
}
.queue-lucky-loading {
margin: 2px 10px 4px;
display: flex;
justify-content: center;
padding: 7px 10px;
}
.queue-lucky-loading__dice {
position: relative;
width: 56px;
height: 30px;
flex: 0 0 auto;
}
.queue-lucky-cube {
position: absolute;
width: 18px;
height: 18px;
border-radius: 5px;
border: 1px solid color-mix(in srgb, #fff 65%, var(--accent) 35%);
background: linear-gradient(
160deg,
color-mix(in srgb, var(--accent) 66%, #fff 34%) 0%,
color-mix(in srgb, var(--accent) 84%, #000 16%) 100%
);
box-shadow:
0 4px 9px rgba(0, 0, 0, 0.28),
inset -2px -3px 5px rgba(0, 0, 0, 0.18),
inset 2px 2px 4px rgba(255, 255, 255, 0.18);
}
.queue-lucky-cube .lucky-mix-pip {
width: 3px;
height: 3px;
}
.queue-lucky-cube .lucky-mix-pip--tl { top: 4px; left: 4px; }
.queue-lucky-cube .lucky-mix-pip--tr { top: 4px; right: 4px; }
.queue-lucky-cube .lucky-mix-pip--bl { bottom: 4px; left: 4px; }
.queue-lucky-cube .lucky-mix-pip--br { bottom: 4px; right: 4px; }
.queue-lucky-cube--a {
left: 0;
top: 11px;
animation: queueLuckyCubeA 760ms ease-in-out infinite;
}
.queue-lucky-cube--b {
left: 18px;
top: 3px;
animation: queueLuckyCubeB 690ms ease-in-out infinite;
}
.queue-lucky-cube--c {
left: 36px;
top: 11px;
animation: queueLuckyCubeC 830ms ease-in-out infinite;
}
@keyframes queueLuckyCubeA {
0% { transform: translateY(0) rotate(0deg); }
50% { transform: translateY(-6px) rotate(-10deg); }
100% { transform: translateY(0) rotate(0deg); }
}
@keyframes queueLuckyCubeB {
0% { transform: translateY(0) rotate(0deg); }
50% { transform: translateY(-8px) rotate(10deg); }
100% { transform: translateY(0) rotate(0deg); }
}
@keyframes queueLuckyCubeC {
0% { transform: translateY(0) rotate(0deg); }
50% { transform: translateY(-5px) rotate(12deg); }
100% { transform: translateY(0) rotate(0deg); }
}
/* ─ Playlist edit modal ─ */
.playlist-edit-modal {
max-width: 560px;