mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-21 23:05:46 +00:00
feat(most-played): add toggle to filter compilation artists
Toggle button in the Top Artists section header hides known compilation artist names (Various Artists, VA, Soundtracks, OST, etc.) from the ranking. Default off. Active state shown in accent color. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -791,6 +791,9 @@ export const deTranslation = {
|
||||
sortLeast: 'Wenigste Plays zuerst',
|
||||
loadMore: 'Mehr Alben laden',
|
||||
noData: 'Noch keine Wiedergabedaten. Fang an zu hören!',
|
||||
noArtists: 'Alle Künstler herausgefiltert.',
|
||||
filterCompilations: 'Sampler-Künstler ausblenden (Various Artists, Soundtracks usw.)',
|
||||
filterCompilationsShort: 'Sampler ausblenden',
|
||||
},
|
||||
radio: {
|
||||
title: 'Internetradio',
|
||||
|
||||
@@ -792,6 +792,9 @@ export const enTranslation = {
|
||||
sortLeast: 'Fewest plays first',
|
||||
loadMore: 'Load more albums',
|
||||
noData: 'No play data yet. Start listening!',
|
||||
noArtists: 'All artists filtered out.',
|
||||
filterCompilations: 'Hide compilation artists (Various Artists, Soundtracks, etc.)',
|
||||
filterCompilationsShort: 'Hide compilations',
|
||||
},
|
||||
radio: {
|
||||
title: 'Internet Radio',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { ArrowUpDown, ArrowDown, ArrowUp, TrendingUp } from 'lucide-react';
|
||||
import { ArrowUpDown, ArrowDown, ArrowUp, TrendingUp, UsersRound } from 'lucide-react';
|
||||
import { getAlbumList, SubsonicAlbum, buildCoverArtUrl, coverArtCacheKey } from '../api/subsonic';
|
||||
import { useAuthStore } from '../store/authStore';
|
||||
import CachedImage from '../components/CachedImage';
|
||||
@@ -16,11 +16,24 @@ interface ArtistEntry {
|
||||
totalPlays: number;
|
||||
}
|
||||
|
||||
function deriveTopArtists(albums: SubsonicAlbum[]): ArtistEntry[] {
|
||||
const COMPILATION_NAMES = new Set([
|
||||
'various artists', 'various', 'va', 'v.a.', 'v.a',
|
||||
'diverse artister', 'diversos artistas', 'artistes variés',
|
||||
'vários artistas', 'verschiedene künstler', 'verscheidene artiesten',
|
||||
'compilations', 'soundtrack', 'original soundtrack', 'ost',
|
||||
'original motion picture soundtrack', 'original score',
|
||||
]);
|
||||
|
||||
function isCompilation(name: string): boolean {
|
||||
return COMPILATION_NAMES.has(name.toLowerCase().trim());
|
||||
}
|
||||
|
||||
function deriveTopArtists(albums: SubsonicAlbum[], filterCompilations: boolean): ArtistEntry[] {
|
||||
const map = new Map<string, ArtistEntry>();
|
||||
for (const a of albums) {
|
||||
const plays = a.playCount ?? 0;
|
||||
if (plays === 0) continue;
|
||||
if (filterCompilations && isCompilation(a.artist ?? '')) continue;
|
||||
const entry = map.get(a.artistId);
|
||||
if (entry) {
|
||||
entry.totalPlays += plays;
|
||||
@@ -46,8 +59,9 @@ export default function MostPlayed() {
|
||||
const [loadingMore, setLoadingMore] = useState(false);
|
||||
const [hasMore, setHasMore] = useState(true);
|
||||
const [sortAsc, setSortAsc] = useState(false); // false = most plays first
|
||||
const [filterCompilations, setFilterCompilations] = useState(false);
|
||||
|
||||
const topArtists = deriveTopArtists(albums).slice(0, 10);
|
||||
const topArtists = deriveTopArtists(albums, filterCompilations).slice(0, 10);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true);
|
||||
@@ -96,9 +110,23 @@ export default function MostPlayed() {
|
||||
</div>
|
||||
|
||||
{/* ── Top Artists ── */}
|
||||
{!loading && topArtists.length > 0 && (
|
||||
{!loading && (
|
||||
<section className="mp-section">
|
||||
<h2 className="mp-section-title">{t('mostPlayed.topArtists')}</h2>
|
||||
<div className="mp-section-header">
|
||||
<h2 className="mp-section-title">{t('mostPlayed.topArtists')}</h2>
|
||||
<button
|
||||
className={`btn btn-ghost mp-filter-btn${filterCompilations ? ' mp-filter-btn--active' : ''}`}
|
||||
onClick={() => setFilterCompilations(v => !v)}
|
||||
data-tooltip={t('mostPlayed.filterCompilations')}
|
||||
data-tooltip-pos="left"
|
||||
>
|
||||
<UsersRound size={14} />
|
||||
{t('mostPlayed.filterCompilationsShort')}
|
||||
</button>
|
||||
</div>
|
||||
{topArtists.length === 0 && (
|
||||
<div className="empty-state" style={{ padding: '12px 0' }}>{t('mostPlayed.noArtists')}</div>
|
||||
)}
|
||||
<div className="mp-artist-grid">
|
||||
{topArtists.map((artist, i) => (
|
||||
<button
|
||||
|
||||
@@ -6417,12 +6417,37 @@
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
|
||||
.mp-section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.mp-filter-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
font-size: 12px;
|
||||
padding: 4px 10px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.mp-filter-btn:hover { opacity: 1; }
|
||||
|
||||
.mp-filter-btn--active {
|
||||
opacity: 1;
|
||||
color: var(--accent);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.mp-section-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
margin-bottom: 0;
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user