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

This commit is contained in:
Matt Bruce 2025-07-19 11:25:38 -05:00
parent 941107f71d
commit e3c7879087
12 changed files with 733 additions and 184 deletions

View File

@ -0,0 +1,122 @@
import React, { useState } from 'react';
import {
IonModal,
IonHeader,
IonToolbar,
IonTitle,
IonContent,
IonList,
IonItem,
IonLabel,
IonButton,
IonIcon
} from '@ionic/react';
import { close } from 'ionicons/icons';
import { useAppSelector } from '../../redux';
import { selectSingersArray, selectControllerName, selectQueueObject } from '../../redux';
import { queueService } from '../../firebase/services';
import { useToast } from '../../hooks/useToast';
import type { Song, Singer, QueueItem } from '../../types';
interface SelectSingerProps {
isOpen: boolean;
onClose: () => void;
song: Song;
}
const SelectSinger: React.FC<SelectSingerProps> = ({ isOpen, onClose, song }) => {
const singers = useAppSelector(selectSingersArray);
const controllerName = useAppSelector(selectControllerName);
const currentQueue = useAppSelector(selectQueueObject);
const { showSuccess, showError } = useToast();
const [isLoading, setIsLoading] = useState(false);
const handleSelectSinger = async (singer: Singer) => {
if (!controllerName) {
showError('Controller not found');
return;
}
setIsLoading(true);
try {
// Calculate the next order by finding the highest order value and adding 1
const queueItems = Object.values(currentQueue) as QueueItem[];
const maxOrder = queueItems.length > 0
? Math.max(...queueItems.map(item => item.order || 0))
: 0;
const nextOrder = maxOrder + 1;
const queueItem: Omit<QueueItem, 'key'> = {
order: nextOrder,
singer: {
name: singer.name,
lastLogin: singer.lastLogin,
},
song: song,
};
await queueService.addToQueue(controllerName, queueItem);
showSuccess(`${song.title} added to queue for ${singer.name}`);
onClose();
} catch (error) {
console.error('Failed to add song to queue:', error);
showError('Failed to add song to queue');
} finally {
setIsLoading(false);
}
};
return (
<IonModal
isOpen={isOpen}
onDidDismiss={onClose}
breakpoints={[0, 0.5, 0.8]}
initialBreakpoint={0.8}
>
<IonHeader>
<IonToolbar>
<IonTitle>Select Singer</IonTitle>
<IonButton slot="end" fill="clear" onClick={onClose}>
<IonIcon icon={close} />
</IonButton>
</IonToolbar>
</IonHeader>
<IonContent>
{/* Song Information */}
<div className="p-4 border-b border-gray-200 dark:border-gray-700">
<h2 className="text-lg font-semibold mb-2">{song.title}</h2>
<p className="text-sm text-gray-600 dark:text-gray-400 italic mb-1">{song.artist}</p>
<p className="text-xs text-gray-500 dark:text-gray-500">{song.path}</p>
</div>
{/* Singers List */}
<IonList>
{singers.map((singer) => (
<IonItem
key={singer.key}
button
onClick={() => handleSelectSinger(singer)}
disabled={isLoading}
>
<IonLabel>
<h2>{singer.name}</h2>
<p className="text-sm text-gray-500 dark:text-gray-400">
Last login: {new Date(singer.lastLogin).toLocaleDateString()}
</p>
</IonLabel>
</IonItem>
))}
</IonList>
{singers.length === 0 && (
<div className="p-4 text-center text-gray-500 dark:text-gray-400">
No singers available
</div>
)}
</IonContent>
</IonModal>
);
};
export default SelectSinger;

View File

