mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 07:15:47 +00:00
feat: add long press to shuffle with a wave animation (#888)
* feat: add long press to shuffle with a wave animation to singnify how long to press * refactor: long-press shuffle cleanup Follow-up on the long-press shuffle PR: shared hook/overlay, playback parity, pointer events, broader surface coverage, locales, and tests. * docs: credit ImAsra for long-press album shuffle (PR #888) Add CHANGELOG entry and Settings credits for the hold-to-shuffle play interaction shipped in Psychotoxical/psysonic#888. * fix: restore playAlbumShuffled and long-press hook wiring The follow-up merge dropped playAlbumShuffled and reverted the shared long-press hook in album play buttons, breaking tsc and vitest on PR #888. --------- Co-authored-by: cucadmuh <49571317+cucadmuh@users.noreply.github.com>
This commit is contained in:
@@ -16,7 +16,9 @@ import { COVER_DENSE_GRID_MIN_CELL_CSS_PX } from '../cover/layoutSizes';
|
||||
import { resolveCoverDisplayTier } from '../cover/tiers';
|
||||
import { acquireUrl } from '../utils/imageCache/urlPool';
|
||||
import { OpenArtistRefInline } from './OpenArtistRefInline';
|
||||
import { playAlbum } from '../utils/playback/playAlbum';
|
||||
import { playAlbum, playAlbumShuffled } from '../utils/playback/playAlbum';
|
||||
import { useLongPressAction } from '../hooks/useLongPressAction';
|
||||
import { LongPressWaveOverlay } from './LongPressWaveOverlay';
|
||||
import { useDragDrop } from '../contexts/DragDropContext';
|
||||
import { isAlbumRecentlyAdded } from '../utils/albumRecency';
|
||||
import { deriveAlbumArtistRefs } from '../utils/album/deriveAlbumHeaderArtistRefs';
|
||||
@@ -59,6 +61,10 @@ function AlbumCard({
|
||||
libraryResolve = false,
|
||||
}: AlbumCardProps) {
|
||||
const { t } = useTranslation();
|
||||
const { isHolding, pressBind } = useLongPressAction({
|
||||
onShortPress: () => playAlbum(album.id),
|
||||
onLongPress: () => playAlbumShuffled(album.id),
|
||||
});
|
||||
const navigate = useNavigate();
|
||||
const openContextMenu = usePlayerStore(s => s.openContextMenu);
|
||||
const enqueue = usePlayerStore(s => s.enqueue);
|
||||
@@ -157,15 +163,18 @@ function AlbumCard({
|
||||
)}
|
||||
{!selectionMode && (
|
||||
<div className="album-card-play-overlay">
|
||||
<button
|
||||
className="album-card-details-btn"
|
||||
onClick={e => { e.stopPropagation(); playAlbum(album.id); }}
|
||||
aria-label={`${album.name} abspielen`}
|
||||
data-tooltip={t('hero.playAlbum')}
|
||||
data-tooltip-pos="top"
|
||||
>
|
||||
<Play size={15} fill="currentColor" />
|
||||
</button>
|
||||
<button
|
||||
className="album-card-details-btn long-press-play-btn"
|
||||
{...pressBind}
|
||||
aria-label={`${t('hero.playAlbumTooltip')} — ${album.name}`}
|
||||
data-tooltip={t('hero.playAlbumTooltip')}
|
||||
data-tooltip-pos="top"
|
||||
>
|
||||
<LongPressWaveOverlay active={isHolding} />
|
||||
<span className="long-press-play-btn__icon">
|
||||
<Play size={15} fill="currentColor" />
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
className="album-card-details-btn"
|
||||
onClick={async e => {
|
||||
|
||||
@@ -19,7 +19,9 @@ import {
|
||||
} from '../store/becauseYouLikeCache';
|
||||
import { usePlayerStore } from '../store/playerStore';
|
||||
import { useAuthStore } from '../store/authStore';
|
||||
import { playAlbum } from '../utils/playback/playAlbum';
|
||||
import { playAlbum, playAlbumShuffled } from '../utils/playback/playAlbum';
|
||||
import { useLongPressAction } from '../hooks/useLongPressAction';
|
||||
import { LongPressWaveOverlay } from './LongPressWaveOverlay';
|
||||
import { formatHumanHoursMinutes } from '../utils/format/formatHumanDuration';
|
||||
import AlbumRow from './AlbumRow';
|
||||
|
||||
@@ -559,6 +561,10 @@ interface CardProps {
|
||||
|
||||
const BecauseCard = memo(function BecauseCard({ album, anchor, disableArtwork, enter }: CardProps) {
|
||||
const { t } = useTranslation();
|
||||
const { isHolding, pressBind } = useLongPressAction({
|
||||
onShortPress: () => playAlbum(album.id),
|
||||
onLongPress: () => playAlbumShuffled(album.id),
|
||||
});
|
||||
const navigate = useNavigate();
|
||||
const enqueue = usePlayerStore(s => s.enqueue);
|
||||
const coverRef = useAlbumCoverRef(album.id, album.coverArt, undefined, { libraryResolve: false });
|
||||
@@ -569,10 +575,6 @@ const BecauseCard = memo(function BecauseCard({ album, anchor, disableArtwork, e
|
||||
const imgSrc = coverImgSrc(coverHandle.src);
|
||||
const bgResolved = coverHandle.src;
|
||||
const handleOpen = () => navigate(`/album/${album.id}`);
|
||||
const handlePlay = (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
playAlbum(album.id);
|
||||
};
|
||||
const handleEnqueue = async (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
try {
|
||||
@@ -624,13 +626,16 @@ const BecauseCard = memo(function BecauseCard({ album, anchor, disableArtwork, e
|
||||
<div className="album-card-play-overlay">
|
||||
<button
|
||||
type="button"
|
||||
className="album-card-details-btn"
|
||||
onClick={handlePlay}
|
||||
className="album-card-details-btn long-press-play-btn"
|
||||
{...pressBind}
|
||||
aria-label={t('hero.playAlbum')}
|
||||
data-tooltip={t('hero.playAlbum')}
|
||||
data-tooltip={t('hero.playAlbumTooltip')}
|
||||
data-tooltip-pos="top"
|
||||
>
|
||||
<Play size={15} fill="currentColor" />
|
||||
<LongPressWaveOverlay active={isHolding} size="compact" />
|
||||
<span className="long-press-play-btn__icon">
|
||||
<Play size={15} fill="currentColor" />
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
+27
-13
@@ -9,13 +9,15 @@ import { useCoverArt } from '../cover/useCoverArt';
|
||||
import { useAlbumCoverRef } from '../cover/useLibraryCoverRef';
|
||||
import { usePlayerStore } from '../store/playerStore';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { playAlbum } from '../utils/playback/playAlbum';
|
||||
import { useIsMobile } from '../hooks/useIsMobile';
|
||||
import { useWindowVisibility } from '../hooks/useWindowVisibility';
|
||||
import { useAuthStore } from '../store/authStore';
|
||||
import { useThemeStore } from '../store/themeStore';
|
||||
import { filterAlbumsByMixRatings, getMixMinRatingsConfigFromAuth } from '../utils/mix/mixRatingFilter';
|
||||
import { usePerfProbeFlags } from '../utils/perf/perfFlags';
|
||||
import { playAlbum, playAlbumShuffled } from '../utils/playback/playAlbum';
|
||||
import { useLongPressAction } from '../hooks/useLongPressAction';
|
||||
import { LongPressWaveOverlay } from './LongPressWaveOverlay';
|
||||
|
||||
const INTERVAL_MS = 10000;
|
||||
const HERO_ALBUM_COUNT = 8;
|
||||
@@ -275,6 +277,10 @@ export default function Hero({ albums: albumsProp }: HeroProps = {}) {
|
||||
// transition (which would cause the background to flash empty before fading in).
|
||||
const stableBgUrl = useRef('');
|
||||
const albumId = album?.id;
|
||||
const { isHolding, pressBind } = useLongPressAction({
|
||||
onShortPress: () => { if (albumId) playAlbum(albumId); },
|
||||
onLongPress: () => { if (albumId) playAlbumShuffled(albumId); },
|
||||
});
|
||||
useEffect(() => {
|
||||
if (bgHandle.src) stableBgUrl.current = bgHandle.src;
|
||||
}, [bgHandle.src, albumId]);
|
||||
@@ -318,11 +324,15 @@ export default function Hero({ albums: albumsProp }: HeroProps = {}) {
|
||||
{isMobile ? (
|
||||
<div className="hero-actions-mobile" onClick={e => e.stopPropagation()}>
|
||||
<button
|
||||
className="album-icon-btn album-icon-btn--play"
|
||||
onClick={e => { e.stopPropagation(); playAlbum(album.id); }}
|
||||
className="album-icon-btn album-icon-btn--play long-press-play-btn"
|
||||
{...pressBind}
|
||||
aria-label={`${t('hero.playAlbum')} ${album.name}`}
|
||||
data-tooltip={t('hero.playAlbumTooltip')}
|
||||
>
|
||||
<Play size={22} fill="currentColor" />
|
||||
<LongPressWaveOverlay active={isHolding} size="compact" />
|
||||
<span className="long-press-play-btn__icon">
|
||||
<Play size={22} fill="currentColor" />
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
className="album-icon-btn album-icon-btn--queue"
|
||||
@@ -341,15 +351,19 @@ export default function Hero({ albums: albumsProp }: HeroProps = {}) {
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ display: 'flex', gap: '1rem', flexWrap: 'wrap' }}>
|
||||
<button
|
||||
className="hero-play-btn"
|
||||
id="hero-play-btn"
|
||||
onClick={e => { e.stopPropagation(); playAlbum(album.id); }}
|
||||
aria-label={`${t('hero.playAlbum')} ${album.name}`}
|
||||
>
|
||||
<Play size={18} fill="currentColor" />
|
||||
{t('hero.playAlbum')}
|
||||
</button>
|
||||
<button
|
||||
className="hero-play-btn long-press-play-btn"
|
||||
id="hero-play-btn"
|
||||
{...pressBind}
|
||||
aria-label={`${t('hero.playAlbum')} ${album.name}`}
|
||||
data-tooltip={t('hero.playAlbumTooltip')}
|
||||
>
|
||||
<LongPressWaveOverlay active={isHolding} />
|
||||
<span className="long-press-play-btn__icon" style={{ gap: '8px' }}>
|
||||
<Play size={18} fill="currentColor" />
|
||||
{t('hero.playAlbum')}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-surface"
|
||||
onClick={async (e) => {
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
interface LongPressWaveOverlayProps {
|
||||
active: boolean;
|
||||
/** Smaller crest/fill tuned for dense album-card play buttons. */
|
||||
size?: 'default' | 'compact';
|
||||
}
|
||||
|
||||
export function LongPressWaveOverlay({ active, size = 'default' }: LongPressWaveOverlayProps) {
|
||||
const compact = size === 'compact';
|
||||
return (
|
||||
<div
|
||||
className={`long-press-wave${active ? ' long-press-wave--active' : ''}${compact ? ' long-press-wave--compact' : ''}`}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<svg className="long-press-wave__crest" viewBox="0 0 200 20" preserveAspectRatio="none">
|
||||
<path d="M0,10 Q25,18 50,10 T100,10 Q125,18 150,10 T200,10 L200,20 L0,20 Z" fill="currentColor" />
|
||||
</svg>
|
||||
<div className="long-press-wave__fill" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user