mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-21 22:15:40 +00:00
fix(playlist): column picker no longer clipped on short lists (#853)
* fix(playlist): columns dropdown no longer clipped on short lists (#839) The column picker rendered inside `.tracklist` (overflow-x: auto, which makes overflow-y compute to auto). On a 1-song playlist the downward popover overflowed the short box → clipped behind suggestions, an extra scrollbar, and the row vanishing when scrolling that inner bar (the virtualizer tracks the main viewport, not the tracklist). Move the picker outside `.tracklist` by reusing the shared TracklistColumnPicker (parametrized with allColumns); fixes the same latent bug in the favorites tracklist and dedupes three inline copies into one. * docs(changelog): playlist/favorites column picker fix (#853)
This commit is contained in:
committed by
GitHub
parent
e8e41752a7
commit
cc8e6cc811
@@ -183,6 +183,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
|
||||
|
||||
### Playlists & Favorites — column picker on short lists
|
||||
|
||||
**By [@Psychotoxical](https://github.com/Psychotoxical), PR [#853](https://github.com/Psychotoxical/psysonic/pull/853)**
|
||||
|
||||
* On a one-song playlist (or short favorites list) the column menu was clipped behind the list, added a stray scrollbar, and could hide the row when scrolled. The picker now sits outside the scroll area, so it opens fully on lists of any length.
|
||||
|
||||
|
||||
|
||||
## [1.46.0] - 2026-05-18
|
||||
|
||||
> **🙏 Special thanks to [@zz5zz](https://github.com/zz5zz)** for his tireless quirk-spotting and bug reports on the [Psysonic Discord](https://discord.gg/AMnDRErm4u) — several of the polish fixes in this release landed directly off the back of his messages.
|
||||
|
||||
@@ -119,6 +119,7 @@ export default function AlbumTrackList({
|
||||
return (
|
||||
<>
|
||||
<TracklistColumnPicker
|
||||
allColumns={COLUMNS}
|
||||
pickerRef={pickerRef}
|
||||
pickerOpen={pickerOpen}
|
||||
setPickerOpen={setPickerOpen}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import React from 'react';
|
||||
import { Check, ChevronDown, RotateCcw } from 'lucide-react';
|
||||
import type { TFunction } from 'i18next';
|
||||
import { COLUMNS } from '../../utils/componentHelpers/albumTrackListHelpers';
|
||||
import type { ColDef } from '../../utils/useTracklistColumns';
|
||||
|
||||
interface Props {
|
||||
/** Every column (required ones are filtered out of the menu). */
|
||||
allColumns: readonly ColDef[];
|
||||
pickerRef: React.RefObject<HTMLDivElement | null>;
|
||||
pickerOpen: boolean;
|
||||
setPickerOpen: (updater: (v: boolean) => boolean) => void;
|
||||
@@ -20,6 +22,7 @@ interface Props {
|
||||
* button.
|
||||
*/
|
||||
export function TracklistColumnPicker({
|
||||
allColumns,
|
||||
pickerRef,
|
||||
pickerOpen,
|
||||
setPickerOpen,
|
||||
@@ -41,7 +44,7 @@ export function TracklistColumnPicker({
|
||||
{pickerOpen && (
|
||||
<div className="tracklist-col-picker-menu">
|
||||
<div className="tracklist-col-picker-label">{t('albumDetail.columns')}</div>
|
||||
{COLUMNS.filter(c => !c.required).map(c => {
|
||||
{allColumns.filter(c => !c.required).map(c => {
|
||||
const label = c.i18nKey ? t(`albumDetail.${c.i18nKey as string}`) : c.key;
|
||||
const isOn = colVisible.has(c.key);
|
||||
return (
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React, { useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||
import FavoriteSongRow, { type FavoriteSongRowCallbacks } from './FavoriteSongRow';
|
||||
import { TracklistColumnPicker } from '../albumTrackList/TracklistColumnPicker';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Check, ChevronDown, RotateCcw } from 'lucide-react';
|
||||
import type { ColDef } from '../../utils/useTracklistColumns';
|
||||
import type { SubsonicSong } from '../../api/subsonicTypes';
|
||||
import { usePlayerStore } from '../../store/playerStore';
|
||||
@@ -168,39 +168,21 @@ export default function FavoritesSongsTracklist({
|
||||
const virtualItems = rowVirtualizer.getVirtualItems();
|
||||
|
||||
return (
|
||||
<>
|
||||
<TracklistColumnPicker
|
||||
allColumns={allColumns}
|
||||
pickerRef={pickerRef}
|
||||
pickerOpen={pickerOpen}
|
||||
setPickerOpen={setPickerOpen}
|
||||
colVisible={colVisible}
|
||||
toggleColumn={toggleColumn}
|
||||
resetColumns={resetColumns}
|
||||
t={t}
|
||||
/>
|
||||
<div className="tracklist" data-preview-loc="favorites" style={{ padding: 0 }} ref={tracklistRef} onClick={e => {
|
||||
if (inSelectMode && e.target === e.currentTarget) useSelectionStore.getState().clearAll();
|
||||
}}>
|
||||
|
||||
{/* Column visibility picker */}
|
||||
<div className="tracklist-col-picker-wrapper" ref={pickerRef}>
|
||||
<div className="tracklist-col-picker">
|
||||
<button className="tracklist-col-picker-btn" onClick={e => { e.stopPropagation(); setPickerOpen(v => !v); }} data-tooltip={t('albumDetail.columns')}>
|
||||
<ChevronDown size={14} />
|
||||
</button>
|
||||
{pickerOpen && (
|
||||
<div className="tracklist-col-picker-menu">
|
||||
<div className="tracklist-col-picker-label">{t('albumDetail.columns')}</div>
|
||||
{allColumns.filter(c => !c.required).map(c => {
|
||||
const label = c.i18nKey ? t(`albumDetail.${c.i18nKey}`) : c.key;
|
||||
const isOn = colVisible.has(c.key);
|
||||
return (
|
||||
<button key={c.key} className={`tracklist-col-picker-item${isOn ? ' active' : ''}`} onClick={() => toggleColumn(c.key)}>
|
||||
<span className="tracklist-col-picker-check">{isOn && <Check size={13} />}</span>
|
||||
{label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
<div className="tracklist-col-picker-divider" />
|
||||
<button className="tracklist-col-picker-reset" onClick={resetColumns}>
|
||||
<RotateCcw size={13} />
|
||||
{t('albumDetail.resetColumns')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ position: 'relative' }}>
|
||||
<div className="tracklist-header tracklist-va" style={gridStyle}>
|
||||
{visibleCols.map((colDef, colIndex) => {
|
||||
@@ -321,5 +303,6 @@ export default function FavoritesSongsTracklist({
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||
import PlaylistRow, { type PlaylistRowCallbacks } from './PlaylistRow';
|
||||
import { TracklistColumnPicker } from '../albumTrackList/TracklistColumnPicker';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { APP_MAIN_SCROLL_VIEWPORT_ID } from '../../constants/appScroll';
|
||||
import { useElementClientHeightById } from '../../hooks/useResizeClientHeight';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
Check, ChevronDown, ListPlus, RotateCcw, Search, Trash2, X,
|
||||
ListPlus, Search, Trash2, X,
|
||||
} from 'lucide-react';
|
||||
import type { ColDef } from '../../utils/useTracklistColumns';
|
||||
import type { SubsonicSong } from '../../api/subsonicTypes';
|
||||
@@ -235,6 +236,17 @@ export default function PlaylistTracklist({
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<TracklistColumnPicker
|
||||
allColumns={allColumns}
|
||||
pickerRef={pickerRef}
|
||||
pickerOpen={pickerOpen}
|
||||
setPickerOpen={setPickerOpen}
|
||||
colVisible={colVisible}
|
||||
toggleColumn={toggleColumn}
|
||||
resetColumns={resetColumns}
|
||||
t={t}
|
||||
/>
|
||||
<div className="tracklist" data-preview-loc="playlists" ref={tracklistRef}>
|
||||
|
||||
{/* Bulk action bar */}
|
||||
@@ -277,43 +289,6 @@ export default function PlaylistTracklist({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Column visibility picker */}
|
||||
<div className="tracklist-col-picker-wrapper" ref={pickerRef}>
|
||||
<div className="tracklist-col-picker">
|
||||
<button
|
||||
className="tracklist-col-picker-btn"
|
||||
onClick={e => { e.stopPropagation(); setPickerOpen(v => !v); }}
|
||||
data-tooltip={t('albumDetail.columns')}
|
||||
>
|
||||
<ChevronDown size={14} />
|
||||
</button>
|
||||
{pickerOpen && (
|
||||
<div className="tracklist-col-picker-menu">
|
||||
<div className="tracklist-col-picker-label">{t('albumDetail.columns')}</div>
|
||||
{allColumns.filter(c => !c.required).map(c => {
|
||||
const label = c.i18nKey ? t(`albumDetail.${c.i18nKey}`) : c.key;
|
||||
const isOn = colVisible.has(c.key);
|
||||
return (
|
||||
<button
|
||||
key={c.key}
|
||||
className={`tracklist-col-picker-item${isOn ? ' active' : ''}`}
|
||||
onClick={() => toggleColumn(c.key)}
|
||||
>
|
||||
<span className="tracklist-col-picker-check">{isOn && <Check size={13} />}</span>
|
||||
{label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
<div className="tracklist-col-picker-divider" />
|
||||
<button className="tracklist-col-picker-reset" onClick={resetColumns}>
|
||||
<RotateCcw size={13} />
|
||||
{t('albumDetail.resetColumns')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Header */}
|
||||
<div style={{ position: 'relative' }}>
|
||||
<div className="tracklist-header tracklist-va" style={gridStyle}>
|
||||
@@ -481,5 +456,6 @@ export default function PlaylistTracklist({
|
||||
|
||||
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user