Unify button tooltips across the app (#972)

* feat(tooltip): 2s open delay and shared tooltipAttrs helper

Add a 2s hover open delay in TooltipPortal (single behaviour source) so
tooltips no longer flash on quick pointer passes; hiding stays immediate.
Add tooltipAttrs() to pair data-tooltip with a matching aria-label for
buttons touched in the unification work. Covered by Vitest.

* feat(tooltip): lower open delay to 1s

2s felt too long in testing; 1s gives the same anti-flash behaviour
without making intentional hovers wait.

* feat(tooltip): action tooltips on the artist overview

Add tooltips describing the action to Last.fm, Wikipedia, Play All,
Shuffle and Radio. Shuffle/Radio now show a tooltip on desktop too,
not just mobile. Strings added to all 9 locales.

* feat(tooltip): action tooltips on the album overview

Add tooltips describing the action to the desktop Play, Artist Bio and
Download (ZIP) buttons, matching the mobile layout. Strings added to all
9 locales.

* feat(tooltip): action tooltips on the All Albums toolbar

Add tooltips describing the action to the sort, year and genre filter
buttons. SortDropdown gains an optional tooltip prop; the year and genre
filter components carry their own, so the tooltips also appear on the
other browse pages that reuse them. Strings added to all 9 locales.

* feat(tooltip): action tooltips on song-list rows

Add Play and Add-to-queue tooltips to the per-row icons in SongRow, used
by the Tracks browse list, Search and Advanced Search. Also localizes the
aria-labels, which were hardcoded English. New common.addToQueue in all 9
locales.

* fix(tooltip): uniform tooltip placement on the Artists toolbar

The favourite and multi-select buttons forced tooltips below while the
view-mode buttons auto-flipped above, so the row looked inconsistent.
Pin the view-mode buttons below too, matching the rest of the row and the
Albums toolbar.

* feat(tooltip): clarify and align the Advanced Search scope row

Add a leading "Search in:" label and per-chip tooltips so the
All/Artists/Albums/Songs row reads as a scope limiter. Drop the forced
below-placement on the small star filter (used only here) so the
favourites chip flips with the others instead of sitting alone below.
Strings added to all 9 locales.

* docs(changelog): tooltip unification (#972)
This commit is contained in:
Frank Stellmacher
2026-06-04 00:53:22 +02:00
committed by GitHub
parent 82c414d7bc
commit c119a32277
60 changed files with 404 additions and 34 deletions
+8
View File
@@ -401,6 +401,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* Single tracks in the discovery rails now show a round, vinyl-style cover so they read as songs rather than albums — clicking one still plays it instantly.
* A new **To album** badge under the artist jumps to the track's album, available in all 9 languages.
### Tooltips — consistent hints on every button
**By [@Psychotoxical](https://github.com/Psychotoxical), PR [#972](https://github.com/Psychotoxical/psysonic/pull/972)**
* Buttons across the app now show a short tooltip describing what they do, appearing after a 1-second hover so they never flash during quick mouse passes.
* Tooltips that were missing — on the artist, album, All Albums, track-list and playlist actions — are filled in, and tooltips that used to point different directions within the same toolbar now line up consistently.
* Advanced Search gains a **Search in:** label so the All / Artists / Albums / Songs row reads clearly as a scope limiter. Available in all 9 languages.
+19 -3
View File
@@ -18,6 +18,7 @@ import { formatLongDuration } from '../utils/format/formatDuration';
import { formatMb } from '../utils/format/formatBytes';
import { sanitizeHtml } from '../utils/sanitizeHtml';
import { OpenArtistRefInline } from './OpenArtistRefInline';
import { tooltipAttrs } from './tooltipAttrs';
/** True when the album artist label means "no single artist" — `getArtistInfo`
* has nothing meaningful to return for these, so the Artist Bio entry is hidden.
@@ -323,7 +324,12 @@ export default function AlbumHeader({
) : (
<div className="album-detail-actions">
<div className="album-detail-actions-primary">
<button className="btn btn-primary" id="album-play-all-btn" onClick={onPlayAll}>
<button
className="btn btn-primary"
id="album-play-all-btn"
onClick={onPlayAll}
{...tooltipAttrs(t('albumDetail.playTooltip'))}
>
<Play size={15} /> {t('common.play', 'Reproducir')}
</button>
{onShuffleAll && (
@@ -361,7 +367,12 @@ export default function AlbumHeader({
</div>
{showBioButton && (
<button className="btn btn-surface" id="album-bio-btn" onClick={onBio}>
<button
className="btn btn-surface"
id="album-bio-btn"
onClick={onBio}
{...tooltipAttrs(t('albumDetail.artistBioTooltip'))}
>
<Highlighter size={16} /> {t('albumDetail.artistBio')}
</button>
)}
@@ -375,7 +386,12 @@ export default function AlbumHeader({
<span className="download-progress-pct">{downloadProgress}%</span>
</div>
) : (
<button className="btn btn-surface" id="album-download-btn" onClick={onDownload}>
<button
className="btn btn-surface"
id="album-download-btn"
onClick={onDownload}
{...tooltipAttrs(t('albumDetail.downloadTooltip'))}
>
<Download size={16} /> {t('albumDetail.download')}{totalSize > 0 ? ` · ${formatMb(totalSize)}` : ''}
</button>
)}
+2
View File
@@ -5,6 +5,7 @@ import { createPortal } from 'react-dom';
import { Check, Filter, X } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import FilterQuickClear from './FilterQuickClear';
import { tooltipAttrs } from './tooltipAttrs';
type GenreRow = GenreFilterOption;
@@ -153,6 +154,7 @@ export default function GenreFilterBar({
onClick={() => setOpen(v => !v)}
aria-haspopup="dialog"
aria-expanded={open}
{...tooltipAttrs(t('common.filterGenreTooltip'), { pos: 'bottom' })}
style={{ display: 'flex', alignItems: 'center', gap: '0.4rem' }}
>
<Filter size={14} />
+3 -2
View File
@@ -10,6 +10,7 @@ import { enqueueAndPlay } from '../utils/playback/playSong';
import { useDragDrop } from '../contexts/DragDropContext';
import { useOrbitSongRowBehavior } from '../hooks/useOrbitSongRowBehavior';
import { formatTrackTime } from '../utils/format/formatDuration';
import { tooltipAttrs } from './tooltipAttrs';
interface Props {
song: SubsonicSong;
@@ -79,14 +80,14 @@ function SongRow({ song, showBpm }: Props) {
<button
className="song-list-row-btn song-list-row-btn--play"
onClick={(e) => { e.stopPropagation(); handlePlay(); }}
aria-label="Play"
{...tooltipAttrs(t('common.play'))}
>
<Play size={14} fill="currentColor" />
</button>
<button
className="song-list-row-btn"
onClick={(e) => { e.stopPropagation(); handleEnqueue(); }}
aria-label="Enqueue"
{...tooltipAttrs(t('common.addToQueue'))}
>
<ListPlus size={14} />
</button>
+5 -1
View File
@@ -1,6 +1,7 @@
import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { ArrowDownUp, Check } from 'lucide-react';
import { tooltipAttrs } from './tooltipAttrs';
export interface SortOption<V extends string> {
value: V;
@@ -12,9 +13,11 @@ interface Props<V extends string> {
options: SortOption<V>[];
onChange: (value: V) => void;
ariaLabel?: string;
/** Hover tooltip describing the action (shown below the trigger). */
tooltip?: string;
}
export default function SortDropdown<V extends string>({ value, options, onChange, ariaLabel }: Props<V>) {
export default function SortDropdown<V extends string>({ value, options, onChange, ariaLabel, tooltip }: Props<V>) {
const [open, setOpen] = useState(false);
const [popStyle, setPopStyle] = useState<React.CSSProperties>({});
@@ -91,6 +94,7 @@ export default function SortDropdown<V extends string>({ value, options, onChang
aria-haspopup="listbox"
aria-expanded={open}
aria-label={ariaLabel}
{...(tooltip ? tooltipAttrs(tooltip, { pos: 'bottom' }) : {})}
style={{ display: 'flex', alignItems: 'center', gap: '0.4rem' }}
>
<ArrowDownUp size={14} />
-1
View File
@@ -41,7 +41,6 @@ export default function StarFilterButton({ active, onChange, size = 'default' }:
onClick={() => onChange(!active)}
aria-pressed={active}
data-tooltip={tooltip}
data-tooltip-pos="bottom"
style={{ fontSize: 12, padding: '4px 14px', display: 'inline-flex', alignItems: 'center', gap: '0.35rem' }}
>
<Star size={12} fill={active ? 'currentColor' : 'none'} />
+69
View File
@@ -0,0 +1,69 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { act, fireEvent, screen } from '@testing-library/react';
import { renderWithProviders } from '@/test/helpers/renderWithProviders';
import TooltipPortal from './TooltipPortal';
function Fixture() {
return (
<>
<TooltipPortal />
<button data-tooltip="Play this album">play</button>
</>
);
}
describe('TooltipPortal open delay', () => {
beforeEach(() => vi.useFakeTimers());
afterEach(() => {
vi.runOnlyPendingTimers();
vi.useRealTimers();
});
it('shows the tooltip only after the 1s open delay', () => {
renderWithProviders(<Fixture />);
const btn = screen.getByText('play');
fireEvent.mouseOver(btn);
expect(screen.queryByText('Play this album')).toBeNull();
act(() => {
vi.advanceTimersByTime(999);
});
expect(screen.queryByText('Play this album')).toBeNull();
act(() => {
vi.advanceTimersByTime(1);
});
expect(screen.getByText('Play this album')).toBeInTheDocument();
});
it('cancels the pending tooltip when the pointer leaves before the delay', () => {
renderWithProviders(<Fixture />);
const btn = screen.getByText('play');
fireEvent.mouseOver(btn);
act(() => {
vi.advanceTimersByTime(500);
});
fireEvent.mouseOut(btn, { relatedTarget: document.body });
act(() => {
vi.advanceTimersByTime(1000);
});
expect(screen.queryByText('Play this album')).toBeNull();
});
it('hides immediately on mousedown', () => {
renderWithProviders(<Fixture />);
const btn = screen.getByText('play');
fireEvent.mouseOver(btn);
act(() => {
vi.advanceTimersByTime(1000);
});
expect(screen.getByText('Play this album')).toBeInTheDocument();
fireEvent.mouseDown(btn);
expect(screen.queryByText('Play this album')).toBeNull();
});
});
+63 -17
View File
@@ -8,42 +8,87 @@ interface TooltipState {
wrap: boolean;
}
/** Pointer must rest on an anchor this long before the tooltip appears. */
const TOOLTIP_OPEN_DELAY_MS = 1000;
export default function TooltipPortal() {
const [tooltip, setTooltip] = useState<TooltipState | null>(null);
const boxRef = useRef<HTMLDivElement>(null);
const [style, setStyle] = useState<React.CSSProperties>({ opacity: 0 });
const tooltipRef = useRef<TooltipState | null>(null);
tooltipRef.current = tooltip;
// Anchor counting down to show, plus its pending timer.
const pendingAnchorRef = useRef<HTMLElement | null>(null);
const pendingTimerRef = useRef<number | null>(null);
// Anchor whose tooltip is currently visible.
const shownAnchorRef = useRef<HTMLElement | null>(null);
useEffect(() => {
const onOver = (e: MouseEvent) => {
const target = (e.target as HTMLElement).closest('[data-tooltip]') as HTMLElement | null;
if (!target) return;
const text = target.getAttribute('data-tooltip');
const clearPending = () => {
if (pendingTimerRef.current !== null) {
clearTimeout(pendingTimerRef.current);
pendingTimerRef.current = null;
}
pendingAnchorRef.current = null;
};
const hide = () => {
clearPending();
shownAnchorRef.current = null;
setTooltip(null);
};
const showAnchor = (anchor: HTMLElement) => {
const text = anchor.getAttribute('data-tooltip');
if (!text) return;
shownAnchorRef.current = anchor;
// Fresh rect: layout may have shifted during the open delay.
setTooltip({
text,
anchorRect: target.getBoundingClientRect(),
preferBottom: target.getAttribute('data-tooltip-pos') === 'bottom',
wrap: target.hasAttribute('data-tooltip-wrap'),
anchorRect: anchor.getBoundingClientRect(),
preferBottom: anchor.getAttribute('data-tooltip-pos') === 'bottom',
wrap: anchor.hasAttribute('data-tooltip-wrap'),
});
};
const onOut = () => setTooltip(null);
const onOver = (e: MouseEvent) => {
const anchor = (e.target as HTMLElement).closest('[data-tooltip]') as HTMLElement | null;
if (!anchor) return;
// Already visible or already counting down for this anchor — leave it running.
if (anchor === shownAnchorRef.current || anchor === pendingAnchorRef.current) return;
// Moved onto a different anchor: drop old state and re-arm the delay.
clearPending();
if (shownAnchorRef.current) {
shownAnchorRef.current = null;
setTooltip(null);
}
pendingAnchorRef.current = anchor;
pendingTimerRef.current = window.setTimeout(() => {
pendingTimerRef.current = null;
pendingAnchorRef.current = null;
showAnchor(anchor);
}, TOOLTIP_OPEN_DELAY_MS);
};
const onOut = (e: MouseEvent) => {
const anchor = (e.target as HTMLElement).closest('[data-tooltip]') as HTMLElement | null;
if (!anchor) return;
if (anchor !== shownAnchorRef.current && anchor !== pendingAnchorRef.current) return;
// Moving within the same anchor (e.g. onto a child icon) must not cancel the delay.
const to = e.relatedTarget as Node | null;
if (to && anchor.contains(to)) return;
hide();
};
const onMove = (e: MouseEvent) => {
if (!tooltipRef.current) return;
const target = (e.target as HTMLElement).closest('[data-tooltip]');
if (!target) setTooltip(null);
if (!pendingAnchorRef.current && !shownAnchorRef.current) return;
const anchor = (e.target as HTMLElement).closest('[data-tooltip]');
if (!anchor) hide();
};
/** Clicking a tooltip anchor (e.g. opening a dropdown) keeps the cursor inside the element, so mouseout never runs — hide immediately. */
const onDown = (e: MouseEvent) => {
const t = (e.target as HTMLElement).closest('[data-tooltip]');
if (t) setTooltip(null);
if ((e.target as HTMLElement).closest('[data-tooltip]')) hide();
};
/** Wheel interactions (e.g. volume on overflow button) should suppress tooltip immediately. */
const onWheel = (e: WheelEvent) => {
const t = (e.target as HTMLElement).closest('[data-tooltip]');
if (t) setTooltip(null);
if ((e.target as HTMLElement).closest('[data-tooltip]')) hide();
};
document.addEventListener('mouseover', onOver);
document.addEventListener('mouseout', onOut);
@@ -51,6 +96,7 @@ export default function TooltipPortal() {
document.addEventListener('mousedown', onDown, true);
document.addEventListener('wheel', onWheel, { capture: true, passive: true });
return () => {
clearPending();
document.removeEventListener('mouseover', onOver);
document.removeEventListener('mouseout', onOut);
document.removeEventListener('mousemove', onMove);
+2
View File
@@ -3,6 +3,7 @@ import { createPortal } from 'react-dom';
import { CalendarRange, X } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import FilterQuickClear from './FilterQuickClear';
import { tooltipAttrs } from './tooltipAttrs';
import {
ALBUM_YEAR_MAX,
ALBUM_YEAR_MIN,
@@ -136,6 +137,7 @@ export default function YearFilterButton({
onClick={() => setOpen(v => !v)}
aria-haspopup="dialog"
aria-expanded={open}
{...tooltipAttrs(t('albums.yearFilterTooltip'), { pos: 'bottom' })}
style={{
display: 'flex', alignItems: 'center', gap: '0.4rem',
...(active ? { background: 'var(--accent)', color: 'var(--ctp-crust)' } : {}),
@@ -15,6 +15,7 @@ import { useCoverLightboxSrc } from '../../cover/lightbox';
import type { CoverArtRef } from '../../cover/types';
import LastfmIcon from '../LastfmIcon';
import StarRating from '../StarRating';
import { tooltipAttrs } from '../tooltipAttrs';
interface Props {
artist: SubsonicArtist;
@@ -142,12 +143,20 @@ export default function ArtistDetailHero({
{(info?.lastFmUrl || artist.name) && (
<div className="artist-detail-links">
{info?.lastFmUrl && (
<button className="artist-ext-link" onClick={() => openLink(info.lastFmUrl!, 'lastfm')}>
<button
className="artist-ext-link"
onClick={() => openLink(info.lastFmUrl!, 'lastfm')}
{...tooltipAttrs(t('artistDetail.lastfmTooltip'))}
>
<LastfmIcon size={14} />
{openedLink === 'lastfm' ? t('artistDetail.openedInBrowser') : 'Last.fm'}
</button>
)}
<button className="artist-ext-link" onClick={() => openLink(wikiUrl, 'wiki')}>
<button
className="artist-ext-link"
onClick={() => openLink(wikiUrl, 'wiki')}
{...tooltipAttrs(t('artistDetail.wikipediaTooltip'))}
>
<ExternalLink size={14} />
{openedLink === 'wiki' ? t('artistDetail.openedInBrowser') : 'Wikipedia'}
</button>
@@ -168,7 +177,12 @@ export default function ArtistDetailHero({
<div style={{ display: 'flex', gap: '8px', marginTop: '1.5rem', flexWrap: 'wrap' }}>
{albums.length > 0 && (
<>
<button className="btn btn-primary" onClick={handlePlayAll} disabled={playAllLoading}>
<button
className="btn btn-primary"
onClick={handlePlayAll}
disabled={playAllLoading}
{...tooltipAttrs(t('artistDetail.playAllTooltip'))}
>
{playAllLoading ? <div className="spinner" style={{ width: 16, height: 16, borderTopColor: 'currentColor' }} /> : <Play size={16} />}
{t('artistDetail.playAll')}
</button>
@@ -176,7 +190,7 @@ export default function ArtistDetailHero({
className="btn btn-surface"
onClick={handleShuffle}
disabled={playAllLoading}
data-tooltip={isMobile ? t('artistDetail.shuffle') : undefined}
{...tooltipAttrs(t('artistDetail.shuffleTooltip'))}
>
{playAllLoading ? <div className="spinner" style={{ width: 16, height: 16, borderTopColor: 'currentColor' }} /> : <Shuffle size={16} />}
{!isMobile && t('artistDetail.shuffle')}
@@ -187,7 +201,7 @@ export default function ArtistDetailHero({
className="btn btn-surface"
onClick={handleStartRadio}
disabled={radioLoading}
data-tooltip={isMobile ? t('artistDetail.radio') : undefined}
{...tooltipAttrs(t('artistDetail.radioTooltip'))}
>
{radioLoading ? <div className="spinner" style={{ width: 16, height: 16, borderTopColor: 'currentColor' }} /> : <Radio size={16} />}
{!isMobile && (radioLoading ? t('artistDetail.loading') : t('artistDetail.radio'))}
+21
View File
@@ -0,0 +1,21 @@
import { describe, it, expect } from 'vitest';
import { tooltipAttrs } from './tooltipAttrs';
describe('tooltipAttrs', () => {
it('pairs data-tooltip with a matching aria-label', () => {
expect(tooltipAttrs('Play this album')).toEqual({
'data-tooltip': 'Play this album',
'aria-label': 'Play this album',
});
});
it('adds the forced position only when requested', () => {
expect(tooltipAttrs('x', { pos: 'bottom' })['data-tooltip-pos']).toBe('bottom');
expect('data-tooltip-pos' in tooltipAttrs('x')).toBe(false);
});
it('adds the wrap marker only when requested', () => {
expect('data-tooltip-wrap' in tooltipAttrs('x', { wrap: true })).toBe(true);
expect('data-tooltip-wrap' in tooltipAttrs('x')).toBe(false);
});
});
+24
View File
@@ -0,0 +1,24 @@
/**
* Pairs a tooltip with its accessible label so the two never drift apart.
*
* Spread onto any element that should show a hover tooltip rendered by
* `TooltipPortal`. The same already-translated string becomes both the
* `data-tooltip` (visual hover label) and the `aria-label` (screen readers).
*
* <button {...tooltipAttrs(t('albums.sortTooltip'))} onClick={}>
*
* `pos: 'bottom'` is a viewport-edge escape hatch only (forces the tooltip
* below the anchor instead of the default auto-flip) do not use it on
* ordinary toolbar buttons. `wrap` enables multi-line wrapping.
*/
export function tooltipAttrs(
text: string,
opts?: { pos?: 'bottom'; wrap?: boolean },
): Record<string, string> {
return {
'data-tooltip': text,
'aria-label': text,
...(opts?.pos ? { 'data-tooltip-pos': opts.pos } : {}),
...(opts?.wrap ? { 'data-tooltip-wrap': '' } : {}),
};
}
+3
View File
@@ -7,6 +7,9 @@ export const albumDetail = {
enqueueTooltip: 'Ganzes Album zur Warteschlange hinzufügen',
artistBio: 'Künstler-Bio',
download: 'Download (ZIP)',
playTooltip: 'Dieses Album abspielen',
artistBioTooltip: 'Künstlerbiografie lesen',
downloadTooltip: 'Dieses Album als ZIP herunterladen',
downloading: 'Lade…',
cacheOffline: 'Offline verfügbar machen',
offlineCached: 'Offline verfügbar',
+2
View File
@@ -8,6 +8,8 @@ export const albums = {
yearTo: 'Bis',
yearFilterClear: 'Jahresfilter zurücksetzen',
yearFilterLabel: 'Jahr',
sortTooltip: 'Sortierung ändern',
yearFilterTooltip: 'Nach Erscheinungsjahr filtern',
compilationLabel: 'Sampler',
compilationOnly: 'Nur Sampler',
compilationHide: 'Ohne Sampler',
+5
View File
@@ -6,6 +6,11 @@ export const artistDetail = {
shareArtist: 'Künstler teilen',
shuffle: 'Zufallswiedergabe',
radio: 'Radio',
lastfmTooltip: 'Diesen Künstler auf Last.fm öffnen',
wikipediaTooltip: 'Diesen Künstler auf Wikipedia öffnen',
playAllTooltip: 'Gesamte Diskografie abspielen',
shuffleTooltip: 'Gesamte Diskografie zufällig abspielen',
radioTooltip: 'Radio basierend auf diesem Künstler starten',
loading: 'Lädt…',
noRadio: 'Keine ähnlichen Titel für diesen Künstler gefunden.',
notFound: 'Künstler nicht gefunden.',
+2
View File
@@ -21,6 +21,7 @@ export const common = {
noFolderSelected: 'Kein Ordner ausgewählt',
rememberDownloadFolder: 'Ordner merken',
filterGenre: 'Genre-Filter',
filterGenreTooltip: 'Nach Genre filtern',
filterSearchGenres: 'Genres suchen…',
filterNoGenres: 'Keine Genres gefunden',
filterClear: 'Zurücksetzen',
@@ -28,6 +29,7 @@ export const common = {
favoritesTooltipOff: 'Nur Favoriten anzeigen',
favoritesTooltipOn: 'Alle anzeigen',
play: 'Abspielen',
addToQueue: 'Zur Warteschlange hinzufügen',
bulkSelected: '{{count}} ausgewählt',
clearSelection: 'Auswahl aufheben',
bulkAddToPlaylist: 'Zur Playlist hinzufügen',
+5
View File
@@ -31,6 +31,11 @@ export const search = {
advancedMoodGroup: 'Mood',
advancedAllMoods: 'All moods',
advancedMoodLocalNote: 'Requires local library index and track analysis',
scopeRowLabel: 'Suchen in:',
scopeAllTooltip: 'Alles durchsuchen',
scopeArtistsChipTooltip: 'Ergebnisse auf Künstler beschränken',
scopeAlbumsChipTooltip: 'Ergebnisse auf Alben beschränken',
scopeSongsChipTooltip: 'Ergebnisse auf Titel beschränken',
advancedAll: 'Alle',
advancedSearch: 'Suchen',
advancedEmpty: 'Suchbegriff eingeben oder Filter wählen, um zu beginnen.',
+3
View File
@@ -7,6 +7,9 @@ export const albumDetail = {
enqueueTooltip: 'Add entire album to queue',
artistBio: 'Artist Bio',
download: 'Download (ZIP)',
playTooltip: 'Play this album',
artistBioTooltip: 'Read the artist biography',
downloadTooltip: 'Download this album as a ZIP',
downloading: 'Loading…',
cacheOffline: 'Make available offline',
offlineCached: 'Available offline',
+2
View File
@@ -8,6 +8,8 @@ export const albums = {
yearTo: 'To',
yearFilterClear: 'Clear year filter',
yearFilterLabel: 'Year',
sortTooltip: 'Change sort order',
yearFilterTooltip: 'Filter by release year',
compilationLabel: 'Compilations',
compilationOnly: 'Only compilations',
compilationHide: 'Hide compilations',
+5
View File
@@ -6,6 +6,11 @@ export const artistDetail = {
shareArtist: 'Share artist',
shuffle: 'Shuffle',
radio: 'Radio',
lastfmTooltip: 'Open this artist on Last.fm',
wikipediaTooltip: 'Open this artist on Wikipedia',
playAllTooltip: 'Play the whole discography',
shuffleTooltip: 'Shuffle the whole discography',
radioTooltip: 'Start a radio based on this artist',
loading: 'Loading…',
noRadio: 'No similar tracks found for this artist.',
notFound: 'Artist not found.',
+2
View File
@@ -21,6 +21,7 @@ export const common = {
noFolderSelected: 'No folder selected',
rememberDownloadFolder: 'Remember this folder',
filterGenre: 'Genre Filter',
filterGenreTooltip: 'Filter by genre',
filterSearchGenres: 'Search genres…',
filterNoGenres: 'No genres match',
filterClear: 'Clear',
@@ -28,6 +29,7 @@ export const common = {
favoritesTooltipOff: 'Show only favorites',
favoritesTooltipOn: 'Show all',
play: 'Play',
addToQueue: 'Add to queue',
bulkSelected: '{{count}} selected',
clearSelection: 'Clear selection',
bulkAddToPlaylist: 'Add to Playlist',
+5
View File
@@ -31,6 +31,11 @@ export const search = {
advancedMoodGroup: 'Mood',
advancedAllMoods: 'All moods',
advancedMoodLocalNote: 'Requires local library index and track analysis',
scopeRowLabel: 'Search in:',
scopeAllTooltip: 'Search everything',
scopeArtistsChipTooltip: 'Limit results to artists',
scopeAlbumsChipTooltip: 'Limit results to albums',
scopeSongsChipTooltip: 'Limit results to songs',
advancedAll: 'All',
advancedSearch: 'Search',
advancedEmpty: 'Enter a search term or select a filter to begin.',
+3
View File
@@ -7,6 +7,9 @@ export const albumDetail = {
enqueueTooltip: 'Agregar álbum completo a la cola',
artistBio: 'Biografía del Artista',
download: 'Descargar (ZIP)',
playTooltip: 'Reproducir este álbum',
artistBioTooltip: 'Leer la biografía del artista',
downloadTooltip: 'Descargar este álbum como ZIP',
downloading: 'Cargando…',
cacheOffline: 'Disponible offline',
offlineCached: 'Disponible offline',
+2
View File
@@ -8,6 +8,8 @@ export const albums = {
yearTo: 'Hasta',
yearFilterClear: 'Limpiar filtro de año',
yearFilterLabel: 'Año',
sortTooltip: 'Cambiar el orden',
yearFilterTooltip: 'Filtrar por año de lanzamiento',
compilationLabel: 'Recopilatorios',
compilationOnly: 'Solo recopilatorios',
compilationHide: 'Ocultar recopilatorios',
+5
View File
@@ -6,6 +6,11 @@ export const artistDetail = {
shareArtist: 'Compartir artista',
shuffle: 'Aleatorio',
radio: 'Radio',
lastfmTooltip: 'Abrir este artista en Last.fm',
wikipediaTooltip: 'Abrir este artista en Wikipedia',
playAllTooltip: 'Reproducir toda la discografía',
shuffleTooltip: 'Reproducir toda la discografía en aleatorio',
radioTooltip: 'Iniciar una radio basada en este artista',
loading: 'Cargando…',
noRadio: 'No se encontraron pistas similares para este artista.',
notFound: 'Artista no encontrado.',
+2
View File
@@ -20,6 +20,7 @@ export const common = {
noFolderSelected: 'Ninguna carpeta seleccionada',
rememberDownloadFolder: 'Recordar esta carpeta',
filterGenre: 'Filtro de Género',
filterGenreTooltip: 'Filtrar por género',
filterSearchGenres: 'Buscar géneros…',
filterNoGenres: 'Ningún género coincide',
filterClear: 'Limpiar',
@@ -27,6 +28,7 @@ export const common = {
favoritesTooltipOff: 'Mostrar solo favoritos',
favoritesTooltipOn: 'Mostrar todo',
play: 'Reproducir',
addToQueue: 'Añadir a la cola',
bulkSelected: '{{count}} seleccionados',
clearSelection: 'Limpiar selección',
bulkAddToPlaylist: 'Agregar a Lista',
+5
View File
@@ -31,6 +31,11 @@ export const search = {
advancedMoodGroup: 'Mood',
advancedAllMoods: 'All moods',
advancedMoodLocalNote: 'Requires local library index and track analysis',
scopeRowLabel: 'Buscar en:',
scopeAllTooltip: 'Buscar en todo',
scopeArtistsChipTooltip: 'Limitar a artistas',
scopeAlbumsChipTooltip: 'Limitar a álbumes',
scopeSongsChipTooltip: 'Limitar a canciones',
advancedAll: 'Todos',
advancedSearch: 'Buscar',
advancedEmpty: 'Ingresa un término de búsqueda o selecciona un filtro para comenzar.',
+3
View File
@@ -7,6 +7,9 @@ export const albumDetail = {
enqueueTooltip: 'Ajouter l\'album entier à la file d\'attente',
artistBio: 'Biographie',
download: 'Télécharger (ZIP)',
playTooltip: 'Lire cet album',
artistBioTooltip: 'Lire la biographie de l\'artiste',
downloadTooltip: 'Télécharger cet album en ZIP',
downloading: 'Chargement…',
cacheOffline: 'Rendre disponible hors ligne',
offlineCached: 'Disponible hors ligne',
+2
View File
@@ -8,6 +8,8 @@ export const albums = {
yearTo: 'À',
yearFilterClear: 'Effacer le filtre année',
yearFilterLabel: 'Année',
sortTooltip: 'Changer l\'ordre de tri',
yearFilterTooltip: 'Filtrer par année de sortie',
compilationLabel: 'Compilations',
compilationOnly: 'Uniquement compilations',
compilationHide: 'Masquer compilations',
+5
View File
@@ -6,6 +6,11 @@ export const artistDetail = {
shareArtist: 'Partager l\'artiste',
shuffle: 'Aléatoire',
radio: 'Radio',
lastfmTooltip: 'Ouvrir cet artiste sur Last.fm',
wikipediaTooltip: 'Ouvrir cet artiste sur Wikipédia',
playAllTooltip: 'Lire toute la discographie',
shuffleTooltip: 'Lire toute la discographie en aléatoire',
radioTooltip: 'Démarrer une radio basée sur cet artiste',
loading: 'Chargement…',
noRadio: 'Aucun morceau similaire trouvé pour cet artiste.',
notFound: 'Artiste introuvable.',
+2
View File
@@ -20,6 +20,7 @@ export const common = {
noFolderSelected: 'Aucun dossier sélectionné',
rememberDownloadFolder: 'Mémoriser ce dossier',
filterGenre: 'Filtre genre',
filterGenreTooltip: 'Filtrer par genre',
filterSearchGenres: 'Rechercher des genres…',
filterNoGenres: 'Aucun genre trouvé',
filterClear: 'Effacer',
@@ -27,6 +28,7 @@ export const common = {
favoritesTooltipOff: 'Afficher uniquement les favoris',
favoritesTooltipOn: 'Tout afficher',
play: 'Lire',
addToQueue: 'Ajouter à la file d\'attente',
bulkSelected: '{{count}} sélectionné(s)',
clearSelection: 'Effacer la sélection',
bulkAddToPlaylist: 'Ajouter à la playlist',
+5
View File
@@ -31,6 +31,11 @@ export const search = {
advancedMoodGroup: 'Mood',
advancedAllMoods: 'All moods',
advancedMoodLocalNote: 'Requires local library index and track analysis',
scopeRowLabel: 'Rechercher dans :',
scopeAllTooltip: 'Tout rechercher',
scopeArtistsChipTooltip: 'Limiter aux artistes',
scopeAlbumsChipTooltip: 'Limiter aux albums',
scopeSongsChipTooltip: 'Limiter aux titres',
advancedAll: 'Tous',
advancedSearch: 'Rechercher',
advancedEmpty: 'Entrez un terme de recherche ou sélectionnez un filtre.',
+3
View File
@@ -7,6 +7,9 @@ export const albumDetail = {
enqueueTooltip: 'Legg hele albumet i kø',
artistBio: 'Artistbiografi',
download: 'Last ned (ZIP)',
playTooltip: 'Spill dette albumet',
artistBioTooltip: 'Les artistbiografien',
downloadTooltip: 'Last ned dette albumet som ZIP',
downloading: 'Laster…',
cacheOffline: 'Gjør tilgjengelig som frakoblet',
offlineCached: 'Tilgjengelig frakoblet',
+2
View File
@@ -8,6 +8,8 @@ export const albums = {
yearTo: 'Til',
yearFilterClear: 'Tøm år filteret',
yearFilterLabel: 'År',
sortTooltip: 'Endre sorteringsrekkefølge',
yearFilterTooltip: 'Filtrer etter utgivelsesår',
compilationLabel: 'Samleplater',
compilationOnly: 'Kun samleplater',
compilationHide: 'Skjul samleplater',
+5
View File
@@ -6,6 +6,11 @@ export const artistDetail = {
shareArtist: 'Del artist',
shuffle: 'Tilfeldig rekkefølge',
radio: 'Radio',
lastfmTooltip: 'Åpne denne artisten på Last.fm',
wikipediaTooltip: 'Åpne denne artisten på Wikipedia',
playAllTooltip: 'Spill hele diskografien',
shuffleTooltip: 'Spill hele diskografien i tilfeldig rekkefølge',
radioTooltip: 'Start en radio basert på denne artisten',
loading: 'Laster…',
noRadio: 'Ingen lignende spor funnet for denne artisten.',
notFound: 'Artisten ble ikke funnet.',
+2
View File
@@ -20,6 +20,7 @@ export const common = {
noFolderSelected: 'Ingen mappe valgt',
rememberDownloadFolder: 'Husk denne mappen',
filterGenre: 'Sjangerfilter',
filterGenreTooltip: 'Filtrer etter sjanger',
filterSearchGenres: 'Søk i sjangre…',
filterNoGenres: 'Ingen sjangre samsvarer',
filterClear: 'Tøm',
@@ -27,6 +28,7 @@ export const common = {
favoritesTooltipOff: 'Vis bare favoritter',
favoritesTooltipOn: 'Vis alle',
play: 'Spill',
addToQueue: 'Legg i kø',
bulkSelected: '{{count}} valgt',
clearSelection: 'Fjern utvalg',
bulkAddToPlaylist: 'Legg til i spilleliste',
+5
View File
@@ -31,6 +31,11 @@ export const search = {
advancedMoodGroup: 'Mood',
advancedAllMoods: 'All moods',
advancedMoodLocalNote: 'Requires local library index and track analysis',
scopeRowLabel: 'Søk i:',
scopeAllTooltip: 'Søk i alt',
scopeArtistsChipTooltip: 'Begrens til artister',
scopeAlbumsChipTooltip: 'Begrens til album',
scopeSongsChipTooltip: 'Begrens til sanger',
advancedAll: 'Alle',
advancedSearch: 'Søk',
advancedEmpty: 'Skriv inn et søkeord eller velg ett filter for å begynne.',
+3
View File
@@ -7,6 +7,9 @@ export const albumDetail = {
enqueueTooltip: 'Volledig album aan wachtrij toevoegen',
artistBio: 'Artiest biografie',
download: 'Downloaden (ZIP)',
playTooltip: 'Dit album afspelen',
artistBioTooltip: 'Artiestbiografie lezen',
downloadTooltip: 'Dit album als ZIP downloaden',
downloading: 'Laden…',
cacheOffline: 'Offline beschikbaar maken',
offlineCached: 'Offline beschikbaar',
+2
View File
@@ -8,6 +8,8 @@ export const albums = {
yearTo: 'Tot',
yearFilterClear: 'Jaarfilter wissen',
yearFilterLabel: 'Jaar',
sortTooltip: 'Sorteervolgorde wijzigen',
yearFilterTooltip: 'Filteren op uitgavejaar',
compilationLabel: 'Compilaties',
compilationOnly: 'Alleen compilaties',
compilationHide: 'Compilaties verbergen',
+5
View File
@@ -6,6 +6,11 @@ export const artistDetail = {
shareArtist: 'Artiest delen',
shuffle: 'Willekeurig',
radio: 'Radio',
lastfmTooltip: 'Deze artiest openen op Last.fm',
wikipediaTooltip: 'Deze artiest openen op Wikipedia',
playAllTooltip: 'Volledige discografie afspelen',
shuffleTooltip: 'Volledige discografie willekeurig afspelen',
radioTooltip: 'Radio op basis van deze artiest starten',
loading: 'Laden…',
noRadio: 'Geen vergelijkbare nummers gevonden voor deze artiest.',
notFound: 'Artiest niet gevonden.',
+2
View File
@@ -20,6 +20,7 @@ export const common = {
noFolderSelected: 'Geen map geselecteerd',
rememberDownloadFolder: 'Deze map onthouden',
filterGenre: 'Genre-filter',
filterGenreTooltip: 'Filteren op genre',
filterSearchGenres: 'Genres zoeken…',
filterNoGenres: 'Geen genres gevonden',
filterClear: 'Wissen',
@@ -27,6 +28,7 @@ export const common = {
favoritesTooltipOff: 'Alleen favorieten tonen',
favoritesTooltipOn: 'Alles tonen',
play: 'Afspelen',
addToQueue: 'Aan wachtrij toevoegen',
bulkSelected: '{{count}} geselecteerd',
clearSelection: 'Selectie wissen',
bulkAddToPlaylist: 'Toevoegen aan afspeellijst',
+5
View File
@@ -31,6 +31,11 @@ export const search = {
advancedMoodGroup: 'Mood',
advancedAllMoods: 'All moods',
advancedMoodLocalNote: 'Requires local library index and track analysis',
scopeRowLabel: 'Zoeken in:',
scopeAllTooltip: 'Alles doorzoeken',
scopeArtistsChipTooltip: 'Beperken tot artiesten',
scopeAlbumsChipTooltip: 'Beperken tot albums',
scopeSongsChipTooltip: 'Beperken tot nummers',
advancedAll: 'Alle',
advancedSearch: 'Zoeken',
advancedEmpty: 'Voer een zoekterm in of selecteer een filter.',
+3
View File
@@ -7,6 +7,9 @@ export const albumDetail = {
enqueueTooltip: 'Adaugă întregul album în coadă',
artistBio: 'Biografia Artistului',
download: 'Descarcă (ZIP)',
playTooltip: 'Redă acest album',
artistBioTooltip: 'Citește biografia artistului',
downloadTooltip: 'Descarcă acest album ca ZIP',
downloading: 'Se încarcă…',
cacheOffline: 'Fă disponibil offline',
offlineCached: 'Disponibil offline',
+2
View File
@@ -8,6 +8,8 @@ export const albums = {
yearTo: 'Până la',
yearFilterClear: 'Golește filtrul de an',
yearFilterLabel: 'An',
sortTooltip: 'Schimbă ordinea de sortare',
yearFilterTooltip: 'Filtrează după anul lansării',
compilationLabel: 'Compilații',
compilationOnly: 'Doar compilații',
compilationHide: 'Ascunde compilațiile',
+5
View File
@@ -6,6 +6,11 @@ export const artistDetail = {
shareArtist: 'Distribuie artistul',
shuffle: 'Amestecă',
radio: 'Radio',
lastfmTooltip: 'Deschide acest artist pe Last.fm',
wikipediaTooltip: 'Deschide acest artist pe Wikipedia',
playAllTooltip: 'Redă întreaga discografie',
shuffleTooltip: 'Redă întreaga discografie amestecat',
radioTooltip: 'Pornește un radio bazat pe acest artist',
loading: 'Se încarcă…',
noRadio: 'Nicio piesă similară găsită pentru acest artist.',
notFound: 'Artistul nu a fost găsit.',
+2
View File
@@ -21,6 +21,7 @@ export const common = {
noFolderSelected: 'Niciun folder selectat',
rememberDownloadFolder: 'Ține minte acest folder',
filterGenre: 'Filtru de gen',
filterGenreTooltip: 'Filtrează după gen',
filterSearchGenres: 'Caută genuri…',
filterNoGenres: 'Niciun gen nu corespunde',
filterClear: 'Golește',
@@ -28,6 +29,7 @@ export const common = {
favoritesTooltipOff: 'Arată doar favorite',
favoritesTooltipOn: 'Arată tot',
play: 'Redă',
addToQueue: 'Adaugă în coadă',
bulkSelected: '{{count}} selectate',
clearSelection: 'Golește selecția',
bulkAddToPlaylist: 'Adaugă la Playlist',
+5
View File
@@ -31,6 +31,11 @@ export const search = {
advancedMoodGroup: 'Mood',
advancedAllMoods: 'All moods',
advancedMoodLocalNote: 'Requires local library index and track analysis',
scopeRowLabel: 'Caută în:',
scopeAllTooltip: 'Caută peste tot',
scopeArtistsChipTooltip: 'Limitează la artiști',
scopeAlbumsChipTooltip: 'Limitează la albume',
scopeSongsChipTooltip: 'Limitează la piese',
advancedAll: 'Toate',
advancedSearch: 'Căutare',
advancedEmpty: 'Introdu un termen de căutare sau alege un filtru pentru a începe',
+3
View File
@@ -7,6 +7,9 @@ export const albumDetail = {
enqueueTooltip: 'Добавить весь альбом в очередь',
artistBio: 'Биография',
download: 'Скачать (ZIP)',
playTooltip: 'Воспроизвести этот альбом',
artistBioTooltip: 'Читать биографию исполнителя',
downloadTooltip: 'Скачать этот альбом в ZIP',
downloading: 'Загрузка…',
cacheOffline: 'Сохранить офлайн',
offlineCached: 'Доступно офлайн',
+2
View File
@@ -8,6 +8,8 @@ export const albums = {
yearTo: 'По',
yearFilterClear: 'Сбросить год',
yearFilterLabel: 'Год',
sortTooltip: 'Изменить порядок сортировки',
yearFilterTooltip: 'Фильтр по году выпуска',
compilationLabel: 'Сборники',
compilationOnly: 'Только сборники',
compilationHide: 'Скрыть сборники',
+5
View File
@@ -6,6 +6,11 @@ export const artistDetail = {
shareArtist: 'Поделиться исполнителем',
shuffle: 'Перемешать',
radio: 'Радио',
lastfmTooltip: 'Открыть этого исполнителя на Last.fm',
wikipediaTooltip: 'Открыть этого исполнителя в Википедии',
playAllTooltip: 'Воспроизвести всю дискографию',
shuffleTooltip: 'Воспроизвести всю дискографию вперемешку',
radioTooltip: 'Запустить радио на основе этого исполнителя',
loading: 'Загрузка…',
noRadio: 'Похожие треки для этого исполнителя не найдены.',
notFound: 'Исполнитель не найден.',
+2
View File
@@ -20,6 +20,7 @@ export const common = {
noFolderSelected: 'Папка не выбрана',
rememberDownloadFolder: 'Запомнить папку',
filterGenre: 'Фильтр по жанру',
filterGenreTooltip: 'Фильтр по жанру',
filterSearchGenres: 'Поиск жанров…',
filterNoGenres: 'Нет совпадений',
filterClear: 'Сбросить',
@@ -27,6 +28,7 @@ export const common = {
favoritesTooltipOff: 'Показывать только избранное',
favoritesTooltipOn: 'Показать всё',
play: 'Воспроизвести',
addToQueue: 'Добавить в очередь',
bulkSelected: 'Выбрано: {{count}}',
clearSelection: 'Сбросить выбор',
bulkAddToPlaylist: 'В плейлист',
+5
View File
@@ -31,6 +31,11 @@ export const search = {
advancedMoodGroup: 'Настроение',
advancedAllMoods: 'Все настроения',
advancedMoodLocalNote: 'Только локальный индекс и анализ треков',
scopeRowLabel: 'Искать в:',
scopeAllTooltip: 'Искать везде',
scopeArtistsChipTooltip: 'Только исполнители',
scopeAlbumsChipTooltip: 'Только альбомы',
scopeSongsChipTooltip: 'Только треки',
advancedAll: 'Все',
advancedSearch: 'Найти',
advancedEmpty: 'Введите запрос или выберите фильтр.',
+3
View File
@@ -7,6 +7,9 @@ export const albumDetail = {
enqueueTooltip: '将整张专辑添加到播放队列',
artistBio: '艺术家简介',
download: '下载 (ZIP)',
playTooltip: '播放此专辑',
artistBioTooltip: '阅读艺术家简介',
downloadTooltip: '将此专辑下载为 ZIP',
downloading: '加载中…',
cacheOffline: '设为离线可用',
offlineCached: '已离线缓存',
+2
View File
@@ -8,6 +8,8 @@ export const albums = {
yearTo: '到',
yearFilterClear: '清除年份筛选',
yearFilterLabel: '年份',
sortTooltip: '更改排序方式',
yearFilterTooltip: '按发行年份筛选',
compilationLabel: '合辑',
compilationOnly: '仅合辑',
compilationHide: '隐藏合辑',
+5
View File
@@ -6,6 +6,11 @@ export const artistDetail = {
shareArtist: '分享艺人',
shuffle: '随机播放',
radio: '电台',
lastfmTooltip: '在 Last.fm 上打开该艺人',
wikipediaTooltip: '在维基百科上打开该艺人',
playAllTooltip: '播放全部作品',
shuffleTooltip: '随机播放全部作品',
radioTooltip: '基于该艺人开始电台',
loading: '加载中…',
noRadio: '未找到该艺术家的相似曲目。',
notFound: '未找到艺术家。',
+2
View File
@@ -20,6 +20,7 @@ export const common = {
noFolderSelected: '未选择文件夹',
rememberDownloadFolder: '记住此文件夹',
filterGenre: '流派筛选',
filterGenreTooltip: '按流派筛选',
filterSearchGenres: '搜索流派…',
filterNoGenres: '未找到匹配流派',
filterClear: '清除',
@@ -27,6 +28,7 @@ export const common = {
favoritesTooltipOff: '仅显示收藏',
favoritesTooltipOn: '显示全部',
play: '播放',
addToQueue: '添加到队列',
bulkSelected: '已选 {{count}} 首',
clearSelection: '清除选择',
bulkAddToPlaylist: '添加到播放列表',
+5
View File
@@ -31,6 +31,11 @@ export const search = {
advancedMoodGroup: 'Mood',
advancedAllMoods: 'All moods',
advancedMoodLocalNote: 'Requires local library index and track analysis',
scopeRowLabel: '搜索范围:',
scopeAllTooltip: '搜索全部',
scopeArtistsChipTooltip: '仅显示艺术家',
scopeAlbumsChipTooltip: '仅显示专辑',
scopeSongsChipTooltip: '仅显示歌曲',
advancedAll: '全部',
advancedSearch: '搜索',
advancedEmpty: '请输入搜索词或选择过滤器。',
+1
View File
@@ -384,6 +384,7 @@ export default function Albums() {
value={sort}
options={sortOptions}
onChange={onSortChange}
tooltip={t('albums.sortTooltip')}
/>
<YearFilterButton
+3
View File
@@ -331,6 +331,7 @@ export default function Artists() {
style={showArtistImages ? { background: 'var(--accent)', color: 'var(--ctp-crust)', padding: '0.5rem' } : { padding: '0.5rem' }}
data-tooltip={showArtistImages ? t('artists.imagesOn') : t('artists.imagesOff')}
data-tooltip-wrap
data-tooltip-pos="bottom"
>
<Images size={20} />
</button>
@@ -339,6 +340,7 @@ export default function Artists() {
onClick={() => setViewMode('grid')}
style={viewMode === 'grid' ? { background: 'var(--accent)', color: 'var(--ctp-crust)', padding: '0.5rem' } : { padding: '0.5rem' }}
data-tooltip={t('artists.gridView')}
data-tooltip-pos="bottom"
>
<LayoutGrid size={20} />
</button>
@@ -347,6 +349,7 @@ export default function Artists() {
onClick={() => setViewMode('list')}
style={viewMode === 'list' ? { background: 'var(--accent)', color: 'var(--ctp-crust)', padding: '0.5rem' } : { padding: '0.5rem' }}
data-tooltip={t('artists.listView')}
data-tooltip-pos="bottom"
>
<List size={20} />
</button>
+12 -5
View File
@@ -11,6 +11,7 @@ import ArtistRow from '../components/ArtistRow';
import PagedSongList from '../components/PagedSongList';
import CustomSelect from '../components/CustomSelect';
import StarFilterButton from '../components/StarFilterButton';
import { tooltipAttrs } from '../components/tooltipAttrs';
import { APP_MAIN_SCROLL_VIEWPORT_ID } from '../constants/appScroll';
import { useAuthStore } from '../store/authStore';
import { usePlayerStore } from '../store/playerStore';
@@ -821,11 +822,11 @@ export default function SearchBrowsePage() {
});
};
const typeOptions: { id: ResultType; label: string }[] = [
{ id: 'all', label: t('search.advancedAll') },
{ id: 'artists', label: t('search.artists') },
{ id: 'albums', label: t('search.albums') },
{ id: 'songs', label: t('search.songs') },
const typeOptions: { id: ResultType; label: string; tooltip: string }[] = [
{ id: 'all', label: t('search.advancedAll'), tooltip: t('search.scopeAllTooltip') },
{ id: 'artists', label: t('search.artists'), tooltip: t('search.scopeArtistsChipTooltip') },
{ id: 'albums', label: t('search.albums'), tooltip: t('search.scopeAlbumsChipTooltip') },
{ id: 'songs', label: t('search.songs'), tooltip: t('search.scopeSongsChipTooltip') },
];
const genreSelectOptions = [
@@ -1059,6 +1060,11 @@ export default function SearchBrowsePage() {
{/* Row 4: Result type + Search button */}
<div style={{ display: 'flex', gap: '0.5rem', alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap' }}>
<div style={{ display: 'flex', gap: '0.3rem', flexWrap: 'wrap', alignItems: 'center' }}>
{!trackFilterActive && (
<span style={{ fontSize: 12, color: 'var(--text-muted)', marginRight: '0.15rem' }}>
{t('search.scopeRowLabel')}
</span>
)}
{!trackFilterActive && typeOptions.map(opt => (
<button
key={opt.id}
@@ -1066,6 +1072,7 @@ export default function SearchBrowsePage() {
className={`btn ${resultType === opt.id ? 'btn-primary' : 'btn-surface'}`}
style={{ fontSize: 12, padding: '4px 14px' }}
onClick={() => setResultType(opt.id)}
{...tooltipAttrs(opt.tooltip)}
>
{opt.label}
</button>