feat(nav): merge random mix & albums into a single Build a Mix hub

Replace the two separate sidebar entries (Random Mix, Random Albums) with
a single Build a Mix item (Wand2 icon) that opens a landing page at
/random. The landing shows two background-icon cards — Mix by Tracks
(/random/mix) and Mix by Albums (/random/albums) — with glow-on-hover
and responsive sizing via clamp(). Old routes updated throughout
(Home discover link, App router). All 7 locales updated
This commit is contained in:
kilyabin
2026-04-11 15:40:01 +04:00
parent dcc3e52ad1
commit 1f567ad94c
14 changed files with 217 additions and 11 deletions
+1 -1
View File
@@ -3397,7 +3397,7 @@ dependencies = [
[[package]]
name = "psysonic"
version = "1.34.7"
version = "1.34.8"
dependencies = [
"biquad",
"discord-rich-presence",
+4 -2
View File
@@ -21,6 +21,7 @@ import ArtistDetail from './pages/ArtistDetail';
import NewReleases from './pages/NewReleases';
import Favorites from './pages/Favorites';
import RandomMix from './pages/RandomMix';
import RandomLanding from './pages/RandomLanding';
import Settings from './pages/Settings';
import Login from './pages/Login';
import AlbumDetail from './pages/AlbumDetail';
@@ -347,13 +348,14 @@ function AppShell() {
<Routes>
<Route path="/" element={<Home />} />
<Route path="/albums" element={<Albums />} />
<Route path="/random-albums" element={<RandomAlbums />} />
<Route path="/random" element={<RandomLanding />} />
<Route path="/random/albums" element={<RandomAlbums />} />
<Route path="/album/:id" element={<AlbumDetail />} />
<Route path="/artists" element={<Artists />} />
<Route path="/artist/:id" element={<ArtistDetail />} />
<Route path="/new-releases" element={<NewReleases />} />
<Route path="/favorites" element={<Favorites />} />
<Route path="/random-mix" element={<RandomMix />} />
<Route path="/random/mix" element={<RandomMix />} />
<Route path="/label/:name" element={<LabelAlbums />} />
<Route path="/search" element={<SearchResults />} />
<Route path="/search/advanced" element={<AdvancedSearch />} />
+4 -5
View File
@@ -8,9 +8,9 @@ import { useSidebarStore } from '../store/sidebarStore';
import { NavLink } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import {
Disc3, Users, Music4, Radio, Settings, Heart, BarChart3, Shuffle,
PanelLeftClose, PanelLeft, HelpCircle, Dices, AudioLines, HardDriveDownload, Tags, ListMusic, Cast,
ChevronDown, Check, Music2, TrendingUp, FolderOpen, X,
Disc3, Users, Music4, Radio, Settings, Heart, BarChart3,
PanelLeftClose, PanelLeft, HelpCircle, AudioLines, HardDriveDownload, Tags, ListMusic, Cast,
ChevronDown, Check, Music2, TrendingUp, FolderOpen, X, Wand2,
} from 'lucide-react';
import PsysonicLogo from './PsysonicLogo';
import PSmallLogo from './PSmallLogo';
@@ -21,10 +21,9 @@ export const ALL_NAV_ITEMS: Record<string, { icon: React.ElementType; labelKey:
mainstage: { icon: Disc3, labelKey: 'sidebar.mainstage', to: '/', section: 'library' },
newReleases: { icon: Radio, labelKey: 'sidebar.newReleases', to: '/new-releases', section: 'library' },
allAlbums: { icon: Music4, labelKey: 'sidebar.allAlbums', to: '/albums', section: 'library' },
randomAlbums: { icon: Dices, labelKey: 'sidebar.randomAlbums', to: '/random-albums', section: 'library' },
randomPicker: { icon: Wand2, labelKey: 'sidebar.randomPicker', to: '/random', section: 'library' },
artists: { icon: Users, labelKey: 'sidebar.artists', to: '/artists', section: 'library' },
genres: { icon: Tags, labelKey: 'sidebar.genres', to: '/genres', section: 'library' },
randomMix: { icon: Shuffle, labelKey: 'sidebar.randomMix', to: '/random-mix', section: 'library' },
favorites: { icon: Heart, labelKey: 'sidebar.favorites', to: '/favorites', section: 'library' },
playlists: { icon: ListMusic, labelKey: 'sidebar.playlists', to: '/playlists', section: 'library' },
mostPlayed: { icon: TrendingUp, labelKey: 'sidebar.mostPlayed', to: '/most-played', section: 'library' },
+8
View File
@@ -5,6 +5,7 @@ export const deTranslation = {
newReleases: 'Neueste',
allAlbums: 'Alle Alben',
randomAlbums: 'Zufallsalben',
randomPicker: 'Mix erstellen',
artists: 'Künstler',
randomMix: 'Zufallsmix',
favorites: 'Favoriten',
@@ -197,6 +198,13 @@ export const deTranslation = {
removeSong: 'Aus Favoriten entfernen',
stations: 'Radiosender',
},
randomLanding: {
title: 'Mix erstellen',
mixByTracks: 'Mix nach Titeln',
mixByTracksDesc: 'Zufällige Auswahl aus deiner gesamten Mediathek',
mixByAlbums: 'Mix nach Alben',
mixByAlbumsDesc: 'Zufällige Alben für neue Entdeckungen',
},
randomAlbums: {
title: 'Zufallsalben',
refresh: 'Neu laden',
+8
View File
@@ -5,6 +5,7 @@ export const enTranslation = {
newReleases: 'New Releases',
allAlbums: 'All Albums',
randomAlbums: 'Random Albums',
randomPicker: 'Build a Mix',
artists: 'Artists',
randomMix: 'Random Mix',
favorites: 'Favorites',
@@ -198,6 +199,13 @@ export const enTranslation = {
removeSong: 'Remove from favorites',
stations: 'Radio Stations',
},
randomLanding: {
title: 'Build a Mix',
mixByTracks: 'Mix by Tracks',
mixByTracksDesc: 'Random selection of tracks from your entire library',
mixByAlbums: 'Mix by Albums',
mixByAlbumsDesc: 'Random album picks for your next discovery',
},
randomAlbums: {
title: 'Random Albums',
refresh: 'Refresh',
+8
View File
@@ -5,6 +5,7 @@ export const frTranslation = {
newReleases: 'Nouveautés',
allAlbums: 'Tous les albums',
randomAlbums: 'Albums aléatoires',
randomPicker: 'Créer un mix',
artists: 'Artistes',
randomMix: 'Mix aléatoire',
favorites: 'Favoris',
@@ -197,6 +198,13 @@ export const frTranslation = {
removeSong: 'Retirer des favoris',
stations: 'Stations de radio',
},
randomLanding: {
title: 'Créer un mix',
mixByTracks: 'Mix par pistes',
mixByTracksDesc: 'Sélection aléatoire de titres depuis toute votre médiathèque',
mixByAlbums: 'Mix par albums',
mixByAlbumsDesc: 'Albums aléatoires pour vos prochaines découvertes',
},
randomAlbums: {
title: 'Albums aléatoires',
refresh: 'Actualiser',
+8
View File
@@ -5,6 +5,7 @@ export const nbTranslation = {
newReleases: 'Nye utgivelser',
allAlbums: 'Alle album',
randomAlbums: 'Tilfeldige album',
randomPicker: 'Lag en miks',
artists: 'Artister',
randomMix: 'Tilfeldig miks',
favorites: 'Favoritter',
@@ -197,6 +198,13 @@ export const nbTranslation = {
removeSong: 'Fjern fra favoritter',
stations: 'Radiostasjoner',
},
randomLanding: {
title: 'Lag en miks',
mixByTracks: 'Miks etter spor',
mixByTracksDesc: 'Tilfeldig utvalg av spor fra hele biblioteket ditt',
mixByAlbums: 'Miks etter album',
mixByAlbumsDesc: 'Tilfeldige album for nye oppdagelser',
},
randomAlbums: {
title: 'Tilfeldige album',
refresh: 'Oppdater',
+8
View File
@@ -5,6 +5,7 @@ export const nlTranslation = {
newReleases: 'Nieuw',
allAlbums: 'Alle albums',
randomAlbums: 'Willekeurige albums',
randomPicker: 'Mix samenstellen',
artists: 'Artiesten',
randomMix: 'Willekeurige mix',
favorites: 'Favorieten',
@@ -197,6 +198,13 @@ export const nlTranslation = {
removeSong: 'Verwijderen uit favorieten',
stations: 'Radiostations',
},
randomLanding: {
title: 'Mix samenstellen',
mixByTracks: 'Mix op nummers',
mixByTracksDesc: 'Willekeurige selectie uit je volledige mediatheek',
mixByAlbums: 'Mix op albums',
mixByAlbumsDesc: 'Willekeurige albums voor nieuwe ontdekkingen',
},
randomAlbums: {
title: 'Willekeurige albums',
refresh: 'Vernieuwen',
+8
View File
@@ -6,6 +6,7 @@ export const ruTranslation = {
newReleases: 'Новинки',
allAlbums: 'Все альбомы',
randomAlbums: 'Случайные альбомы',
randomPicker: 'Собрать микс',
artists: 'Исполнители',
randomMix: 'Случайный микс',
favorites: 'Избранное',
@@ -201,6 +202,13 @@ export const ruTranslation = {
removeSong: 'Убрать из избранного',
stations: 'Радиостанции',
},
randomLanding: {
title: 'Собрать микс',
mixByTracks: 'Микс по трекам',
mixByTracksDesc: 'Случайная подборка треков со всей медиатеки',
mixByAlbums: 'Микс по альбомам',
mixByAlbumsDesc: 'Случайная подборка альбомов для открытий',
},
randomAlbums: {
title: 'Случайные альбомы',
refresh: 'Обновить',
+8
View File
@@ -5,6 +5,7 @@ export const zhTranslation = {
newReleases: '新发布',
allAlbums: '全部专辑',
randomAlbums: '随机专辑',
randomPicker: '创建混音',
artists: '艺术家',
randomMix: '随机混音',
favorites: '收藏夹',
@@ -197,6 +198,13 @@ export const zhTranslation = {
removeSong: '从收藏中移除',
stations: '广播电台',
},
randomLanding: {
title: '创建混音',
mixByTracks: '按曲目混音',
mixByTracksDesc: '从整个媒体库随机选取曲目',
mixByAlbums: '按专辑混音',
mixByAlbumsDesc: '随机选取专辑,探索新音乐',
},
randomAlbums: {
title: '随机专辑',
refresh: '刷新',
+1 -1
View File
@@ -120,7 +120,7 @@ export default function Home() {
{isVisible('discover') && (
<AlbumRow
title={t('home.discover')}
titleLink="/random-albums"
titleLink="/random/albums"
albums={random}
onLoadMore={() => loadMore('random', random, setRandom)}
moreText={t('home.discoverMore')}
+52
View File
@@ -0,0 +1,52 @@
import React from 'react';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { Shuffle, Dices } from 'lucide-react';
interface MixCard {
icon: React.ElementType;
labelKey: string;
descKey: string;
to: string;
}
const CARDS: MixCard[] = [
{
icon: Shuffle,
labelKey: 'randomLanding.mixByTracks',
descKey: 'randomLanding.mixByTracksDesc',
to: '/random/mix',
},
{
icon: Dices,
labelKey: 'randomLanding.mixByAlbums',
descKey: 'randomLanding.mixByAlbumsDesc',
to: '/random/albums',
},
];
export default function RandomLanding() {
const { t } = useTranslation();
const navigate = useNavigate();
return (
<div className="random-landing">
<div className="random-landing-grid">
{CARDS.map(({ icon: Icon, labelKey, descKey, to }) => (
<button
key={to}
className="mix-pick-card"
onClick={() => navigate(to)}
>
<Icon className="mix-pick-card-bg-icon" strokeWidth={1} aria-hidden />
<div className="mix-pick-card-content">
<Icon size={28} strokeWidth={1.5} className="mix-pick-card-icon" aria-hidden />
<span className="mix-pick-card-label">{t(labelKey)}</span>
<span className="mix-pick-card-desc">{t(descKey)}</span>
</div>
</button>
))}
</div>
</div>
);
}
+1 -2
View File
@@ -12,10 +12,9 @@ export const DEFAULT_SIDEBAR_ITEMS: SidebarItemConfig[] = [
{ id: 'mainstage', visible: true },
{ id: 'newReleases', visible: true },
{ id: 'allAlbums', visible: true },
{ id: 'randomAlbums', visible: true },
{ id: 'randomPicker', visible: true },
{ id: 'artists', visible: true },
{ id: 'genres', visible: true },
{ id: 'randomMix', visible: true },
{ id: 'favorites', visible: true },
{ id: 'playlists', visible: true },
{ id: 'mostPlayed', visible: true },
+98
View File
@@ -5589,6 +5589,104 @@
margin: 0.2rem 0 0;
}
/* ─ Random Landing ─ */
.random-landing {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100%;
padding: var(--space-8);
box-sizing: border-box;
}
.random-landing-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: clamp(var(--space-4), 2vw, var(--space-6));
width: 100%;
max-width: min(860px, 90vw);
}
.mix-pick-card {
position: relative;
overflow: hidden;
display: flex;
align-items: flex-end;
min-height: clamp(200px, 22vh, 320px);
padding: var(--space-6);
border-radius: 16px;
background: var(--ctp-surface0);
border: 1px solid rgba(255, 255, 255, 0.06);
cursor: pointer;
text-align: left;
transition: transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease;
color: inherit;
}
.mix-pick-card:hover {
transform: translateY(-4px) scale(1.015);
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.35), 0 0 0 1px var(--accent), 0 0 24px -4px var(--accent);
border-color: var(--accent);
}
.mix-pick-card-bg-icon {
position: absolute;
top: 50%;
right: -16px;
transform: translateY(-50%);
width: clamp(140px, 14vw, 200px);
height: clamp(140px, 14vw, 200px);
color: var(--text-primary);
opacity: 0.07;
pointer-events: none;
flex-shrink: 0;
}
.mix-pick-card-content {
position: relative;
display: flex;
flex-direction: column;
gap: var(--space-2);
z-index: 1;
}
.mix-pick-card-icon {
color: var(--accent);
margin-bottom: var(--space-1);
}
.mix-pick-card-label {
font-family: var(--font-display);
font-size: 17px;
font-weight: 700;
color: var(--text-primary);
line-height: 1.2;
}
.mix-pick-card-desc {
font-size: 12px;
color: var(--text-muted);
line-height: 1.45;
max-width: 22ch;
}
@media (max-width: 640px) {
.random-landing {
padding: var(--space-5) var(--space-4);
justify-content: flex-start;
}
.random-landing-grid {
grid-template-columns: 1fr;
}
.mix-pick-card {
min-height: 160px;
}
}
/* ─ Playlists overview header ─ */
.playlists-header {
display: flex;