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={{
|
style={{
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
fontSize: '1rem',
|
fontSize: '1rem',
|
||||||
color: 'black'
|
color: 'var(--ion-color-dark)',
|
||||||
|
marginBottom: '4px'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{song.title}
|
{song.title}
|
||||||
@ -45,7 +46,8 @@ export const SongInfoDisplay: React.FC<{
|
|||||||
style={{
|
style={{
|
||||||
fontSize: '0.875rem',
|
fontSize: '0.875rem',
|
||||||
fontStyle: 'italic',
|
fontStyle: 'italic',
|
||||||
color: '#6b7280'
|
color: 'var(--ion-color-medium)',
|
||||||
|
marginBottom: '4px'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{song.artist}
|
{song.artist}
|
||||||
@ -56,7 +58,7 @@ export const SongInfoDisplay: React.FC<{
|
|||||||
className="ion-text-sm ion-color-medium"
|
className="ion-text-sm ion-color-medium"
|
||||||
style={{
|
style={{
|
||||||
fontSize: '0.75rem',
|
fontSize: '0.75rem',
|
||||||
color: '#9ca3af'
|
color: 'var(--ion-color-medium)'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{extractFilename(song.path)}
|
{extractFilename(song.path)}
|
||||||
@ -68,7 +70,7 @@ export const SongInfoDisplay: React.FC<{
|
|||||||
className="ion-text-sm ion-color-medium"
|
className="ion-text-sm ion-color-medium"
|
||||||
style={{
|
style={{
|
||||||
fontSize: '0.75rem',
|
fontSize: '0.75rem',
|
||||||
color: '#9ca3af'
|
color: 'var(--ion-color-medium)'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Played {song.count} times
|
Played {song.count} times
|
||||||
|
|||||||
@ -16,8 +16,8 @@ interface TwoLineDisplayProps {
|
|||||||
export const TwoLineDisplay: React.FC<TwoLineDisplayProps> = ({
|
export const TwoLineDisplay: React.FC<TwoLineDisplayProps> = ({
|
||||||
primaryText,
|
primaryText,
|
||||||
secondaryText,
|
secondaryText,
|
||||||
primaryColor = 'black',
|
primaryColor = 'var(--ion-color-dark)',
|
||||||
secondaryColor = '#6b7280',
|
secondaryColor = 'var(--ion-color-medium)',
|
||||||
primarySize = '1rem',
|
primarySize = '1rem',
|
||||||
secondarySize = '0.875rem'
|
secondarySize = '0.875rem'
|
||||||
}) => {
|
}) => {
|
||||||
@ -30,7 +30,8 @@ export const TwoLineDisplay: React.FC<TwoLineDisplayProps> = ({
|
|||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
fontSize: primarySize,
|
fontSize: primarySize,
|
||||||
color: primaryColor,
|
color: primaryColor,
|
||||||
lineHeight: '1.5'
|
lineHeight: '1.5',
|
||||||
|
marginBottom: '4px'
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{primaryText}
|
{primaryText}
|
||||||
|
|||||||
@ -7,3 +7,4 @@ export { default as PlayerControls } from './PlayerControls';
|
|||||||
export { default as SongItem, SongInfoDisplay, SongActionButtons } from './SongItem';
|
export { default as SongItem, SongInfoDisplay, SongActionButtons } from './SongItem';
|
||||||
export { default as Toast } from './Toast';
|
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 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 { close, list } from 'ionicons/icons';
|
||||||
import { InfiniteScrollList, SongItem, TwoLineDisplay } from '../../components/common';
|
import { InfiniteScrollList, SongItem, ListItem } from '../../components/common';
|
||||||
import { useArtists } from '../../hooks';
|
import { useArtists } from '../../hooks';
|
||||||
import { useAppSelector } from '../../redux';
|
import { useAppSelector } from '../../redux';
|
||||||
import { selectSongs } from '../../redux';
|
import { selectSongs } from '../../redux';
|
||||||
@ -42,13 +42,12 @@ const Artists: React.FC = () => {
|
|||||||
|
|
||||||
// Render artist item for InfiniteScrollList
|
// Render artist item for InfiniteScrollList
|
||||||
const renderArtistItem = (artist: string) => (
|
const renderArtistItem = (artist: string) => (
|
||||||
<IonItem button onClick={() => handleArtistClick(artist)} detail={false} style={{ '--min-height': '60px' }}>
|
<ListItem
|
||||||
<TwoLineDisplay
|
primaryText={artist}
|
||||||
primaryText={artist}
|
secondaryText={`${getSongCountByArtist(artist)} song${getSongCountByArtist(artist) !== 1 ? 's' : ''}`}
|
||||||
secondaryText={`${getSongCountByArtist(artist)} song${getSongCountByArtist(artist) !== 1 ? 's' : ''}`}
|
icon={list}
|
||||||
/>
|
onClick={() => handleArtistClick(artist)}
|
||||||
<IonIcon icon={list} slot="end" color="primary" />
|
/>
|
||||||
</IonItem>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import React, { useState, useMemo, useCallback } from 'react';
|
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 { close, list } from 'ionicons/icons';
|
||||||
import { useTopPlayed } from '../../hooks';
|
import { useTopPlayed } from '../../hooks';
|
||||||
import { useAppSelector } from '../../redux';
|
import { useAppSelector } from '../../redux';
|
||||||
import { selectTopPlayed, selectSongsArray } 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 { filterSongs } from '../../utils/dataProcessing';
|
||||||
import { debugLog } from '../../utils/logger';
|
import { debugLog } from '../../utils/logger';
|
||||||
import { useSongOperations } from '../../hooks';
|
import { useSongOperations } from '../../hooks';
|
||||||
@ -104,30 +104,21 @@ const Top100: React.FC = () => {
|
|||||||
hasMore={displayHasMore}
|
hasMore={displayHasMore}
|
||||||
onLoadMore={loadMore}
|
onLoadMore={loadMore}
|
||||||
renderItem={(item, index) => (
|
renderItem={(item, index) => (
|
||||||
<IonItem
|
<ListItem
|
||||||
button
|
primaryText={item.title}
|
||||||
|
secondaryText={item.artist}
|
||||||
|
showNumber={true}
|
||||||
|
number={index + 1}
|
||||||
onClick={() => handleTopPlayedClick(item)}
|
onClick={() => handleTopPlayedClick(item)}
|
||||||
detail={false}
|
endContent={
|
||||||
style={{ '--min-height': '60px' }}
|
<>
|
||||||
>
|
<IonChip color="primary">
|
||||||
{/* Number */}
|
{item.count} plays
|
||||||
<div slot="start" className="ion-text-center" style={{ marginLeft: '-8px', marginRight: '12px' }}>
|
</IonChip>
|
||||||
<div className="ion-text-bold ion-color-medium" style={{ fontSize: '1rem', minWidth: '2rem' }}>
|
<IonIcon icon={list} color="primary" />
|
||||||
{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>
|
|
||||||
)}
|
)}
|
||||||
emptyTitle="No top played songs"
|
emptyTitle="No top played songs"
|
||||||
emptyMessage="Play some songs to see the top played list"
|
emptyMessage="Play some songs to see the top played list"
|
||||||
|
|||||||
@ -44,6 +44,28 @@ ion-item.ion-activated {
|
|||||||
--color: var(--ion-color-primary-contrast);
|
--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 */
|
/* Ensure mobile menu appears above other content */
|
||||||
ion-menu {
|
ion-menu {
|
||||||
--z-index: 1000;
|
--z-index: 1000;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user