Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>

This commit is contained in:
Matt Bruce 2025-07-19 12:04:20 -05:00
parent b4353000d8
commit 0f53848671
7 changed files with 141 additions and 43 deletions

View File

@ -0,0 +1,82 @@
import React from 'react';
import { IonItem, IonIcon, IonChip } from '@ionic/react';
import { TwoLineDisplay } from './TwoLineDisplay';
interface ListItemProps {
primaryText: string;
secondaryText: string;
onClick?: () => void;
icon?: string;
iconColor?: string;
className?: string;
showNumber?: boolean;
number?: number;
endContent?: React.ReactNode;
chip?: string;
chipColor?: string;
disabled?: boolean;
}
export const ListItem: React.FC<ListItemProps> = ({
primaryText,
secondaryText,
onClick,
icon,
iconColor = 'primary',
className = '',
showNumber = false,
number,
endContent,
chip,
chipColor = 'primary',
disabled = false
}) => {
const itemClassName = `list-item ${className}`.trim();
// Determine end content
let finalEndContent = endContent;
if (!finalEndContent) {
if (chip) {
finalEndContent = (
<>
<IonChip color={chipColor}>
{chip}
</IonChip>
{icon && <IonIcon icon={icon} color={iconColor} />}
</>
);
} else if (icon) {
finalEndContent = <IonIcon icon={icon} color={iconColor} />;
}
}
return (
<IonItem
button={!!onClick && !disabled}
onClick={onClick}
detail={false}
className={itemClassName}
style={{ '--min-height': '60px' }}
>
{/* Number (if enabled) */}
{showNumber && (
<div slot="start" className="ion-text-center" style={{ marginLeft: '-8px', marginRight: '12px' }}>
<div className="ion-text-bold ion-color-medium" style={{ fontSize: '1rem', minWidth: '2rem' }}>
{number}
</div>
</div>
)}
{/* Main content */}
<TwoLineDisplay
primaryText={primaryText}
secondaryText={secondaryText}
primaryColor={disabled ? 'var(--ion-color-medium)' : undefined}
secondaryColor={disabled ? 'var(--ion-color-light)' : undefined}
/>
{/* End content - render directly without wrapper div */}
{finalEndContent}
</IonItem>
);
};

View File

