Signed-off-by: mbrucedogs <mbrucedogs@gmail.com>
This commit is contained in:
parent
3aee1fc44e
commit
d98a8a6d14
@ -5,7 +5,7 @@ import TopPlayed from './features/TopPlayed/Top100';
|
|||||||
import { FirebaseProvider } from './firebase/FirebaseProvider';
|
import { FirebaseProvider } from './firebase/FirebaseProvider';
|
||||||
import { ErrorBoundary } from './components/common';
|
import { ErrorBoundary } from './components/common';
|
||||||
import { AuthInitializer } from './components/Auth';
|
import { AuthInitializer } from './components/Auth';
|
||||||
import { SongInfoProvider } from './components/common/SongInfoProvider';
|
import { ModalProvider } from './components/common/ModalProvider';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
@ -13,7 +13,7 @@ function App() {
|
|||||||
<FirebaseProvider>
|
<FirebaseProvider>
|
||||||
<Router>
|
<Router>
|
||||||
<AuthInitializer>
|
<AuthInitializer>
|
||||||
<SongInfoProvider>
|
<ModalProvider>
|
||||||
<Layout>
|
<Layout>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Navigate to="/queue" replace />} />
|
<Route path="/" element={<Navigate to="/queue" replace />} />
|
||||||
@ -30,7 +30,7 @@ function App() {
|
|||||||
<Route path="*" element={<Navigate to="/queue" replace />} />
|
<Route path="*" element={<Navigate to="/queue" replace />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</Layout>
|
</Layout>
|
||||||
</SongInfoProvider>
|
</ModalProvider>
|
||||||
</AuthInitializer>
|
</AuthInitializer>
|
||||||
</Router>
|
</Router>
|
||||||
</FirebaseProvider>
|
</FirebaseProvider>
|
||||||
|
|||||||
@ -13,7 +13,8 @@ const ActionButton: React.FC<ActionButtonProps> = ({
|
|||||||
icon,
|
icon,
|
||||||
iconSlot = ActionButtonIconSlot.START,
|
iconSlot = ActionButtonIconSlot.START,
|
||||||
iconSize = ActionButtonIconSize.LARGE,
|
iconSize = ActionButtonIconSize.LARGE,
|
||||||
fill = 'solid'
|
fill = 'solid',
|
||||||
|
'aria-label': ariaLabel
|
||||||
}) => {
|
}) => {
|
||||||
const getVariant = () => {
|
const getVariant = () => {
|
||||||
switch (variant) {
|
switch (variant) {
|
||||||
@ -51,6 +52,7 @@ const ActionButton: React.FC<ActionButtonProps> = ({
|
|||||||
color={getVariant()}
|
color={getVariant()}
|
||||||
size={getSize()}
|
size={getSize()}
|
||||||
className={className}
|
className={className}
|
||||||
|
aria-label={ariaLabel}
|
||||||
style={{
|
style={{
|
||||||
minWidth: isIconOnly ? '40px' : '40px',
|
minWidth: isIconOnly ? '40px' : '40px',
|
||||||
minHeight: '40px',
|
minHeight: '40px',
|
||||||
|
|||||||
36
src/components/common/ModalHeader.tsx
Normal file
36
src/components/common/ModalHeader.tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { IonHeader, IonToolbar, IonTitle } from '@ionic/react';
|
||||||
|
import { ActionButton } from './index';
|
||||||
|
import { ActionButtonVariant, ActionButtonSize, ActionButtonIconSlot } from '../../types';
|
||||||
|
import { Icons } from '../../constants';
|
||||||
|
|
||||||
|
interface ModalHeaderProps {
|
||||||
|
title: string;
|
||||||
|
onClose: () => void;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ModalHeader: React.FC<ModalHeaderProps> = ({
|
||||||
|
title,
|
||||||
|
onClose,
|
||||||
|
className = ''
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<IonHeader className={className}>
|
||||||
|
<IonToolbar>
|
||||||
|
<IonTitle>{title}</IonTitle>
|
||||||
|
<div slot="end">
|
||||||
|
<ActionButton
|
||||||
|
onClick={onClose}
|
||||||
|
variant={ActionButtonVariant.SECONDARY}
|
||||||
|
size={ActionButtonSize.SMALL}
|
||||||
|
icon={Icons.CLOSE}
|
||||||
|
iconSlot={ActionButtonIconSlot.ICON_ONLY}
|
||||||
|
fill="clear"
|
||||||
|
aria-label={`Close ${title} modal`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</IonToolbar>
|
||||||
|
</IonHeader>
|
||||||
|
);
|
||||||
|
};
|
||||||
98
src/components/common/ModalProvider.tsx
Normal file
98
src/components/common/ModalProvider.tsx
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
import React, { useState, useCallback } from 'react';
|
||||||
|
import { ModalContext } from '../../contexts/ModalContext';
|
||||||
|
import { ModalType, type ModalViewType, type ModalState, type ModalItem } from '../../types/modal';
|
||||||
|
import SongInfo from './SongInfo';
|
||||||
|
import SelectSinger from './SelectSinger';
|
||||||
|
import type { Song } from '../../types';
|
||||||
|
|
||||||
|
interface ModalProviderProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ModalProvider: React.FC<ModalProviderProps> = ({ children }) => {
|
||||||
|
const [modalState, setModalState] = useState<ModalState>({
|
||||||
|
stack: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const openModal = useCallback((type: ModalViewType, data: Song): string => {
|
||||||
|
const id = Math.random().toString(36).substr(2, 9);
|
||||||
|
const newModal: ModalItem = {
|
||||||
|
id,
|
||||||
|
type,
|
||||||
|
data,
|
||||||
|
isOpen: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
setModalState(prev => ({
|
||||||
|
stack: [...prev.stack, newModal],
|
||||||
|
}));
|
||||||
|
|
||||||
|
return id;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const closeModal = useCallback((id: string) => {
|
||||||
|
setModalState(prev => ({
|
||||||
|
stack: prev.stack.filter(modal => modal.id !== id),
|
||||||
|
}));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const closeTopModal = useCallback(() => {
|
||||||
|
setModalState(prev => ({
|
||||||
|
stack: prev.stack.slice(0, -1),
|
||||||
|
}));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const openSongInfo = useCallback((song: Song): string => {
|
||||||
|
return openModal(ModalType.SONG_INFO, song);
|
||||||
|
}, [openModal]);
|
||||||
|
|
||||||
|
const openSelectSinger = useCallback((song: Song): string => {
|
||||||
|
return openModal(ModalType.SELECT_SINGER, song);
|
||||||
|
}, [openModal]);
|
||||||
|
|
||||||
|
const contextValue = {
|
||||||
|
openModal,
|
||||||
|
closeModal,
|
||||||
|
closeTopModal,
|
||||||
|
openSongInfo,
|
||||||
|
openSelectSinger,
|
||||||
|
modalState,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Render all modals in the stack
|
||||||
|
const renderModals = () => {
|
||||||
|
return modalState.stack.map((modal) => {
|
||||||
|
const handleClose = () => closeModal(modal.id);
|
||||||
|
|
||||||
|
switch (modal.type) {
|
||||||
|
case ModalType.SONG_INFO:
|
||||||
|
return (
|
||||||
|
<SongInfo
|
||||||
|
key={modal.id}
|
||||||
|
isOpen={modal.isOpen}
|
||||||
|
onClose={handleClose}
|
||||||
|
song={modal.data}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case ModalType.SELECT_SINGER:
|
||||||
|
return (
|
||||||
|
<SelectSinger
|
||||||
|
key={modal.id}
|
||||||
|
isOpen={modal.isOpen}
|
||||||
|
onClose={handleClose}
|
||||||
|
song={modal.data}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContext.Provider value={contextValue}>
|
||||||
|
{children}
|
||||||
|
{renderModals()}
|
||||||
|
</ModalContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,9 +1,6 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import {
|
import {
|
||||||
IonModal,
|
IonModal,
|
||||||
IonHeader,
|
|
||||||
IonToolbar,
|
|
||||||
IonTitle,
|
|
||||||
IonContent,
|
IonContent,
|
||||||
IonList,
|
IonList,
|
||||||
IonItem,
|
IonItem,
|
||||||
@ -13,9 +10,7 @@ import { useAppSelector } from '../../redux';
|
|||||||
import { selectSingersArray, selectControllerName, selectQueueObject } from '../../redux';
|
import { selectSingersArray, selectControllerName, selectQueueObject } from '../../redux';
|
||||||
import { queueService } from '../../firebase/services';
|
import { queueService } from '../../firebase/services';
|
||||||
import { useToast } from '../../hooks/useToast';
|
import { useToast } from '../../hooks/useToast';
|
||||||
import { ActionButton } from './index';
|
import { ModalHeader } from './ModalHeader';
|
||||||
import { ActionButtonVariant, ActionButtonSize, ActionButtonIconSlot } from '../../types';
|
|
||||||
import { Icons } from '../../constants';
|
|
||||||
import type { Song, Singer, QueueItem } from '../../types';
|
import type { Song, Singer, QueueItem } from '../../types';
|
||||||
|
|
||||||
interface SelectSingerProps {
|
interface SelectSingerProps {
|
||||||
@ -72,22 +67,10 @@ const SelectSinger: React.FC<SelectSingerProps> = ({ isOpen, onClose, song }) =>
|
|||||||
onDidDismiss={onClose}
|
onDidDismiss={onClose}
|
||||||
breakpoints={[0, 0.5, 0.8]}
|
breakpoints={[0, 0.5, 0.8]}
|
||||||
initialBreakpoint={0.8}
|
initialBreakpoint={0.8}
|
||||||
|
keepContentsMounted={false}
|
||||||
|
backdropDismiss={true}
|
||||||
>
|
>
|
||||||
<IonHeader>
|
<ModalHeader title="Select Singer" onClose={onClose} />
|
||||||
<IonToolbar>
|
|
||||||
<IonTitle>Select Singer</IonTitle>
|
|
||||||
<div slot="end">
|
|
||||||
<ActionButton
|
|
||||||
onClick={onClose}
|
|
||||||
variant={ActionButtonVariant.SECONDARY}
|
|
||||||
size={ActionButtonSize.SMALL}
|
|
||||||
icon={Icons.CLOSE}
|
|
||||||
iconSlot={ActionButtonIconSlot.ICON_ONLY}
|
|
||||||
fill="clear"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</IonToolbar>
|
|
||||||
</IonHeader>
|
|
||||||
|
|
||||||
<IonContent>
|
<IonContent>
|
||||||
{/* Song Information */}
|
{/* Song Information */}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import {
|
import {
|
||||||
IonModal, IonHeader, IonToolbar, IonTitle, IonContent,
|
IonModal, IonContent,
|
||||||
IonButton, IonIcon, IonList, IonItem, IonLabel
|
IonButton, IonIcon, IonList, IonItem, IonLabel
|
||||||
} from '@ionic/react';
|
} from '@ionic/react';
|
||||||
import {
|
import {
|
||||||
@ -10,12 +10,10 @@ import { useAppSelector } from '../../redux';
|
|||||||
import { selectIsAdmin, selectFavorites, selectSongs, selectQueue } from '../../redux';
|
import { selectIsAdmin, selectFavorites, selectSongs, selectQueue } from '../../redux';
|
||||||
import { useSongOperations } from '../../hooks/useSongOperations';
|
import { useSongOperations } from '../../hooks/useSongOperations';
|
||||||
import { useDisabledSongs } from '../../hooks/useDisabledSongs';
|
import { useDisabledSongs } from '../../hooks/useDisabledSongs';
|
||||||
import { useSelectSinger } from '../../hooks/useSelectSinger';
|
import { useModal } from '../../hooks/useModalContext';
|
||||||
import { useToast } from '../../hooks/useToast';
|
import { useToast } from '../../hooks/useToast';
|
||||||
import SelectSinger from './SelectSinger';
|
|
||||||
import { ActionButton } from './index';
|
import { ModalHeader } from './ModalHeader';
|
||||||
import { ActionButtonVariant, ActionButtonSize, ActionButtonIconSlot } from '../../types';
|
|
||||||
import { Icons } from '../../constants';
|
|
||||||
import { SongInfoDisplay } from './SongItem';
|
import { SongInfoDisplay } from './SongItem';
|
||||||
import type { Song, QueueItem } from '../../types';
|
import type { Song, QueueItem } from '../../types';
|
||||||
|
|
||||||
@ -34,12 +32,7 @@ const SongInfo: React.FC<SongInfoProps> = ({ isOpen, onClose, song }) => {
|
|||||||
const { isSongDisabled, addDisabledSong, removeDisabledSong } = useDisabledSongs();
|
const { isSongDisabled, addDisabledSong, removeDisabledSong } = useDisabledSongs();
|
||||||
const { showSuccess, showError } = useToast();
|
const { showSuccess, showError } = useToast();
|
||||||
|
|
||||||
const {
|
const { openSelectSinger } = useModal();
|
||||||
isOpen: isSelectSingerOpen,
|
|
||||||
selectedSong: selectSingerSong,
|
|
||||||
openSelectSinger,
|
|
||||||
closeSelectSinger
|
|
||||||
} = useSelectSinger();
|
|
||||||
const [showArtistSongs, setShowArtistSongs] = useState(false);
|
const [showArtistSongs, setShowArtistSongs] = useState(false);
|
||||||
|
|
||||||
const isInFavorites = (Object.values(favorites) as Song[]).some(favSong => favSong.path === song.path);
|
const isInFavorites = (Object.values(favorites) as Song[]).some(favSong => favSong.path === song.path);
|
||||||
@ -89,22 +82,10 @@ const SongInfo: React.FC<SongInfoProps> = ({ isOpen, onClose, song }) => {
|
|||||||
onDidDismiss={onClose}
|
onDidDismiss={onClose}
|
||||||
breakpoints={[0, 0.5, 0.8]}
|
breakpoints={[0, 0.5, 0.8]}
|
||||||
initialBreakpoint={0.8}
|
initialBreakpoint={0.8}
|
||||||
|
keepContentsMounted={false}
|
||||||
|
backdropDismiss={true}
|
||||||
>
|
>
|
||||||
<IonHeader>
|
<ModalHeader title="Song Info" onClose={onClose} />
|
||||||
<IonToolbar>
|
|
||||||
<IonTitle>Song Info</IonTitle>
|
|
||||||
<div slot="end">
|
|
||||||
<ActionButton
|
|
||||||
onClick={onClose}
|
|
||||||
variant={ActionButtonVariant.SECONDARY}
|
|
||||||
size={ActionButtonSize.SMALL}
|
|
||||||
icon={Icons.CLOSE}
|
|
||||||
iconSlot={ActionButtonIconSlot.ICON_ONLY}
|
|
||||||
fill="clear"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</IonToolbar>
|
|
||||||
</IonHeader>
|
|
||||||
|
|
||||||
<IonContent>
|
<IonContent>
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
@ -177,15 +158,6 @@ const SongInfo: React.FC<SongInfoProps> = ({ isOpen, onClose, song }) => {
|
|||||||
</IonContent>
|
</IonContent>
|
||||||
</IonModal>
|
</IonModal>
|
||||||
|
|
||||||
{/* Select Singer Modal */}
|
|
||||||
{selectSingerSong && (
|
|
||||||
<SelectSinger
|
|
||||||
isOpen={isSelectSingerOpen}
|
|
||||||
onClose={closeSelectSinger}
|
|
||||||
song={selectSingerSong}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Artist Songs Modal */}
|
{/* Artist Songs Modal */}
|
||||||
<IonModal
|
<IonModal
|
||||||
isOpen={showArtistSongs}
|
isOpen={showArtistSongs}
|
||||||
@ -193,18 +165,10 @@ const SongInfo: React.FC<SongInfoProps> = ({ isOpen, onClose, song }) => {
|
|||||||
breakpoints={[0, 0.5, 0.8]}
|
breakpoints={[0, 0.5, 0.8]}
|
||||||
initialBreakpoint={0.8}
|
initialBreakpoint={0.8}
|
||||||
>
|
>
|
||||||
<IonHeader>
|
<ModalHeader
|
||||||
<IonToolbar>
|
title={`Songs by ${song.artist}`}
|
||||||
<IonTitle>Songs by {song.artist}</IonTitle>
|
onClose={() => setShowArtistSongs(false)}
|
||||||
<ActionButton
|
|
||||||
onClick={() => setShowArtistSongs(false)}
|
|
||||||
variant={ActionButtonVariant.SECONDARY}
|
|
||||||
size={ActionButtonSize.SMALL}
|
|
||||||
icon={Icons.CLOSE}
|
|
||||||
iconSlot={ActionButtonIconSlot.ICON_ONLY}
|
|
||||||
/>
|
/>
|
||||||
</IonToolbar>
|
|
||||||
</IonHeader>
|
|
||||||
|
|
||||||
<IonContent>
|
<IonContent>
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
|
|||||||
@ -1,48 +0,0 @@
|
|||||||
import React, { useState, useCallback } from 'react';
|
|
||||||
import type { ReactNode } from 'react';
|
|
||||||
import SongInfo from './SongInfo';
|
|
||||||
import { SongInfoContext, type SongInfoContextType } from '../../contexts/SongInfoContext';
|
|
||||||
import type { Song } from '../../types';
|
|
||||||
|
|
||||||
interface SongInfoProviderProps {
|
|
||||||
children: ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SongInfoProvider: React.FC<SongInfoProviderProps> = ({ children }) => {
|
|
||||||
const [selectedSong, setSelectedSong] = useState<Song | null>(null);
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
|
|
||||||
const openSongInfo = useCallback((song: Song) => {
|
|
||||||
setSelectedSong(song);
|
|
||||||
setIsOpen(true);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const closeSongInfo = useCallback(() => {
|
|
||||||
setIsOpen(false);
|
|
||||||
setSelectedSong(null);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const contextValue: SongInfoContextType = {
|
|
||||||
openSongInfo,
|
|
||||||
closeSongInfo,
|
|
||||||
isOpen,
|
|
||||||
selectedSong,
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SongInfoContext.Provider value={contextValue}>
|
|
||||||
{children}
|
|
||||||
|
|
||||||
{/* Song Info Modal */}
|
|
||||||
{selectedSong && (
|
|
||||||
<SongInfo
|
|
||||||
isOpen={isOpen}
|
|
||||||
onClose={closeSongInfo}
|
|
||||||
song={selectedSong}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</SongInfoContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
@ -5,7 +5,7 @@ import { useAppSelector } from '../../redux';
|
|||||||
import { selectQueue, selectFavorites } from '../../redux';
|
import { selectQueue, selectFavorites } from '../../redux';
|
||||||
import { useSongOperations } from '../../hooks/useSongOperations';
|
import { useSongOperations } from '../../hooks/useSongOperations';
|
||||||
import { useToast } from '../../hooks/useToast';
|
import { useToast } from '../../hooks/useToast';
|
||||||
import { useSongInfo } from '../../hooks/useSongInfoContext';
|
import { useModal } from '../../hooks/useModalContext';
|
||||||
import { debugLog } from '../../utils/logger';
|
import { debugLog } from '../../utils/logger';
|
||||||
import type { SongItemProps, QueueItem, Song } from '../../types';
|
import type { SongItemProps, QueueItem, Song } from '../../types';
|
||||||
import { ActionButtonVariant, ActionButtonSize, ActionButtonIconSlot } from '../../types';
|
import { ActionButtonVariant, ActionButtonSize, ActionButtonIconSlot } from '../../types';
|
||||||
@ -217,7 +217,7 @@ const SongItem: React.FC<SongItemProps> = ({
|
|||||||
// Get song operations and hooks
|
// Get song operations and hooks
|
||||||
const { addToQueue, removeFromQueue, toggleFavorite } = useSongOperations();
|
const { addToQueue, removeFromQueue, toggleFavorite } = useSongOperations();
|
||||||
const { showSuccess, showError } = useToast();
|
const { showSuccess, showError } = useToast();
|
||||||
const { openSongInfo } = useSongInfo();
|
const { openSongInfo } = useModal();
|
||||||
|
|
||||||
// Check if song is in queue or favorites based on path
|
// 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 isInQueue = (Object.values(queue) as QueueItem[]).some(item => item.song.path === song.path);
|
||||||
|
|||||||
@ -10,3 +10,4 @@ export { default as Toast } from './Toast';
|
|||||||
export { TwoLineDisplay } from './TwoLineDisplay';
|
export { TwoLineDisplay } from './TwoLineDisplay';
|
||||||
export { ListItem } from './ListItem';
|
export { ListItem } from './ListItem';
|
||||||
export { NumberDisplay } from './NumberDisplay';
|
export { NumberDisplay } from './NumberDisplay';
|
||||||
|
export { ModalHeader } from './ModalHeader';
|
||||||
4
src/contexts/ModalContext.tsx
Normal file
4
src/contexts/ModalContext.tsx
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import { createContext } from 'react';
|
||||||
|
import type { ModalContextType } from '../types/modal';
|
||||||
|
|
||||||
|
export const ModalContext = createContext<ModalContextType | undefined>(undefined);
|
||||||
@ -1,11 +0,0 @@
|
|||||||
import { createContext } from 'react';
|
|
||||||
import type { Song } from '../types';
|
|
||||||
|
|
||||||
export interface SongInfoContextType {
|
|
||||||
openSongInfo: (song: Song) => void;
|
|
||||||
closeSongInfo: () => void;
|
|
||||||
isOpen: boolean;
|
|
||||||
selectedSong: Song | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SongInfoContext = createContext<SongInfoContextType | null>(null);
|
|
||||||
@ -1,9 +1,7 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { IonSearchbar, IonModal, IonHeader, IonToolbar, IonTitle, IonContent, IonItem } from '@ionic/react';
|
import { IonSearchbar, IonModal, IonContent, IonItem } from '@ionic/react';
|
||||||
import { list } from 'ionicons/icons';
|
import { list } from 'ionicons/icons';
|
||||||
import { InfiniteScrollList, SongItem, ListItem, NumberDisplay, ActionButton } from '../../components/common';
|
import { InfiniteScrollList, SongItem, ListItem, NumberDisplay, ModalHeader } from '../../components/common';
|
||||||
import { ActionButtonVariant, ActionButtonSize, ActionButtonIconSlot } from '../../types';
|
|
||||||
import { Icons } from '../../constants';
|
|
||||||
import { useArtists } from '../../hooks';
|
import { useArtists } from '../../hooks';
|
||||||
import { useAppSelector } from '../../redux';
|
import { useAppSelector } from '../../redux';
|
||||||
import { selectSongs } from '../../redux';
|
import { selectSongs } from '../../redux';
|
||||||
@ -82,21 +80,7 @@ const Artists: React.FC = () => {
|
|||||||
isOpen={!!selectedArtist}
|
isOpen={!!selectedArtist}
|
||||||
onDidDismiss={handleCloseArtistSongs}
|
onDidDismiss={handleCloseArtistSongs}
|
||||||
>
|
>
|
||||||
<IonHeader>
|
<ModalHeader title={`Songs by ${selectedArtist}`} onClose={handleCloseArtistSongs} />
|
||||||
<IonToolbar>
|
|
||||||
<IonTitle>Songs by {selectedArtist}</IonTitle>
|
|
||||||
<div slot="end">
|
|
||||||
<ActionButton
|
|
||||||
onClick={handleCloseArtistSongs}
|
|
||||||
variant={ActionButtonVariant.SECONDARY}
|
|
||||||
size={ActionButtonSize.SMALL}
|
|
||||||
icon={Icons.CLOSE}
|
|
||||||
iconSlot={ActionButtonIconSlot.ICON_ONLY}
|
|
||||||
fill="clear"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</IonToolbar>
|
|
||||||
</IonHeader>
|
|
||||||
|
|
||||||
<IonContent>
|
<IonContent>
|
||||||
<div style={{ padding: '10px' }}>
|
<div style={{ padding: '10px' }}>
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { IonItem, IonLabel, IonModal, IonHeader, IonToolbar, IonTitle, IonButton, IonContent, IonInput, IonLabel as IonInputLabel } from '@ionic/react';
|
import { IonItem, IonLabel, IonModal, IonButton, IonContent, IonInput, IonLabel as IonInputLabel } from '@ionic/react';
|
||||||
import { InfiniteScrollList, ActionButton, NumberDisplay } from '../../components/common';
|
import { InfiniteScrollList, ActionButton, NumberDisplay, ModalHeader } from '../../components/common';
|
||||||
import { useSingers } from '../../hooks';
|
import { useSingers } from '../../hooks';
|
||||||
import { useAppSelector } from '../../redux';
|
import { useAppSelector } from '../../redux';
|
||||||
import { selectSingers } from '../../redux';
|
import { selectSingers } from '../../redux';
|
||||||
@ -109,21 +109,7 @@ const Singers: React.FC = () => {
|
|||||||
breakpoints={[0, 0.5, 0.8]}
|
breakpoints={[0, 0.5, 0.8]}
|
||||||
initialBreakpoint={0.8}
|
initialBreakpoint={0.8}
|
||||||
>
|
>
|
||||||
<IonHeader>
|
<ModalHeader title="Add New Singer" onClose={handleCloseAddModal} />
|
||||||
<IonToolbar>
|
|
||||||
<IonTitle>Add New Singer</IonTitle>
|
|
||||||
<div slot="end">
|
|
||||||
<ActionButton
|
|
||||||
onClick={handleCloseAddModal}
|
|
||||||
variant={ActionButtonVariant.SECONDARY}
|
|
||||||
size={ActionButtonSize.SMALL}
|
|
||||||
icon={Icons.CLOSE}
|
|
||||||
iconSlot={ActionButtonIconSlot.ICON_ONLY}
|
|
||||||
fill="clear"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</IonToolbar>
|
|
||||||
</IonHeader>
|
|
||||||
|
|
||||||
<IonContent className="ion-padding">
|
<IonContent className="ion-padding">
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@ -1,9 +1,7 @@
|
|||||||
import React, { useState, useMemo, useCallback } from 'react';
|
import React, { useState, useMemo, useCallback } from 'react';
|
||||||
import { IonItem, IonModal, IonHeader, IonToolbar, IonTitle, IonChip, IonContent, IonList, IonAccordionGroup, IonAccordion } from '@ionic/react';
|
import { IonItem, IonModal, IonChip, IonContent, IonList, IonAccordionGroup, IonAccordion } from '@ionic/react';
|
||||||
import { list } from 'ionicons/icons';
|
import { list } from 'ionicons/icons';
|
||||||
import { InfiniteScrollList, SongItem, ListItem, TwoLineDisplay, NumberDisplay, ActionButton } from '../../components/common';
|
import { InfiniteScrollList, SongItem, ListItem, TwoLineDisplay, NumberDisplay, ModalHeader } from '../../components/common';
|
||||||
import { ActionButtonVariant, ActionButtonSize, ActionButtonIconSlot } from '../../types';
|
|
||||||
import { Icons } from '../../constants';
|
|
||||||
import { useSongLists } from '../../hooks';
|
import { useSongLists } from '../../hooks';
|
||||||
import { useAppSelector } from '../../redux';
|
import { useAppSelector } from '../../redux';
|
||||||
import { selectSongList } from '../../redux';
|
import { selectSongList } from '../../redux';
|
||||||
@ -90,21 +88,7 @@ const SongLists: React.FC = () => {
|
|||||||
isOpen={!!finalSelectedList}
|
isOpen={!!finalSelectedList}
|
||||||
onDidDismiss={handleCloseSongList}
|
onDidDismiss={handleCloseSongList}
|
||||||
>
|
>
|
||||||
<IonHeader>
|
<ModalHeader title={finalSelectedList?.title || ''} onClose={handleCloseSongList} />
|
||||||
<IonToolbar>
|
|
||||||
<IonTitle>{finalSelectedList?.title}</IonTitle>
|
|
||||||
<div slot="end">
|
|
||||||
<ActionButton
|
|
||||||
onClick={handleCloseSongList}
|
|
||||||
variant={ActionButtonVariant.SECONDARY}
|
|
||||||
size={ActionButtonSize.SMALL}
|
|
||||||
icon={Icons.CLOSE}
|
|
||||||
iconSlot={ActionButtonIconSlot.ICON_ONLY}
|
|
||||||
fill="clear"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</IonToolbar>
|
|
||||||
</IonHeader>
|
|
||||||
|
|
||||||
<IonContent>
|
<IonContent>
|
||||||
<IonAccordionGroup value={expandedSongKey}>
|
<IonAccordionGroup value={expandedSongKey}>
|
||||||
|
|||||||
@ -1,15 +1,14 @@
|
|||||||
import React, { useState, useMemo, useCallback } from 'react';
|
import React, { useState, useMemo, useCallback } from 'react';
|
||||||
import { IonChip, IonModal, IonHeader, IonToolbar, IonTitle, IonIcon, IonContent, IonList } from '@ionic/react';
|
import { IonChip, IonModal, IonIcon, IonContent, IonList } from '@ionic/react';
|
||||||
import { list } from 'ionicons/icons';
|
import { 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, ListItem, ActionButton } from '../../components/common';
|
import { InfiniteScrollList, SongItem, ListItem, ModalHeader } from '../../components/common';
|
||||||
import { filterSongs } from '../../utils/dataProcessing';
|
import { filterSongs } from '../../utils/dataProcessing';
|
||||||
import { debugLog } from '../../utils/logger';
|
import { debugLog } from '../../utils/logger';
|
||||||
|
|
||||||
import { ActionButtonVariant, ActionButtonSize, ActionButtonIconSlot } from '../../types';
|
|
||||||
import { Icons } from '../../constants';
|
|
||||||
import type { TopPlayed } from '../../types';
|
import type { TopPlayed } from '../../types';
|
||||||
|
|
||||||
const Top100: React.FC = () => {
|
const Top100: React.FC = () => {
|
||||||
@ -117,21 +116,7 @@ const Top100: React.FC = () => {
|
|||||||
breakpoints={[0, 0.5, 0.8]}
|
breakpoints={[0, 0.5, 0.8]}
|
||||||
initialBreakpoint={0.8}
|
initialBreakpoint={0.8}
|
||||||
>
|
>
|
||||||
<IonHeader>
|
<ModalHeader title={selectedTopPlayed?.artist || ''} onClose={handleCloseModal} />
|
||||||
<IonToolbar>
|
|
||||||
<IonTitle>{selectedTopPlayed?.artist}</IonTitle>
|
|
||||||
<div slot="end">
|
|
||||||
<ActionButton
|
|
||||||
onClick={handleCloseModal}
|
|
||||||
variant={ActionButtonVariant.SECONDARY}
|
|
||||||
size={ActionButtonSize.SMALL}
|
|
||||||
icon={Icons.CLOSE}
|
|
||||||
iconSlot={ActionButtonIconSlot.ICON_ONLY}
|
|
||||||
fill="clear"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</IonToolbar>
|
|
||||||
</IonHeader>
|
|
||||||
|
|
||||||
<IonContent>
|
<IonContent>
|
||||||
<IonList>
|
<IonList>
|
||||||
|
|||||||
@ -11,5 +11,5 @@ export { useArtists } from './useArtists';
|
|||||||
export { useSingers } from './useSingers';
|
export { useSingers } from './useSingers';
|
||||||
export { useSongLists } from './useSongLists';
|
export { useSongLists } from './useSongLists';
|
||||||
export { useDisabledSongs } from './useDisabledSongs';
|
export { useDisabledSongs } from './useDisabledSongs';
|
||||||
export { useSelectSinger } from './useSelectSinger';
|
|
||||||
export { useSongInfo } from './useSongInfo';
|
export { useSongInfo } from './useSongInfo';
|
||||||
12
src/hooks/useModalContext.ts
Normal file
12
src/hooks/useModalContext.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { useContext } from 'react';
|
||||||
|
import { ModalContext } from '../contexts/ModalContext';
|
||||||
|
|
||||||
|
export const useModal = () => {
|
||||||
|
const context = useContext(ModalContext);
|
||||||
|
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error('useModal must be used within a ModalProvider');
|
||||||
|
}
|
||||||
|
|
||||||
|
return context;
|
||||||
|
};
|
||||||
@ -1,24 +0,0 @@
|
|||||||
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,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
import { useContext } from 'react';
|
|
||||||
import { SongInfoContext, type SongInfoContextType } from '../contexts/SongInfoContext';
|
|
||||||
|
|
||||||
export const useSongInfo = (): SongInfoContextType => {
|
|
||||||
const context = useContext(SongInfoContext);
|
|
||||||
if (context === null) {
|
|
||||||
throw new Error('useSongInfo must be used within a SongInfoProvider');
|
|
||||||
}
|
|
||||||
return context;
|
|
||||||
};
|
|
||||||
@ -160,6 +160,7 @@ export interface ActionButtonProps {
|
|||||||
iconSlot?: ActionButtonIconSlotType;
|
iconSlot?: ActionButtonIconSlotType;
|
||||||
iconSize?: ActionButtonIconSizeType;
|
iconSize?: ActionButtonIconSizeType;
|
||||||
fill?: 'solid' | 'outline' | 'clear';
|
fill?: 'solid' | 'outline' | 'clear';
|
||||||
|
'aria-label'?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SongItemProps {
|
export interface SongItemProps {
|
||||||
@ -196,3 +197,7 @@ export interface RootState {
|
|||||||
error: string | null;
|
error: string | null;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Modal types
|
||||||
|
export { ModalType } from './modal';
|
||||||
|
export type { ModalViewType, ModalState, ModalContextType, ModalItem } from './modal';
|
||||||
33
src/types/modal.ts
Normal file
33
src/types/modal.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import type { Song } from './index';
|
||||||
|
|
||||||
|
export const ModalType = {
|
||||||
|
SONG_INFO: 'songInfo',
|
||||||
|
SELECT_SINGER: 'selectSinger'
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type ModalViewType = typeof ModalType[keyof typeof ModalType];
|
||||||
|
|
||||||
|
export interface ModalItem {
|
||||||
|
id: string;
|
||||||
|
type: ModalViewType;
|
||||||
|
data: Song;
|
||||||
|
isOpen: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ModalState {
|
||||||
|
stack: ModalItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ModalContextType {
|
||||||
|
// Generic modal methods
|
||||||
|
openModal: (type: ModalViewType, data: Song) => string; // Returns modal ID
|
||||||
|
closeModal: (id: string) => void;
|
||||||
|
closeTopModal: () => void;
|
||||||
|
|
||||||
|
// Specific modal methods for convenience
|
||||||
|
openSongInfo: (song: Song) => string;
|
||||||
|
openSelectSinger: (song: Song) => string;
|
||||||
|
|
||||||
|
// State
|
||||||
|
modalState: ModalState;
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user