From 0f53848671631c9fd50ddfd2a662fc9c941a6f44 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Sat, 19 Jul 2025 12:04:20 -0500 Subject: [PATCH] Signed-off-by: Matt Bruce --- src/components/common/ListItem.tsx | 82 ++++++++++++++++++++++++ src/components/common/SongItem.tsx | 10 +-- src/components/common/TwoLineDisplay.tsx | 7 +- src/components/common/index.ts | 3 +- src/features/Artists/Artists.tsx | 17 +++-- src/features/TopPlayed/Top100.tsx | 43 +++++-------- src/index.css | 22 +++++++ 7 files changed, 141 insertions(+), 43 deletions(-) create mode 100644 src/components/common/ListItem.tsx diff --git a/src/components/common/ListItem.tsx b/src/components/common/ListItem.tsx new file mode 100644 index 0000000..64c13bd --- /dev/null +++ b/src/components/common/ListItem.tsx @@ -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 = ({ + 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 = ( + <> + + {chip} + + {icon && } + + ); + } else if (icon) { + finalEndContent = ; + } + } + + return ( + + {/* Number (if enabled) */} + {showNumber && ( +
+
+ {number} +
+
+ )} + + {/* Main content */} + + + {/* End content - render directly without wrapper div */} + {finalEndContent} +
+ ); +}; \ No newline at end of file diff --git a/src/components/common/SongItem.tsx b/src/components/common/SongItem.tsx index 1d34ddb..1c95400 100644 --- a/src/components/common/SongItem.tsx +++ b/src/components/common/SongItem.tsx @@ -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 diff --git a/src/components/common/TwoLineDisplay.tsx b/src/components/common/TwoLineDisplay.tsx index bc37471..0f7a729 100644 --- a/src/components/common/TwoLineDisplay.tsx +++ b/src/components/common/TwoLineDisplay.tsx @@ -16,8 +16,8 @@ interface TwoLineDisplayProps { export const TwoLineDisplay: React.FC = ({ 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 = ({ fontWeight: 'bold', fontSize: primarySize, color: primaryColor, - lineHeight: '1.5' + lineHeight: '1.5', + marginBottom: '4px' }} > {primaryText} diff --git a/src/components/common/index.ts b/src/components/common/index.ts index 629a076..f295491 100644 --- a/src/components/common/index.ts +++ b/src/components/common/index.ts @@ -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'; \ No newline at end of file +export { TwoLineDisplay } from './TwoLineDisplay'; +export { ListItem } from './ListItem'; \ No newline at end of file diff --git a/src/features/Artists/Artists.tsx b/src/features/Artists/Artists.tsx index d42baf1..f13c24c 100644 --- a/src/features/Artists/Artists.tsx +++ b/src/features/Artists/Artists.tsx @@ -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) => ( - handleArtistClick(artist)} detail={false} style={{ '--min-height': '60px' }}> - - - + handleArtistClick(artist)} + /> ); return ( diff --git a/src/features/TopPlayed/Top100.tsx b/src/features/TopPlayed/Top100.tsx index 4c3b62f..346e8c5 100644 --- a/src/features/TopPlayed/Top100.tsx +++ b/src/features/TopPlayed/Top100.tsx @@ -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) => ( - handleTopPlayedClick(item)} - detail={false} - style={{ '--min-height': '60px' }} - > - {/* Number */} -
-
- {index + 1} -
-
- - - - - {item.count} plays - - - -
+ handleTopPlayedClick(item)} + endContent={ + <> + + {item.count} plays + + + + } + /> )} emptyTitle="No top played songs" emptyMessage="Play some songs to see the top played list" diff --git a/src/index.css b/src/index.css index 031dce7..e06831e 100644 --- a/src/index.css +++ b/src/index.css @@ -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;