mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-22 14:35:41 +00:00
feat: Last.fm beta, Similar Artists, Statistics Last.fm stats, TooltipPortal, CustomSelect, Psychowave theme (v1.7.0)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,8 @@ import { ArrowLeft, Users, ExternalLink, Star, Play, Shuffle, Radio } from 'luci
|
||||
import { open } from '@tauri-apps/plugin-shell';
|
||||
import { usePlayerStore } from '../store/playerStore';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { lastfmGetSimilarArtists, lastfmIsConfigured } from '../api/lastfm';
|
||||
import LastfmIcon from '../components/LastfmIcon';
|
||||
|
||||
function formatDuration(seconds: number): string {
|
||||
const m = Math.floor(seconds / 60);
|
||||
@@ -31,13 +33,6 @@ function sanitizeHtml(html: string): string {
|
||||
return doc.body.innerHTML;
|
||||
}
|
||||
|
||||
function LastfmIcon({ size = 16 }: { size?: number }) {
|
||||
return (
|
||||
<svg width={size} height={size} viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
||||
<path d="M11.344 16.143l-.917-2.494s-1.485 1.662-3.716 1.662c-1.97 0-3.373-1.714-3.373-4.46 0-3.514 1.773-4.777 3.52-4.777 2.508 0 3.306 1.625 3.997 3.714l.918 2.88c.918 2.8 2.642 5.047 7.615 5.047 3.563 0 5.98-1.094 5.98-3.972 0-2.326-1.327-3.53-3.797-4.11l-1.836-.41c-1.27-.29-1.645-.82-1.645-1.693 0-.987.778-1.56 2.047-1.56 1.384 0 2.132.52 2.245 1.756l2.878-.347C24.883 5.116 23.3 4 20.5 4c-3.26 0-4.945 1.537-4.945 3.824 0 1.843.91 3.008 3.2 3.562l1.947.46c1.404.327 1.97.874 1.97 1.894 0 1.13-.988 1.593-2.948 1.593-2.858 0-4.052-1.497-4.742-3.634l-.943-2.887C13.22 6.162 11.73 4 7.897 4 3.847 4 1 6.61 1 11.022c0 4.235 2.617 6.638 6.19 6.638 2.566 0 4.154-1.517 4.154-1.517z"/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ArtistDetail() {
|
||||
const { t } = useTranslation();
|
||||
@@ -52,6 +47,8 @@ export default function ArtistDetail() {
|
||||
const [radioLoading, setRadioLoading] = useState(false);
|
||||
const [isStarred, setIsStarred] = useState(false);
|
||||
const [openedLink, setOpenedLink] = useState<string | null>(null);
|
||||
const [similarArtists, setSimilarArtists] = useState<SubsonicArtist[]>([]);
|
||||
const [similarLoading, setSimilarLoading] = useState(false);
|
||||
|
||||
const playTrack = usePlayerStore(state => state.playTrack);
|
||||
const enqueue = usePlayerStore(state => state.enqueue);
|
||||
@@ -107,6 +104,32 @@ export default function ArtistDetail() {
|
||||
});
|
||||
}, [id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!artist || !lastfmIsConfigured()) return;
|
||||
setSimilarArtists([]);
|
||||
setSimilarLoading(true);
|
||||
lastfmGetSimilarArtists(artist.name).then(async names => {
|
||||
if (names.length === 0) { setSimilarLoading(false); return; }
|
||||
const results = await Promise.all(
|
||||
names.slice(0, 30).map(name =>
|
||||
search(name, { artistCount: 3, albumCount: 0, songCount: 0 }).catch(() => ({ artists: [], albums: [], songs: [] }))
|
||||
)
|
||||
);
|
||||
const seen = new Set<string>([artist.id]);
|
||||
const found: SubsonicArtist[] = [];
|
||||
for (let i = 0; i < results.length; i++) {
|
||||
const targetName = names[i].toLowerCase();
|
||||
const match = results[i].artists.find(a => a.name.toLowerCase() === targetName);
|
||||
if (match && !seen.has(match.id)) {
|
||||
seen.add(match.id);
|
||||
found.push(match);
|
||||
}
|
||||
}
|
||||
setSimilarArtists(found);
|
||||
setSimilarLoading(false);
|
||||
}).catch(() => setSimilarLoading(false));
|
||||
}, [artist?.id]);
|
||||
|
||||
const openLink = (url: string, key: string) => {
|
||||
open(url);
|
||||
setOpenedLink(key);
|
||||
@@ -325,8 +348,35 @@ export default function ArtistDetail() {
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Similar Artists (Last.fm) */}
|
||||
{lastfmIsConfigured() && (similarLoading || similarArtists.length > 0) && (
|
||||
<>
|
||||
<h2 className="section-title" style={{ marginTop: '2rem', marginBottom: '1rem' }}>
|
||||
{t('artistDetail.similarArtists')}
|
||||
</h2>
|
||||
{similarLoading ? (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem', color: 'var(--text-muted)', fontSize: '0.875rem' }}>
|
||||
<div className="spinner" style={{ width: 16, height: 16, borderTopColor: 'currentColor' }} />
|
||||
{t('artistDetail.loading')}
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.5rem' }}>
|
||||
{similarArtists.map(a => (
|
||||
<button
|
||||
key={a.id}
|
||||
className="artist-ext-link"
|
||||
onClick={() => navigate(`/artist/${a.id}`)}
|
||||
>
|
||||
{a.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Albums */}
|
||||
<h2 className="section-title" style={{ marginTop: (info?.biography || topSongs.length > 0) ? '2rem' : '0', marginBottom: '1rem' }}>
|
||||
<h2 className="section-title" style={{ marginTop: (info?.biography || topSongs.length > 0 || lastfmIsConfigured()) ? '2rem' : '0', marginBottom: '1rem' }}>
|
||||
{t('artistDetail.albumsBy', { name: artist.name })}
|
||||
</h2>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user