@ -0,0 +1,218 @@
import React, { useState } from 'react';
import {
IonModal, IonHeader, IonToolbar, IonTitle, IonContent,
IonButton, IonIcon, IonList, IonItem, IonLabel
} from '@ionic/react';
import {
add, heart, heartOutline, ban, checkmark, close, people
} from 'ionicons/icons';
import { useAppSelector } from '../../redux';
import { selectIsAdmin, selectFavorites, selectSongs } from '../../redux';
import { useSongOperations } from '../../hooks/useSongOperations';
import { useDisabledSongs } from '../../hooks/useDisabledSongs';
import { useSelectSinger } from '../../hooks/useSelectSinger';
import { useToast } from '../../hooks/useToast';
import SelectSinger from './SelectSinger';
import SongItem from './SongItem';
import type { Song } from '../../types';
interface SongInfoProps {
isOpen: boolean;
onClose: () => void;
song: Song;
}
const SongInfo: React.FC<SongInfoProps> = ({ isOpen, onClose, song }) => {
const isAdmin = useAppSelector(selectIsAdmin);
const favorites = useAppSelector(selectFavorites);
const allSongs = useAppSelector(selectSongs);
const { toggleFavorite } = useSongOperations();
const { isSongDisabled, addDisabledSong, removeDisabledSong } = useDisabledSongs();
const { showSuccess, showError } = useToast();
const {
isOpen: isSelectSingerOpen,
selectedSong: selectSingerSong,
openSelectSinger,
closeSelectSinger
} = useSelectSinger();
const [showArtistSongs, setShowArtistSongs] = useState(false);
const isInFavorites = (Object.values(favorites) as Song[]).some(favSong => favSong.path === song.path);
const isDisabled = isSongDisabled(song);
const artistSongs = (Object.values(allSongs) as Song[]).filter(s =>
s.artist.toLowerCase() === song.artist.toLowerCase() && s.path !== song.path
);
const handleQueueSong = () => {
openSelectSinger(song);
};
const handleArtistSongs = () => {
setShowArtistSongs(true);
};
const handleToggleFavorite = async () => {
try {
await toggleFavorite(song);
showSuccess(isInFavorites ? 'Removed from favorites' : 'Added to favorites');
} catch {
showError('Failed to update favorites');
}
};
const handleToggleDisabled = async () => {
try {
if (isDisabled) {
await removeDisabledSong(song);
showSuccess('Song enabled');
} else {
await addDisabledSong(song);
showSuccess('Song disabled');
}
} catch {
showError('Failed to update song status');
}
};
return (
<>
{/* Main Song Info Modal */}
<IonModal
isOpen={isOpen}
onDidDismiss={onClose}
breakpoints={[0, 0.5, 0.8]}
initialBreakpoint={0.8}
>
<IonHeader>
<IonToolbar>
<IonTitle>Song Info</IonTitle>
<IonButton slot="end" fill="clear" onClick={onClose}>
<IonIcon icon={close} />
</IonButton>
</IonToolbar>
</IonHeader>
<IonContent>
<div className="p-4">
{/* Song Information using SongItem component */}
<div className="mb-6">
<SongItem
song={song}
context="queue" // This context doesn't show any buttons
isAdmin={isAdmin}
showActions={false}
showPath={false}
showCount={false}
className="border-b border-gray-200 dark:border-gray-700"
/>
</div>
{/* Action Buttons */}
<div className="flex flex-col items-center space-y-4">
{/* Queue Song Button */}
<IonButton
fill="solid"
color="primary"
onClick={handleQueueSong}
className="h-12 w-80"
style={{ width: '320px' }}
>
<IonIcon icon={people} slot="start" />
Queue Song
</IonButton>
{/* Artist Songs Button */}
<IonButton
fill="solid"
color="primary"
onClick={handleArtistSongs}
className="h-12 w-80"
style={{ width: '320px' }}
>
<IonIcon icon={add} slot="start" />
Artist Songs
</IonButton>
{/* Favorite/Unfavorite Button */}
<IonButton
fill="solid"
color={isInFavorites ? "danger" : "primary"}
onClick={handleToggleFavorite}
className="h-12 w-80"
style={{ width: '320px' }}
>
<IonIcon icon={isInFavorites ? heart : heartOutline} slot="start" />
{isInFavorites ? 'Unfavorite Song' : 'Favorite Song'}
</IonButton>
{/* Disable/Enable Button (Admin Only) */}
{isAdmin && (
<IonButton
fill="solid"
color={isDisabled ? "success" : "warning"}
onClick={handleToggleDisabled}
className="h-12 w-80"
style={{ width: '320px' }}
>
<IonIcon icon={isDisabled ? checkmark : ban} slot="start" />
{isDisabled ? 'Enable Song' : 'Disable Song'}
</IonButton>
)}
</div>
</div>
</IonContent>
</IonModal>
{/* Select Singer Modal */}
{selectSingerSong && (
<SelectSinger
isOpen={isSelectSingerOpen}
onClose={closeSelectSinger}
song={selectSingerSong}
/>
)}
{/* Artist Songs Modal */}
<IonModal
isOpen={showArtistSongs}
onDidDismiss={() => setShowArtistSongs(false)}
breakpoints={[0, 0.5, 0.8]}
initialBreakpoint={0.8}
>
<IonHeader>
<IonToolbar>
<IonTitle>Songs by {song.artist}</IonTitle>
<IonButton slot="end" fill="clear" onClick={() => setShowArtistSongs(false)}>
<IonIcon icon={close} />
</IonButton>
</IonToolbar>
</IonHeader>
<IonContent>
<div className="p-4">
{artistSongs.length > 0 ? (
<IonList>
{artistSongs.map((artistSong) => (
<IonItem key={artistSong.path}>
<IonLabel>
<div className="text-base font-bold">{artistSong.title}</div>
<div className="text-sm text-gray-500 dark:text-gray-400">{artistSong.path}</div>
</IonLabel>
</IonItem>
))}
</IonList>
) : (
<div className="p-4 text-center text-gray-500 dark:text-gray-400">
No other songs found by this artist
</div>
)}
</div>
</IonContent>
</IonModal>
</>
);
};
export default SongInfo;

