mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-21 14:05:41 +00:00
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:
Generated
+1
-1
@@ -3397,7 +3397,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "psysonic"
|
||||
version = "1.34.7"
|
||||
version = "1.34.8"
|
||||
dependencies = [
|
||||
"biquad",
|
||||
"discord-rich-presence",
|
||||
|
||||
+4
-2
@@ -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 />} />
|
||||
|
||||
@@ -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' },
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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: 'Обновить',
|
||||
|
||||
@@ -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
@@ -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')}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 },
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user