mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-21 22:15:40 +00:00
Merge pull request #192: fix tracklist column picker alignment and toggle functionality
This commit is contained in:
@@ -36,9 +36,6 @@ export default function AdvancedSearch() {
|
|||||||
const qFromUrl = params.get('q') ?? '';
|
const qFromUrl = params.get('q') ?? '';
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const psyDrag = useDragDrop();
|
const psyDrag = useDragDrop();
|
||||||
const total = results
|
|
||||||
? results.artists.length + results.albums.length + results.songs.length
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
const { playTrack, openContextMenu } = usePlayerStore(
|
const { playTrack, openContextMenu } = usePlayerStore(
|
||||||
useShallow(s => ({
|
useShallow(s => ({
|
||||||
@@ -61,6 +58,9 @@ export default function AdvancedSearch() {
|
|||||||
const [resultType, setResultType] = useState<ResultType>('all');
|
const [resultType, setResultType] = useState<ResultType>('all');
|
||||||
const [genres, setGenres] = useState<SubsonicGenre[]>([]);
|
const [genres, setGenres] = useState<SubsonicGenre[]>([]);
|
||||||
const [results, setResults] = useState<Results | null>(null);
|
const [results, setResults] = useState<Results | null>(null);
|
||||||
|
const total = results
|
||||||
|
? results.artists.length + results.albums.length + results.songs.length
|
||||||
|
: 0;
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [hasSearched, setHasSearched] = useState(false);
|
const [hasSearched, setHasSearched] = useState(false);
|
||||||
const [genreNote, setGenreNote] = useState(false);
|
const [genreNote, setGenreNote] = useState(false);
|
||||||
|
|||||||
+24
-20
@@ -472,6 +472,30 @@ export default function Favorites() {
|
|||||||
</div>
|
</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>
|
||||||
|
{FAV_COLUMNS.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>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div style={{ position: 'relative' }}>
|
<div style={{ position: 'relative' }}>
|
||||||
<div className="tracklist-header tracklist-va" style={gridStyle}>
|
<div className="tracklist-header tracklist-va" style={gridStyle}>
|
||||||
{visibleCols.map((colDef, colIndex) => {
|
{visibleCols.map((colDef, colIndex) => {
|
||||||
@@ -551,26 +575,6 @@ export default function Favorites() {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
<div className="tracklist-col-picker" ref={pickerRef}>
|
|
||||||
<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>
|
|
||||||
{FAV_COLUMNS.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>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{visibleSongs.map((song, i) => {
|
{visibleSongs.map((song, i) => {
|
||||||
const track = songToTrack(song);
|
const track = songToTrack(song);
|
||||||
|
|||||||
@@ -385,11 +385,6 @@ export default function PlaylistDetail() {
|
|||||||
const [suggestions, setSuggestions] = useState<SubsonicSong[]>([]);
|
const [suggestions, setSuggestions] = useState<SubsonicSong[]>([]);
|
||||||
const [loadingSuggestions, setLoadingSuggestions] = useState(false);
|
const [loadingSuggestions, setLoadingSuggestions] = useState(false);
|
||||||
|
|
||||||
// ── Column picker portal dropdown state ────────────────────────────────────
|
|
||||||
const [pickerPos, setPickerPos] = useState<{ top: number; right: number } | null>(null);
|
|
||||||
const pickerBtnRef = useRef<HTMLButtonElement>(null);
|
|
||||||
const pickerMenuRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
// ── Column resize/visibility ──────────────────────────────────────────────
|
// ── Column resize/visibility ──────────────────────────────────────────────
|
||||||
const {
|
const {
|
||||||
colVisible, visibleCols, gridStyle,
|
colVisible, visibleCols, gridStyle,
|
||||||
@@ -404,41 +399,6 @@ export default function PlaylistDetail() {
|
|||||||
if (!contextMenuOpen) setContextMenuSongId(null);
|
if (!contextMenuOpen) setContextMenuSongId(null);
|
||||||
}, [contextMenuOpen]);
|
}, [contextMenuOpen]);
|
||||||
|
|
||||||
// Click-outside handler for column picker portal dropdown
|
|
||||||
useEffect(() => {
|
|
||||||
if (!pickerOpen) return;
|
|
||||||
const handler = (e: MouseEvent) => {
|
|
||||||
const target = e.target as Node;
|
|
||||||
if (
|
|
||||||
pickerBtnRef.current?.contains(target) ||
|
|
||||||
pickerRef.current?.contains(target) ||
|
|
||||||
pickerMenuRef.current?.contains(target)
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setPickerOpen(false);
|
|
||||||
};
|
|
||||||
document.addEventListener('mousedown', handler);
|
|
||||||
return () => document.removeEventListener('mousedown', handler);
|
|
||||||
}, [pickerOpen, setPickerOpen]);
|
|
||||||
|
|
||||||
// Update picker position on resize/scroll while open
|
|
||||||
useEffect(() => {
|
|
||||||
if (!pickerOpen) return;
|
|
||||||
const updatePos = () => {
|
|
||||||
if (pickerBtnRef.current) {
|
|
||||||
const rect = pickerBtnRef.current.getBoundingClientRect();
|
|
||||||
setPickerPos({ top: rect.bottom + 4, right: window.innerWidth - rect.right });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
window.addEventListener('resize', updatePos);
|
|
||||||
window.addEventListener('scroll', updatePos, true);
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('resize', updatePos);
|
|
||||||
window.removeEventListener('scroll', updatePos, true);
|
|
||||||
};
|
|
||||||
}, [pickerOpen]);
|
|
||||||
|
|
||||||
// ── Load ─────────────────────────────────────────────────────
|
// ── Load ─────────────────────────────────────────────────────
|
||||||
const lastModified = usePlaylistStore(s => (id ? s.lastModified[id] : undefined));
|
const lastModified = usePlaylistStore(s => (id ? s.lastModified[id] : undefined));
|
||||||
|
|
||||||
@@ -1432,6 +1392,38 @@ export default function PlaylistDetail() {
|
|||||||
</div>
|
</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>
|
||||||
|
{PL_COLUMNS.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>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div style={{ position: 'relative' }}>
|
<div style={{ position: 'relative' }}>
|
||||||
<div className="tracklist-header tracklist-va" style={gridStyle}>
|
<div className="tracklist-header tracklist-va" style={gridStyle}>
|
||||||
@@ -1543,47 +1535,6 @@ export default function PlaylistDetail() {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
<div className="tracklist-col-picker" ref={pickerRef}>
|
|
||||||
<button
|
|
||||||
ref={pickerBtnRef}
|
|
||||||
className="tracklist-col-picker-btn"
|
|
||||||
onClick={e => {
|
|
||||||
e.stopPropagation();
|
|
||||||
if (!pickerOpen && pickerBtnRef.current) {
|
|
||||||
const rect = pickerBtnRef.current.getBoundingClientRect();
|
|
||||||
setPickerPos({ top: rect.bottom + 4, right: window.innerWidth - rect.right });
|
|
||||||
}
|
|
||||||
setPickerOpen(v => !v);
|
|
||||||
}}
|
|
||||||
data-tooltip={t('albumDetail.columns')}
|
|
||||||
>
|
|
||||||
<ChevronDown size={14} />
|
|
||||||
</button>
|
|
||||||
{pickerOpen && pickerPos && createPortal(
|
|
||||||
<div
|
|
||||||
ref={pickerMenuRef}
|
|
||||||
className="tracklist-col-picker-menu"
|
|
||||||
style={{ position: 'fixed', top: pickerPos.top, right: pickerPos.right, zIndex: 9999 }}
|
|
||||||
>
|
|
||||||
<div className="tracklist-col-picker-label">{t('albumDetail.columns')}</div>
|
|
||||||
{PL_COLUMNS.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>,
|
|
||||||
document.body
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{songs.length === 0 && (
|
{songs.length === 0 && (
|
||||||
|
|||||||
Reference in New Issue
Block a user