mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-21 22:15:40 +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',
|
||||
recentlyPlayed: 'Kürzlich gespielt',
|
||||
discover: 'Entdecken',
|
||||
discoverSongs: 'Titel entdecken',
|
||||
loadMore: 'Mehr laden',
|
||||
discoverMore: 'Mehr entdecken',
|
||||
discoverArtists: 'Künstler entdecken',
|
||||
|
||||
@@ -43,6 +43,7 @@ export const enTranslation = {
|
||||
mostPlayed: 'Most Played',
|
||||
recentlyPlayed: 'Recently Played',
|
||||
discover: 'Discover',
|
||||
discoverSongs: 'Discover Songs',
|
||||
loadMore: 'Load More',
|
||||
discoverMore: 'Discover More',
|
||||
discoverArtists: 'Discover Artists',
|
||||
|
||||
@@ -42,6 +42,7 @@ export const esTranslation = {
|
||||
mostPlayed: 'Más Reproducidos',
|
||||
recentlyPlayed: 'Reproducidos Recientemente',
|
||||
discover: 'Descubrir',
|
||||
discoverSongs: 'Descubrir canciones',
|
||||
loadMore: 'Cargar Más',
|
||||
discoverMore: 'Descubrir Más',
|
||||
discoverArtists: 'Descubrir Artistas',
|
||||
|
||||
@@ -41,6 +41,7 @@ export const frTranslation = {
|
||||
mostPlayed: 'Les plus écoutés',
|
||||
recentlyPlayed: 'Récemment écoutés',
|
||||
discover: 'Découvrir',
|
||||
discoverSongs: 'Découvrir des titres',
|
||||
loadMore: 'Charger plus',
|
||||
discoverMore: 'Découvrir plus',
|
||||
discoverArtists: 'Découvrir des artistes',
|
||||
|
||||
@@ -41,6 +41,7 @@ export const nbTranslation = {
|
||||
mostPlayed: 'Mest spilt',
|
||||
recentlyPlayed: 'Nylig spilt',
|
||||
discover: 'Oppdag',
|
||||
discoverSongs: 'Oppdag spor',
|
||||
loadMore: 'Last inn flere',
|
||||
discoverMore: 'Oppdag flere',
|
||||
discoverArtists: 'Oppdag artister',
|
||||
|
||||
@@ -41,6 +41,7 @@ export const nlTranslation = {
|
||||
mostPlayed: 'Meest gespeeld',
|
||||
recentlyPlayed: 'Recent afgespeeld',
|
||||
discover: 'Ontdekken',
|
||||
discoverSongs: 'Nummers ontdekken',
|
||||
loadMore: 'Meer laden',
|
||||
discoverMore: 'Meer ontdekken',
|
||||
discoverArtists: 'Artiesten ontdekken',
|
||||
|
||||
@@ -43,6 +43,7 @@ export const ruTranslation = {
|
||||
mostPlayed: 'Популярное',
|
||||
recentlyPlayed: 'Недавно проиграно',
|
||||
discover: 'Обзор',
|
||||
discoverSongs: 'Открыть треки',
|
||||
loadMore: 'Ещё',
|
||||
discoverMore: 'Смотреть ещё',
|
||||
discoverArtists: 'Исполнители',
|
||||
|
||||
@@ -41,6 +41,7 @@ export const zhTranslation = {
|
||||
mostPlayed: '最常播放',
|
||||
recentlyPlayed: '最近播放',
|
||||
discover: '发现',
|
||||
discoverSongs: '发现曲目',
|
||||
loadMore: '加载更多',
|
||||
discoverMore: '发现更多',
|
||||
discoverArtists: '发现艺术家',
|
||||
|
||||
+15
-2
@@ -1,7 +1,8 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import Hero from '../components/Hero';
|
||||
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 { NavLink, useNavigate } from 'react-router-dom';
|
||||
import { ChevronRight } from 'lucide-react';
|
||||
@@ -13,6 +14,7 @@ import { filterAlbumsByMixRatings, getMixMinRatingsConfigFromAuth } from '../uti
|
||||
const HOME_RANDOM_FETCH = 100;
|
||||
const HOME_HERO_COUNT = 8;
|
||||
const HOME_DISCOVER_SLICE = 20;
|
||||
const HOME_DISCOVER_SONGS_SIZE = 18;
|
||||
|
||||
export default function Home() {
|
||||
const homeSections = useHomeStore(s => s.sections);
|
||||
@@ -30,6 +32,7 @@ export default function Home() {
|
||||
const [mostPlayed, setMostPlayed] = useState<SubsonicAlbum[]>([]);
|
||||
const [recentlyPlayed, setRecentlyPlayed] = useState<SubsonicAlbum[]>([]);
|
||||
const [randomArtists, setRandomArtists] = useState<SubsonicArtist[]>([]);
|
||||
const [discoverSongs, setDiscoverSongs] = useState<SubsonicSong[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -41,13 +44,16 @@ export default function Home() {
|
||||
const albumMix =
|
||||
mixCfg.enabled && (mixCfg.minAlbum > 0 || mixCfg.minArtist > 0);
|
||||
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('newest', 12).catch(() => []),
|
||||
getAlbumList('random', randomSize).catch(() => []),
|
||||
getAlbumList('frequent', 12).catch(() => []),
|
||||
getAlbumList('recent', 12).catch(() => []),
|
||||
isVisible('discoverArtists') ? getArtists().catch(() => []) : Promise.resolve<SubsonicArtist[]>([]),
|
||||
isVisible('discoverSongs')
|
||||
? getRandomSongs(HOME_DISCOVER_SONGS_SIZE).catch(() => [] as SubsonicSong[])
|
||||
: Promise.resolve<SubsonicSong[]>([]),
|
||||
]);
|
||||
if (cancelled) return;
|
||||
const r = await filterAlbumsByMixRatings(rRaw, mixCfg);
|
||||
@@ -57,6 +63,7 @@ export default function Home() {
|
||||
setRandom(r.slice(HOME_HERO_COUNT, HOME_DISCOVER_SLICE));
|
||||
setMostPlayed(f);
|
||||
setRecentlyPlayed(rp);
|
||||
setDiscoverSongs(songs);
|
||||
const shuffled = [...artists];
|
||||
for (let i = shuffled.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
@@ -128,6 +135,12 @@ export default function Home() {
|
||||
moreText={t('home.discoverMore')}
|
||||
/>
|
||||
)}
|
||||
{isVisible('discoverSongs') && discoverSongs.length > 0 && (
|
||||
<SongRail
|
||||
title={t('home.discoverSongs')}
|
||||
songs={discoverSongs}
|
||||
/>
|
||||
)}
|
||||
{isVisible('discoverArtists') && randomArtists.length > 0 && (
|
||||
<section className="album-row-section">
|
||||
<div className="album-row-header">
|
||||
|
||||
@@ -4209,6 +4209,7 @@ function HomeCustomizer() {
|
||||
hero: t('home.hero'),
|
||||
recent: t('home.recent'),
|
||||
discover: t('home.discover'),
|
||||
discoverSongs: t('home.discoverSongs'),
|
||||
discoverArtists: t('home.discoverArtists'),
|
||||
recentlyPlayed: t('home.recentlyPlayed'),
|
||||
starred: t('home.starred'),
|
||||
|
||||
+16
-2
@@ -1,7 +1,7 @@
|
||||
import { create } from 'zustand';
|
||||
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 {
|
||||
id: HomeSectionId;
|
||||
@@ -12,6 +12,7 @@ export const DEFAULT_HOME_SECTIONS: HomeSectionConfig[] = [
|
||||
{ id: 'hero', visible: true },
|
||||
{ id: 'recent', visible: true },
|
||||
{ id: 'discover', visible: true },
|
||||
{ id: 'discoverSongs', visible: true },
|
||||
{ id: 'discoverArtists', visible: true },
|
||||
{ id: 'recentlyPlayed', visible: true },
|
||||
{ id: 'starred', visible: true },
|
||||
@@ -33,6 +34,19 @@ export const useHomeStore = create<HomeStore>()(
|
||||
})),
|
||||
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