mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 07:15:47 +00:00
refactor(artist-detail): G.85 — extract Hero + TopTracks + SimilarArtists + action utilities (cluster) (#652)
Four-cut cluster closing out the major ArtistDetail extraction. 707 → 339 LOC (−368). ArtistDetailHero — the full hero header: back button, lightbox trigger, avatar with hover upload overlay + camera/loader icon + hidden file input, glow effect from extractCoverColors onLoad, title + album count, entity-rating row, Last.fm + Wikipedia + favourite link row, and the action button strip (play all, shuffle, radio, share, offline cache with progress / done state). Subscribes to useOfflineStore / useOfflineJobStore / useAuthStore directly so the page doesn't have to thread bulk-progress through. ArtistDetailTopTracks — the four-column tracklist with each row's inline play-next + preview ring + track cover thumbnail + click into playTopSongWithContinuation. Subscribes to playerStore / previewStore / useOrbitSongRowBehavior directly. ArtistDetailSimilarArtists — section header (with show-more toggle on mobile), loading spinner, and the chip list (using serverSimilarArtists vs. similarArtists depending on which path fed it). runArtistDetailActions — four parameterized async actions: runArtistEntityRating (with full / track_only fallback + saveFailed toast), runArtistToggleStar (optimistic state + revert on error), runArtistShare (copy link + success / failure toast), runArtistImageUpload (upload + invalidate cover-art cache + bump revision). ArtistDetail drops the inline definitions; formatDuration moves into the TopTracks component. Pure code move otherwise.
This commit is contained in:
committed by
GitHub
parent
ba0bf8aa9d
commit
bb0fe828bf
@@ -0,0 +1,115 @@
|
||||
import type React from 'react';
|
||||
import type { TFunction } from 'i18next';
|
||||
import { uploadArtistImage } from '../api/subsonicPlaylists';
|
||||
import { setRating, star, unstar } from '../api/subsonicStarRating';
|
||||
import type { SubsonicArtist } from '../api/subsonicTypes';
|
||||
import { useAuthStore } from '../store/authStore';
|
||||
import { copyEntityShareLink } from './copyEntityShareLink';
|
||||
import { invalidateCoverArt } from './imageCache';
|
||||
import { showToast } from './toast';
|
||||
|
||||
export interface RunArtistEntityRatingDeps {
|
||||
artist: SubsonicArtist | null;
|
||||
id: string | undefined;
|
||||
rating: number;
|
||||
artistEntityRatingSupport: string;
|
||||
activeServerId: string;
|
||||
t: TFunction;
|
||||
setArtistEntityRating: (v: number) => void;
|
||||
setArtist: React.Dispatch<React.SetStateAction<SubsonicArtist | null>>;
|
||||
}
|
||||
|
||||
export async function runArtistEntityRating(deps: RunArtistEntityRatingDeps): Promise<void> {
|
||||
const { artist, id, rating, artistEntityRatingSupport, activeServerId, t, setArtistEntityRating, setArtist } = deps;
|
||||
if (!artist || artist.id !== id) return;
|
||||
const artistId = artist.id;
|
||||
const ratingAtStart = artist.userRating ?? 0;
|
||||
|
||||
setArtistEntityRating(rating);
|
||||
|
||||
if (artistEntityRatingSupport !== 'full') return;
|
||||
|
||||
try {
|
||||
await setRating(artistId, rating);
|
||||
setArtist(a => (a && a.id === artistId ? { ...a, userRating: rating } : a));
|
||||
} catch (err) {
|
||||
setArtistEntityRating(ratingAtStart);
|
||||
useAuthStore.getState().setEntityRatingSupport(activeServerId, 'track_only');
|
||||
showToast(
|
||||
typeof err === 'string' ? err : err instanceof Error ? err.message : t('entityRating.saveFailed'),
|
||||
4500,
|
||||
'error',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export interface RunArtistToggleStarDeps {
|
||||
artist: SubsonicArtist | null;
|
||||
isStarred: boolean;
|
||||
setIsStarred: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
export async function runArtistToggleStar(deps: RunArtistToggleStarDeps): Promise<void> {
|
||||
const { artist, isStarred, setIsStarred } = deps;
|
||||
if (!artist) return;
|
||||
const currentlyStarred = isStarred;
|
||||
setIsStarred(!currentlyStarred);
|
||||
try {
|
||||
if (currentlyStarred) await unstar(artist.id, 'artist');
|
||||
else await star(artist.id, 'artist');
|
||||
} catch (e) {
|
||||
console.error('Failed to toggle star', e);
|
||||
setIsStarred(currentlyStarred);
|
||||
}
|
||||
}
|
||||
|
||||
export interface RunArtistShareDeps {
|
||||
artist: SubsonicArtist;
|
||||
t: TFunction;
|
||||
}
|
||||
|
||||
export async function runArtistShare(deps: RunArtistShareDeps): Promise<void> {
|
||||
const { artist, t } = deps;
|
||||
try {
|
||||
const ok = await copyEntityShareLink('artist', artist.id);
|
||||
if (ok) showToast(t('contextMenu.shareCopied'));
|
||||
else showToast(t('contextMenu.shareCopyFailed'), 4000, 'error');
|
||||
} catch {
|
||||
showToast(t('contextMenu.shareCopyFailed'), 4000, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
export interface RunArtistImageUploadDeps {
|
||||
e: React.ChangeEvent<HTMLInputElement>;
|
||||
artist: SubsonicArtist | null;
|
||||
t: TFunction;
|
||||
setUploading: (v: boolean) => void;
|
||||
setCoverRevision: React.Dispatch<React.SetStateAction<number>>;
|
||||
}
|
||||
|
||||
export async function runArtistImageUpload(deps: RunArtistImageUploadDeps): Promise<void> {
|
||||
const { e, artist, t, setUploading, setCoverRevision } = deps;
|
||||
const file = e.target.files?.[0];
|
||||
e.target.value = '';
|
||||
if (!file || !artist) return;
|
||||
setUploading(true);
|
||||
try {
|
||||
await uploadArtistImage(artist.id, file);
|
||||
const coverId = artist.coverArt || artist.id;
|
||||
await invalidateCoverArt(coverId);
|
||||
// Also invalidate with bare artist.id in case coverArt differs
|
||||
if (artist.coverArt && artist.coverArt !== artist.id) {
|
||||
await invalidateCoverArt(artist.id);
|
||||
}
|
||||
setCoverRevision(r => r + 1);
|
||||
showToast(t('artistDetail.uploadImage'));
|
||||
} catch (err) {
|
||||
showToast(
|
||||
typeof err === 'string' ? err : err instanceof Error ? err.message : t('artistDetail.uploadImageError'),
|
||||
4000,
|
||||
'error',
|
||||
);
|
||||
} finally {
|
||||
setUploading(false);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user