Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
b4353000d8
commit
0f53848671
82
src/components/common/ListItem.tsx
Normal file
82
src/components/common/ListItem.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@ -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
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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';
|
||||
@ -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 (
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user