@ -35,7 +35,8 @@ export const SongInfoDisplay: React.FC<{
style={{
fontWeight: 'bold',
fontSize: '1rem',
color: 'black'
color: 'var(--ion-color-dark)',
marginBottom: '4px'
}}
>
{song.title}
@ -45,7 +46,8 @@ export const SongInfoDisplay: React.FC<{
style={{
fontSize: '0.875rem',
fontStyle: 'italic',
color: '#6b7280'
color: 'var(--ion-color-medium)',
marginBottom: '4px'
}}
>
{song.artist}
@ -56,7 +58,7 @@ export const SongInfoDisplay: React.FC<{
className="ion-text-sm ion-color-medium"
style={{
fontSize: '0.75rem',
color: '#9ca3af'
color: 'var(--ion-color-medium)'
}}
>
{extractFilename(song.path)}
@ -68,7 +70,7 @@ export const SongInfoDisplay: React.FC<{
className="ion-text-sm ion-color-medium"
style={{
fontSize: '0.75rem',
color: '#9ca3af'
color: 'var(--ion-color-medium)'
}}
>
Played {song.count} times

View File

@ -16,8 +16,8 @@ interface TwoLineDisplayProps {
export const TwoLineDisplay: React.FC<TwoLineDisplayProps> = ({
primaryText,
secondaryText,
primaryColor = 'black',
secondaryColor = '#6b7280',
primaryColor = 'var(--ion-color-dark)',
secondaryColor = 'var(--ion-color-medium)',
primarySize = '1rem',
secondarySize = '0.875rem'
}) => {
@ -30,7 +30,8 @@ export const TwoLineDisplay: React.FC<TwoLineDisplayProps> = ({
fontWeight: 'bold',
fontSize: primarySize,
color: primaryColor,
lineHeight: '1.5'
lineHeight: '1.5',
marginBottom: '4px'
}}
>
{primaryText}

View File

@ -6,4 +6,5 @@ export { default as PageHeader } from './PageHeader';
export { default as PlayerControls } from './PlayerControls';
export { default as SongItem, SongInfoDisplay, SongActionButtons } from './SongItem';
export { default as Toast } from './Toast';
export { TwoLineDisplay } from './TwoLineDisplay';
export { TwoLineDisplay } from './TwoLineDisplay';
export { ListItem } from './ListItem';

View File

@ -1,7 +1,7 @@
import React, { useState } from 'react';
import { IonSearchbar, IonItem, IonModal, IonHeader, IonToolbar, IonTitle, IonButton, IonIcon, IonContent } from '@ionic/react';
import { IonSearchbar, IonModal, IonHeader, IonToolbar, IonTitle, IonButton, IonIcon, IonContent } from '@ionic/react';
import { close, list } from 'ionicons/icons';
import { InfiniteScrollList, SongItem, TwoLineDisplay } from '../../components/common';
import { InfiniteScrollList, SongItem, ListItem } from '../../components/common';
import { useArtists } from '../../hooks';
import { useAppSelector } from '../../redux';
import { selectSongs } from '../../redux';
@ -42,13 +42,12 @@ const Artists: React.FC = () => {
// Render artist item for InfiniteScrollList
const renderArtistItem = (artist: string) => (
<IonItem button onClick={() => handleArtistClick(artist)} detail={false} style={{ '--min-height': '60px' }}>
<TwoLineDisplay
primaryText={artist}
secondaryText={`${getSongCountByArtist(artist)} song${getSongCountByArtist(artist) !== 1 ? 's' : ''}`}
/>
<IonIcon icon={list} slot="end" color="primary" />
</IonItem>
<ListItem
primaryText={artist}
secondaryText={`${getSongCountByArtist(artist)} song${getSongCountByArtist(artist) !== 1 ? 's' : ''}`}
icon={list}
onClick={() => handleArtistClick(artist)}
/>
);
return (

View File

@ -1,10 +1,10 @@
import React, { useState, useMemo, useCallback } from 'react';
import { IonItem, IonChip, IonModal, IonHeader, IonToolbar, IonTitle, IonButton, IonIcon, IonContent, IonList } from '@ionic/react';
import { IonChip, IonModal, IonHeader, IonToolbar, IonTitle, IonButton, IonIcon, IonContent, IonList } from '@ionic/react';
import { close, list } from 'ionicons/icons';
import { useTopPlayed } from '../../hooks';
import { useAppSelector } from '../../redux';
import { selectTopPlayed, selectSongsArray } from '../../redux';
import { InfiniteScrollList, SongItem, TwoLineDisplay } from '../../components/common';
import { InfiniteScrollList, SongItem, ListItem } from '../../components/common';
import { filterSongs } from '../../utils/dataProcessing';
import { debugLog } from '../../utils/logger';
import { useSongOperations } from '../../hooks';
@ -104,30 +104,21 @@ const Top100: React.FC = () => {
hasMore={displayHasMore}
onLoadMore={loadMore}
renderItem={(item, index) => (
<IonItem
button
onClick={() => handleTopPlayedClick(item)}
detail={false}
style={{ '--min-height': '60px' }}
>
{/* Number */}
<div slot="start" className="ion-text-center" style={{ marginLeft: '-8px', marginRight: '12px' }}>
<div className="ion-text-bold ion-color-medium" style={{ fontSize: '1rem', minWidth: '2rem' }}>
{index + 1}
</div>
</div>
<TwoLineDisplay
primaryText={item.title}
secondaryText={item.artist}
/>
<IonChip slot="end" color="primary">
{item.count} plays
</IonChip>
<IonIcon icon={list} slot="end" color="primary" />
</IonItem>
<ListItem
primaryText={item.title}
secondaryText={item.artist}
showNumber={true}
number={index + 1}
onClick={() => handleTopPlayedClick(item)}
endContent={
<>
<IonChip color="primary">
{item.count} plays
</IonChip>
<IonIcon icon={list} color="primary" />
</>
}
/>
)}
emptyTitle="No top played songs"
emptyMessage="Play some songs to see the top played list"

View File

@ -44,6 +44,28 @@ ion-item.ion-activated {
--color: var(--ion-color-primary-contrast);
}
/* List item press states for Artists, Top100, SongLists */
ion-item.artist-item.ion-activated,
ion-item.top100-item.ion-activated,
ion-item.songlist-item.ion-activated,
ion-item.list-item.ion-activated {
--background: rgba(var(--ion-color-primary-rgb), 0.1);
--color: var(--ion-color-dark);
}
ion-item.artist-item.ion-focused,
ion-item.top100-item.ion-focused,
ion-item.songlist-item.ion-focused,
ion-item.list-item.ion-focused {
--background: rgba(var(--ion-color-primary-rgb), 0.05);
--color: var(--ion-color-dark);
}
/* Ensure icons remain visible during press states */
ion-item.ion-activated ion-icon {
color: var(--ion-color-primary) !important;
}
/* Ensure mobile menu appears above other content */
ion-menu {
--z-index: 1000;