View File

@ -1,6 +1,6 @@
import React from 'react';
import { IonItem, IonLabel, IonIcon } from '@ionic/react';
import { add, heart, heartOutline, trash } from 'ionicons/icons';
import { add, heart, heartOutline, trash, informationCircle } from 'ionicons/icons';
import ActionButton from './ActionButton';
import { useAppSelector } from '../../redux';
import { selectQueue, selectFavorites } from '../../redux';
@ -16,28 +16,114 @@ const extractFilename = (path: string): string => {
return parts[parts.length - 1] || '';
};
const SongItem: React.FC<SongItemProps> = ({
// Song Information Display Component
export const SongInfoDisplay: React.FC<{
song: Song;
showPath?: boolean;
showCount?: boolean;
}> = ({
song,
context,
showPath = false,
showCount = false
}) => {
return (
<IonLabel>
<div
className="ion-text-bold"
style={{
fontWeight: 'bold',
fontSize: '1rem',
color: 'black'
}}
>
{song.title}
</div>
<div
className="ion-text-italic ion-color-medium"
style={{
fontSize: '0.875rem',
fontStyle: 'italic',
color: '#6b7280'
}}
>
{song.artist}
</div>
{/* Show filename if showPath is true */}
{showPath && song.path && (
<div
className="ion-text-sm ion-color-medium"
style={{
fontSize: '0.75rem',
color: '#9ca3af'
}}
>
{extractFilename(song.path)}
</div>
)}
{/* Show play count if showCount is true */}
{showCount && song.count && (
<div
className="ion-text-sm ion-color-medium"
style={{
fontSize: '0.75rem',
color: '#9ca3af'
}}
>
Played {song.count} times
</div>
)}
</IonLabel>
);
};
// Action Buttons Component
export const SongActionButtons: React.FC<{
isAdmin: boolean;
isInQueue: boolean;
isInFavorites: boolean;
showInfoButton?: boolean;
showAddButton?: boolean;
showRemoveButton?: boolean;
showDeleteButton?: boolean;
showFavoriteButton?: boolean;
onAddToQueue?: () => void;
onRemoveFromQueue?: () => void;
onToggleFavorite?: () => void;
onDelete?: () => void;
onSelectSinger?: () => void;
}> = ({
isAdmin,
isInQueue,
isInFavorites,
showInfoButton = false,
showAddButton = false,
showRemoveButton = false,
showDeleteButton = false,
showFavoriteButton = false,
onAddToQueue,
onRemoveFromQueue,
onToggleFavorite,
onDelete,
isAdmin = false,
className = ''
onSelectSinger
}) => {
// Get current state from Redux
const queue = useAppSelector(selectQueue);
const favorites = useAppSelector(selectFavorites);
// Check if song is in queue or favorites based on path
const isInQueue = (Object.values(queue) as QueueItem[]).some(item => item.song.path === song.path);
const isInFavorites = (Object.values(favorites) as Song[]).some(favSong => favSong.path === song.path);
const renderActionPanel = () => {
const buttons = [];
// Add to Queue button (for all contexts except queue, only if not already in queue)
if (context !== 'queue' && !isInQueue) {
// Info button
if (showInfoButton && onSelectSinger) {
buttons.push(
<ActionButton
key="info"
onClick={onSelectSinger}
variant="secondary"
size="sm"
>
<IonIcon icon={informationCircle} />
</ActionButton>
);
}
// Add to Queue button
if (showAddButton && !isInQueue) {
buttons.push(
<ActionButton
key="add"
@ -50,8 +136,8 @@ const SongItem: React.FC<SongItemProps> = ({
);
}
// Remove from Queue button (only for queue context, admin only)
if (context === 'queue' && isAdmin && onRemoveFromQueue) {
// Remove from Queue button
if (showRemoveButton && isAdmin && onRemoveFromQueue) {
buttons.push(
<ActionButton
key="remove"
@ -64,8 +150,8 @@ const SongItem: React.FC<SongItemProps> = ({
);
}
// Delete from Favorites button (only for favorites context)
if (context === 'favorites' && onDelete) {
// Delete from Favorites button
if (showDeleteButton && onDelete) {
buttons.push(
<ActionButton
key="delete"
@ -78,8 +164,8 @@ const SongItem: React.FC<SongItemProps> = ({
);
}
// Toggle Favorite button (for all contexts except favorites)
if (context !== 'favorites') {
// Toggle Favorite button
if (showFavoriteButton) {
buttons.push(
<ActionButton
key="favorite"
@ -99,31 +185,72 @@ const SongItem: React.FC<SongItemProps> = ({
) : null;
};
// Main SongItem Component
const SongItem: React.FC<SongItemProps> = ({
song,
context,
onAddToQueue,
onRemoveFromQueue,
onToggleFavorite,
onDelete,
onSelectSinger,
isAdmin = false,
className = '',
showActions = true,
showPath,
showCount,
showInfoButton,
showAddButton,
showRemoveButton,
showDeleteButton,
showFavoriteButton
}) => {
// Get current state from Redux
const queue = useAppSelector(selectQueue);
const favorites = useAppSelector(selectFavorites);
// Check if song is in queue or favorites based on path
const isInQueue = (Object.values(queue) as QueueItem[]).some(item => item.song.path === song.path);
const isInFavorites = (Object.values(favorites) as Song[]).some(favSong => favSong.path === song.path);
// Default values based on context if not explicitly provided
const shouldShowPath = showPath !== undefined ? showPath : context !== 'queue';
const shouldShowCount = showCount !== undefined ? showCount : context === 'queue';
// Default values for action buttons based on context if not explicitly provided
const shouldShowInfoButton = showInfoButton !== undefined ? showInfoButton : context !== 'queue';
const shouldShowAddButton = showAddButton !== undefined ? showAddButton : context !== 'queue';
const shouldShowRemoveButton = showRemoveButton !== undefined ? showRemoveButton : context === 'queue' && isAdmin;
const shouldShowDeleteButton = showDeleteButton !== undefined ? showDeleteButton : context === 'favorites';
const shouldShowFavoriteButton = showFavoriteButton !== undefined ? showFavoriteButton : context !== 'favorites';
return (
<IonItem className={className}>
<IonLabel className="flex-1 min-w-0">
<h3 className="text-base bold-title break-words">
{song.title}
</h3>
<p className="text-sm italic text-gray-500 break-words">
{song.artist}
</p>
{/* Show filename for all contexts except queue */}
{context !== 'queue' && song.path && (
<p className="text-xs text-gray-400 break-words">
{extractFilename(song.path)}
</p>
)}
{song.count && (
<p className="text-xs text-gray-400">
Played {song.count} times
</p>
)}
</IonLabel>
<SongInfoDisplay
song={song}
showPath={shouldShowPath}
showCount={shouldShowCount}
/>
{showActions && (
<div slot="end" className="flex gap-2 flex-shrink-0 ml-2">
{renderActionPanel()}
<SongActionButtons
isAdmin={isAdmin}
isInQueue={isInQueue}
isInFavorites={isInFavorites}
showInfoButton={shouldShowInfoButton}
showAddButton={shouldShowAddButton}
showRemoveButton={shouldShowRemoveButton}
showDeleteButton={shouldShowDeleteButton}
showFavoriteButton={shouldShowFavoriteButton}
onAddToQueue={onAddToQueue}
onRemoveFromQueue={onRemoveFromQueue}
onToggleFavorite={onToggleFavorite}
onDelete={onDelete}
onSelectSinger={onSelectSinger}
/>
</div>
)}
</IonItem>
);
};

View File

@ -6,3 +6,5 @@ export { default as InfiniteScrollList } from './InfiniteScrollList';
export { default as PageHeader } from './PageHeader';
export { default as SongItem } from './SongItem';
export { default as PlayerControls } from './PlayerControls';
export { default as SelectSinger } from './SelectSinger';
export { default as SongInfo } from './SongInfo';

View File

@ -1,13 +1,14 @@
import React, { useState, useEffect } from 'react';
import { IonButton, IonIcon, IonReorderGroup, IonReorder, IonItem, IonLabel, IonItemSliding, IonItemOptions, IonItemOption } from '@ionic/react';
import { IonItem, IonLabel, IonItemSliding, IonItemOptions, IonItemOption, IonButton, IonIcon, IonReorderGroup, IonReorder } from '@ionic/react';
import { trash, reorderThreeOutline, reorderTwoOutline } from 'ionicons/icons';
import { ActionButton } from '../../components/common';
import { useQueue } from '../../hooks';
import { useAppSelector } from '../../redux';
import { selectQueueLength, selectPlayerStateMemoized, selectIsAdmin, selectControllerName } from '../../redux';
import { PlayerState } from '../../types';
import { ActionButton } from '../../components/common';
import { SongInfoDisplay } from '../../components/common/SongItem';
import { queueService } from '../../firebase/services';
import { debugLog } from '../../utils/logger';
import { PlayerState } from '../../types';
import type { QueueItem } from '../../types';
type QueueMode = 'delete' | 'reorder';
@ -101,27 +102,34 @@ const Queue: React.FC = () => {
return (
<IonItemSliding key={queueItem.key}>
<IonItem className={`${canReorder && queueMode === 'reorder' ? 'border-l-4 border-blue-200 bg-blue-50' : ''}`}>
<IonItem
className={`${canReorder && queueMode === 'reorder' ? 'ion-border-start ion-border-primary ion-bg-primary-tint' : ''}`}
style={{ '--padding-start': '0px' }}
>
{/* Order Number */}
<div slot="start" className={`relative flex-shrink-0 w-12 h-12 flex items-center justify-center font-medium rounded-full bg-gray-100 text-gray-600 !border-none`}>
<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' }}>
{queueItem.order}
</div>
</div>
{/* Song Info */}
{/* Song Info with Singer Name on Top */}
<IonLabel>
<p className="text-sm font-semibold truncate">
{/* Singer Name */}
<div className="ion-text-bold ion-color-primary">
{queueItem.singer.name}
</p>
<h3 className="text-sm font-semibold truncate">
{queueItem.song.title}
</h3>
<p className="text-sm text-gray-500 truncate">
{queueItem.song.artist}
</p>
</div>
{/* Song Info Display */}
<SongInfoDisplay
song={queueItem.song}
showPath={false}
showCount={false}
/>
</IonLabel>
{/* Delete Button or Drag Handle */}
<div slot="end" className="flex items-center gap-2 ml-2">
<div slot="end" style={{ marginRight: '-16px' }}>
{canDelete && (
<div onClick={(e) => e.stopPropagation()}>
<ActionButton
@ -134,7 +142,7 @@ const Queue: React.FC = () => {
</div>
)}
{canReorder && queueMode === 'reorder' && (
<div className="text-gray-400">
<div className="ion-color-medium">
<IonIcon icon={reorderTwoOutline} size="large" />
</div>
)}
@ -165,34 +173,37 @@ const Queue: React.FC = () => {
return (
<IonItemSliding key={firstItem.key}>
<IonItem>
<IonItem style={{ '--padding-start': '0px' }}>
{/* Order Number */}
<div slot="start" className={`relative flex-shrink-0 w-12 h-12 flex items-center justify-center font-medium rounded-full bg-gray-100 text-gray-600 !border-none`}>
<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' }}>
{firstItem.order}
</div>
</div>
{/* Song Info */}
{/* Song Info with Singer Name on Top */}
<IonLabel>
<p className="text-sm font-semibold truncate">
{/* Singer Name */}
<div className="ion-text-bold ion-color-primary">
{firstItem.singer.name}
</p>
<h3 className="text-sm font-semibold truncate">
{firstItem.song.title}
</h3>
<p className="text-sm text-gray-500 truncate">
{firstItem.song.artist}
</p>
</div>
{/* Song Info Display */}
<SongInfoDisplay
song={firstItem.song}
showPath={false}
showCount={false}
/>
</IonLabel>
{/* Delete Button */}
<div slot="end" className="flex items-center gap-2 ml-2">
<div slot="end" style={{ marginRight: '-16px' }}>
{canDeleteFirstItem && queueMode === 'delete' && (
<div onClick={(e) => e.stopPropagation()}>
<ActionButton
onClick={() => handleRemoveFromQueue(firstItem)}
variant="danger"
size="sm"
className="opacity-100"
>
<IonIcon icon={trash} />
</ActionButton>
@ -218,22 +229,19 @@ const Queue: React.FC = () => {
return (
<>
<div className="flex justify-end items-center mb-4 pr-4 right-button-container">
<div className="ion-padding ion-text-end">
{isAdmin && (
<IonButton
onClick={toggleQueueMode}
fill="outline"
size="small"
className="flex items-center gap-2"
>
<IonIcon icon={queueMode === 'delete' ? reorderThreeOutline : trash} />
</IonButton>
)}
</div>
<div className="max-w-4xl mx-auto p-6">
<div className="ion-padding">
{/* First Item (Currently Playing) */}
{renderFirstItem()}
@ -241,13 +249,13 @@ const Queue: React.FC = () => {
{canReorder && queueMode === 'reorder' ? (
<IonReorderGroup disabled={false} onIonItemReorder={doReorder}>
{listItems.map((queueItem, index) => (
<IonReorder key={queueItem.key} style={{ minHeight: '60px' }}>
<IonReorder key={queueItem.key}>
{renderQueueItem(queueItem, index)}
</IonReorder>
))}
</IonReorderGroup>
) : (
<div className="space-y-2">
<div>
{listItems.map((queueItem, index) => renderQueueItem(queueItem, index))}
</div>
)}

View File

@ -1,7 +1,7 @@
import React from 'react';
import { IonSearchbar } from '@ionic/react';
import { InfiniteScrollList, SongItem } from '../../components/common';
import { useSearch } from '../../hooks';
import { InfiniteScrollList, SongItem, SongInfo } from '../../components/common';
import { useSearch, useSongInfo } from '../../hooks';
import { useAppSelector } from '../../redux';
import { selectIsAdmin, selectSongs } from '../../redux';
import { debugLog } from '../../utils/logger';
@ -17,6 +17,8 @@ const Search: React.FC = () => {
loadMore,
} = useSearch();
const { isOpen, selectedSong, openSongInfo, closeSongInfo } = useSongInfo();
const isAdmin = useAppSelector(selectIsAdmin);
const songs = useAppSelector(selectSongs);
const songsCount = Object.keys(songs).length;
@ -39,6 +41,7 @@ const Search: React.FC = () => {
debugLog('Search component - showing:', searchResults.songs.length, 'of', searchResults.count);
return (
<>
<div className="max-w-4xl mx-auto p-6">
<div className="mb-6">
{/* Search Input */}
@ -63,6 +66,7 @@ const Search: React.FC = () => {
context="search"
onAddToQueue={() => handleAddToQueue(song)}
onToggleFavorite={() => handleToggleFavorite(song)}
onSelectSinger={() => openSongInfo(song)}
isAdmin={isAdmin}
/>
)}
@ -80,6 +84,16 @@ const Search: React.FC = () => {
</div>
)}
</div>
{/* Song Info Modal */}
{selectedSong && (
<SongInfo
isOpen={isOpen}
onClose={closeSongInfo}
song={selectedSong}
/>
)}
</>
);
};

View File

@ -60,12 +60,12 @@ const SongLists: React.FC = () => {
const renderSongListItem = (songList: SongList) => (
<IonItem button onClick={() => handleSongListClick(songList.key!)} detail={false}>
<IonLabel>
<h3 className="text-sm font-medium text-gray-900">
<div className="text-sm font-medium text-gray-900">
{songList.title}
</h3>
<p className="text-sm text-gray-500">
</div>
<div className="text-sm text-gray-500">
{songList.songs.length} song{songList.songs.length !== 1 ? 's' : ''}
</p>
</div>
</IonLabel>
<IonIcon icon={list} slot="end" color="primary" />
</IonItem>
@ -120,12 +120,12 @@ const SongLists: React.FC = () => {
</div>
<IonLabel>
<h3 className="text-sm font-medium text-gray-900">
{songListSong.artist}
</h3>
<p className="text-sm text-gray-500">
<div className="text-sm font-semibold text-gray-900">
{songListSong.title}
</p>
</div>
<div className="text-sm italic text-gray-500">
{songListSong.artist}
</div>
</IonLabel>
<IonChip slot="end" color="success">
@ -162,12 +162,12 @@ const SongLists: React.FC = () => {
</div>
<IonLabel>
<h3 className="text-sm font-medium text-gray-400">
{songListSong.artist}
</h3>
<p className="text-sm text-gray-300">
<div className="text-sm font-semibold text-gray-400">
{songListSong.title}
</p>
</div>
<div className="text-sm italic text-gray-300">
{songListSong.artist}
</div>
</IonLabel>
</IonItem>
);

View File

@ -11,3 +11,5 @@ export { useArtists } from './useArtists';
export { useSingers } from './useSingers';
export { useSongLists } from './useSongLists';
export { useDisabledSongs } from './useDisabledSongs';
export { useSelectSinger } from './useSelectSinger';
export { useSongInfo } from './useSongInfo';

View File

@ -0,0 +1,24 @@
import { useState, useCallback } from 'react';
import type { Song } from '../types';
export const useSelectSinger = () => {
const [isOpen, setIsOpen] = useState(false);
const [selectedSong, setSelectedSong] = useState<Song | null>(null);
const openSelectSinger = useCallback((song: Song) => {
setSelectedSong(song);
setIsOpen(true);
}, []);
const closeSelectSinger = useCallback(() => {
setIsOpen(false);
setSelectedSong(null);
}, []);
return {
isOpen,
selectedSong,
openSelectSinger,
closeSelectSinger,
};
};

24
src/hooks/useSongInfo.ts Normal file
View File

@ -0,0 +1,24 @@
import { useState, useCallback } from 'react';
import type { Song } from '../types';
export const useSongInfo = () => {
const [isOpen, setIsOpen] = useState(false);
const [selectedSong, setSelectedSong] = useState<Song | null>(null);
const openSongInfo = useCallback((song: Song) => {
setSelectedSong(song);
setIsOpen(true);
}, []);
const closeSongInfo = useCallback(() => {
setIsOpen(false);
setSelectedSong(null);
}, []);
return {
isOpen,
selectedSong,
openSongInfo,
closeSongInfo,
};
};

View File

@ -118,8 +118,7 @@ ion-accordion ion-item {
/* Custom modal styling for Singers component */
ion-modal ion-input-label,
ion-modal .ion-input-label,
ion-modal ion-label {
ion-modal .ion-input-label {
font-weight: bold !important;
font-size: 1rem !important;
color: var(--ion-text-color) !important;

View File

@ -128,13 +128,22 @@ export interface ActionButtonProps {
export interface SongItemProps {
song: Song;
context: 'search' | 'queue' | 'history' | 'favorites' | 'topPlayed';
context: 'search' | 'queue' | 'favorites' | 'history' | 'songlists' | 'top100' | 'new';
onAddToQueue?: () => void;
onRemoveFromQueue?: () => void;
onToggleFavorite?: () => void;
onDelete?: () => void;
onSelectSinger?: () => void;
isAdmin?: boolean;
className?: string;
showActions?: boolean;
showPath?: boolean;
showCount?: boolean;
showInfoButton?: boolean;
showAddButton?: boolean;
showRemoveButton?: boolean;
showDeleteButton?: boolean;
showFavoriteButton?: boolean;
}
export interface LayoutProps {