mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-21 23:05:46 +00:00
feat(settings): add a Compact buttons appearance toggle (#1189)
* feat(ui): prototype compact hero and toolbar buttons (Large/Small appearance setting) * feat(settings): rename action-button size toggle to "Compact buttons" Promote the hero-button prototype to a real, app-wide setting. - rename heroButtonSize → buttonSize, data-hero-buttons → data-button-size, hero-action-bar/hero-btn-label → compact-action-bar/compact-btn-label - relabel "Hero buttons" → "Compact buttons" and broaden the description (action + toolbar buttons across detail pages and browse views) in all 11 locales - move the feature CSS out of cover-lightbox.css into its own compact-buttons.css - add tests: themeStore buttonSize toggle, SelectionToggleButton * docs(changelog): compact buttons appearance toggle (#1189) * docs(credits): compact buttons appearance toggle (#1189)
This commit is contained in:
@@ -115,6 +115,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
* Album details now surface every genre a release spans instead of just the first one: the main genre shows inline with a **+N** chip that opens the full, clickable list, each genre linking to its genre page.
|
||||
* Genres combine album and track tags (matching the genre browser) and read from the local library index when it is ready, so they also work offline.
|
||||
|
||||
### Compact buttons — switch action and toolbar buttons to icon-only
|
||||
|
||||
**By [@Psychotoxical](https://github.com/Psychotoxical), PR [#1189](https://github.com/Psychotoxical/psysonic/pull/1189)**
|
||||
|
||||
* New **Compact buttons** setting under Settings → Appearance. Switch the action and toolbar buttons between large labelled buttons and small icon-only ones — across album, artist and playlist headers, the shared browse toolbars (sort, filters, multi-select), and the Most Played sort/filter controls. Defaults to large, so nothing changes unless you turn it on. On phones the album header keeps its large touch targets.
|
||||
|
||||
|
||||
## Changed
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ export default function App() {
|
||||
useThemeStore(s => s.theme);
|
||||
const effectiveTheme = useThemeScheduler();
|
||||
const font = useFontStore(s => s.font);
|
||||
const buttonSize = useThemeStore(s => s.buttonSize);
|
||||
const installedThemes = useInstalledThemesStore(s => s.themes);
|
||||
|
||||
// Document-attribute hooks are shared between both window kinds — each
|
||||
@@ -81,6 +82,10 @@ export default function App() {
|
||||
document.documentElement.setAttribute('data-font', font);
|
||||
}, [font]);
|
||||
|
||||
useEffect(() => {
|
||||
document.documentElement.setAttribute('data-button-size', buttonSize);
|
||||
}, [buttonSize]);
|
||||
|
||||
// Hide all inline track-preview buttons when the user opts out — single
|
||||
// CSS hook (`html[data-track-previews="off"]`) instead of conditional
|
||||
// rendering in every tracklist. Per-location toggles use additional
|
||||
|
||||
@@ -475,7 +475,7 @@ export default function AlbumHeader({
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="album-detail-actions">
|
||||
<div className="album-detail-actions compact-action-bar">
|
||||
<div className="album-detail-actions-primary">
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
@@ -483,7 +483,7 @@ export default function AlbumHeader({
|
||||
onClick={onPlayAll}
|
||||
{...tooltipAttrs(t('albumDetail.playTooltip'))}
|
||||
>
|
||||
<Play size={15} /> {t('common.play', 'Reproducir')}
|
||||
<Play size={15} /> <span className="compact-btn-label">{t('common.play', 'Reproducir')}</span>
|
||||
</button>
|
||||
{onShuffleAll && (
|
||||
<button
|
||||
@@ -528,7 +528,7 @@ export default function AlbumHeader({
|
||||
onClick={onBio}
|
||||
{...tooltipAttrs(t('albumDetail.artistBioTooltip'))}
|
||||
>
|
||||
<Highlighter size={16} /> {t('albumDetail.artistBio')}
|
||||
<Highlighter size={16} /> <span className="compact-btn-label">{t('albumDetail.artistBio')}</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
@@ -548,7 +548,7 @@ export default function AlbumHeader({
|
||||
onClick={onDownload}
|
||||
{...tooltipAttrs(t('albumDetail.downloadTooltip'))}
|
||||
>
|
||||
<Download size={16} /> {t('albumDetail.download')}{totalSize > 0 ? ` · ${formatMb(totalSize)}` : ''}
|
||||
<Download size={16} /> <span className="compact-btn-label">{t('albumDetail.download')}{totalSize > 0 ? ` · ${formatMb(totalSize)}` : ''}</span>
|
||||
</button>
|
||||
)
|
||||
)}
|
||||
@@ -562,28 +562,31 @@ export default function AlbumHeader({
|
||||
<button
|
||||
className="btn btn-surface offline-cache-btn offline-cache-btn--queued"
|
||||
onClick={onCacheOffline}
|
||||
aria-label={t('albumDetail.offlineQueued')}
|
||||
data-tooltip={t('albumDetail.removeFromOfflineQueue')}
|
||||
>
|
||||
<HardDriveDownload size={16} />
|
||||
{t('albumDetail.offlineQueued')}
|
||||
<span className="compact-btn-label">{t('albumDetail.offlineQueued')}</span>
|
||||
</button>
|
||||
) : offlineStatus === 'cached' ? (
|
||||
<button
|
||||
className="btn btn-surface offline-cache-btn offline-cache-btn--cached"
|
||||
onClick={onRemoveOffline}
|
||||
aria-label={t('albumDetail.offlineCached')}
|
||||
data-tooltip={t('albumDetail.removeOffline')}
|
||||
>
|
||||
<HardDriveDownload size={16} />
|
||||
{t('albumDetail.offlineCached')}
|
||||
<span className="compact-btn-label">{t('albumDetail.offlineCached')}</span>
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
className="btn btn-surface offline-cache-btn"
|
||||
onClick={onCacheOffline}
|
||||
aria-label={t('albumDetail.cacheOffline')}
|
||||
data-tooltip={t('albumDetail.cacheOffline')}
|
||||
>
|
||||
<HardDriveDownload size={16} />
|
||||
{t('albumDetail.cacheOffline')}
|
||||
<span className="compact-btn-label">{t('albumDetail.cacheOffline')}</span>
|
||||
</button>
|
||||
)
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import SelectionToggleButton from './SelectionToggleButton';
|
||||
|
||||
describe('SelectionToggleButton', () => {
|
||||
it('shows the select label and calls onToggle on click', async () => {
|
||||
const onToggle = vi.fn();
|
||||
const user = userEvent.setup();
|
||||
render(
|
||||
<SelectionToggleButton
|
||||
active={false}
|
||||
onToggle={onToggle}
|
||||
selectLabel="Multi-select"
|
||||
cancelLabel="Cancel selection"
|
||||
/>,
|
||||
);
|
||||
const btn = screen.getByRole('button', { name: 'Multi-select' });
|
||||
await user.click(btn);
|
||||
expect(onToggle).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('shows the cancel label and active styling when active', () => {
|
||||
render(
|
||||
<SelectionToggleButton
|
||||
active
|
||||
onToggle={() => {}}
|
||||
selectLabel="Multi-select"
|
||||
cancelLabel="Cancel selection"
|
||||
/>,
|
||||
);
|
||||
const btn = screen.getByRole('button', { name: 'Cancel selection' });
|
||||
expect(btn).toHaveClass('btn-sort-active');
|
||||
});
|
||||
|
||||
it('keeps the label in a toolbar-btn-label span for compact-mode hiding', () => {
|
||||
render(
|
||||
<SelectionToggleButton
|
||||
active={false}
|
||||
onToggle={() => {}}
|
||||
selectLabel="Multi-select"
|
||||
cancelLabel="Cancel"
|
||||
/>,
|
||||
);
|
||||
expect(document.querySelector('.toolbar-btn-label')?.textContent).toBe('Multi-select');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
import { CheckSquare2 } from 'lucide-react';
|
||||
|
||||
interface Props {
|
||||
/** Whether selection mode is currently active. */
|
||||
active: boolean;
|
||||
onToggle: () => void;
|
||||
/** Label when not selecting (e.g. "Multi-select"). */
|
||||
selectLabel: string;
|
||||
/** Label while selecting (e.g. "Cancel selection"). */
|
||||
cancelLabel: string;
|
||||
/** Tooltip when inactive — defaults to `selectLabel`. */
|
||||
startTooltip?: string;
|
||||
iconSize?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared multi-select toggle for browse-page toolbars (Albums, Artists,
|
||||
* New Releases, Random Albums, Lossless Albums). The label sits in a
|
||||
* `toolbar-btn-label` span so the existing mobile / compact-mode rule can
|
||||
* collapse it to icon-only while keeping the icon + tooltip + aria-label.
|
||||
*/
|
||||
export default function SelectionToggleButton({
|
||||
active,
|
||||
onToggle,
|
||||
selectLabel,
|
||||
cancelLabel,
|
||||
startTooltip,
|
||||
iconSize = 15,
|
||||
}: Props) {
|
||||
const label = active ? cancelLabel : selectLabel;
|
||||
return (
|
||||
<button
|
||||
className={`btn btn-surface${active ? ' btn-sort-active' : ''}`}
|
||||
onClick={onToggle}
|
||||
aria-label={label}
|
||||
data-tooltip={active ? cancelLabel : (startTooltip ?? selectLabel)}
|
||||
data-tooltip-pos="bottom"
|
||||
style={active ? { background: 'var(--accent)', color: 'var(--text-on-accent)' } : undefined}
|
||||
>
|
||||
<CheckSquare2 size={iconSize} />
|
||||
<span className="toolbar-btn-label">{label}</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export default function WikipediaIcon({ size = 16 }: { size?: number }) {
|
||||
return (
|
||||
<svg width={size} height={size} viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
||||
<path d="M12.09 13.119c-.936 1.932-2.217 4.548-2.853 5.728-.616 1.074-1.127.931-1.532.029-1.406-3.321-4.293-9.144-5.651-12.409-.251-.601-.441-.987-.619-1.139-.181-.15-.554-.24-1.122-.271C.103 5.033 0 4.982 0 4.898v-.455l.052-.045c.924-.005 5.401 0 5.401 0l.051.045v.434c0 .119-.075.176-.225.176l-.564.031c-.485.029-.727.164-.727.436 0 .135.053.33.166.601 1.082 2.646 4.818 10.521 4.818 10.521l.136.046 2.411-4.81-.482-1.067-1.658-3.264s-.318-.654-.428-.872c-.728-1.443-.712-1.518-1.447-1.617-.207-.023-.313-.05-.313-.149v-.468l.06-.045h4.292l.113.037v.451c0 .105-.076.15-.227.15l-.308.047c-.792.061-.661.381-.136 1.422l1.582 3.252 1.758-3.504c.293-.64.233-.801.111-.947-.07-.084-.305-.22-.812-.24l-.201-.021c-.052 0-.098-.015-.145-.051-.045-.031-.067-.076-.067-.129v-.427l.061-.045c1.247-.008 4.043 0 4.043 0l.059.045v.436c0 .121-.059.178-.193.178-.646.03-.782.095-1.023.439-.12.186-.375.589-.646 1.039l-2.301 4.273-.065.135 2.792 5.712.17.048 4.396-10.438c.154-.422.129-.722-.064-.895-.197-.172-.346-.273-.857-.295l-.42-.016c-.061 0-.105-.014-.152-.045-.043-.029-.072-.075-.072-.119v-.436l.059-.045h4.961l.041.045v.437c0 .119-.074.18-.209.18-.648.03-1.127.18-1.443.421-.314.255-.557.616-.736 1.067 0 0-4.043 9.258-5.426 12.339-.525 1.007-1.053.917-1.503-.031-.571-1.171-1.773-3.786-2.646-5.71l.053-.036z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import React, { useMemo, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useAlbumDetailBack } from '../../hooks/useAlbumDetailBack';
|
||||
import {
|
||||
ArrowLeft, Camera, Check, ExternalLink, HardDriveDownload, Heart,
|
||||
ArrowLeft, Camera, Check, HardDriveDownload, Heart,
|
||||
Loader2, Play, Radio, Share2, Shuffle, Users,
|
||||
} from 'lucide-react';
|
||||
import type { SubsonicAlbum, SubsonicArtist, SubsonicArtistInfo } from '../../api/subsonicTypes';
|
||||
@@ -17,6 +17,7 @@ import { useCachedUrl } from '../CachedImage';
|
||||
import { useCoverLightboxSrc } from '../../cover/lightbox';
|
||||
import type { CoverArtRef } from '../../cover/types';
|
||||
import LastfmIcon from '../LastfmIcon';
|
||||
import WikipediaIcon from '../WikipediaIcon';
|
||||
import StarRating from '../StarRating';
|
||||
import { tooltipAttrs } from '../tooltipAttrs';
|
||||
import { offlineActionPolicy, type OfflineActionPolicy } from '../../utils/offline/offlineActionPolicy';
|
||||
@@ -241,7 +242,7 @@ export default function ArtistDetailHero({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
|
||||
<div className="compact-action-bar" style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
|
||||
{(info?.lastFmUrl || artist.name) && (
|
||||
<div className="artist-detail-links">
|
||||
{info?.lastFmUrl && (
|
||||
@@ -251,7 +252,7 @@ export default function ArtistDetailHero({
|
||||
{...tooltipAttrs(t('artistDetail.lastfmTooltip'))}
|
||||
>
|
||||
<LastfmIcon size={14} />
|
||||
{openedLink === 'lastfm' ? t('artistDetail.openedInBrowser') : 'Last.fm'}
|
||||
<span className="compact-btn-label">{openedLink === 'lastfm' ? t('artistDetail.openedInBrowser') : 'Last.fm'}</span>
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
@@ -259,8 +260,8 @@ export default function ArtistDetailHero({
|
||||
onClick={() => openLink(wikiUrl, 'wiki')}
|
||||
{...tooltipAttrs(t('artistDetail.wikipediaTooltip'))}
|
||||
>
|
||||
<ExternalLink size={14} />
|
||||
{openedLink === 'wiki' ? t('artistDetail.openedInBrowser') : 'Wikipedia'}
|
||||
<WikipediaIcon size={14} />
|
||||
<span className="compact-btn-label">{openedLink === 'wiki' ? t('artistDetail.openedInBrowser') : 'Wikipedia'}</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
@@ -269,16 +270,17 @@ export default function ArtistDetailHero({
|
||||
<button
|
||||
className="artist-ext-link"
|
||||
onClick={toggleStar}
|
||||
aria-label={isStarred ? t('artistDetail.favoriteRemove') : t('artistDetail.favoriteAdd')}
|
||||
data-tooltip={isStarred ? t('artistDetail.favoriteRemove') : t('artistDetail.favoriteAdd')}
|
||||
style={{ color: isStarred ? 'var(--accent)' : 'inherit', border: isStarred ? '1px solid var(--accent)' : undefined }}
|
||||
>
|
||||
<Heart size={14} fill={isStarred ? "currentColor" : "none"} />
|
||||
{t('artistDetail.favorite')}
|
||||
<span className="compact-btn-label">{t('artistDetail.favorite')}</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: '8px', marginTop: '1.5rem', flexWrap: 'wrap' }}>
|
||||
<div className="compact-action-bar" style={{ display: 'flex', gap: '8px', marginTop: '1.5rem', flexWrap: 'wrap' }}>
|
||||
{albums.length > 0 && (
|
||||
<>
|
||||
<button
|
||||
@@ -288,7 +290,7 @@ export default function ArtistDetailHero({
|
||||
{...tooltipAttrs(t('artistDetail.playAllTooltip'))}
|
||||
>
|
||||
{playAllLoading ? <div className="spinner" style={{ width: 16, height: 16, borderTopColor: 'currentColor' }} /> : <Play size={16} />}
|
||||
{t('artistDetail.playAll')}
|
||||
<span className="compact-btn-label">{t('artistDetail.playAll')}</span>
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-surface"
|
||||
@@ -297,7 +299,7 @@ export default function ArtistDetailHero({
|
||||
{...tooltipAttrs(t('artistDetail.shuffleTooltip'))}
|
||||
>
|
||||
{playAllLoading ? <div className="spinner" style={{ width: 16, height: 16, borderTopColor: 'currentColor' }} /> : <Shuffle size={16} />}
|
||||
{!isMobile && t('artistDetail.shuffle')}
|
||||
{!isMobile && <span className="compact-btn-label">{t('artistDetail.shuffle')}</span>}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
@@ -308,7 +310,7 @@ export default function ArtistDetailHero({
|
||||
{...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'))}
|
||||
{!isMobile && <span className="compact-btn-label">{radioLoading ? t('artistDetail.loading') : t('artistDetail.radio')}</span>}
|
||||
</button>
|
||||
{id && artist && (
|
||||
<button
|
||||
@@ -353,6 +355,7 @@ export default function ArtistDetailHero({
|
||||
? <Check size={16} />
|
||||
: <HardDriveDownload size={16} />}
|
||||
{!isMobile && (
|
||||
<span className="compact-btn-label">{
|
||||
artistOfflineStatus === 'downloading' && artistOfflineProgress
|
||||
? t('artistDetail.offlineDownloading', {
|
||||
done: artistOfflineProgress.done,
|
||||
@@ -363,6 +366,7 @@ export default function ArtistDetailHero({
|
||||
: artistOfflineStatus === 'cached'
|
||||
? t('artistDetail.offlineCached')
|
||||
: t('artistDetail.cacheOffline')
|
||||
}</span>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
|
||||
@@ -67,10 +67,12 @@ export default function FavoritesSongsSectionHeader({
|
||||
</div>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="favorites-songs-toolbar" style={{ display: 'flex', alignItems: 'center', gap: '0.75rem', flexWrap: 'wrap' }}>
|
||||
<div className="favorites-songs-toolbar compact-action-bar" style={{ display: 'flex', alignItems: 'center', gap: '0.75rem', flexWrap: 'wrap' }}>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
disabled={targetSongs.length === 0}
|
||||
aria-label={inSelectMode ? t('favorites.playSelected') : t('favorites.playAll')}
|
||||
data-tooltip={inSelectMode ? t('favorites.playSelected') : t('favorites.playAll')}
|
||||
onClick={() => {
|
||||
if (targetSongs.length === 0) return;
|
||||
const tracks = targetSongs.map(songToTrack);
|
||||
@@ -78,11 +80,13 @@ export default function FavoritesSongsSectionHeader({
|
||||
}}
|
||||
>
|
||||
<Play size={15} />
|
||||
{inSelectMode ? t('favorites.playSelected') : t('favorites.playAll')}
|
||||
<span className="compact-btn-label">{inSelectMode ? t('favorites.playSelected') : t('favorites.playAll')}</span>
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-surface"
|
||||
disabled={targetSongs.length === 0}
|
||||
aria-label={inSelectMode ? t('favorites.enqueueSelected') : t('favorites.enqueueAll')}
|
||||
data-tooltip={inSelectMode ? t('favorites.enqueueSelected') : t('favorites.enqueueAll')}
|
||||
onClick={() => {
|
||||
if (targetSongs.length === 0) return;
|
||||
const tracks = targetSongs.map(songToTrack);
|
||||
@@ -90,21 +94,25 @@ export default function FavoritesSongsSectionHeader({
|
||||
}}
|
||||
>
|
||||
<ListPlus size={15} />
|
||||
{inSelectMode ? t('favorites.enqueueSelected') : t('favorites.enqueueAll')}
|
||||
<span className="compact-btn-label">{inSelectMode ? t('favorites.enqueueSelected') : t('favorites.enqueueAll')}</span>
|
||||
</button>
|
||||
|
||||
{/* Filter Toggle Button */}
|
||||
<button
|
||||
className={`btn ${showFilters || selectedGenres.length > 0 || yearRange[0] !== minYear || yearRange[1] !== currentYear ? 'btn-primary' : 'btn-surface'}`}
|
||||
onClick={() => setShowFilters(v => !v)}
|
||||
aria-label={t('common.filters')}
|
||||
data-tooltip={t('common.filters')}
|
||||
>
|
||||
<SlidersHorizontal size={14} />
|
||||
{t('common.filters')}
|
||||
<span className="compact-btn-label">{t('common.filters')}</span>
|
||||
</button>
|
||||
|
||||
{(selectedArtist || selectedGenres.length > 0 || yearRange[0] !== minYear || yearRange[1] !== currentYear) && (
|
||||
<button
|
||||
className="btn btn-ghost"
|
||||
aria-label={t('common.clearAll')}
|
||||
data-tooltip={t('common.clearAll')}
|
||||
onClick={() => {
|
||||
setSelectedArtist(null);
|
||||
setSelectedGenres([]);
|
||||
@@ -114,7 +122,7 @@ export default function FavoritesSongsSectionHeader({
|
||||
}}
|
||||
>
|
||||
<X size={13} />
|
||||
{t('common.clearAll')}
|
||||
<span className="compact-btn-label">{t('common.clearAll')}</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
|
||||
@@ -148,15 +148,16 @@ export default function PlaylistHero({
|
||||
)}
|
||||
{saving && <Loader2 size={12} className="spin-slow" style={{ display: 'inline', marginLeft: 4 }} />}
|
||||
</div>
|
||||
<div className="album-detail-actions">
|
||||
<div className="album-detail-actions compact-action-bar">
|
||||
<div className="album-detail-actions-primary">
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
disabled={songs.length === 0}
|
||||
onClick={handlePlayAll}
|
||||
aria-label={t('playlists.playTooltip')}
|
||||
data-tooltip={t('playlists.playTooltip')}
|
||||
>
|
||||
<Play size={15} /> {t('common.play', 'Reproducir')}
|
||||
<Play size={15} /> <span className="compact-btn-label">{t('common.play', 'Reproducir')}</span>
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-ghost"
|
||||
@@ -179,9 +180,10 @@ export default function PlaylistHero({
|
||||
<button
|
||||
className={`btn btn-ghost ${searchOpen ? 'active' : ''}`}
|
||||
onClick={() => { setSearchOpen(v => !v); setSearchQuery(''); setSearchResults([]); setSelectedSearchIds(new Set()); setSearchPlPickerOpen(false); }}
|
||||
aria-label={t('playlists.addSongsTooltip')}
|
||||
data-tooltip={t('playlists.addSongsTooltip')}
|
||||
>
|
||||
<Search size={16} /> {t('playlists.addSongs')}
|
||||
<Search size={16} /> <span className="compact-btn-label">{t('playlists.addSongs')}</span>
|
||||
</button>
|
||||
)}
|
||||
{actionPolicy.canEditPlaylist && isLayoutVisible('importCsv') && (
|
||||
@@ -189,10 +191,11 @@ export default function PlaylistHero({
|
||||
className="btn btn-ghost"
|
||||
onClick={handleImportCsv}
|
||||
disabled={csvImporting}
|
||||
aria-label={t('playlists.importCSVTooltip')}
|
||||
data-tooltip={t('playlists.importCSVTooltip')}
|
||||
>
|
||||
{csvImporting ? <Loader2 size={16} className="spin-slow" /> : <FileUp size={16} />}
|
||||
{t('playlists.importCSV')}
|
||||
<span className="compact-btn-label">{t('playlists.importCSV')}</span>
|
||||
</button>
|
||||
)}
|
||||
{actionPolicy.canDownload && isLayoutVisible('downloadZip') && songs.length > 0 && (
|
||||
@@ -205,8 +208,8 @@ export default function PlaylistHero({
|
||||
<span className="download-progress-pct">{activeZip.total ? Math.round((activeZip.bytes / activeZip.total) * 100) : '…'}%</span>
|
||||
</div>
|
||||
) : (
|
||||
<button className="btn btn-ghost" onClick={handleDownload} data-tooltip={t('playlists.downloadZip')}>
|
||||
<Download size={16} /> {t('playlists.downloadZip')}{songs.reduce((acc, s) => acc + (s.size ?? 0), 0) > 0 ? ` · ${formatSize(songs.reduce((acc, s) => acc + (s.size ?? 0), 0))}` : ''}
|
||||
<button className="btn btn-ghost" onClick={handleDownload} aria-label={t('playlists.downloadZip')} data-tooltip={t('playlists.downloadZip')}>
|
||||
<Download size={16} /> <span className="compact-btn-label">{t('playlists.downloadZip')}{songs.reduce((acc, s) => acc + (s.size ?? 0), 0) > 0 ? ` · ${formatSize(songs.reduce((acc, s) => acc + (s.size ?? 0), 0))}` : ''}</span>
|
||||
</button>
|
||||
)
|
||||
)}
|
||||
@@ -235,22 +238,22 @@ export default function PlaylistHero({
|
||||
{offlineStatus === 'downloading' ? (
|
||||
<>
|
||||
<div className="spinner" style={{ width: 14, height: 14, borderTopColor: 'currentColor' }} />
|
||||
{t('albumDetail.offlineDownloading', { n: offlineProgress?.done ?? 0, total: offlineProgress?.total ?? 0 })}
|
||||
<span className="compact-btn-label">{t('albumDetail.offlineDownloading', { n: offlineProgress?.done ?? 0, total: offlineProgress?.total ?? 0 })}</span>
|
||||
</>
|
||||
) : offlineStatus === 'queued' ? (
|
||||
<>
|
||||
<HardDriveDownload size={16} />
|
||||
{t('albumDetail.offlineQueued')}
|
||||
<span className="compact-btn-label">{t('albumDetail.offlineQueued')}</span>
|
||||
</>
|
||||
) : offlineStatus === 'cached' ? (
|
||||
<>
|
||||
<Trash2 size={16} />
|
||||
{t('playlists.removeOffline')}
|
||||
<span className="compact-btn-label">{t('playlists.removeOffline')}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<HardDriveDownload size={16} />
|
||||
{t('playlists.cacheOffline')}
|
||||
<span className="compact-btn-label">{t('playlists.cacheOffline')}</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
@@ -65,7 +65,7 @@ export default function PlaylistSuggestions({
|
||||
|
||||
return (
|
||||
<div className="playlist-suggestions tracklist" data-preview-loc="suggestions">
|
||||
<div className="playlist-suggestions-header">
|
||||
<div className="playlist-suggestions-header compact-action-bar">
|
||||
<div className="playlist-suggestions-title">
|
||||
<h2 className="section-title" style={{ marginBottom: 0 }}>{t('playlists.suggestions')}</h2>
|
||||
<span className="playlist-suggestions-hint">{t('playlists.suggestionsHint')}</span>
|
||||
@@ -74,10 +74,11 @@ export default function PlaylistSuggestions({
|
||||
className="btn btn-surface"
|
||||
onClick={() => loadSuggestions(songs)}
|
||||
disabled={loadingSuggestions || songs.length === 0}
|
||||
aria-label={t('playlists.refreshSuggestions')}
|
||||
data-tooltip={t('playlists.refreshSuggestions')}
|
||||
>
|
||||
<RefreshCw size={14} className={loadingSuggestions ? 'spin-slow' : ''} />
|
||||
{t('playlists.refreshSuggestions')}
|
||||
<span className="compact-btn-label">{t('playlists.refreshSuggestions')}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { CheckSquare2, Plus, Trash2 } from 'lucide-react';
|
||||
import { CheckSquare2, Plus, Sparkles, Trash2 } from 'lucide-react';
|
||||
import type { SubsonicPlaylist } from '../../api/subsonicTypes';
|
||||
import {
|
||||
defaultSmartFilters, type SmartFilters,
|
||||
@@ -48,7 +48,7 @@ export default function PlaylistsHeader({
|
||||
? t('playlists.selectionCount', { count: selectedIds.size })
|
||||
: t('playlists.title')}
|
||||
</h1>
|
||||
<div style={{ display: 'flex', gap: '0.5rem', alignItems: 'center', flexWrap: 'wrap', justifyContent: 'flex-start' }}>
|
||||
<div className="compact-action-bar" style={{ display: 'flex', gap: '0.5rem', alignItems: 'center', flexWrap: 'wrap', justifyContent: 'flex-start' }}>
|
||||
{policy.canEditPlaylist && !(selectionMode && selectedIds.size > 0) && (<>
|
||||
{creating ? (
|
||||
<>
|
||||
@@ -72,8 +72,8 @@ export default function PlaylistsHeader({
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<button className="btn btn-primary" onClick={() => { setCreatingSmart(false); setCreating(true); }}>
|
||||
<Plus size={15} /> {t('playlists.newPlaylist')}
|
||||
<button className="btn btn-primary" onClick={() => { setCreatingSmart(false); setCreating(true); }} aria-label={t('playlists.newPlaylist')} data-tooltip={t('playlists.newPlaylist')}>
|
||||
<Plus size={15} /> <span className="compact-btn-label">{t('playlists.newPlaylist')}</span>
|
||||
</button>
|
||||
)}
|
||||
{!creating && isNavidromeServer && (
|
||||
@@ -83,8 +83,8 @@ export default function PlaylistsHeader({
|
||||
setSmartFilters(defaultSmartFilters);
|
||||
setGenreQuery('');
|
||||
setCreatingSmart(v => !v);
|
||||
}}>
|
||||
<Plus size={15} /> {t('smartPlaylists.create')}
|
||||
}} aria-label={t('smartPlaylists.create')} data-tooltip={t('smartPlaylists.create')}>
|
||||
<Sparkles size={15} /> <span className="compact-btn-label">{t('smartPlaylists.create')}</span>
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
@@ -98,25 +98,27 @@ export default function PlaylistsHeader({
|
||||
className="btn btn-danger"
|
||||
onClick={handleDeleteSelected}
|
||||
disabled={deletableCount === 0}
|
||||
aria-label={t('playlists.deleteSelected')}
|
||||
data-tooltip={deletableCount === selectedIds.size
|
||||
? undefined
|
||||
: t('playlists.deleteSelectedPartial', { n: deletableCount, total: selectedIds.size })}
|
||||
data-tooltip-pos="bottom"
|
||||
>
|
||||
<Trash2 size={15} />
|
||||
{t('playlists.deleteSelected')}
|
||||
<span className="compact-btn-label">{t('playlists.deleteSelected')}</span>
|
||||
</button>
|
||||
);
|
||||
})()}
|
||||
<button
|
||||
className={`btn btn-surface${selectionMode ? ' btn-sort-active' : ''}`}
|
||||
onClick={toggleSelectionMode}
|
||||
aria-label={selectionMode ? t('playlists.cancelSelect') : t('playlists.select')}
|
||||
data-tooltip={selectionMode ? t('playlists.cancelSelect') : t('playlists.startSelect')}
|
||||
data-tooltip-pos="bottom"
|
||||
style={selectionMode ? { background: 'var(--accent)', color: 'var(--text-on-accent)' } : {}}
|
||||
>
|
||||
<CheckSquare2 size={15} />
|
||||
{selectionMode ? t('playlists.cancelSelect') : t('playlists.select')}
|
||||
<span className="compact-btn-label">{selectionMode ? t('playlists.cancelSelect') : t('playlists.select')}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -61,8 +61,8 @@ export default function PlaylistsNewFolderButton() {
|
||||
}
|
||||
|
||||
return (
|
||||
<button className="btn btn-surface" onClick={() => setCreating(true)}>
|
||||
<FolderPlus size={15} /> {t('playlists.folders.newFolder')}
|
||||
<button className="btn btn-surface" onClick={() => setCreating(true)} aria-label={t('playlists.folders.newFolder')} data-tooltip={t('playlists.folders.newFolder')}>
|
||||
<FolderPlus size={15} /> <span className="compact-btn-label">{t('playlists.folders.newFolder')}</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -28,28 +28,31 @@ export default function RandomMixHeader({
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '2rem' }}>
|
||||
<h1 className="page-title">{t('randomMix.title')}</h1>
|
||||
|
||||
<div style={{ display: 'flex', gap: '0.5rem' }}>
|
||||
<div className="compact-action-bar" style={{ display: 'flex', gap: '0.5rem' }}>
|
||||
<button
|
||||
className="btn btn-surface"
|
||||
onClick={onRefresh}
|
||||
disabled={selectedGenre ? genreMixLoading : loading}
|
||||
aria-label={selectedGenre ? t('randomMix.remixGenre', { genre: selectedGenre }) : t('randomMix.remix')}
|
||||
data-tooltip={selectedGenre
|
||||
? t('randomMix.remixTooltipGenre', { genre: selectedGenre })
|
||||
: t('randomMix.remixTooltip')
|
||||
}
|
||||
>
|
||||
<RefreshCw size={18} className={(selectedGenre ? genreMixLoading : loading) ? 'spin' : ''} />
|
||||
{selectedGenre ? t('randomMix.remixGenre', { genre: selectedGenre }) : t('randomMix.remix')}
|
||||
<span className="compact-btn-label">{selectedGenre ? t('randomMix.remixGenre', { genre: selectedGenre }) : t('randomMix.remix')}</span>
|
||||
</button>
|
||||
<button
|
||||
className={`btn ${isGenreLoading ? 'btn-surface' : 'btn-primary'}`}
|
||||
onClick={onPlayAll}
|
||||
disabled={isPlayDisabled}
|
||||
aria-label={t('randomMix.playAll')}
|
||||
data-tooltip={t('randomMix.playAll')}
|
||||
>
|
||||
{isGenreLoading ? (
|
||||
<><div className="spinner" style={{ width: 14, height: 14, borderWidth: 2 }} /> {Math.min(genreMixSongsLength, randomMixSize)} / {randomMixSize}</>
|
||||
<><div className="spinner" style={{ width: 14, height: 14, borderWidth: 2 }} /> <span className="compact-btn-label">{Math.min(genreMixSongsLength, randomMixSize)} / {randomMixSize}</span></>
|
||||
) : (
|
||||
<><Play size={18} fill="currentColor" /> {t('randomMix.playAll')}</>
|
||||
<><Play size={18} fill="currentColor" /> <span className="compact-btn-label">{t('randomMix.playAll')}</span></>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { LayoutGrid, Palette, Sliders, Type, ZoomIn } from 'lucide-react';
|
||||
import { LayoutGrid, Maximize2, Palette, Sliders, Type, ZoomIn } from 'lucide-react';
|
||||
import { useAuthStore } from '../../store/authStore';
|
||||
import {
|
||||
LIBRARY_GRID_MAX_COLUMNS_MAX,
|
||||
@@ -308,6 +308,34 @@ export function AppearanceTab() {
|
||||
</SettingsGroup>
|
||||
</div>
|
||||
</SettingsSubSection>
|
||||
|
||||
<SettingsSubSection
|
||||
title={t('settings.buttonSizeTitle')}
|
||||
icon={<Maximize2 size={16} />}
|
||||
>
|
||||
<div className="settings-card">
|
||||
<SettingsGroup>
|
||||
<SettingsSubCard>
|
||||
<SettingsField
|
||||
label={t('settings.buttonSizeLabel')}
|
||||
desc={t('settings.buttonSizeDesc')}
|
||||
>
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
{(['large', 'small'] as const).map(size => (
|
||||
<button
|
||||
key={size}
|
||||
className={`btn ${theme.buttonSize === size ? 'btn-primary' : 'btn-ghost'}`}
|
||||
onClick={() => theme.setButtonSize(size)}
|
||||
>
|
||||
{t(`settings.buttonSize_${size}`)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</SettingsField>
|
||||
</SettingsSubCard>
|
||||
</SettingsGroup>
|
||||
</div>
|
||||
</SettingsSubSection>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -166,12 +166,12 @@ export default function TracksPageChrome({
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
<div className="tracks-hero-actions">
|
||||
<button className="btn btn-primary" onClick={() => playSongNow(hero)}>
|
||||
<Play size={16} fill="currentColor" /> {t('tracks.playSong')}
|
||||
<div className="tracks-hero-actions compact-action-bar">
|
||||
<button className="btn btn-primary" onClick={() => playSongNow(hero)} aria-label={t('tracks.playSong')} data-tooltip={t('tracks.playSong')}>
|
||||
<Play size={16} fill="currentColor" /> <span className="compact-btn-label">{t('tracks.playSong')}</span>
|
||||
</button>
|
||||
<button className="btn btn-surface" onClick={() => enqueue([songToTrack(hero)])}>
|
||||
<ListPlus size={16} /> {t('tracks.enqueueSong')}
|
||||
<button className="btn btn-surface" onClick={() => enqueue([songToTrack(hero)])} aria-label={t('tracks.enqueueSong')} data-tooltip={t('tracks.enqueueSong')}>
|
||||
<ListPlus size={16} /> <span className="compact-btn-label">{t('tracks.enqueueSong')}</span>
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-surface"
|
||||
|
||||
@@ -387,6 +387,7 @@ const CONTRIBUTOR_ENTRIES = [
|
||||
'Per-device equalizer profiles — opt-in, remembers and restores the EQ for each audio output device (PR #1146)',
|
||||
'Hungarian (Magyar) translation (PR #1149)',
|
||||
'Theme scheduler — follow the OS light/dark setting as an alternative to the time-based day/night schedule (PR #1163)',
|
||||
'Compact buttons — appearance toggle that switches action and toolbar buttons to icon-only across detail headers and browse views, app-wide (PR #1189)',
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -648,6 +648,11 @@ export const settings = {
|
||||
sidebarLyricsStyleAppleDesc: 'Aktive Zeile oben mit sanfter Animation.',
|
||||
seekbarStyle: 'Seekbar-Stil',
|
||||
seekbarStyleDesc: 'Aussehen der Wiedergabe-Seekbar auswählen',
|
||||
buttonSizeTitle: 'Kompakte Buttons',
|
||||
buttonSizeLabel: 'Buttongröße',
|
||||
buttonSizeDesc: 'Verkleinert die Aktions- und Toolbar-Buttons auf Symbole — in Detailseiten und Übersichten.',
|
||||
buttonSize_large: 'Groß',
|
||||
buttonSize_small: 'Klein',
|
||||
seekbarTruewave: 'Echte Wellenform',
|
||||
seekbarPseudowave: 'Pseudo-Wellenform',
|
||||
seekbarLinedot: 'Linie & Punkt',
|
||||
|
||||
@@ -715,6 +715,11 @@ export const settings = {
|
||||
sidebarLyricsStyleAppleDesc: 'Active line anchors near the top with smooth spring transitions.',
|
||||
seekbarStyle: 'Seekbar Style',
|
||||
seekbarStyleDesc: 'Choose the look of the player seek bar',
|
||||
buttonSizeTitle: 'Compact buttons',
|
||||
buttonSizeLabel: 'Button size',
|
||||
buttonSizeDesc: 'Shrink the action and toolbar buttons to icons on detail pages and browse views.',
|
||||
buttonSize_large: 'Large',
|
||||
buttonSize_small: 'Small',
|
||||
seekbarTruewave: 'Truewave',
|
||||
seekbarPseudowave: 'Pseudowave',
|
||||
seekbarLinedot: 'Line & Dot',
|
||||
|
||||
@@ -647,6 +647,11 @@ export const settings = {
|
||||
sidebarLyricsStyleAppleDesc: 'La línea activa se ancla arriba con animación suave.',
|
||||
seekbarStyle: 'Estilo de Barra de Progreso',
|
||||
seekbarStyleDesc: 'Elige la apariencia de la barra de progreso del reproductor',
|
||||
buttonSizeTitle: 'Botones compactos',
|
||||
buttonSizeLabel: 'Tamaño de los botones',
|
||||
buttonSizeDesc: 'Reduce los botones de acción y de barra de herramientas a iconos en las páginas de detalle y las vistas de exploración.',
|
||||
buttonSize_large: 'Grande',
|
||||
buttonSize_small: 'Pequeño',
|
||||
seekbarTruewave: 'Forma de Onda Real',
|
||||
seekbarPseudowave: 'Forma de Onda Pseudo',
|
||||
seekbarLinedot: 'Línea y Punto',
|
||||
|
||||
@@ -635,6 +635,11 @@ export const settings = {
|
||||
sidebarLyricsStyleAppleDesc: "La ligne active reste en haut avec une animation fluide.",
|
||||
seekbarStyle: 'Style de la barre de lecture',
|
||||
seekbarStyleDesc: 'Choisir l\'apparence de la barre de progression',
|
||||
buttonSizeTitle: 'Boutons compacts',
|
||||
buttonSizeLabel: 'Taille des boutons',
|
||||
buttonSizeDesc: 'Réduit les boutons d\'action et de barre d\'outils à des icônes sur les pages de détail et les vues de navigation.',
|
||||
buttonSize_large: 'Grande',
|
||||
buttonSize_small: 'Petite',
|
||||
seekbarTruewave: 'Forme d\'onde réelle',
|
||||
seekbarPseudowave: 'Forme d\'onde pseudo',
|
||||
seekbarLinedot: 'Ligne & point',
|
||||
|
||||
@@ -715,6 +715,11 @@ export const settings = {
|
||||
sidebarLyricsStyleAppleDesc: 'Az aktív sor a tetejéhez közel rögzül, sima rugós átmenetekkel.',
|
||||
seekbarStyle: 'Keresősáv stílusa',
|
||||
seekbarStyleDesc: 'Válaszd ki a lejátszó keresősávjának kinézetét',
|
||||
buttonSizeTitle: 'Kompakt gombok',
|
||||
buttonSizeLabel: 'Gombméret',
|
||||
buttonSizeDesc: 'Ikonokká zsugorítja a műveleti és eszköztár gombokat a részletoldalakon és a böngészőnézetekben.',
|
||||
buttonSize_large: 'Nagy',
|
||||
buttonSize_small: 'Kicsi',
|
||||
seekbarTruewave: 'Truewave',
|
||||
seekbarPseudowave: 'Pseudowave',
|
||||
seekbarLinedot: 'Vonal és pont',
|
||||
|
||||
@@ -709,6 +709,11 @@ export const settings = {
|
||||
sidebarLyricsStyleAppleDesc: 'アクティブ行を上部付近に固定し、滑らかなスプリング遷移を使います。',
|
||||
seekbarStyle: 'シークバー形式',
|
||||
seekbarStyleDesc: 'プレイヤーシークバーの見た目を選びます',
|
||||
buttonSizeTitle: 'コンパクトボタン',
|
||||
buttonSizeLabel: 'ボタンサイズ',
|
||||
buttonSizeDesc: '詳細ページや一覧ビューで、アクションボタンやツールバーボタンをアイコンのみに縮小します。',
|
||||
buttonSize_large: '大',
|
||||
buttonSize_small: '小',
|
||||
seekbarTruewave: 'Truewave',
|
||||
seekbarPseudowave: 'Pseudowave',
|
||||
seekbarLinedot: 'Line & Dot',
|
||||
|
||||
@@ -634,6 +634,11 @@ export const settings = {
|
||||
sidebarLyricsStyleAppleDesc: 'Aktiv linje forankres øverst med jevn animasjon.',
|
||||
seekbarStyle: 'Søkefelt-stil',
|
||||
seekbarStyleDesc: 'Velg utseendet på avspillingssøkefeltet',
|
||||
buttonSizeTitle: 'Kompakte knapper',
|
||||
buttonSizeLabel: 'Knappestørrelse',
|
||||
buttonSizeDesc: 'Krymper handlings- og verktøylinjeknappene til ikoner på detaljsider og oversikter.',
|
||||
buttonSize_large: 'Stor',
|
||||
buttonSize_small: 'Liten',
|
||||
seekbarTruewave: 'Ekte bølgeform',
|
||||
seekbarPseudowave: 'Pseudo-bølgeform',
|
||||
seekbarLinedot: 'Linje & punkt',
|
||||
|
||||
@@ -635,6 +635,11 @@ export const settings = {
|
||||
sidebarLyricsStyleAppleDesc: 'Actieve regel verankerd bovenaan met vloeiende animatie.',
|
||||
seekbarStyle: 'Zoekbalkstijl',
|
||||
seekbarStyleDesc: 'Kies het uiterlijk van de afspeelbalk',
|
||||
buttonSizeTitle: 'Compacte knoppen',
|
||||
buttonSizeLabel: 'Knopgrootte',
|
||||
buttonSizeDesc: 'Verkleint de actie- en werkbalkknoppen tot pictogrammen op detailpagina\'s en overzichten.',
|
||||
buttonSize_large: 'Groot',
|
||||
buttonSize_small: 'Klein',
|
||||
seekbarTruewave: 'Echte golfvorm',
|
||||
seekbarPseudowave: 'Pseudo-golfvorm',
|
||||
seekbarLinedot: 'Lijn & punt',
|
||||
|
||||
@@ -650,6 +650,11 @@ export const settings = {
|
||||
sidebarLyricsStyleAppleDesc: 'Linia activă este ancorată aproape de vârf cu animații netede.',
|
||||
seekbarStyle: 'Stilul barei de redare',
|
||||
seekbarStyleDesc: 'Alege aspectul barei de redare a playerului',
|
||||
buttonSizeTitle: 'Butoane compacte',
|
||||
buttonSizeLabel: 'Dimensiunea butoanelor',
|
||||
buttonSizeDesc: 'Reduce butoanele de acțiune și din bara de instrumente la pictograme pe paginile de detalii și vizualizările de navigare.',
|
||||
buttonSize_large: 'Mari',
|
||||
buttonSize_small: 'Mici',
|
||||
seekbarTruewave: 'Truewave',
|
||||
seekbarPseudowave: 'Pseudowave',
|
||||
seekbarLinedot: 'Linie & Punct',
|
||||
|
||||
@@ -735,6 +735,11 @@ export const settings = {
|
||||
sidebarLyricsStyleAppleDesc: 'Активная строка фиксируется сверху с плавной анимацией.',
|
||||
seekbarStyle: 'Стиль прогресс-бара',
|
||||
seekbarStyleDesc: 'Выбор внешнего вида полосы воспроизведения',
|
||||
buttonSizeTitle: 'Компактные кнопки',
|
||||
buttonSizeLabel: 'Размер кнопок',
|
||||
buttonSizeDesc: 'Уменьшает кнопки действий и панели инструментов до значков на страницах сведений и в списках.',
|
||||
buttonSize_large: 'Большие',
|
||||
buttonSize_small: 'Маленькие',
|
||||
seekbarTruewave: 'Реальная форма волны',
|
||||
seekbarPseudowave: 'Псевдо форма волны',
|
||||
seekbarLinedot: 'Линия и точка',
|
||||
|
||||
@@ -634,6 +634,11 @@ export const settings = {
|
||||
sidebarLyricsStyleAppleDesc: '活动行固定在顶部,带有平滑弹簧动画。',
|
||||
seekbarStyle: '进度条样式',
|
||||
seekbarStyleDesc: '选择播放进度条的外观',
|
||||
buttonSizeTitle: '紧凑按钮',
|
||||
buttonSizeLabel: '按钮大小',
|
||||
buttonSizeDesc: '在详情页和浏览视图中将操作按钮和工具栏按钮缩小为图标。',
|
||||
buttonSize_large: '大',
|
||||
buttonSize_small: '小',
|
||||
seekbarTruewave: '真实波形',
|
||||
seekbarPseudowave: '伪波形',
|
||||
seekbarLinedot: '线条与点',
|
||||
|
||||
+9
-11
@@ -22,7 +22,8 @@ import { invoke } from '@tauri-apps/api/core';
|
||||
import { join } from '@tauri-apps/api/path';
|
||||
import { showToast } from '../utils/ui/toast';
|
||||
import { useZipDownloadStore } from '../store/zipDownloadStore';
|
||||
import { CheckSquare2, Download, HardDriveDownload, Disc3, ListPlus } from 'lucide-react';
|
||||
import { Download, HardDriveDownload, Disc3, ListPlus } from 'lucide-react';
|
||||
import SelectionToggleButton from '../components/SelectionToggleButton';
|
||||
import FilterQuickClear from '../components/FilterQuickClear';
|
||||
import { usePerfProbeFlags } from '../utils/perf/perfFlags';
|
||||
import { useRangeSelection } from '../hooks/useRangeSelection';
|
||||
@@ -445,16 +446,13 @@ export default function Albums() {
|
||||
</>
|
||||
)}
|
||||
|
||||
<button
|
||||
className={`btn btn-surface${selectionMode ? ' btn-sort-active' : ''}`}
|
||||
onClick={toggleSelectionMode}
|
||||
data-tooltip={selectionMode ? t('albums.cancelSelect') : t('albums.startSelect')}
|
||||
data-tooltip-pos="bottom"
|
||||
style={selectionMode ? { background: 'var(--accent)', color: 'var(--text-on-accent)' } : {}}
|
||||
>
|
||||
<CheckSquare2 size={15} />
|
||||
<span className="toolbar-btn-label">{selectionMode ? t('albums.cancelSelect') : t('albums.select')}</span>
|
||||
</button>
|
||||
<SelectionToggleButton
|
||||
active={selectionMode}
|
||||
onToggle={toggleSelectionMode}
|
||||
selectLabel={t('albums.select')}
|
||||
cancelLabel={t('albums.cancelSelect')}
|
||||
startTooltip={t('albums.startSelect')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+10
-11
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useState, useCallback, useRef } from 'react';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { LayoutGrid, List, Images, CheckSquare2 } from 'lucide-react';
|
||||
import { LayoutGrid, List, Images } from 'lucide-react';
|
||||
import SelectionToggleButton from '../components/SelectionToggleButton';
|
||||
import StarFilterButton from '../components/StarFilterButton';
|
||||
import OverlayScrollArea from '../components/OverlayScrollArea';
|
||||
import { usePlayerStore } from '../store/playerStore';
|
||||
@@ -361,16 +362,14 @@ export default function Artists() {
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
<button
|
||||
className={`btn btn-surface${selectionMode ? ' btn-sort-active' : ''}`}
|
||||
onClick={toggleSelectionMode}
|
||||
data-tooltip={selectionMode ? t('artists.cancelSelect') : t('artists.startSelect')}
|
||||
data-tooltip-pos="bottom"
|
||||
style={selectionMode ? { background: 'var(--accent)', color: 'var(--text-on-accent)' } : {}}
|
||||
>
|
||||
<CheckSquare2 size={15} />
|
||||
{selectionMode ? t('artists.cancelSelect') : t('artists.select')}
|
||||
</button>
|
||||
<SelectionToggleButton
|
||||
active={selectionMode}
|
||||
onToggle={toggleSelectionMode}
|
||||
selectLabel={t('artists.select')}
|
||||
cancelLabel={t('artists.cancelSelect')}
|
||||
startTooltip={t('artists.startSelect')}
|
||||
iconSize={20}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -8,7 +8,8 @@ import AlbumCard from '../components/AlbumCard';
|
||||
import { ArtistHeroCover } from '../cover/artistHero';
|
||||
import { coverArtRef } from '../cover/ref';
|
||||
import { useCoverLightboxSrc } from '../cover/lightbox';
|
||||
import { ArrowLeft, Users, ExternalLink, Heart, Feather, Share2 } from 'lucide-react';
|
||||
import { ArrowLeft, Users, Heart, Feather, Share2 } from 'lucide-react';
|
||||
import WikipediaIcon from '../components/WikipediaIcon';
|
||||
import { open } from '@tauri-apps/plugin-shell';
|
||||
import { usePlayerStore } from '../store/playerStore';
|
||||
import { useAuthStore } from '../store/authStore';
|
||||
@@ -211,12 +212,12 @@ export default function ComposerDetail() {
|
||||
<span>{t('composerDetail.workCount', { count: albums.length })}</span>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
|
||||
<div className="compact-action-bar" style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
|
||||
{wikiUrl && (
|
||||
<div className="artist-detail-links">
|
||||
<button className="artist-ext-link" onClick={() => openLink(wikiUrl, 'wiki')}>
|
||||
<ExternalLink size={14} />
|
||||
{openedLink === 'wiki' ? t('artistDetail.openedInBrowser') : 'Wikipedia'}
|
||||
<button className="artist-ext-link" onClick={() => openLink(wikiUrl, 'wiki')} aria-label={t('artistDetail.wikipediaTooltip')} data-tooltip={t('artistDetail.wikipediaTooltip')}>
|
||||
<WikipediaIcon size={14} />
|
||||
<span className="compact-btn-label">{openedLink === 'wiki' ? t('artistDetail.openedInBrowser') : 'Wikipedia'}</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
@@ -225,11 +226,12 @@ export default function ComposerDetail() {
|
||||
<button
|
||||
className="artist-ext-link"
|
||||
onClick={toggleStar}
|
||||
aria-label={isStarred ? t('artistDetail.favoriteRemove') : t('artistDetail.favoriteAdd')}
|
||||
data-tooltip={isStarred ? t('artistDetail.favoriteRemove') : t('artistDetail.favoriteAdd')}
|
||||
style={{ color: isStarred ? 'var(--accent)' : 'inherit', border: isStarred ? '1px solid var(--accent)' : undefined }}
|
||||
>
|
||||
<Heart size={14} fill={isStarred ? 'currentColor' : 'none'} />
|
||||
{t('artistDetail.favorite')}
|
||||
<span className="compact-btn-label">{t('artistDetail.favorite')}</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
|
||||
@@ -166,10 +166,12 @@ export default function GenreDetail() {
|
||||
<button
|
||||
className="btn btn-ghost"
|
||||
onClick={handleBack}
|
||||
aria-label={t('genres.back')}
|
||||
data-tooltip={t('genres.back')}
|
||||
style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', marginRight: '0.25rem' }}
|
||||
>
|
||||
<ArrowLeft size={16} />
|
||||
<span>{t('genres.back')}</span>
|
||||
<span className="toolbar-btn-label">{t('genres.back')}</span>
|
||||
</button>
|
||||
<h1 className="page-title" style={{ marginBottom: 0 }}>{genre}</h1>
|
||||
{headerCount != null && headerCount > 0 && (
|
||||
@@ -185,12 +187,13 @@ export default function GenreDetail() {
|
||||
className="btn btn-primary long-press-play-btn"
|
||||
{...pressBind}
|
||||
disabled={bulkLoading}
|
||||
aria-label={t('genres.playTooltip')}
|
||||
data-tooltip={t('genres.playTooltip')}
|
||||
>
|
||||
<LongPressWaveOverlay active={isHolding} size="compact" />
|
||||
<span className="long-press-play-btn__icon" style={{ gap: '0.35rem' }}>
|
||||
{bulkLoading ? <Loader2 size={15} className="spin" /> : <Play size={15} fill="currentColor" />}
|
||||
{t('common.play')}
|
||||
<span className="toolbar-btn-label">{t('common.play')}</span>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
|
||||
@@ -230,12 +230,12 @@ export default function InternetRadio() {
|
||||
<div className="playlists-header">
|
||||
<h1 className="page-title" style={{ marginBottom: 0 }}>{t('radio.title')}</h1>
|
||||
{canManage && (
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
<button className="btn btn-primary" onClick={() => setBrowseOpen(true)}>
|
||||
<Search size={14} /> {t('radio.browseDirectory')}
|
||||
<div className="compact-action-bar" style={{ display: 'flex', gap: 8 }}>
|
||||
<button className="btn btn-primary" onClick={() => setBrowseOpen(true)} aria-label={t('radio.browseDirectory')} data-tooltip={t('radio.browseDirectory')}>
|
||||
<Search size={14} /> <span className="compact-btn-label">{t('radio.browseDirectory')}</span>
|
||||
</button>
|
||||
<button className="btn btn-primary" onClick={() => setModalStation('new')}>
|
||||
<Plus size={15} /> {t('radio.addStation')}
|
||||
<button className="btn btn-primary" onClick={() => setModalStation('new')} aria-label={t('radio.addStation')} data-tooltip={t('radio.addStation')}>
|
||||
<Plus size={15} /> <span className="compact-btn-label">{t('radio.addStation')}</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -18,7 +18,8 @@ import { usePerfProbeFlags } from '../utils/perf/perfFlags';
|
||||
import { showToast } from '../utils/ui/toast';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { join } from '@tauri-apps/api/path';
|
||||
import { CheckSquare2, Download, HardDriveDownload, ListPlus } from 'lucide-react';
|
||||
import { Download, HardDriveDownload, ListPlus } from 'lucide-react';
|
||||
import SelectionToggleButton from '../components/SelectionToggleButton';
|
||||
import { albumGridWarmCovers } from '../cover/layoutSizes';
|
||||
import { VirtualCardGrid } from '../components/VirtualCardGrid';
|
||||
import OverlayScrollArea from '../components/OverlayScrollArea';
|
||||
@@ -330,16 +331,13 @@ export default function LosslessAlbums() {
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
<button
|
||||
className={`btn btn-surface${selectionMode ? ' btn-sort-active' : ''}`}
|
||||
onClick={toggleSelectionMode}
|
||||
data-tooltip={selectionMode ? t('albums.cancelSelect') : t('albums.startSelect')}
|
||||
data-tooltip-pos="bottom"
|
||||
style={selectionMode ? { background: 'var(--accent)', color: 'var(--text-on-accent)' } : {}}
|
||||
>
|
||||
<CheckSquare2 size={15} />
|
||||
{selectionMode ? t('albums.cancelSelect') : t('albums.select')}
|
||||
</button>
|
||||
<SelectionToggleButton
|
||||
active={selectionMode}
|
||||
onToggle={toggleSelectionMode}
|
||||
selectLabel={t('albums.select')}
|
||||
cancelLabel={t('albums.cancelSelect')}
|
||||
startTooltip={t('albums.startSelect')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -157,10 +157,11 @@ export default function MostPlayed() {
|
||||
<button
|
||||
className="btn btn-surface mp-sort-btn"
|
||||
onClick={() => setSortAsc(v => !v)}
|
||||
aria-label={sortAsc ? t('mostPlayed.sortLeast') : t('mostPlayed.sortMost')}
|
||||
data-tooltip={sortAsc ? t('mostPlayed.sortMost') : t('mostPlayed.sortLeast')}
|
||||
>
|
||||
{sortAsc ? <ArrowUp size={14} /> : <ArrowDown size={14} />}
|
||||
{sortAsc ? t('mostPlayed.sortLeast') : t('mostPlayed.sortMost')}
|
||||
<span className="compact-btn-label">{sortAsc ? t('mostPlayed.sortLeast') : t('mostPlayed.sortMost')}</span>
|
||||
<ArrowUpDown size={12} style={{ opacity: 0.45 }} />
|
||||
</button>
|
||||
</div>
|
||||
@@ -173,11 +174,12 @@ export default function MostPlayed() {
|
||||
<button
|
||||
className={`btn btn-surface mp-filter-btn${filterCompilations ? ' mp-filter-btn--active' : ''}`}
|
||||
onClick={() => setFilterCompilations(v => !v)}
|
||||
aria-label={t('mostPlayed.filterCompilations')}
|
||||
data-tooltip={t('mostPlayed.filterCompilations')}
|
||||
data-tooltip-pos="left"
|
||||
>
|
||||
<UsersRound size={14} />
|
||||
{t('mostPlayed.filterCompilationsShort')}
|
||||
<span className="compact-btn-label">{t('mostPlayed.filterCompilationsShort')}</span>
|
||||
</button>
|
||||
</div>
|
||||
{topArtists.length === 0 && (
|
||||
|
||||
@@ -5,7 +5,8 @@ import { resolveAlbum } from '../utils/offline/offlineMediaResolve';
|
||||
import type { SubsonicAlbum } from '../api/subsonicTypes';
|
||||
import { dedupeById } from '../utils/dedupeById';
|
||||
import { useEffect, useLayoutEffect, useState, useCallback, useRef, useMemo } from 'react';
|
||||
import { CheckSquare2, Download, HardDriveDownload } from 'lucide-react';
|
||||
import { Download, HardDriveDownload } from 'lucide-react';
|
||||
import SelectionToggleButton from '../components/SelectionToggleButton';
|
||||
import AlbumCard from '../components/AlbumCard';
|
||||
import GenreFilterBar from '../components/GenreFilterBar';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -271,16 +272,13 @@ export default function NewReleases() {
|
||||
) : (
|
||||
<GenreFilterBar selected={selectedGenres} onSelectionChange={setSelectedGenres} />
|
||||
)}
|
||||
<button
|
||||
className={`btn btn-surface${selectionMode ? ' btn-sort-active' : ''}`}
|
||||
onClick={toggleSelectionMode}
|
||||
data-tooltip={selectionMode ? t('albums.cancelSelect') : t('albums.startSelect')}
|
||||
data-tooltip-pos="bottom"
|
||||
style={selectionMode ? { background: 'var(--accent)', color: 'var(--text-on-accent)' } : {}}
|
||||
>
|
||||
<CheckSquare2 size={15} />
|
||||
{selectionMode ? t('albums.cancelSelect') : t('albums.select')}
|
||||
</button>
|
||||
<SelectionToggleButton
|
||||
active={selectionMode}
|
||||
onToggle={toggleSelectionMode}
|
||||
selectLabel={t('albums.select')}
|
||||
cancelLabel={t('albums.cancelSelect')}
|
||||
startTooltip={t('albums.startSelect')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+15
-16
@@ -6,7 +6,8 @@ import type { SubsonicAlbum } from '../api/subsonicTypes';
|
||||
import { dedupeById } from '../utils/dedupeById';
|
||||
import { shuffleArray } from '../utils/playback/shuffleArray';
|
||||
import React, { useEffect, useLayoutEffect, useState, useCallback, useRef } from 'react';
|
||||
import { RefreshCw, CheckSquare2, Download, HardDriveDownload } from 'lucide-react';
|
||||
import { RefreshCw, Download, HardDriveDownload } from 'lucide-react';
|
||||
import SelectionToggleButton from '../components/SelectionToggleButton';
|
||||
import AlbumCard from '../components/AlbumCard';
|
||||
import GenreFilterBar from '../components/GenreFilterBar';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -314,13 +315,13 @@ export default function RandomAlbums() {
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', flexWrap: 'wrap' }}>
|
||||
{selectionMode && selectedIds.size > 0 ? (
|
||||
<>
|
||||
<button className="btn btn-surface albums-selection-action-btn" onClick={handleAddOffline}>
|
||||
<button className="btn btn-surface albums-selection-action-btn" onClick={handleAddOffline} aria-label={t('albums.addOffline')} data-tooltip={t('albums.addOffline')}>
|
||||
<HardDriveDownload size={15} />
|
||||
{t('albums.addOffline')}
|
||||
<span className="toolbar-btn-label">{t('albums.addOffline')}</span>
|
||||
</button>
|
||||
<button className="btn btn-surface albums-selection-action-btn" onClick={handleDownloadZips}>
|
||||
<button className="btn btn-surface albums-selection-action-btn" onClick={handleDownloadZips} aria-label={t('albums.downloadZips')} data-tooltip={t('albums.downloadZips')}>
|
||||
<Download size={15} />
|
||||
{t('albums.downloadZips')}
|
||||
<span className="toolbar-btn-label">{t('albums.downloadZips')}</span>
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
@@ -330,23 +331,21 @@ export default function RandomAlbums() {
|
||||
className="btn btn-surface"
|
||||
onClick={handleRefresh}
|
||||
disabled={loading}
|
||||
aria-label={t('randomAlbums.refresh')}
|
||||
data-tooltip={t('randomAlbums.refresh')}
|
||||
>
|
||||
<RefreshCw size={15} className={loading ? 'animate-spin' : ''} />
|
||||
{t('randomAlbums.refresh')}
|
||||
<span className="toolbar-btn-label">{t('randomAlbums.refresh')}</span>
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
<button
|
||||
className={`btn btn-surface${selectionMode ? ' btn-sort-active' : ''}`}
|
||||
onClick={toggleSelectionMode}
|
||||
data-tooltip={selectionMode ? t('albums.cancelSelect') : t('albums.startSelect')}
|
||||
data-tooltip-pos="bottom"
|
||||
style={selectionMode ? { background: 'var(--accent)', color: 'var(--text-on-accent)' } : {}}
|
||||
>
|
||||
<CheckSquare2 size={15} />
|
||||
{selectionMode ? t('albums.cancelSelect') : t('albums.select')}
|
||||
</button>
|
||||
<SelectionToggleButton
|
||||
active={selectionMode}
|
||||
onToggle={toggleSelectionMode}
|
||||
selectLabel={t('albums.select')}
|
||||
cancelLabel={t('albums.cancelSelect')}
|
||||
startTooltip={t('albums.startSelect')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { getScheduledTheme } from './themeStore';
|
||||
import { getScheduledTheme, useThemeStore } from './themeStore';
|
||||
|
||||
type SchedState = Parameters<typeof getScheduledTheme>[0];
|
||||
|
||||
@@ -77,3 +77,20 @@ describe('getScheduledTheme', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('buttonSize', () => {
|
||||
afterEach(() => {
|
||||
useThemeStore.getState().setButtonSize('large');
|
||||
});
|
||||
|
||||
it('defaults to large', () => {
|
||||
expect(useThemeStore.getState().buttonSize).toBe('large');
|
||||
});
|
||||
|
||||
it('setButtonSize switches to small and back to large', () => {
|
||||
useThemeStore.getState().setButtonSize('small');
|
||||
expect(useThemeStore.getState().buttonSize).toBe('small');
|
||||
useThemeStore.getState().setButtonSize('large');
|
||||
expect(useThemeStore.getState().buttonSize).toBe('large');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -43,6 +43,9 @@ interface ThemeState {
|
||||
setEnablePlaylistCoverPhoto: (v: boolean) => void;
|
||||
showBitrate: boolean;
|
||||
setShowBitrate: (v: boolean) => void;
|
||||
/** Compact (icon-only) vs. large action/toolbar buttons across detail pages and browse views. */
|
||||
buttonSize: 'large' | 'small';
|
||||
setButtonSize: (v: 'large' | 'small') => void;
|
||||
showRemainingTime: boolean;
|
||||
setShowRemainingTime: (v: boolean) => void;
|
||||
expandReplayGain: boolean;
|
||||
@@ -102,6 +105,8 @@ export const useThemeStore = create<ThemeState>()(
|
||||
setEnablePlaylistCoverPhoto: (v) => set({ enablePlaylistCoverPhoto: v }),
|
||||
showBitrate: true,
|
||||
setShowBitrate: (v) => set({ showBitrate: v }),
|
||||
buttonSize: 'large',
|
||||
setButtonSize: (v) => set({ buttonSize: v }),
|
||||
showRemainingTime: false,
|
||||
setShowRemainingTime: (v) => set({ showRemainingTime: v }),
|
||||
expandReplayGain: false,
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
/* ─ Compact buttons (Settings → Appearance → Compact buttons) ─
|
||||
Toggled by `html[data-button-size="small"]` (themeStore `buttonSize`).
|
||||
Reusable opt-in: a button row adds `compact-action-bar`, and each button
|
||||
wraps its text in a `compact-btn-label` span. In Small the labels hide and
|
||||
the buttons collapse to icon-only — across detail-page hero rows, shared
|
||||
browse toolbars (`mainstage-inpage-toolbar` → `toolbar-btn-label`) and the
|
||||
standalone Most Played sort/filter buttons.
|
||||
|
||||
These rules are viewport-agnostic, so the toggle applies on mobile too. The
|
||||
one deliberate exception is the AlbumHeader's mobile layout: it renders its
|
||||
own large touch-target buttons (`album-icon-btn`, ~44–54px) instead of a
|
||||
`compact-action-bar`, so Small never shrinks them below the touch target. */
|
||||
|
||||
html[data-button-size="small"] .compact-action-bar {
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
html[data-button-size="small"] .compact-action-bar .btn,
|
||||
html[data-button-size="small"] .compact-action-bar .artist-ext-link {
|
||||
padding: 5px;
|
||||
gap: 5px;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
html[data-button-size="small"] .compact-action-bar .btn svg,
|
||||
html[data-button-size="small"] .compact-action-bar .artist-ext-link svg {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
}
|
||||
|
||||
/* Labelled buttons (Play, Bio, Download, Offline, …) become icon-only in Small. */
|
||||
html[data-button-size="small"] .compact-action-bar .compact-btn-label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Album header keeps its inner primary cluster tight in Small too. */
|
||||
html[data-button-size="small"] .album-detail-actions-primary {
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
/* Browse-page toolbars (All Albums style: Sort, Year, Genre, Favorites, Lossless,
|
||||
Sampler, multi-select) collapse to icon-only in Small, reusing the same pattern
|
||||
as the mobile toolbar — labels live in `.toolbar-btn-label`, the icon + tooltip
|
||||
+ aria-label stay. One rule covers every shared `mainstage-inpage-toolbar`. */
|
||||
html[data-button-size="small"] .mainstage-inpage-toolbar .toolbar-btn-label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Once labels are hidden, keep all toolbar buttons square / equal size. */
|
||||
html[data-button-size="small"] .mainstage-inpage-toolbar .btn {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
/* Most Played has its own standalone sort/filter buttons (not in a shared bar). */
|
||||
html[data-button-size="small"] .mp-sort-btn .compact-btn-label,
|
||||
html[data-button-size="small"] .mp-filter-btn .compact-btn-label {
|
||||
display: none;
|
||||
}
|
||||
@@ -11,6 +11,7 @@
|
||||
@import './album-detail.css';
|
||||
@import './offline-library.css';
|
||||
@import './cover-lightbox.css';
|
||||
@import './compact-buttons.css';
|
||||
@import './offline-cache-button.css';
|
||||
@import './download-folder-modal.css';
|
||||
@import './song-info-modal.css';
|
||||
|
||||
Reference in New Issue
Block a user