mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-22 14:35:41 +00:00
feat(home): add Discover Songs rail to Mainstage (#301)
Reuses the SongRail component from the Tracks hub — 18 random songs fetched in parallel with the other Home queries. No reroll button. Click-to-play uses enqueueAndPlay so the existing queue stays intact. New homeStore section id 'discoverSongs' inserted after 'discover' in DEFAULT_HOME_SECTIONS. onRehydrateStorage now appends any newly introduced sections to a previously persisted layout — existing users get the rail without a manual Reset. Settings → Personalisation Home-Customizer surfaces it automatically via SECTION_LABELS. i18n key home.discoverSongs added to all 8 locales. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
committed by
GitHub
parent
e3aabd98b7
commit
807e7b4520
@@ -41,6 +41,7 @@ export const deTranslation = {
|
|||||||
mostPlayed: 'Meistgehört',
|
mostPlayed: 'Meistgehört',
|
||||||
recentlyPlayed: 'Kürzlich gespielt',
|
recentlyPlayed: 'Kürzlich gespielt',
|
||||||
discover: 'Entdecken',
|
discover: 'Entdecken',
|
||||||
|
discoverSongs: 'Titel entdecken',
|
||||||
loadMore: 'Mehr laden',
|
loadMore: 'Mehr laden',
|
||||||
discoverMore: 'Mehr entdecken',
|
discoverMore: 'Mehr entdecken',
|
||||||
discoverArtists: 'Künstler entdecken',
|
discoverArtists: 'Künstler entdecken',
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ export const enTranslation = {
|
|||||||
mostPlayed: 'Most Played',
|
mostPlayed: 'Most Played',
|
||||||
recentlyPlayed: 'Recently Played',
|
recentlyPlayed: 'Recently Played',
|
||||||
discover: 'Discover',
|
discover: 'Discover',
|
||||||
|
discoverSongs: 'Discover Songs',
|
||||||
loadMore: 'Load More',
|
loadMore: 'Load More',
|
||||||
discoverMore: 'Discover More',
|
discoverMore: 'Discover More',
|
||||||
discoverArtists: 'Discover Artists',
|
discoverArtists: 'Discover Artists',
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ export const esTranslation = {
|
|||||||
mostPlayed: 'Más Reproducidos',
|
mostPlayed: 'Más Reproducidos',
|
||||||
recentlyPlayed: 'Reproducidos Recientemente',
|
recentlyPlayed: 'Reproducidos Recientemente',
|
||||||
discover: 'Descubrir',
|
discover: 'Descubrir',
|
||||||
|
discoverSongs: 'Descubrir canciones',
|
||||||
loadMore: 'Cargar Más',
|
loadMore: 'Cargar Más',
|
||||||
discoverMore: 'Descubrir Más',
|
discoverMore: 'Descubrir Más',
|
||||||
discoverArtists: 'Descubrir Artistas',
|
discoverArtists: 'Descubrir Artistas',
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ export const frTranslation = {
|
|||||||
mostPlayed: 'Les plus écoutés',
|
mostPlayed: 'Les plus écoutés',
|
||||||
recentlyPlayed: 'Récemment écoutés',
|
recentlyPlayed: 'Récemment écoutés',
|
||||||
discover: 'Découvrir',
|
discover: 'Découvrir',
|
||||||
|
discoverSongs: 'Découvrir des titres',
|
||||||
loadMore: 'Charger plus',
|
loadMore: 'Charger plus',
|
||||||
discoverMore: 'Découvrir plus',
|
discoverMore: 'Découvrir plus',
|
||||||
discoverArtists: 'Découvrir des artistes',
|
discoverArtists: 'Découvrir des artistes',
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ export const nbTranslation = {
|
|||||||
mostPlayed: 'Mest spilt',
|
mostPlayed: 'Mest spilt',
|
||||||
recentlyPlayed: 'Nylig spilt',
|
recentlyPlayed: 'Nylig spilt',
|
||||||
discover: 'Oppdag',
|
discover: 'Oppdag',
|
||||||
|
discoverSongs: 'Oppdag spor',
|
||||||
loadMore: 'Last inn flere',
|
loadMore: 'Last inn flere',
|
||||||
discoverMore: 'Oppdag flere',
|
discoverMore: 'Oppdag flere',
|
||||||
discoverArtists: 'Oppdag artister',
|
discoverArtists: 'Oppdag artister',
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ export const nlTranslation = {
|
|||||||
mostPlayed: 'Meest gespeeld',
|
mostPlayed: 'Meest gespeeld',
|
||||||
recentlyPlayed: 'Recent afgespeeld',
|
recentlyPlayed: 'Recent afgespeeld',
|
||||||
discover: 'Ontdekken',
|
discover: 'Ontdekken',
|
||||||
|
discoverSongs: 'Nummers ontdekken',
|
||||||
loadMore: 'Meer laden',
|
loadMore: 'Meer laden',
|
||||||
discoverMore: 'Meer ontdekken',
|
discoverMore: 'Meer ontdekken',
|
||||||
discoverArtists: 'Artiesten ontdekken',
|
discoverArtists: 'Artiesten ontdekken',
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ export const ruTranslation = {
|
|||||||
mostPlayed: 'Популярное',
|
mostPlayed: 'Популярное',
|
||||||
recentlyPlayed: 'Недавно проиграно',
|
recentlyPlayed: 'Недавно проиграно',
|
||||||
discover: 'Обзор',
|
discover: 'Обзор',
|
||||||
|
discoverSongs: 'Открыть треки',
|
||||||
loadMore: 'Ещё',
|
loadMore: 'Ещё',
|
||||||
discoverMore: 'Смотреть ещё',
|
discoverMore: 'Смотреть ещё',
|
||||||
discoverArtists: 'Исполнители',
|
discoverArtists: 'Исполнители',
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ export const zhTranslation = {
|
|||||||
mostPlayed: '最常播放',
|
mostPlayed: '最常播放',
|
||||||
recentlyPlayed: '最近播放',
|
recentlyPlayed: '最近播放',
|
||||||
discover: '发现',
|
discover: '发现',
|
||||||
|
discoverSongs: '发现曲目',
|
||||||
loadMore: '加载更多',
|
loadMore: '加载更多',
|
||||||
discoverMore: '发现更多',
|
discoverMore: '发现更多',
|
||||||
discoverArtists: '发现艺术家',
|
discoverArtists: '发现艺术家',
|
||||||
|
|||||||
+15
-2
@@ -1,7 +1,8 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import Hero from '../components/Hero';
|
import Hero from '../components/Hero';
|
||||||
import AlbumRow from '../components/AlbumRow';
|
import AlbumRow from '../components/AlbumRow';
|
||||||
import { getAlbumList, getArtists, SubsonicAlbum, SubsonicArtist } from '../api/subsonic';
|
import SongRail from '../components/SongRail';
|
||||||
|
import { getAlbumList, getArtists, getRandomSongs, SubsonicAlbum, SubsonicArtist, SubsonicSong } from '../api/subsonic';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { NavLink, useNavigate } from 'react-router-dom';
|
import { NavLink, useNavigate } from 'react-router-dom';
|
||||||
import { ChevronRight } from 'lucide-react';
|
import { ChevronRight } from 'lucide-react';
|
||||||
@@ -13,6 +14,7 @@ import { filterAlbumsByMixRatings, getMixMinRatingsConfigFromAuth } from '../uti
|
|||||||
const HOME_RANDOM_FETCH = 100;
|
const HOME_RANDOM_FETCH = 100;
|
||||||
const HOME_HERO_COUNT = 8;
|
const HOME_HERO_COUNT = 8;
|
||||||
const HOME_DISCOVER_SLICE = 20;
|
const HOME_DISCOVER_SLICE = 20;
|
||||||
|
const HOME_DISCOVER_SONGS_SIZE = 18;
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const homeSections = useHomeStore(s => s.sections);
|
const homeSections = useHomeStore(s => s.sections);
|
||||||
@@ -30,6 +32,7 @@ export default function Home() {
|
|||||||
const [mostPlayed, setMostPlayed] = useState<SubsonicAlbum[]>([]);
|
const [mostPlayed, setMostPlayed] = useState<SubsonicAlbum[]>([]);
|
||||||
const [recentlyPlayed, setRecentlyPlayed] = useState<SubsonicAlbum[]>([]);
|
const [recentlyPlayed, setRecentlyPlayed] = useState<SubsonicAlbum[]>([]);
|
||||||
const [randomArtists, setRandomArtists] = useState<SubsonicArtist[]>([]);
|
const [randomArtists, setRandomArtists] = useState<SubsonicArtist[]>([]);
|
||||||
|
const [discoverSongs, setDiscoverSongs] = useState<SubsonicSong[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -41,13 +44,16 @@ export default function Home() {
|
|||||||
const albumMix =
|
const albumMix =
|
||||||
mixCfg.enabled && (mixCfg.minAlbum > 0 || mixCfg.minArtist > 0);
|
mixCfg.enabled && (mixCfg.minAlbum > 0 || mixCfg.minArtist > 0);
|
||||||
const randomSize = albumMix ? HOME_RANDOM_FETCH : HOME_DISCOVER_SLICE;
|
const randomSize = albumMix ? HOME_RANDOM_FETCH : HOME_DISCOVER_SLICE;
|
||||||
const [s, n, rRaw, f, rp, artists] = await Promise.all([
|
const [s, n, rRaw, f, rp, artists, songs] = await Promise.all([
|
||||||
getAlbumList('starred', 12).catch(() => []),
|
getAlbumList('starred', 12).catch(() => []),
|
||||||
getAlbumList('newest', 12).catch(() => []),
|
getAlbumList('newest', 12).catch(() => []),
|
||||||
getAlbumList('random', randomSize).catch(() => []),
|
getAlbumList('random', randomSize).catch(() => []),
|
||||||
getAlbumList('frequent', 12).catch(() => []),
|
getAlbumList('frequent', 12).catch(() => []),
|
||||||
getAlbumList('recent', 12).catch(() => []),
|
getAlbumList('recent', 12).catch(() => []),
|
||||||
isVisible('discoverArtists') ? getArtists().catch(() => []) : Promise.resolve<SubsonicArtist[]>([]),
|
isVisible('discoverArtists') ? getArtists().catch(() => []) : Promise.resolve<SubsonicArtist[]>([]),
|
||||||
|
isVisible('discoverSongs')
|
||||||
|
? getRandomSongs(HOME_DISCOVER_SONGS_SIZE).catch(() => [] as SubsonicSong[])
|
||||||
|
: Promise.resolve<SubsonicSong[]>([]),
|
||||||
]);
|
]);
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
const r = await filterAlbumsByMixRatings(rRaw, mixCfg);
|
const r = await filterAlbumsByMixRatings(rRaw, mixCfg);
|
||||||
@@ -57,6 +63,7 @@ export default function Home() {
|
|||||||
setRandom(r.slice(HOME_HERO_COUNT, HOME_DISCOVER_SLICE));
|
setRandom(r.slice(HOME_HERO_COUNT, HOME_DISCOVER_SLICE));
|
||||||
setMostPlayed(f);
|
setMostPlayed(f);
|
||||||
setRecentlyPlayed(rp);
|
setRecentlyPlayed(rp);
|
||||||
|
setDiscoverSongs(songs);
|
||||||
const shuffled = [...artists];
|
const shuffled = [...artists];
|
||||||
for (let i = shuffled.length - 1; i > 0; i--) {
|
for (let i = shuffled.length - 1; i > 0; i--) {
|
||||||
const j = Math.floor(Math.random() * (i + 1));
|
const j = Math.floor(Math.random() * (i + 1));
|
||||||
@@ -128,6 +135,12 @@ export default function Home() {
|
|||||||
moreText={t('home.discoverMore')}
|
moreText={t('home.discoverMore')}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{isVisible('discoverSongs') && discoverSongs.length > 0 && (
|
||||||
|
<SongRail
|
||||||
|
title={t('home.discoverSongs')}
|
||||||
|
songs={discoverSongs}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{isVisible('discoverArtists') && randomArtists.length > 0 && (
|
{isVisible('discoverArtists') && randomArtists.length > 0 && (
|
||||||
<section className="album-row-section">
|
<section className="album-row-section">
|
||||||
<div className="album-row-header">
|
<div className="album-row-header">
|
||||||
|
|||||||
@@ -4209,6 +4209,7 @@ function HomeCustomizer() {
|
|||||||
hero: t('home.hero'),
|
hero: t('home.hero'),
|
||||||
recent: t('home.recent'),
|
recent: t('home.recent'),
|
||||||
discover: t('home.discover'),
|
discover: t('home.discover'),
|
||||||
|
discoverSongs: t('home.discoverSongs'),
|
||||||
discoverArtists: t('home.discoverArtists'),
|
discoverArtists: t('home.discoverArtists'),
|
||||||
recentlyPlayed: t('home.recentlyPlayed'),
|
recentlyPlayed: t('home.recentlyPlayed'),
|
||||||
starred: t('home.starred'),
|
starred: t('home.starred'),
|
||||||
|
|||||||
+16
-2
@@ -1,7 +1,7 @@
|
|||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
import { persist } from 'zustand/middleware';
|
import { persist } from 'zustand/middleware';
|
||||||
|
|
||||||
export type HomeSectionId = 'hero' | 'recent' | 'discover' | 'discoverArtists' | 'recentlyPlayed' | 'starred' | 'mostPlayed';
|
export type HomeSectionId = 'hero' | 'recent' | 'discover' | 'discoverSongs' | 'discoverArtists' | 'recentlyPlayed' | 'starred' | 'mostPlayed';
|
||||||
|
|
||||||
export interface HomeSectionConfig {
|
export interface HomeSectionConfig {
|
||||||
id: HomeSectionId;
|
id: HomeSectionId;
|
||||||
@@ -12,6 +12,7 @@ export const DEFAULT_HOME_SECTIONS: HomeSectionConfig[] = [
|
|||||||
{ id: 'hero', visible: true },
|
{ id: 'hero', visible: true },
|
||||||
{ id: 'recent', visible: true },
|
{ id: 'recent', visible: true },
|
||||||
{ id: 'discover', visible: true },
|
{ id: 'discover', visible: true },
|
||||||
|
{ id: 'discoverSongs', visible: true },
|
||||||
{ id: 'discoverArtists', visible: true },
|
{ id: 'discoverArtists', visible: true },
|
||||||
{ id: 'recentlyPlayed', visible: true },
|
{ id: 'recentlyPlayed', visible: true },
|
||||||
{ id: 'starred', visible: true },
|
{ id: 'starred', visible: true },
|
||||||
@@ -33,6 +34,19 @@ export const useHomeStore = create<HomeStore>()(
|
|||||||
})),
|
})),
|
||||||
reset: () => set({ sections: DEFAULT_HOME_SECTIONS }),
|
reset: () => set({ sections: DEFAULT_HOME_SECTIONS }),
|
||||||
}),
|
}),
|
||||||
{ name: 'psysonic_home' }
|
{
|
||||||
|
name: 'psysonic_home',
|
||||||
|
onRehydrateStorage: () => (state) => {
|
||||||
|
// Append any sections introduced after the user first persisted their order,
|
||||||
|
// so new defaults show up without forcing a manual Reset.
|
||||||
|
if (!state) return;
|
||||||
|
const safe = (state.sections ?? []).filter(
|
||||||
|
(s): s is HomeSectionConfig => s != null && typeof s.id === 'string',
|
||||||
|
);
|
||||||
|
const known = new Set(safe.map(s => s.id));
|
||||||
|
const missing = DEFAULT_HOME_SECTIONS.filter(s => !known.has(s.id));
|
||||||
|
state.sections = missing.length > 0 ? [...safe, ...missing] : safe;
|
||||||
|
},
|
||||||
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user